Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 98 additions & 4 deletions src/converter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ use crate::vulkan::VideoContext;
use ash::vk;
use tracing::debug;

/// Color space for RGB→YUV conversion matrix selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorSpace {
/// BT.709 (standard SDR). Used for all HD/UHD SDR content.
#[default]
Bt709,
/// BT.2020 (HDR / wide color gamut).
Bt2020,
}

/// Supported input pixel formats for color conversion.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
Expand All @@ -26,12 +36,38 @@ pub enum InputFormat {
BGRA,
/// RGBA (32-bit, red first, alpha last).
RGBA,
/// ABGR2101010 (packed 10-bit per channel, 2-bit alpha).
/// Maps to DRM_FORMAT_ABGR2101010 / VK_FORMAT_A2B10G10R10_UNORM_PACK32.
ABGR2101010,
/// RGBA16F (64-bit, 16-bit float per channel).
/// Maps to DRM_FORMAT_ABGR16161616F / VK_FORMAT_R16G16B16A16_SFLOAT.
///
/// Expected input is linear-light scRGB where 1.0 = 80 nits.
/// The converter applies the PQ (ST 2084) transfer function internally.
RGBA16F,
}

impl InputFormat {
/// Bytes per pixel for this format.
pub fn bytes_per_pixel(&self) -> usize {
4 // All current formats are 32-bit
match self {
InputFormat::BGRx
| InputFormat::RGBx
| InputFormat::BGRA
| InputFormat::RGBA
| InputFormat::ABGR2101010 => 4,
InputFormat::RGBA16F => 8,
}
}

/// Vulkan format for creating image views of this input format.
pub fn vk_format(&self) -> vk::Format {
match self {
InputFormat::BGRx | InputFormat::BGRA => vk::Format::B8G8R8A8_UNORM,
InputFormat::RGBx | InputFormat::RGBA => vk::Format::R8G8B8A8_UNORM,
InputFormat::ABGR2101010 => vk::Format::A2B10G10R10_UNORM_PACK32,
InputFormat::RGBA16F => vk::Format::R16G16B16A16_SFLOAT,
}
}
}

Expand Down Expand Up @@ -91,6 +127,7 @@ impl OutputFormat {

/// Configuration for the color converter.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ColorConverterConfig {
/// Input frame width.
pub width: u32,
Expand All @@ -100,6 +137,31 @@ pub struct ColorConverterConfig {
pub input_format: InputFormat,
/// Output YUV format.
pub output_format: OutputFormat,
/// Color space for the RGB→YUV matrix.
/// Use Bt709 for SDR, Bt2020 for HDR.
pub color_space: ColorSpace,
/// Full range (0-255 luma) or limited/studio range (16-235 luma).
/// Must match the `full_range` flag in `ColorDescription` for correct playback.
pub full_range: bool,
}

impl ColorConverterConfig {
/// Create a new configuration with BT.709 color space and full range.
pub fn new(
width: u32,
height: u32,
input_format: InputFormat,
output_format: OutputFormat,
) -> Self {
Self {
width,
height,
input_format,
output_format,
color_space: ColorSpace::Bt709,
full_range: true,
}
}
}

/// GPU-based color format converter.
Expand Down Expand Up @@ -551,12 +613,14 @@ impl ColorConverter {
&[],
);

// Push constants: width, height, input_format, output_format.
let push_constants: [u32; 4] = [
// Push constants: width, height, input_format, output_format, color_space, full_range.
let push_constants: [u32; 6] = [
self.config.width,
self.config.height,
self.config.input_format as u32,
self.config.output_format as u32,
self.config.color_space as u32,
self.config.full_range as u32,
];
let push_constants_bytes: &[u8] = std::slice::from_raw_parts(
push_constants.as_ptr() as *const u8,
Expand Down Expand Up @@ -718,7 +782,7 @@ impl ColorConverter {
let view_info = vk::ImageViewCreateInfo::default()
.image(src_image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::B8G8R8A8_UNORM)
.format(self.config.input_format.vk_format())
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
Expand Down Expand Up @@ -779,6 +843,8 @@ mod tests {
assert_eq!(InputFormat::RGBx.bytes_per_pixel(), 4);
assert_eq!(InputFormat::BGRA.bytes_per_pixel(), 4);
assert_eq!(InputFormat::RGBA.bytes_per_pixel(), 4);
assert_eq!(InputFormat::ABGR2101010.bytes_per_pixel(), 4);
assert_eq!(InputFormat::RGBA16F.bytes_per_pixel(), 8);
}

#[test]
Expand All @@ -788,6 +854,24 @@ mod tests {
assert_eq!(InputFormat::RGBx as u32, 1);
assert_eq!(InputFormat::BGRA as u32, 2);
assert_eq!(InputFormat::RGBA as u32, 3);
assert_eq!(InputFormat::ABGR2101010 as u32, 4);
assert_eq!(InputFormat::RGBA16F as u32, 5);
}

#[test]
fn test_input_format_vk_format() {
assert_eq!(InputFormat::BGRx.vk_format(), vk::Format::B8G8R8A8_UNORM);
assert_eq!(InputFormat::BGRA.vk_format(), vk::Format::B8G8R8A8_UNORM);
assert_eq!(InputFormat::RGBx.vk_format(), vk::Format::R8G8B8A8_UNORM);
assert_eq!(InputFormat::RGBA.vk_format(), vk::Format::R8G8B8A8_UNORM);
assert_eq!(
InputFormat::ABGR2101010.vk_format(),
vk::Format::A2B10G10R10_UNORM_PACK32
);
assert_eq!(
InputFormat::RGBA16F.vk_format(),
vk::Format::R16G16B16A16_SFLOAT
);
}

// ========================
Expand Down Expand Up @@ -875,13 +959,17 @@ mod tests {
height: 1080,
input_format: InputFormat::BGRx,
output_format: OutputFormat::NV12,
color_space: ColorSpace::Bt709,
full_range: true,
};

let cloned = config.clone();
assert_eq!(cloned.width, 1920);
assert_eq!(cloned.height, 1080);
assert_eq!(cloned.input_format, InputFormat::BGRx);
assert_eq!(cloned.output_format, OutputFormat::NV12);
assert_eq!(cloned.color_space, ColorSpace::Bt709);
assert!(cloned.full_range);
}

#[test]
Expand All @@ -891,6 +979,8 @@ mod tests {
height: 480,
input_format: InputFormat::RGBA,
output_format: OutputFormat::I420,
color_space: ColorSpace::Bt709,
full_range: true,
};

let debug_str = format!("{:?}", config);
Expand Down Expand Up @@ -930,6 +1020,8 @@ mod tests {
height: 64,
input_format: InputFormat::BGRx,
output_format: OutputFormat::NV12,
color_space: ColorSpace::Bt709,
full_range: true,
};

let result = ColorConverter::new(context, config);
Expand Down Expand Up @@ -962,6 +1054,8 @@ mod tests {
height: 32,
input_format: *input_format,
output_format: *output_format,
color_space: ColorSpace::Bt709,
full_range: true,
};

let result = ColorConverter::new(context.clone(), config);
Expand Down
4 changes: 2 additions & 2 deletions src/converter/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub fn create_converter(
let push_constant_range = vk::PushConstantRange::default()
.stage_flags(vk::ShaderStageFlags::COMPUTE)
.offset(0)
.size(16); // 4 x u32: width, height, input_format, output_format
.size(24); // 6 x u32: width, height, input_format, output_format, color_space, full_range

let pipeline_layout_info = vk::PipelineLayoutCreateInfo::default()
.set_layouts(std::slice::from_ref(&descriptor_set_layout))
Expand All @@ -48,7 +48,7 @@ pub fn create_converter(
.map_err(|e| PixelForgeError::ResourceCreation(e.to_string()))?;

// Create compute shader module.
let shader_code = super::shader::get_spirv_code();
let shader_code = super::shader::get_spirv_code()?;
let shader_info = vk::ShaderModuleCreateInfo::default().code(&shader_code);

let shader_module = unsafe { device.create_shader_module(&shader_info, None) }
Expand Down
Loading