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