Skip to content

Commit f174cc4

Browse files
committed
feat(encoder): RGB-direct encode via VK_VALVE_video_encode_rgb_conversion
1 parent bd51fbd commit f174cc4

4 files changed

Lines changed: 348 additions & 71 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ network-interface = "2.0.5"
4040
notify-rust = "4.12.0"
4141
open = "5.3.3"
4242
opus = "0.3.1"
43-
pixelforge = { git = "https://github.com/hgaiser/pixelforge", rev = "fc60c32", features = ["dmabuf"] }
43+
pixelforge = { git = "https://github.com/porkloin/pixelforge", branch = "vk-valve-video", features = ["dmabuf"] }
4444
pkcs8 = "0.10.2"
4545
pulseaudio = "0.3.1"
4646
rand = "0.8.5"
@@ -76,3 +76,12 @@ zeroconf = "0.17.0"
7676

7777
[dev-dependencies]
7878
tempfile = "3.26.0"
79+
80+
# Local-development convenience: when this repo and a sibling pixelforge
81+
# checkout exist on the same machine, build against whatever branch is
82+
# checked out in `../pixelforge` instead of the pinned rev. Safe to leave
83+
# even when the sibling doesn't exist (cargo just uses the rev).
84+
[patch."https://github.com/porkloin/pixelforge"]
85+
pixelforge = { path = "../pixelforge" }
86+
[patch."https://github.com/hgaiser/pixelforge"]
87+
pixelforge = { path = "../pixelforge" }

src/session/stream/video/pipeline/mod.rs

Lines changed: 133 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
mod dmabuf;
77
mod hdr_sei;
8+
mod rgb_blitter;
89

910
use std::sync::atomic::{AtomicBool, Ordering};
1011
use 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

Comments
 (0)