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
84 changes: 42 additions & 42 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 30 additions & 2 deletions src/converter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ pub enum ColorSpace {
Bt709,
/// BT.2020 (HDR / wide color gamut).
Bt2020,
/// sRGB input converted to BT.2020 + PQ output.
/// Used for SDR content during HDR streaming sessions.
/// Applies: inverse sRGB EOTF → BT.709→BT.2020 gamut mapping → PQ OETF.
SrgbToBt2020Pq,
}

/// Supported input pixel formats for color conversion.
Expand All @@ -42,8 +46,11 @@ pub enum InputFormat {
/// 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.
/// The converter treats FP16 data the same as other formats: passthrough
/// for `ColorSpace::Bt709` / `Bt2020`, and sRGB→BT.2020+PQ conversion
/// for `ColorSpace::SrgbToBt2020Pq`. No linear→PQ transfer function is
/// applied automatically; if the source is PQ-encoded (e.g. via the
/// gamescope WSI layer), use `ColorSpace::Bt2020` for passthrough.
RGBA16F,
}

Expand Down Expand Up @@ -216,6 +223,14 @@ impl ColorConverter {
self.output_buffer
}

/// Set the color space for subsequent conversions.
///
/// This takes effect on the next `convert()` call without recreating the pipeline,
/// since the color space is passed via push constants.
pub fn set_color_space(&mut self, color_space: ColorSpace) {
self.config.color_space = color_space;
}

/// Build buffer-to-image copy regions for multi-planar YUV formats.
///
/// For multi-planar formats like NV12, I420, and YUV444, we need separate.
Expand Down Expand Up @@ -874,6 +889,19 @@ mod tests {
);
}

// ========================
// ColorSpace tests.
// ========================

#[test]
fn test_color_space_enum_values() {
// Verify enum values match shader push constant expectations:
// 0=BT.709, 1=BT.2020, 2=sRGB→BT.2020+PQ.
assert_eq!(ColorSpace::Bt709 as u32, 0);
assert_eq!(ColorSpace::Bt2020 as u32, 1);
assert_eq!(ColorSpace::SrgbToBt2020Pq as u32, 2);
}

// ========================
// OutputFormat tests.
// ========================
Expand Down
55 changes: 43 additions & 12 deletions src/converter/shader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ layout(push_constant) uniform PushConstants {
uint height;
uint input_format; // 0=BGRx, 1=RGBx, 2=BGRA, 3=RGBA, 4=ABGR2101010, 5=RGBA16F
uint output_format; // 0=NV12, 1=I420, 2=YUV444, 3=P010, 4=YUV444P10
uint color_space; // 0=BT.709, 1=BT.2020
uint color_space; // 0=BT.709, 1=BT.2020, 2=sRGB→BT.2020+PQ
uint full_range; // 0=limited/studio range, 1=full range
} params;

Expand Down Expand Up @@ -89,28 +89,59 @@ vec3 linear_to_pq(vec3 L) {
return N;
}

// Inverse sRGB EOTF: sRGB gamma-encoded [0,1] → linear light [0,1].
// Uses the standard piecewise sRGB transfer function.
vec3 srgb_to_linear(vec3 srgb) {
// Clamp to [0,1] to avoid NaN from pow() on negative/out-of-range FP16 inputs.
srgb = clamp(srgb, 0.0, 1.0);
// sRGB piecewise: linear below 0.04045, gamma above.
return mix(
pow((srgb + 0.055) / 1.055, vec3(2.4)),
srgb / 12.92,
lessThanEqual(srgb, vec3(0.04045))
);
}

// BT.709 to BT.2020 gamut mapping matrix (applied in linear light).
// Derived from the BT.709 and BT.2020 primaries via the CIE XYZ intermediate.
vec3 bt709_to_bt2020(vec3 rgb709) {
return vec3(
0.6274 * rgb709.r + 0.3293 * rgb709.g + 0.0433 * rgb709.b,
0.0691 * rgb709.r + 0.9195 * rgb709.g + 0.0114 * rgb709.b,
0.0164 * rgb709.r + 0.0880 * rgb709.g + 0.8956 * rgb709.b
);
}

// Read normalized RGB from source image via texelFetch.
// Returns values in [0, 1] range for all formats.
// For RGBA16F (HDR), applies PQ transfer function to map linear-light to [0, 1].
// Returns values in [0, 1] range representing the final signal level for YUV conversion.
//
// The compositor (Smithay) does NOT perform color-managed compositing: it blits
// client textures to the GBM output without applying any transfer function.
// Therefore the GBM buffer preserves the source's encoding:
// - PQ surfaces → PQ-encoded values (even in FP16)
// - sRGB surfaces → sRGB gamma-encoded values (even in FP16)
//
// color_space 0 (BT.709) and 1 (BT.2020): passthrough — data is already encoded.
// color_space 2 (sRGB→BT.2020+PQ): decode sRGB, convert gamut, apply PQ.
vec3 read_rgb(ivec2 coord) {
vec4 rgba = texelFetch(inputImage, coord, 0);
if (params.input_format == 5u) {
// RGBA16F: linear-light floats in scene-referred scRGB scale
// where 1.0 = 80 nits (the sRGB / scRGB reference white).
// PQ EOTF input must be absolute luminance normalized to [0, 1]
// where 1.0 = 10 000 nits, hence the factor 10000 / 80 = 125.
return linear_to_pq(rgba.rgb / 125.0);
if (params.color_space == 2u) {
// sRGB→BT.2020+PQ: decode sRGB gamma → linear BT.709 → BT.2020 gamut → PQ.
vec3 linear_709 = srgb_to_linear(rgba.rgb);
vec3 linear_2020 = bt709_to_bt2020(linear_709);
// SDR reference white at 203 nits (ITU-R BT.2408) → normalize to PQ's 10000 nit scale.
return linear_to_pq(linear_2020 * (203.0 / 10000.0));
}
// UNORM formats (8-bit and 10-bit): texelFetch returns [0.0, 1.0].
// BT.709 or BT.2020 passthrough: values are already properly encoded.
return rgba.rgb;
}

// Convert normalized RGB [0,1] to YUV.
// Returns Y in [0, 1], U and V in [0, 1] centered at 0.5.
vec3 rgb_to_yuv(vec3 rgb) {
float yr, yg, yb, ur, ug, ub, vr, vg, vb;
if (params.color_space == 1u) {
// BT.2020
if (params.color_space >= 1u) {
// BT.2020 (also used for sRGB→BT.2020+PQ, since read_rgb already converted).
yr = BT2020_Y_R; yg = BT2020_Y_G; yb = BT2020_Y_B;
ur = BT2020_U_R; ug = BT2020_U_G; ub = BT2020_U_B;
vr = BT2020_V_R; vg = BT2020_V_G; vb = BT2020_V_B;
Expand Down