Skip to main content

blit_server/
surface_encoder.rs

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