55
66mod dmabuf;
77mod hdr_sei;
8+ mod rgb_blitter;
89
910use std:: sync:: atomic:: { AtomicBool , Ordering } ;
1011use std:: sync:: Arc ;
@@ -210,7 +211,7 @@ impl VideoPipelineInner {
210211 let _delay_stop = stop_session_manager. delay_shutdown_token ( ) ;
211212
212213 // Create the encoder.
213- let ( context, encoder) = match self . create_encoder ( ) {
214+ let ( context, encoder, use_rgb_input ) = match self . create_encoder ( ) {
214215 Ok ( result) => result,
215216 Err ( e) => {
216217 tracing:: error!( "Failed to create video encoder: {e}" ) ;
@@ -223,6 +224,7 @@ impl VideoPipelineInner {
223224 frame_rx,
224225 context,
225226 encoder,
227+ use_rgb_input,
226228 packet_tx,
227229 idr_frame_request_rx,
228230 stop_session_manager,
@@ -234,7 +236,7 @@ impl VideoPipelineInner {
234236 tracing:: debug!( "Video pipeline stopped." ) ;
235237 }
236238
237- fn create_encoder ( & self ) -> Result < ( VideoContext , Encoder ) , String > {
239+ fn create_encoder ( & self ) -> Result < ( VideoContext , Encoder , bool ) , String > {
238240 // Create Vulkan video context.
239241 let context = VideoContextBuilder :: new ( )
240242 . build ( )
@@ -247,6 +249,14 @@ impl VideoPipelineInner {
247249 VideoFormat :: Av1 => Codec :: AV1 ,
248250 } ;
249251
252+ // RGB-direct encode skips the compute-shader RGB→YUV pass and lets
253+ // the video encoder hardware do the conversion inline. Enabled
254+ // whenever the device advertises VK_VALVE_video_encode_rgb_conversion.
255+ let use_rgb_input = context. supports_rgb_direct_encode ( ) ;
256+ if use_rgb_input {
257+ tracing:: info!( "RGB-direct encode path active (VK_VALVE_video_encode_rgb_conversion)" ) ;
258+ }
259+
250260 // Convert pixel format.
251261 let pixel_format = match self . chroma_sampling {
252262 VideoChromaSampling :: Yuv420 => PixelFormat :: Yuv420 ,
@@ -281,11 +291,12 @@ impl VideoPipelineInner {
281291 . with_b_frames ( 0 ) // No B-frames for low latency
282292 . with_max_reference_frames ( self . max_reference_frames )
283293 . with_virtual_buffer_size_ms ( 1000 / self . framerate )
284- . with_initial_virtual_buffer_size_ms ( 0 ) ;
294+ . with_initial_virtual_buffer_size_ms ( 0 )
295+ . with_rgb_input ( use_rgb_input) ;
285296
286297 let encoder = Encoder :: new ( context. clone ( ) , config) . map_err ( |e| format ! ( "Failed to create encoder: {e}" ) ) ?;
287298
288- Ok ( ( context, encoder) )
299+ Ok ( ( context, encoder, use_rgb_input ) )
289300 }
290301
291302 #[ allow( clippy:: too_many_arguments) ]
@@ -294,6 +305,7 @@ impl VideoPipelineInner {
294305 frame_rx : std:: sync:: mpsc:: Receiver < ExportedFrame > ,
295306 context : VideoContext ,
296307 mut encoder : Encoder ,
308+ use_rgb_input : bool ,
297309 packet_tx : mpsc:: Sender < ShardBatch > ,
298310 mut idr_frame_request_rx : broadcast:: Receiver < ( ) > ,
299311 stop_session_manager : ShutdownManager < SessionShutdownReason > ,
@@ -337,7 +349,9 @@ impl VideoPipelineInner {
337349 } ;
338350
339351 // Color converter will be initialized on first frame.
352+ // In RGB-direct mode this stays `None` and we use `rgb_blitter` instead.
340353 let mut color_converter: Option < ColorConverter > = None ;
354+ let mut rgb_blitter: Option < rgb_blitter:: RgbBlitter > = None ;
341355
342356 // Encoding loop - receives frames from compositor.
343357 let frame_interval = std:: time:: Duration :: from_secs_f64 ( 1.0 / self . framerate as f64 ) ;
@@ -491,83 +505,133 @@ impl VideoPipelineInner {
491505
492506 let t2_imported = std:: time:: Instant :: now ( ) ;
493507
494- // Recreate the converter if the input format changed (e.g. GBM pool
495- // ABGR2101010 → direct scanout XBGR8888). The converter's image view
496- // format must match the source image format.
497- if let Some ( ref conv) = color_converter {
498- if conv. config ( ) . input_format != frame_input_format {
499- tracing:: info!(
500- "Input format changed from {:?} to {:?}, recreating color converter" ,
501- conv. config( ) . input_format,
502- frame_input_format,
503- ) ;
504- color_converter = None ;
505- }
506- }
507-
508- // Initialize converter if needed.
509- let converter = match & mut color_converter {
510- Some ( conv) => conv,
511- None => {
512- let ( color_space, full_range) = match self . dynamic_range {
513- VideoDynamicRange :: Sdr => ( ColorSpace :: Bt709 , true ) ,
514- VideoDynamicRange :: Hdr => ( ColorSpace :: Bt2020 , false ) ,
515- } ;
516- let mut config =
517- ColorConverterConfig :: new ( self . width , self . height , frame_input_format, output_format) ;
518- config. color_space = color_space;
519- config. full_range = full_range;
520- match ColorConverter :: new ( context. clone ( ) , config) {
521- Ok ( conv) => {
522- color_converter = Some ( conv) ;
523- color_converter. as_mut ( ) . unwrap ( )
508+ if use_rgb_input {
509+ // RGB-direct path: skip the compute-shader converter
510+ // and copy the imported DMA-BUF straight into the
511+ // encoder's RGB-formatted input image. The encoder
512+ // hardware does the RGB→YUV conversion inline.
513+ let blitter = match & mut rgb_blitter {
514+ Some ( b) => b,
515+ None => match rgb_blitter:: RgbBlitter :: new ( context. clone ( ) , self . width , self . height ) {
516+ Ok ( b) => {
517+ rgb_blitter = Some ( b) ;
518+ rgb_blitter. as_mut ( ) . unwrap ( )
524519 } ,
525520 Err ( e) => {
526- tracing:: warn!( "Failed to create color converter : {e}" ) ;
521+ tracing:: warn!( "Failed to create RGB blitter : {e}" ) ;
527522 frame. consumed . store ( true , Ordering :: Release ) ;
528523 continue ;
529524 } ,
525+ } ,
526+ } ;
527+
528+ // In HDR mode, switch encoder VUI per-frame based on
529+ // the frame's actual color space. With RGB-direct there
530+ // is no software converter to keep in sync; the encoder
531+ // handles the color matrix internally.
532+ if self . dynamic_range == VideoDynamicRange :: Hdr {
533+ let frame_cs = frame. color_space ;
534+ let color_desc = match frame_cs {
535+ FrameColorSpace :: Srgb => ColorDescription :: bt709 ( ) ,
536+ FrameColorSpace :: Bt2020Pq => ColorDescription :: bt2020_pq ( ) ,
537+ } ;
538+ if encoder_color_desc != Some ( color_desc) {
539+ tracing:: info!(
540+ "Switching encoder color description to {color_desc:?} (frame_cs: {frame_cs:?})"
541+ ) ;
542+ match encoder. set_color_description ( color_desc) {
543+ Ok ( ( ) ) => encoder_color_desc = Some ( color_desc) ,
544+ Err ( e) => return Err ( format ! ( "Failed to update encoder color description: {e}" ) ) ,
545+ }
530546 }
531- } ,
532- } ;
547+ }
533548
534- // In HDR mode, select per-frame color space and encoder VUI
535- // based on the frame's actual color space. SDR frames are
536- // encoded as BT.709 and HDR frames as BT.2020+PQ, with
537- // dynamic VUI switching in the encoder.
538- if self . dynamic_range == VideoDynamicRange :: Hdr {
539- let frame_cs = frame. color_space ;
540- let ( cs, full_range, color_desc) = match frame_cs {
541- FrameColorSpace :: Srgb => ( ColorSpace :: Bt709 , true , ColorDescription :: bt709 ( ) ) ,
542- FrameColorSpace :: Bt2020Pq => ( ColorSpace :: Bt2020 , false , ColorDescription :: bt2020_pq ( ) ) ,
549+ if let Err ( e) = blitter. copy ( source_image, src_layout, encoder. input_image ( ) ) {
550+ tracing:: warn!( "RGB blit failed: {e}" ) ;
551+ frame. consumed . store ( true , Ordering :: Release ) ;
552+ continue ;
553+ }
554+ } else {
555+ // Compute-shader path: convert RGB → YUV before encoding.
556+ // Recreate the converter if the input format changed (e.g.
557+ // GBM pool ABGR2101010 → direct scanout XBGR8888). The
558+ // converter's image view format must match the source
559+ // image format.
560+ if let Some ( ref conv) = color_converter {
561+ if conv. config ( ) . input_format != frame_input_format {
562+ tracing:: info!(
563+ "Input format changed from {:?} to {:?}, recreating color converter" ,
564+ conv. config( ) . input_format,
565+ frame_input_format,
566+ ) ;
567+ color_converter = None ;
568+ }
569+ }
570+
571+ // Initialize converter if needed.
572+ let converter = match & mut color_converter {
573+ Some ( conv) => conv,
574+ None => {
575+ let ( color_space, full_range) = match self . dynamic_range {
576+ VideoDynamicRange :: Sdr => ( ColorSpace :: Bt709 , true ) ,
577+ VideoDynamicRange :: Hdr => ( ColorSpace :: Bt2020 , false ) ,
578+ } ;
579+ let mut config =
580+ ColorConverterConfig :: new ( self . width , self . height , frame_input_format, output_format) ;
581+ config. color_space = color_space;
582+ config. full_range = full_range;
583+ match ColorConverter :: new ( context. clone ( ) , config) {
584+ Ok ( conv) => {
585+ color_converter = Some ( conv) ;
586+ color_converter. as_mut ( ) . unwrap ( )
587+ } ,
588+ Err ( e) => {
589+ tracing:: warn!( "Failed to create color converter: {e}" ) ;
590+ frame. consumed . store ( true , Ordering :: Release ) ;
591+ continue ;
592+ } ,
593+ }
594+ } ,
543595 } ;
544596
545- // Switch encoder VUI first. Only update the converter if
546- // the encoder switch succeeds, so that the converter's
547- // color space stays in sync with the encoder's VUI.
548- if encoder_color_desc != Some ( color_desc) {
549- tracing:: info!(
550- "Switching encoder color description to {color_desc:?} (frame_cs: {frame_cs:?})"
551- ) ;
552- match encoder. set_color_description ( color_desc) {
553- Ok ( ( ) ) => {
554- encoder_color_desc = Some ( color_desc) ;
555- converter. set_color_space ( cs) ;
556- converter. set_full_range ( full_range) ;
557- } ,
558- Err ( e) => return Err ( format ! ( "Failed to update encoder color description: {e}" ) ) ,
597+ // In HDR mode, select per-frame color space and encoder VUI
598+ // based on the frame's actual color space. SDR frames are
599+ // encoded as BT.709 and HDR frames as BT.2020+PQ, with
600+ // dynamic VUI switching in the encoder.
601+ if self . dynamic_range == VideoDynamicRange :: Hdr {
602+ let frame_cs = frame. color_space ;
603+ let ( cs, full_range, color_desc) = match frame_cs {
604+ FrameColorSpace :: Srgb => ( ColorSpace :: Bt709 , true , ColorDescription :: bt709 ( ) ) ,
605+ FrameColorSpace :: Bt2020Pq => ( ColorSpace :: Bt2020 , false , ColorDescription :: bt2020_pq ( ) ) ,
606+ } ;
607+
608+ // Switch encoder VUI first. Only update the converter if
609+ // the encoder switch succeeds, so that the converter's
610+ // color space stays in sync with the encoder's VUI.
611+ if encoder_color_desc != Some ( color_desc) {
612+ tracing:: info!(
613+ "Switching encoder color description to {color_desc:?} (frame_cs: {frame_cs:?})"
614+ ) ;
615+ match encoder. set_color_description ( color_desc) {
616+ Ok ( ( ) ) => {
617+ encoder_color_desc = Some ( color_desc) ;
618+ converter. set_color_space ( cs) ;
619+ converter. set_full_range ( full_range) ;
620+ } ,
621+ Err ( e) => return Err ( format ! ( "Failed to update encoder color description: {e}" ) ) ,
622+ }
623+ } else {
624+ converter. set_color_space ( cs) ;
625+ converter. set_full_range ( full_range) ;
559626 }
560- } else {
561- converter. set_color_space ( cs) ;
562- converter. set_full_range ( full_range) ;
563627 }
564- }
565628
566- // Convert to YUV.
567- if let Err ( e) = converter. convert ( source_image, src_layout, encoder. input_image ( ) ) {
568- tracing:: warn!( "GPU color conversion failed: {e}" ) ;
569- frame. consumed . store ( true , Ordering :: Release ) ;
570- continue ;
629+ // Convert to YUV.
630+ if let Err ( e) = converter. convert ( source_image, src_layout, encoder. input_image ( ) ) {
631+ tracing:: warn!( "GPU color conversion failed: {e}" ) ;
632+ frame. consumed . store ( true , Ordering :: Release ) ;
633+ continue ;
634+ }
571635 }
572636
573637 // The DMA-BUF content has been read into the encoder's input
0 commit comments