Skip to main content

blit_server/
surface_encoder.rs

1#![allow(clippy::too_many_arguments)]
2
3use blit_compositor::PixelData;
4#[cfg(unix)]
5use blit_remote::SURFACE_FRAME_CODEC_H265;
6use blit_remote::{
7    CODEC_SUPPORT_AV1, CODEC_SUPPORT_H264, CODEC_SUPPORT_H265, SURFACE_FRAME_CODEC_AV1,
8    SURFACE_FRAME_CODEC_H264,
9};
10use openh264::encoder::Encoder as OpenH264Encoder;
11use openh264::formats::YUVBuffer;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum SurfaceEncoderPreference {
15    H264Software,
16    H264Vaapi,
17    H265Vaapi,
18    NvencH264,
19    NvencH265,
20    NvencAV1,
21    AV1,
22}
23
24// Type alias for backwards compatibility in tests.
25pub type SurfaceH264EncoderPreference = SurfaceEncoderPreference;
26
27/// openh264 hard limit: 3840x2160 horizontal or 2160x3840 vertical.
28const H264_MAX_WIDTH: u16 = 3840;
29const H264_MAX_HEIGHT: u16 = 2160;
30
31impl SurfaceEncoderPreference {
32    pub fn parse(value: &str) -> Option<Self> {
33        match value.trim() {
34            "h264-software" | "software" => Some(Self::H264Software),
35            "h264-vaapi" | "vaapi" => Some(Self::H264Vaapi),
36            "h265-vaapi" | "hevc-vaapi" => Some(Self::H265Vaapi),
37            "nvenc-h264" | "h264-nvenc" => Some(Self::NvencH264),
38            "nvenc-h265" | "h265-nvenc" | "nvenc-hevc" | "hevc-nvenc" => Some(Self::NvencH265),
39            "nvenc-av1" | "av1-nvenc" => Some(Self::NvencAV1),
40            "av1" => Some(Self::AV1),
41            _ => None,
42        }
43    }
44
45    /// Parse a comma-separated list of encoder preferences.
46    pub fn parse_list(value: &str) -> Result<Vec<Self>, String> {
47        let mut result = Vec::new();
48        for item in value.split(',') {
49            let item = item.trim();
50            if item.is_empty() {
51                continue;
52            }
53            result.push(Self::parse(item).ok_or_else(|| format!("unknown encoder: {item}"))?);
54        }
55        Ok(result)
56    }
57
58    /// Sensible default: hardware before software, H.265 > H.264 > AV1.
59    ///
60    /// Override at runtime with `BLIT_SURFACE_ENCODERS=nvenc-h265,h264-software`
61    /// (comma-separated list).
62    pub fn defaults() -> Vec<Self> {
63        if let Some(list) = std::env::var("BLIT_SURFACE_ENCODERS")
64            .ok()
65            .and_then(|v| Self::parse_list(&v).ok())
66        {
67            return list;
68        }
69        vec![
70            Self::NvencH265,
71            Self::H265Vaapi,
72            Self::NvencAV1,
73            Self::NvencH264,
74            Self::H264Vaapi,
75            Self::H264Software,
76            Self::AV1,
77        ]
78    }
79
80    /// Returns true if the given codec_support bitmask allows this encoder.
81    /// A codec_support of 0 means "accept anything".
82    pub fn supported_by_client(self, codec_support: u8) -> bool {
83        if codec_support == 0 {
84            return true;
85        }
86        match self {
87            Self::H264Software | Self::H264Vaapi | Self::NvencH264 => {
88                codec_support & CODEC_SUPPORT_H264 != 0
89            }
90            Self::H265Vaapi | Self::NvencH265 => codec_support & CODEC_SUPPORT_H265 != 0,
91            Self::AV1 | Self::NvencAV1 => codec_support & CODEC_SUPPORT_AV1 != 0,
92        }
93    }
94
95    /// Maximum surface dimensions the encoder can handle.
96    /// Returns `None` if there is no practical limit.
97    pub fn max_dimensions(self) -> Option<(u16, u16)> {
98        match self {
99            Self::H264Software | Self::H264Vaapi | Self::NvencH264 => {
100                Some((H264_MAX_WIDTH, H264_MAX_HEIGHT))
101            }
102            Self::H265Vaapi | Self::NvencH265 | Self::NvencAV1 | Self::AV1 => None,
103        }
104    }
105
106    /// Tightest max dimensions across a list of preferences.
107    pub fn max_dimensions_for_list(prefs: &[Self]) -> Option<(u16, u16)> {
108        let mut result: Option<(u16, u16)> = None;
109        for p in prefs {
110            if let Some((w, h)) = p.max_dimensions() {
111                result = Some(match result {
112                    Some((rw, rh)) => (rw.min(w), rh.min(h)),
113                    None => (w, h),
114                });
115            }
116        }
117        result
118    }
119}
120
121/// Video quality preset.  Higher quality uses more CPU.
122///
123/// - **Low**: speed 10, quantizer 180 — minimal CPU, visibly lossy
124/// - **Medium** (default): speed 10, quantizer 120 — good balance
125/// - **High**: speed 8, quantizer 80 — sharp, noticeable CPU use
126/// - **Lossless-ish**: speed 6, quantizer 40 — near-lossless, heavy CPU
127///
128/// Set via `BLIT_SURFACE_QUALITY=low|medium|high|lossless`.
129#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
130pub enum SurfaceQuality {
131    Low,
132    #[default]
133    Medium,
134    High,
135    Lossless,
136}
137
138impl SurfaceQuality {
139    pub fn parse(value: &str) -> Option<Self> {
140        match value {
141            "low" => Some(Self::Low),
142            "medium" => Some(Self::Medium),
143            "high" => Some(Self::High),
144            "lossless" => Some(Self::Lossless),
145            _ => None,
146        }
147    }
148
149    /// rav1e speed preset (0 = slowest/best, 10 = fastest/worst).
150    fn av1_speed(self) -> u8 {
151        match self {
152            Self::Low => 10,
153            Self::Medium => 10,
154            Self::High => 8,
155            Self::Lossless => 6,
156        }
157    }
158
159    /// rav1e quantizer (0 = lossless, 255 = worst).
160    fn av1_quantizer(self) -> usize {
161        match self {
162            Self::Low => 180,
163            Self::Medium => 120,
164            Self::High => 80,
165            Self::Lossless => 40,
166        }
167    }
168
169    /// rav1e min_quantizer.
170    fn av1_min_quantizer(self) -> u8 {
171        match self {
172            Self::Low => 120,
173            Self::Medium => 80,
174            Self::High => 40,
175            Self::Lossless => 0,
176        }
177    }
178}
179
180pub struct SurfaceEncoder {
181    /// Dimensions the encoder actually operates at (may be padded to even for H.264).
182    width: u32,
183    height: u32,
184    /// Original surface dimensions before any padding.
185    source_width: u32,
186    source_height: u32,
187    kind: SurfaceEncoderKind,
188}
189
190enum SurfaceEncoderKind {
191    H264Software(Box<SoftwareH264Encoder>),
192    NvencH264(Box<crate::nvenc_encode::NvencDirectEncoder>),
193    NvencH265(Box<crate::nvenc_encode::NvencDirectEncoder>),
194    NvencAV1(Box<crate::nvenc_encode::NvencDirectEncoder>),
195    #[cfg(unix)]
196    H264Vaapi(Box<crate::vaapi_encode::VaapiDirectEncoder>),
197    #[cfg(unix)]
198    H265Vaapi(Box<crate::vaapi_encode::VaapiHevcEncoder>),
199    AV1Software(Box<SoftwareAV1Encoder>),
200}
201
202impl SurfaceEncoder {
203    /// Try each preference in order; return the first that succeeds and
204    /// the client can decode.  `codec_support` is a bitmask of
205    /// `CODEC_SUPPORT_*` (0 = accept anything).
206    pub fn new(
207        preferences: &[SurfaceEncoderPreference],
208        width: u32,
209        height: u32,
210        vaapi_device: &str,
211        quality: SurfaceQuality,
212        verbose: bool,
213        codec_support: u8,
214    ) -> Result<Self, String> {
215        let source_width = width;
216        let source_height = height;
217        let mut last_err = String::from("no encoders configured");
218
219        for &pref in preferences {
220            if !pref.supported_by_client(codec_support) {
221                continue;
222            }
223            match Self::try_one(
224                pref,
225                width,
226                height,
227                source_width,
228                source_height,
229                vaapi_device,
230                quality,
231            ) {
232                Ok(enc) => {
233                    if verbose {
234                        eprintln!(
235                            "[surface-encoder] using {:?} for {source_width}x{source_height}",
236                            pref
237                        );
238                    }
239                    return Ok(enc);
240                }
241                Err(err) => {
242                    if verbose {
243                        eprintln!(
244                            "[surface-encoder] {:?} unavailable for {source_width}x{source_height}: {err}",
245                            pref
246                        );
247                    }
248                    last_err = err;
249                }
250            }
251        }
252        Err(last_err)
253    }
254
255    fn try_one(
256        pref: SurfaceEncoderPreference,
257        width: u32,
258        height: u32,
259        source_width: u32,
260        source_height: u32,
261        vaapi_device: &str,
262        quality: SurfaceQuality,
263    ) -> Result<Self, String> {
264        let _ = vaapi_device;
265        validate_surface_dimensions(width, height, pref)?;
266
267        match pref {
268            SurfaceEncoderPreference::NvencH264 => {
269                let (width, height) = ((width + 1) & !1, (height + 1) & !1);
270                Ok(Self {
271                    width,
272                    height,
273                    source_width,
274                    source_height,
275                    kind: SurfaceEncoderKind::NvencH264(Box::new(
276                        crate::nvenc_encode::NvencDirectEncoder::try_new("h264", width, height)?,
277                    )),
278                })
279            }
280            SurfaceEncoderPreference::NvencH265 => {
281                let (width, height) = ((width + 1) & !1, (height + 1) & !1);
282                Ok(Self {
283                    width,
284                    height,
285                    source_width,
286                    source_height,
287                    kind: SurfaceEncoderKind::NvencH265(Box::new(
288                        crate::nvenc_encode::NvencDirectEncoder::try_new("h265", width, height)?,
289                    )),
290                })
291            }
292            SurfaceEncoderPreference::NvencAV1 => Ok(Self {
293                width,
294                height,
295                source_width,
296                source_height,
297                kind: SurfaceEncoderKind::NvencAV1(Box::new(
298                    crate::nvenc_encode::NvencDirectEncoder::try_new("av1", width, height)?,
299                )),
300            }),
301            #[cfg(unix)]
302            SurfaceEncoderPreference::H264Vaapi => {
303                let (width, height) = ((width + 1) & !1, (height + 1) & !1);
304                Ok(Self {
305                    width,
306                    height,
307                    source_width,
308                    source_height,
309                    kind: SurfaceEncoderKind::H264Vaapi(Box::new(
310                        crate::vaapi_encode::VaapiDirectEncoder::try_new(
311                            width,
312                            height,
313                            vaapi_device,
314                        )?,
315                    )),
316                })
317            }
318            #[cfg(unix)]
319            SurfaceEncoderPreference::H265Vaapi => {
320                let (width, height) = ((width + 1) & !1, (height + 1) & !1);
321                Ok(Self {
322                    width,
323                    height,
324                    source_width,
325                    source_height,
326                    kind: SurfaceEncoderKind::H265Vaapi(Box::new(
327                        crate::vaapi_encode::VaapiHevcEncoder::try_new(
328                            width,
329                            height,
330                            vaapi_device,
331                        )?,
332                    )),
333                })
334            }
335            #[cfg(not(unix))]
336            SurfaceEncoderPreference::H264Vaapi | SurfaceEncoderPreference::H265Vaapi => {
337                Err("VA-API is only available on Unix".into())
338            }
339            SurfaceEncoderPreference::AV1 => Ok(Self {
340                width,
341                height,
342                source_width,
343                source_height,
344                kind: SurfaceEncoderKind::AV1Software(Box::new(SoftwareAV1Encoder::new(
345                    width, height, quality,
346                )?)),
347            }),
348            SurfaceEncoderPreference::H264Software => {
349                let (width, height) = ((width + 1) & !1, (height + 1) & !1);
350                Ok(Self {
351                    width,
352                    height,
353                    source_width,
354                    source_height,
355                    kind: SurfaceEncoderKind::H264Software(Box::new(SoftwareH264Encoder::new()?)),
356                })
357            }
358        }
359    }
360
361    #[allow(dead_code)]
362    pub fn dimensions(&self) -> (u32, u32) {
363        (self.width, self.height)
364    }
365
366    /// The original surface dimensions before any encoder padding.
367    pub fn source_dimensions(&self) -> (u32, u32) {
368        (self.source_width, self.source_height)
369    }
370
371    #[allow(dead_code)]
372    pub fn kind_name(&self) -> &'static str {
373        match &self.kind {
374            SurfaceEncoderKind::H264Software(_) => "h264-software",
375            SurfaceEncoderKind::NvencH264(_) => "nvenc-h264",
376            SurfaceEncoderKind::NvencH265(_) => "nvenc-h265",
377            SurfaceEncoderKind::NvencAV1(_) => "nvenc-av1",
378            #[cfg(unix)]
379            SurfaceEncoderKind::H264Vaapi(_) => "h264-vaapi",
380            #[cfg(unix)]
381            SurfaceEncoderKind::H265Vaapi(_) => "h265-vaapi",
382            SurfaceEncoderKind::AV1Software(_) => "av1-software",
383        }
384    }
385
386    pub fn codec_flag(&self) -> u8 {
387        match &self.kind {
388            SurfaceEncoderKind::H264Software(_) => SURFACE_FRAME_CODEC_H264,
389            #[cfg(unix)]
390            SurfaceEncoderKind::H264Vaapi(_) => SURFACE_FRAME_CODEC_H264,
391            #[cfg(unix)]
392            SurfaceEncoderKind::H265Vaapi(_) => SURFACE_FRAME_CODEC_H265,
393            SurfaceEncoderKind::NvencH264(enc)
394            | SurfaceEncoderKind::NvencH265(enc)
395            | SurfaceEncoderKind::NvencAV1(enc) => enc.codec_flag(),
396            SurfaceEncoderKind::AV1Software(_) => SURFACE_FRAME_CODEC_AV1,
397        }
398    }
399
400    pub fn request_keyframe(&mut self) {
401        match &mut self.kind {
402            SurfaceEncoderKind::H264Software(enc) => enc.request_keyframe(),
403            SurfaceEncoderKind::NvencH264(enc)
404            | SurfaceEncoderKind::NvencH265(enc)
405            | SurfaceEncoderKind::NvencAV1(enc) => enc.request_keyframe(),
406            #[cfg(unix)]
407            SurfaceEncoderKind::H264Vaapi(enc) => enc.request_keyframe(),
408            #[cfg(unix)]
409            SurfaceEncoderKind::H265Vaapi(enc) => enc.request_keyframe(),
410            SurfaceEncoderKind::AV1Software(enc) => enc.request_keyframe(),
411        }
412    }
413
414    pub fn encode(&mut self, rgba: &[u8]) -> Option<(Vec<u8>, bool)> {
415        let enc_len = expected_rgba_len(self.width, self.height);
416        let enc_len = match enc_len {
417            Some(v) => v,
418            None => {
419                eprintln!(
420                    "[surface-encoder] expected_rgba_len overflow {}x{}",
421                    self.width, self.height
422                );
423                return None;
424            }
425        };
426        let rgba = if rgba.len() == enc_len {
427            std::borrow::Cow::Borrowed(rgba)
428        } else {
429            // The source buffer may be smaller when the original surface had
430            // odd dimensions (H.264 rounds up to even).  Pad with edge-pixel
431            // duplication.
432            let total_px = rgba.len() / 4;
433            if total_px == 0 {
434                return None;
435            }
436            // Infer source width: try self.width, then self.width - 1
437            let src_w = [self.width as usize, (self.width - 1) as usize]
438                .into_iter()
439                .find(|&w| w > 0 && total_px.is_multiple_of(w))?;
440            let src_h = total_px / src_w;
441            if src_h == 0 {
442                return None;
443            }
444            let dst_w = self.width as usize;
445            let dst_h = self.height as usize;
446            let mut padded = vec![0u8; enc_len];
447            for row in 0..dst_h {
448                let src_row = row.min(src_h - 1);
449                for col in 0..dst_w {
450                    let src_col = col.min(src_w - 1);
451                    let si = (src_row * src_w + src_col) * 4;
452                    let di = (row * dst_w + col) * 4;
453                    padded[di..di + 4].copy_from_slice(&rgba[si..si + 4]);
454                }
455            }
456            std::borrow::Cow::Owned(padded)
457        };
458
459        match &mut self.kind {
460            SurfaceEncoderKind::H264Software(encoder) => {
461                encoder.encode(&rgba, self.width, self.height)
462            }
463            SurfaceEncoderKind::NvencH264(enc)
464            | SurfaceEncoderKind::NvencH265(enc)
465            | SurfaceEncoderKind::NvencAV1(enc) => {
466                let mut bgra = rgba.into_owned();
467                for px in bgra.chunks_exact_mut(4) {
468                    px.swap(0, 2);
469                }
470                enc.encode_bgra(&bgra)
471            }
472            #[cfg(unix)]
473            SurfaceEncoderKind::H264Vaapi(enc) => {
474                let mut bgra = rgba.into_owned();
475                for px in bgra.chunks_exact_mut(4) {
476                    px.swap(0, 2);
477                }
478                let (sw, sh) = (self.source_width as usize, self.source_height as usize);
479                enc.encode_bgra_padded(&bgra, sw, sh)
480            }
481            #[cfg(unix)]
482            SurfaceEncoderKind::H265Vaapi(enc) => {
483                let mut bgra = rgba.into_owned();
484                for px in bgra.chunks_exact_mut(4) {
485                    px.swap(0, 2);
486                }
487                let (sw, sh) = (self.source_width as usize, self.source_height as usize);
488                enc.encode_bgra_padded(&bgra, sw, sh)
489            }
490            SurfaceEncoderKind::AV1Software(encoder) => encoder.encode(&rgba),
491        }
492    }
493
494    /// Encode a single keyframe.  For AV1 (rav1e with rdo_lookahead=1) the
495    /// first encode primes the pipeline and the second flushes the keyframe.
496    /// H.264 encoders produce output on the first call.
497    #[cfg(test)]
498    fn encode_keyframe(&mut self, rgba: &[u8]) -> Option<(Vec<u8>, bool)> {
499        self.request_keyframe();
500        let first = self.encode(rgba);
501        if first.is_some() {
502            return first;
503        }
504        // Pipeline wasn't ready — flush to force output.
505        self.flush_keyframe(rgba)
506    }
507
508    /// Feed a frame, flush the encoder, and drain packets.
509    fn flush_keyframe(&mut self, rgba: &[u8]) -> Option<(Vec<u8>, bool)> {
510        match &mut self.kind {
511            SurfaceEncoderKind::AV1Software(enc) => enc.flush_encode(rgba),
512            _ => self.encode(rgba),
513        }
514    }
515
516    /// Encode a frame from native pixel data (BGRA, NV12, RGBA, or DMA-BUF).
517    /// Dispatches to the most efficient path for each format.
518    pub fn encode_pixels(&mut self, pixels: &PixelData) -> Option<(Vec<u8>, bool)> {
519        match pixels {
520            PixelData::Nv12 {
521                data,
522                y_stride,
523                uv_stride,
524            } => self.encode_nv12(data, *y_stride, *uv_stride),
525            PixelData::Bgra(bgra) => self.encode_bgra(bgra),
526            PixelData::Rgba(rgba) => self.encode(rgba),
527            #[cfg(unix)]
528            PixelData::DmaBuf {
529                fd,
530                fourcc,
531                modifier,
532                stride,
533                offset,
534            } => self.encode_dmabuf(fd, *fourcc, *modifier, *stride, *offset),
535            #[cfg(not(unix))]
536            PixelData::DmaBuf { .. } => None,
537        }
538    }
539
540    /// Encode a keyframe from native pixel data.
541    pub fn encode_keyframe_pixels(&mut self, pixels: &PixelData) -> Option<(Vec<u8>, bool)> {
542        self.request_keyframe();
543        let first = self.encode_pixels(pixels);
544        if first.is_some() {
545            return first;
546        }
547        // Pipeline wasn't ready — flush to force output.
548        match pixels {
549            PixelData::Rgba(rgba) => self.flush_keyframe(rgba),
550            PixelData::Bgra(bgra) => self.flush_keyframe_bgra(bgra),
551            PixelData::Nv12 {
552                data,
553                y_stride,
554                uv_stride,
555            } => self.flush_keyframe_nv12(data, *y_stride, *uv_stride),
556            #[cfg(unix)]
557            PixelData::DmaBuf {
558                fd,
559                fourcc,
560                modifier,
561                stride,
562                offset,
563            } => {
564                // DmaBuf path always produces output on first call (no pipeline priming).
565                self.encode_dmabuf(fd, *fourcc, *modifier, *stride, *offset)
566            }
567            #[cfg(not(unix))]
568            PixelData::DmaBuf { .. } => None,
569        }
570    }
571
572    /// Encode from a DMA-BUF fd — tries zero-copy GPU import first,
573    /// falls back to CPU mmap readback if no GPU path is available.
574    #[cfg(unix)]
575    fn encode_dmabuf(
576        &mut self,
577        fd: &std::os::fd::OwnedFd,
578        fourcc: u32,
579        _modifier: u64,
580        stride: u32,
581        offset: u32,
582    ) -> Option<(Vec<u8>, bool)> {
583        // TODO: Try VA-API VPP zero-copy or CUDA-EGL zero-copy here.
584        // When implemented, a successful GPU encode returns early.
585
586        // --- CPU readback fallback ---
587        // The DMA-BUF fd is still valid (dup'd from the Wayland buffer).
588        // mmap it and read the pixels just like the compositor would.
589        self.encode_dmabuf_cpu_fallback(fd, fourcc, stride, offset)
590    }
591
592    /// CPU-side fallback for DMA-BUF encoding: mmap the fd, read pixels,
593    /// and encode through the normal BGRA/NV12 path.
594    #[cfg(unix)]
595    fn encode_dmabuf_cpu_fallback(
596        &mut self,
597        fd: &std::os::fd::OwnedFd,
598        fourcc: u32,
599        stride: u32,
600        _offset: u32,
601    ) -> Option<(Vec<u8>, bool)> {
602        use std::os::fd::AsRawFd;
603
604        let w = self.source_width as usize;
605        let h = self.source_height as usize;
606        let stride = stride as usize;
607        let raw_fd = fd.as_raw_fd();
608
609        // Determine total mmap size from fd (seek to end).
610        let file_size = unsafe { libc::lseek(raw_fd, 0, libc::SEEK_END) };
611        if file_size <= 0 {
612            return None;
613        }
614        let map_len = file_size as usize;
615
616        // DMA-BUF sync: start read
617        #[repr(C)]
618        struct DmaBufSync {
619            flags: u64,
620        }
621        const DMA_BUF_SYNC_READ: u64 = 1;
622        const DMA_BUF_SYNC_START: u64 = 0;
623        const DMA_BUF_SYNC_END: u64 = 4;
624        // ioctl number for DMA_BUF_IOCTL_SYNC — use c_ulong and cast at
625        // call sites so this works on both x86_64 (ioctl takes c_ulong)
626        // and aarch64 (ioctl takes c_int).
627        const DMA_BUF_IOCTL_SYNC: libc::c_ulong = 0x40086200;
628
629        let sync_start = DmaBufSync {
630            flags: DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ,
631        };
632        unsafe {
633            libc::ioctl(raw_fd, DMA_BUF_IOCTL_SYNC as _, &sync_start);
634        }
635
636        // mmap the DMA-BUF for reading.
637        let ptr = unsafe {
638            libc::mmap(
639                std::ptr::null_mut(),
640                map_len,
641                libc::PROT_READ,
642                libc::MAP_SHARED,
643                raw_fd,
644                0,
645            )
646        };
647        if ptr == libc::MAP_FAILED {
648            let sync_end = DmaBufSync {
649                flags: DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ,
650            };
651            unsafe {
652                libc::ioctl(raw_fd, DMA_BUF_IOCTL_SYNC as _, &sync_end);
653            }
654            return None;
655        }
656        let plane_data = unsafe { std::slice::from_raw_parts(ptr as *const u8, map_len) };
657
658        let result = if fourcc == blit_compositor::drm_fourcc::ARGB8888
659            || fourcc == blit_compositor::drm_fourcc::XRGB8888
660        {
661            // BGRA in memory — encode directly from the mmap'd buffer.
662            if stride == w * 4 && map_len >= w * h * 4 {
663                self.encode_bgra(&plane_data[..w * h * 4])
664            } else {
665                // Pack rows (strip stride padding).
666                let mut packed = Vec::with_capacity(w * h * 4);
667                for row in 0..h {
668                    let start = row * stride;
669                    let end = start + w * 4;
670                    if end <= plane_data.len() {
671                        packed.extend_from_slice(&plane_data[start..end]);
672                    }
673                }
674                self.encode_bgra(&packed)
675            }
676        } else if fourcc == blit_compositor::drm_fourcc::ABGR8888
677            || fourcc == blit_compositor::drm_fourcc::XBGR8888
678        {
679            // RGBA in memory.
680            if stride == w * 4 && map_len >= w * h * 4 {
681                self.encode(&plane_data[..w * h * 4])
682            } else {
683                let mut packed = Vec::with_capacity(w * h * 4);
684                for row in 0..h {
685                    let start = row * stride;
686                    let end = start + w * 4;
687                    if end <= plane_data.len() {
688                        packed.extend_from_slice(&plane_data[start..end]);
689                    }
690                }
691                self.encode(&packed)
692            }
693        } else {
694            None
695        };
696
697        // Unmap and end sync.
698        unsafe {
699            libc::munmap(ptr, map_len);
700        }
701        let sync_end = DmaBufSync {
702            flags: DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ,
703        };
704        unsafe {
705            libc::ioctl(raw_fd, DMA_BUF_IOCTL_SYNC as _, &sync_end);
706        }
707
708        result
709    }
710
711    /// Encode from BGRA pixels — converts directly to YUV, skipping RGBA.
712    fn encode_bgra(&mut self, bgra: &[u8]) -> Option<(Vec<u8>, bool)> {
713        let enc_w = self.width as usize;
714        let enc_h = self.height as usize;
715        let src_w = self.source_width as usize;
716        let src_h = self.source_height as usize;
717
718        let mut result = match &mut self.kind {
719            SurfaceEncoderKind::H264Software(encoder) => {
720                let yuv = bgra_to_yuv420_padded(bgra, src_w, src_h, enc_w, enc_h);
721                let yuv_buf = YUVBuffer::from_vec(yuv, enc_w, enc_h);
722                encoder.encode_yuv(&yuv_buf, self.width, self.height)
723            }
724            SurfaceEncoderKind::NvencH264(enc)
725            | SurfaceEncoderKind::NvencH265(enc)
726            | SurfaceEncoderKind::NvencAV1(enc) => enc.encode_bgra_padded(bgra, src_w, src_h),
727            #[cfg(unix)]
728            SurfaceEncoderKind::H264Vaapi(enc) => enc.encode_bgra_padded(bgra, src_w, src_h),
729            #[cfg(unix)]
730            SurfaceEncoderKind::H265Vaapi(enc) => enc.encode_bgra_padded(bgra, src_w, src_h),
731            SurfaceEncoderKind::AV1Software(encoder) => {
732                let yuv = bgra_to_yuv420_padded(bgra, src_w, src_h, enc_w, enc_h);
733                encoder.encode_yuv_planes(&yuv)
734            }
735        };
736        // Hardware encoders (NVENC, VA-API) may report the wrong picture
737        // type due to struct layout mismatches.  Re-detect from the
738        // bitstream to be safe — this is a cheap scan.
739        if let Some((ref data, ref mut is_key)) = result
740            && !*is_key
741        {
742            *is_key = match &self.kind {
743                SurfaceEncoderKind::NvencH264(_) => h264_stream_contains_idr(data),
744                SurfaceEncoderKind::NvencH265(_) => h265_stream_contains_idr(data),
745                #[cfg(unix)]
746                SurfaceEncoderKind::H264Vaapi(_) => h264_stream_contains_idr(data),
747                #[cfg(unix)]
748                SurfaceEncoderKind::H265Vaapi(_) => h265_stream_contains_idr(data),
749                _ => false,
750            };
751        }
752        result
753    }
754
755    /// Encode from NV12 data — zero colorspace conversion for VA-API/NVENC,
756    /// and only a deinterleave for software encoders.
757    fn encode_nv12(
758        &mut self,
759        data: &[u8],
760        y_stride: usize,
761        uv_stride: usize,
762    ) -> Option<(Vec<u8>, bool)> {
763        // NV12 data was captured at source dimensions.
764        let src_w = self.source_width as usize;
765        let src_h = self.source_height as usize;
766
767        match &mut self.kind {
768            SurfaceEncoderKind::H264Software(encoder) => {
769                let enc_w = self.width as usize;
770                let enc_h = self.height as usize;
771                if enc_w == src_w && enc_h == src_h {
772                    let yuv = nv12_to_yuv420(data, y_stride, uv_stride, src_w, src_h);
773                    let yuv_buf = YUVBuffer::from_vec(yuv, enc_w, enc_h);
774                    encoder.encode_yuv(&yuv_buf, self.width, self.height)
775                } else {
776                    let pd = PixelData::Nv12 {
777                        data: std::sync::Arc::new(data.to_vec()),
778                        y_stride,
779                        uv_stride,
780                    };
781                    let rgba = pd.to_rgba(self.source_width, self.source_height);
782                    self.encode(&rgba)
783                }
784            }
785            SurfaceEncoderKind::NvencH264(_)
786            | SurfaceEncoderKind::NvencH265(_)
787            | SurfaceEncoderKind::NvencAV1(_) => {
788                // NVENC accepts BGRA; convert NV12→RGBA→BGRA (uncommon path).
789                let pd = PixelData::Nv12 {
790                    data: std::sync::Arc::new(data.to_vec()),
791                    y_stride,
792                    uv_stride,
793                };
794                let rgba = pd.to_rgba(self.source_width, self.source_height);
795                self.encode(&rgba)
796            }
797            #[cfg(unix)]
798            SurfaceEncoderKind::H264Vaapi(enc) => {
799                let uv_offset = y_stride * src_h;
800                let y_data = &data[..uv_offset];
801                let uv_data = &data[uv_offset..];
802                let mut r = enc.encode_nv12(y_data, uv_data, y_stride, uv_stride);
803                if let Some((ref d, ref mut k)) = r
804                    && !*k
805                {
806                    *k = h264_stream_contains_idr(d);
807                }
808                r
809            }
810            #[cfg(unix)]
811            SurfaceEncoderKind::H265Vaapi(enc) => {
812                let uv_offset = y_stride * src_h;
813                let y_data = &data[..uv_offset];
814                let uv_data = &data[uv_offset..];
815                let mut r = enc.encode_nv12(y_data, uv_data, y_stride, uv_stride);
816                if let Some((ref d, ref mut k)) = r
817                    && !*k
818                {
819                    *k = h265_stream_contains_idr(d);
820                }
821                r
822            }
823            SurfaceEncoderKind::AV1Software(encoder) => {
824                encoder.encode_nv12(data, y_stride, uv_stride, src_w, src_h)
825            }
826        }
827    }
828
829    fn flush_keyframe_bgra(&mut self, bgra: &[u8]) -> Option<(Vec<u8>, bool)> {
830        match &mut self.kind {
831            SurfaceEncoderKind::AV1Software(enc) => enc.flush_encode_bgra(bgra),
832            _ => self.encode_bgra(bgra),
833        }
834    }
835
836    fn flush_keyframe_nv12(
837        &mut self,
838        data: &[u8],
839        y_stride: usize,
840        uv_stride: usize,
841    ) -> Option<(Vec<u8>, bool)> {
842        match &mut self.kind {
843            SurfaceEncoderKind::AV1Software(enc) => {
844                let w = self.width as usize;
845                let h = self.height as usize;
846                enc.flush_encode_nv12(data, y_stride, uv_stride, w, h)
847            }
848            _ => self.encode_nv12(data, y_stride, uv_stride),
849        }
850    }
851}
852
853fn validate_surface_dimensions(
854    width: u32,
855    height: u32,
856    _preference: SurfaceEncoderPreference,
857) -> Result<(), String> {
858    if width == 0 || height == 0 {
859        return Err("surface encoder requires non-zero dimensions".into());
860    }
861    // Odd dimensions are fine — H.264 constructors pad to even internally,
862    // and AV1/rav1e handles odd dimensions natively.
863    let _ = expected_rgba_len(width, height)
864        .ok_or_else(|| format!("surface encoder dimensions overflow for {width}x{height}"))?;
865    Ok(())
866}
867
868fn expected_rgba_len(width: u32, height: u32) -> Option<usize> {
869    (width as usize)
870        .checked_mul(height as usize)?
871        .checked_mul(4)
872}
873
874// ---------------------------------------------------------------------------
875// Per-pixel math — #[inline(always)] so LLVM sees through the call in the
876// hot loop and auto-vectorises the surrounding code.
877// ---------------------------------------------------------------------------
878
879#[inline(always)]
880fn rgb_to_y(r: i32, g: i32, b: i32) -> u8 {
881    ((66 * r + 129 * g + 25 * b + 128) >> 8)
882        .wrapping_add(16)
883        .clamp(0, 255) as u8
884}
885
886#[inline(always)]
887fn rgb_to_u(r: i32, g: i32, b: i32) -> u8 {
888    ((-38 * r - 74 * g + 112 * b + 128) >> 8)
889        .wrapping_add(128)
890        .clamp(0, 255) as u8
891}
892
893#[inline(always)]
894fn rgb_to_v(r: i32, g: i32, b: i32) -> u8 {
895    ((112 * r - 94 * g - 18 * b + 128) >> 8)
896        .wrapping_add(128)
897        .clamp(0, 255) as u8
898}
899
900// ---------------------------------------------------------------------------
901// Bulk colorspace helpers — written for auto-vectorisation: flat pre-allocated
902// output, direct indexing, no branches, no extend_from_slice.
903// ---------------------------------------------------------------------------
904
905/// Flat Y-plane pass over packed 4-byte pixels.  `pixel_r/g/b` closures
906/// extract R, G, B from the pixel at byte offset `i` (always a multiple of 4).
907/// This is shared between RGBA, BGRA, and any other 4-byte packed format.
908#[inline(always)]
909fn compute_y_plane(
910    src: &[u8],
911    width: usize,
912    height: usize,
913    y_plane: &mut [u8],
914    r_off: usize,
915    g_off: usize,
916    b_off: usize,
917) {
918    let total = width * height;
919    for (px, y_out) in y_plane[..total].iter_mut().enumerate() {
920        let i = px * 4;
921        let r = src[i + r_off] as i32;
922        let g = src[i + g_off] as i32;
923        let b = src[i + b_off] as i32;
924        *y_out = rgb_to_y(r, g, b);
925    }
926}
927
928/// Flat chroma pass (2x2 subsampling) over packed 4-byte pixels.
929#[inline(always)]
930fn compute_uv_planes(
931    src: &[u8],
932    width: usize,
933    height: usize,
934    u_plane: &mut [u8],
935    v_plane: &mut [u8],
936    r_off: usize,
937    g_off: usize,
938    b_off: usize,
939) {
940    let chroma_w = width / 2;
941    let chroma_h = height / 2;
942    for cy in 0..chroma_h {
943        for cx in 0..chroma_w {
944            let row = cy * 2;
945            let col = cx * 2;
946            // Average 2x2 block
947            let mut u_sum = 0i32;
948            let mut v_sum = 0i32;
949            for dy in 0..2u32 {
950                for dx in 0..2u32 {
951                    let i = ((row + dy as usize) * width + col + dx as usize) * 4;
952                    let r = src[i + r_off] as i32;
953                    let g = src[i + g_off] as i32;
954                    let b = src[i + b_off] as i32;
955                    u_sum += rgb_to_u(r, g, b) as i32;
956                    v_sum += rgb_to_v(r, g, b) as i32;
957                }
958            }
959            let idx = cy * chroma_w + cx;
960            u_plane[idx] = (u_sum / 4) as u8;
961            v_plane[idx] = (v_sum / 4) as u8;
962        }
963    }
964}
965
966/// Padded Y-plane: produces `enc_w × enc_h` luma samples from a
967/// `src_w × src_h` packed-pixel source, clamping coordinates to source bounds.
968#[inline(always)]
969fn compute_y_plane_padded(
970    src: &[u8],
971    src_w: usize,
972    src_h: usize,
973    enc_w: usize,
974    enc_h: usize,
975    y_plane: &mut [u8],
976    r_off: usize,
977    g_off: usize,
978    b_off: usize,
979) {
980    for row in 0..enc_h {
981        let sr = row.min(src_h - 1);
982        for col in 0..enc_w {
983            let sc = col.min(src_w - 1);
984            let i = (sr * src_w + sc) * 4;
985            let r = src[i + r_off] as i32;
986            let g = src[i + g_off] as i32;
987            let b = src[i + b_off] as i32;
988            y_plane[row * enc_w + col] = rgb_to_y(r, g, b);
989        }
990    }
991}
992
993/// Padded chroma planes: produces `enc_w/2 × enc_h/2` chroma samples with
994/// edge-pixel duplication for pixels beyond `src_w × src_h`.
995#[inline(always)]
996fn compute_uv_planes_padded(
997    src: &[u8],
998    src_w: usize,
999    src_h: usize,
1000    enc_w: usize,
1001    enc_h: usize,
1002    u_plane: &mut [u8],
1003    v_plane: &mut [u8],
1004    r_off: usize,
1005    g_off: usize,
1006    b_off: usize,
1007) {
1008    let chroma_w = enc_w / 2;
1009    let chroma_h = enc_h / 2;
1010    for cy in 0..chroma_h {
1011        for cx in 0..chroma_w {
1012            let row = cy * 2;
1013            let col = cx * 2;
1014            let mut u_sum = 0i32;
1015            let mut v_sum = 0i32;
1016            for dy in 0..2u32 {
1017                for dx in 0..2u32 {
1018                    let sr = (row + dy as usize).min(src_h - 1);
1019                    let sc = (col + dx as usize).min(src_w - 1);
1020                    let i = (sr * src_w + sc) * 4;
1021                    let r = src[i + r_off] as i32;
1022                    let g = src[i + g_off] as i32;
1023                    let b = src[i + b_off] as i32;
1024                    u_sum += rgb_to_u(r, g, b) as i32;
1025                    v_sum += rgb_to_v(r, g, b) as i32;
1026                }
1027            }
1028            let idx = cy * chroma_w + cx;
1029            u_plane[idx] = (u_sum / 4) as u8;
1030            v_plane[idx] = (v_sum / 4) as u8;
1031        }
1032    }
1033}
1034
1035/// BGRA -> I420 (Y + U + V planar).  No intermediate RGBA step.
1036fn bgra_to_yuv420(bgra: &[u8], width: usize, height: usize) -> Vec<u8> {
1037    bgra_to_yuv420_padded(bgra, width, height, width, height)
1038}
1039
1040/// BGRA -> I420 with edge-pixel padding to encoder dimensions.
1041/// `src_w × src_h` is the actual pixel count in `bgra`.
1042/// `enc_w × enc_h` is the encoder output dimensions (>= src).
1043fn bgra_to_yuv420_padded(
1044    bgra: &[u8],
1045    src_w: usize,
1046    src_h: usize,
1047    enc_w: usize,
1048    enc_h: usize,
1049) -> Vec<u8> {
1050    let y_size = enc_w * enc_h;
1051    let uv_w = enc_w / 2;
1052    let uv_size = uv_w * (enc_h / 2);
1053    let mut yuv = vec![0u8; y_size + uv_size * 2];
1054    let (y_plane, uv) = yuv.split_at_mut(y_size);
1055    let (u_plane, v_plane) = uv.split_at_mut(uv_size);
1056    // BGRA offsets: B=0, G=1, R=2, A=3
1057    compute_y_plane_padded(bgra, src_w, src_h, enc_w, enc_h, y_plane, 2, 1, 0);
1058    compute_uv_planes_padded(bgra, src_w, src_h, enc_w, enc_h, u_plane, v_plane, 2, 1, 0);
1059    yuv
1060}
1061
1062/// RGBA -> I420 (Y + U + V planar).
1063fn rgba_to_yuv420(rgba: &[u8], width: usize, height: usize) -> Vec<u8> {
1064    let y_size = width * height;
1065    let uv_w = width / 2;
1066    let uv_size = uv_w * (height / 2);
1067    let mut yuv = vec![0u8; y_size + uv_size * 2];
1068    let (y_plane, uv) = yuv.split_at_mut(y_size);
1069    let (u_plane, v_plane) = uv.split_at_mut(uv_size);
1070    // RGBA offsets: R=0, G=1, B=2, A=3
1071    compute_y_plane(rgba, width, height, y_plane, 0, 1, 2);
1072    compute_uv_planes(rgba, width, height, u_plane, v_plane, 0, 1, 2);
1073    yuv
1074}
1075
1076/// Simple BGRA -> RGBA bulk swizzle for fallback paths.
1077#[cfg(test)]
1078fn bgra_to_rgba(bgra: &[u8]) -> Vec<u8> {
1079    let mut rgba = vec![0u8; bgra.len()];
1080    for (src, dst) in bgra.chunks_exact(4).zip(rgba.chunks_exact_mut(4)) {
1081        dst[0] = src[2];
1082        dst[1] = src[1];
1083        dst[2] = src[0];
1084        dst[3] = src[3];
1085    }
1086    rgba
1087}
1088
1089/// Write Y plane from packed 4-byte pixels into an NV12 Y plane buffer.
1090#[cfg(test)]
1091#[inline(always)]
1092fn write_nv12_y_plane_packed(
1093    src: &[u8],
1094    width: usize,
1095    height: usize,
1096    y_plane: &mut [u8],
1097    y_stride: usize,
1098    r_off: usize,
1099    g_off: usize,
1100    b_off: usize,
1101) -> Option<()> {
1102    if y_plane.len() < y_stride.checked_mul(height)? {
1103        return None;
1104    }
1105    for row in 0..height {
1106        let dst_start = row * y_stride;
1107        let src_row = row * width * 4;
1108        for col in 0..width {
1109            let i = src_row + col * 4;
1110            let r = src[i + r_off] as i32;
1111            let g = src[i + g_off] as i32;
1112            let b = src[i + b_off] as i32;
1113            y_plane[dst_start + col] = rgb_to_y(r, g, b);
1114        }
1115    }
1116    Some(())
1117}
1118
1119/// Write UV plane from packed 4-byte pixels into an NV12 UV plane buffer.
1120#[cfg(test)]
1121#[inline(always)]
1122fn write_nv12_uv_plane_packed(
1123    src: &[u8],
1124    width: usize,
1125    height: usize,
1126    uv_plane: &mut [u8],
1127    uv_stride: usize,
1128    r_off: usize,
1129    g_off: usize,
1130    b_off: usize,
1131) -> Option<()> {
1132    if uv_plane.len() < uv_stride.checked_mul(height / 2)? {
1133        return None;
1134    }
1135    let chroma_w = (width / 2) * 2;
1136    let chroma_h = (height / 2) * 2;
1137    for row in (0..chroma_h).step_by(2) {
1138        let dst_start = (row / 2) * uv_stride;
1139        for col in (0..chroma_w).step_by(2) {
1140            let mut u_sum = 0i32;
1141            let mut v_sum = 0i32;
1142            for dy in 0..2usize {
1143                for dx in 0..2usize {
1144                    let i = ((row + dy) * width + col + dx) * 4;
1145                    let r = src[i + r_off] as i32;
1146                    let g = src[i + g_off] as i32;
1147                    let b = src[i + b_off] as i32;
1148                    u_sum += rgb_to_u(r, g, b) as i32;
1149                    v_sum += rgb_to_v(r, g, b) as i32;
1150                }
1151            }
1152            uv_plane[dst_start + col] = (u_sum / 4) as u8;
1153            uv_plane[dst_start + col + 1] = (v_sum / 4) as u8;
1154        }
1155    }
1156    Some(())
1157}
1158
1159/// NV12 -> I420: Y plane memcpy + UV deinterleave.
1160/// Input: contiguous buffer with Y at data[..y_stride*height],
1161///        UV at data[y_stride*height..].
1162fn nv12_to_yuv420(
1163    data: &[u8],
1164    y_stride: usize,
1165    uv_stride: usize,
1166    width: usize,
1167    height: usize,
1168) -> Vec<u8> {
1169    let y_size = width * height;
1170    let uv_w = width / 2;
1171    let uv_h = height / 2;
1172    let uv_size = uv_w * uv_h;
1173    let mut yuv = vec![0u8; y_size + uv_size * 2];
1174    let (y_out, uv_out) = yuv.split_at_mut(y_size);
1175    let (u_out, v_out) = uv_out.split_at_mut(uv_size);
1176
1177    let uv_offset = y_stride * height;
1178
1179    // Copy Y plane (strip stride padding)
1180    for row in 0..height {
1181        let src = row * y_stride;
1182        let dst = row * width;
1183        y_out[dst..dst + width].copy_from_slice(&data[src..src + width]);
1184    }
1185
1186    // Deinterleave UV -> separate U, V
1187    for row in 0..uv_h {
1188        let src_start = uv_offset + row * uv_stride;
1189        let dst_start = row * uv_w;
1190        for col in 0..uv_w {
1191            u_out[dst_start + col] = data[src_start + col * 2];
1192            v_out[dst_start + col] = data[src_start + col * 2 + 1];
1193        }
1194    }
1195
1196    yuv
1197}
1198
1199#[cfg(test)]
1200fn write_nv12_y_plane(
1201    rgba: &[u8],
1202    width: usize,
1203    height: usize,
1204    y_plane: &mut [u8],
1205    y_stride: usize,
1206) -> Option<()> {
1207    write_nv12_y_plane_packed(rgba, width, height, y_plane, y_stride, 0, 1, 2)
1208}
1209
1210#[cfg(test)]
1211fn write_nv12_uv_plane(
1212    rgba: &[u8],
1213    width: usize,
1214    height: usize,
1215    uv_plane: &mut [u8],
1216    uv_stride: usize,
1217) -> Option<()> {
1218    write_nv12_uv_plane_packed(rgba, width, height, uv_plane, uv_stride, 0, 1, 2)
1219}
1220
1221#[cfg(test)]
1222fn rgba_to_nv12(
1223    rgba: &[u8],
1224    width: usize,
1225    height: usize,
1226    y_plane: &mut [u8],
1227    y_stride: usize,
1228    uv_plane: &mut [u8],
1229    uv_stride: usize,
1230) -> Option<()> {
1231    write_nv12_y_plane(rgba, width, height, y_plane, y_stride)?;
1232    write_nv12_uv_plane(rgba, width, height, uv_plane, uv_stride)?;
1233    Some(())
1234}
1235
1236/// Scan an Annex B H.264 bitstream for an IDR NAL unit (type 5).
1237fn h264_stream_contains_idr(data: &[u8]) -> bool {
1238    annex_b_contains_nal(data, |byte| (byte & 0x1f) == 5)
1239}
1240
1241/// Scan an Annex B H.265 bitstream for an IDR NAL unit (types 19–20).
1242fn h265_stream_contains_idr(data: &[u8]) -> bool {
1243    annex_b_contains_nal(data, |byte| {
1244        let nal_type = (byte >> 1) & 0x3f;
1245        nal_type == 19 || nal_type == 20 // IDR_W_RADL, IDR_N_LP
1246    })
1247}
1248
1249/// Walk Annex B start codes and return true if any NAL's first byte satisfies `pred`.
1250fn annex_b_contains_nal(data: &[u8], pred: impl Fn(u8) -> bool) -> bool {
1251    let mut i = 0usize;
1252    while i < data.len() {
1253        let start_code_len = if data[i..].starts_with(&[0, 0, 0, 1]) {
1254            4
1255        } else if data[i..].starts_with(&[0, 0, 1]) {
1256            3
1257        } else {
1258            i += 1;
1259            continue;
1260        };
1261
1262        let nal_header = i + start_code_len;
1263        if let Some(&byte) = data.get(nal_header)
1264            && pred(byte)
1265        {
1266            return true;
1267        }
1268
1269        i = nal_header.saturating_add(1);
1270    }
1271
1272    false
1273}
1274
1275struct SoftwareH264Encoder {
1276    encoder: OpenH264Encoder,
1277}
1278
1279impl SoftwareH264Encoder {
1280    fn new() -> Result<Self, String> {
1281        let encoder = OpenH264Encoder::new()
1282            .map_err(|err| format!("failed to create OpenH264 encoder: {err:?}"))?;
1283        Ok(Self { encoder })
1284    }
1285
1286    fn request_keyframe(&mut self) {
1287        self.encoder.force_intra_frame();
1288    }
1289
1290    fn encode(&mut self, rgba: &[u8], width: u32, height: u32) -> Option<(Vec<u8>, bool)> {
1291        let yuv = rgba_to_yuv420(rgba, width as usize, height as usize);
1292        let yuv_buf = YUVBuffer::from_vec(yuv, width as usize, height as usize);
1293        self.encode_yuv(&yuv_buf, width, height)
1294    }
1295
1296    /// Encode from a pre-built YUV buffer (avoids redundant conversion).
1297    fn encode_yuv(
1298        &mut self,
1299        yuv_buf: &YUVBuffer,
1300        width: u32,
1301        height: u32,
1302    ) -> Option<(Vec<u8>, bool)> {
1303        let bitstream = match self.encoder.encode(yuv_buf) {
1304            Ok(bs) => bs,
1305            Err(e) => {
1306                eprintln!("[surface-encoder] openh264 encode failed {width}x{height}: {e:?}");
1307                return None;
1308            }
1309        };
1310        let nal_data = bitstream.to_vec();
1311        if nal_data.is_empty() {
1312            eprintln!("[surface-encoder] openh264 produced empty NAL {width}x{height}");
1313            return None;
1314        }
1315        let is_keyframe = h264_stream_contains_idr(&nal_data);
1316        Some((nal_data, is_keyframe))
1317    }
1318}
1319
1320// ---------------------------------------------------------------------------
1321// AV1 (rav1e)
1322// ---------------------------------------------------------------------------
1323
1324struct SoftwareAV1Encoder {
1325    ctx: rav1e::Context<u8>,
1326    width: usize,
1327    height: usize,
1328    force_keyframe: bool,
1329}
1330
1331impl SoftwareAV1Encoder {
1332    fn new(width: u32, height: u32, quality: SurfaceQuality) -> Result<Self, String> {
1333        use rav1e::prelude::*;
1334
1335        let mut speed = SpeedSettings::from_preset(quality.av1_speed());
1336        speed.rdo_lookahead_frames = 1;
1337        let enc = EncoderConfig {
1338            width: width as usize,
1339            height: height as usize,
1340            chroma_sampling: ChromaSampling::Cs420,
1341            chroma_sample_position: ChromaSamplePosition::Unknown,
1342            speed_settings: speed,
1343            low_latency: true,
1344            min_key_frame_interval: 0,
1345            max_key_frame_interval: 60,
1346            quantizer: quality.av1_quantizer(),
1347            min_quantizer: quality.av1_min_quantizer(),
1348            bitrate: 0,
1349            ..Default::default()
1350        };
1351        let cfg = Config::new().with_encoder_config(enc);
1352        let ctx = cfg
1353            .new_context()
1354            .map_err(|e| format!("rav1e context creation failed: {e}"))?;
1355        Ok(Self {
1356            ctx,
1357            width: width as usize,
1358            height: height as usize,
1359            force_keyframe: false,
1360        })
1361    }
1362
1363    fn request_keyframe(&mut self) {
1364        self.force_keyframe = true;
1365    }
1366
1367    fn encode(&mut self, rgba: &[u8]) -> Option<(Vec<u8>, bool)> {
1368        let yuv = rgba_to_yuv420(rgba, self.width, self.height);
1369        self.encode_yuv_planes(&yuv)
1370    }
1371
1372    #[allow(dead_code)]
1373    fn encode_bgra(&mut self, bgra: &[u8]) -> Option<(Vec<u8>, bool)> {
1374        let yuv = bgra_to_yuv420(bgra, self.width, self.height);
1375        self.encode_yuv_planes(&yuv)
1376    }
1377
1378    fn encode_nv12(
1379        &mut self,
1380        data: &[u8],
1381        y_stride: usize,
1382        uv_stride: usize,
1383        width: usize,
1384        height: usize,
1385    ) -> Option<(Vec<u8>, bool)> {
1386        let yuv = nv12_to_yuv420(data, y_stride, uv_stride, width, height);
1387        self.encode_yuv_planes(&yuv)
1388    }
1389
1390    /// Encode from pre-converted I420 planar YUV data (Y + U + V contiguous).
1391    fn encode_yuv_planes(&mut self, yuv: &[u8]) -> Option<(Vec<u8>, bool)> {
1392        let width = self.width;
1393        let height = self.height;
1394        let y_size = width * height;
1395        let uv_w = width.div_ceil(2);
1396        let uv_h = height.div_ceil(2);
1397        let uv_size = uv_w * uv_h;
1398
1399        let y_plane = &yuv[..y_size];
1400        let u_plane = &yuv[y_size..y_size + uv_size];
1401        let v_plane = &yuv[y_size + uv_size..];
1402
1403        let mut frame = self.ctx.new_frame();
1404        frame.planes[0].copy_from_raw_u8(y_plane, width, 1);
1405        frame.planes[1].copy_from_raw_u8(u_plane, uv_w, 1);
1406        frame.planes[2].copy_from_raw_u8(v_plane, uv_w, 1);
1407
1408        self.send_and_receive(frame)
1409    }
1410
1411    fn send_and_receive(&mut self, frame: rav1e::Frame<u8>) -> Option<(Vec<u8>, bool)> {
1412        use rav1e::prelude::*;
1413
1414        if self.force_keyframe {
1415            let params = FrameParameters {
1416                frame_type_override: FrameTypeOverride::Key,
1417                ..Default::default()
1418            };
1419            if self.ctx.send_frame((frame, params)).is_ok() {
1420                self.force_keyframe = false;
1421            }
1422        } else {
1423            let _ = self.ctx.send_frame(frame);
1424        }
1425
1426        match self.ctx.receive_packet() {
1427            Ok(packet) => {
1428                let is_key = packet.frame_type == rav1e::prelude::FrameType::KEY;
1429                Some((packet.data, is_key))
1430            }
1431            Err(rav1e::EncoderStatus::Encoded) | Err(rav1e::EncoderStatus::NeedMoreData) => None,
1432            Err(_) => None,
1433        }
1434    }
1435
1436    /// Feed a frame, flush, and drain until a packet is produced.
1437    fn flush_encode(&mut self, rgba: &[u8]) -> Option<(Vec<u8>, bool)> {
1438        let yuv = rgba_to_yuv420(rgba, self.width, self.height);
1439        self.flush_yuv_planes(&yuv)
1440    }
1441
1442    fn flush_encode_bgra(&mut self, bgra: &[u8]) -> Option<(Vec<u8>, bool)> {
1443        let yuv = bgra_to_yuv420(bgra, self.width, self.height);
1444        self.flush_yuv_planes(&yuv)
1445    }
1446
1447    fn flush_encode_nv12(
1448        &mut self,
1449        data: &[u8],
1450        y_stride: usize,
1451        uv_stride: usize,
1452        width: usize,
1453        height: usize,
1454    ) -> Option<(Vec<u8>, bool)> {
1455        let yuv = nv12_to_yuv420(data, y_stride, uv_stride, width, height);
1456        self.flush_yuv_planes(&yuv)
1457    }
1458
1459    fn flush_yuv_planes(&mut self, yuv: &[u8]) -> Option<(Vec<u8>, bool)> {
1460        use rav1e::prelude::*;
1461
1462        let width = self.width;
1463        let height = self.height;
1464        let y_size = width * height;
1465        let uv_w = width.div_ceil(2);
1466        let uv_h = height.div_ceil(2);
1467        let uv_size = uv_w * uv_h;
1468
1469        let y_plane = &yuv[..y_size];
1470        let u_plane = &yuv[y_size..y_size + uv_size];
1471        let v_plane = &yuv[y_size + uv_size..];
1472
1473        let mut frame = self.ctx.new_frame();
1474        frame.planes[0].copy_from_raw_u8(y_plane, width, 1);
1475        frame.planes[1].copy_from_raw_u8(u_plane, uv_w, 1);
1476        frame.planes[2].copy_from_raw_u8(v_plane, uv_w, 1);
1477
1478        let params = FrameParameters {
1479            frame_type_override: FrameTypeOverride::Key,
1480            ..Default::default()
1481        };
1482        if self.ctx.send_frame((frame, params)).is_err() {
1483            return None;
1484        }
1485        self.ctx.flush();
1486
1487        loop {
1488            match self.ctx.receive_packet() {
1489                Ok(packet) => {
1490                    let is_key = packet.frame_type == FrameType::KEY;
1491                    return Some((packet.data, is_key));
1492                }
1493                Err(rav1e::EncoderStatus::NeedMoreData) | Err(rav1e::EncoderStatus::Encoded) => {
1494                    continue;
1495                }
1496                Err(rav1e::EncoderStatus::LimitReached) => return None,
1497                Err(_) => return None,
1498            }
1499        }
1500    }
1501}