diff --git a/Cargo.lock b/Cargo.lock index 95b68a1..178c669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.49" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -87,9 +87,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cmake" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b042e5d8a74ae91bb0961acd039822472ec99f8ab0948cbf6d1369588f8be586" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" dependencies = [ "cc", ] @@ -102,9 +102,9 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" dependencies = [ "log", "regex", @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" dependencies = [ "anstream", "anstyle", @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "is_terminal_polyfill" @@ -137,9 +137,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "jiff" -version = "0.2.16" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "log", @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.16" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -167,9 +167,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "nu-ansi-term" @@ -225,9 +225,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pixelforge" @@ -243,42 +243,42 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -288,9 +288,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -299,9 +299,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "roxmltree" @@ -376,9 +376,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -416,9 +416,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -438,9 +438,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -477,9 +477,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" diff --git a/src/converter/mod.rs b/src/converter/mod.rs index b0d6b8f..53f4bd4 100644 --- a/src/converter/mod.rs +++ b/src/converter/mod.rs @@ -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. @@ -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, } @@ -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. @@ -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. // ======================== diff --git a/src/converter/shader.rs b/src/converter/shader.rs index 0b9f3a4..2a7df19 100644 --- a/src/converter/shader.rs +++ b/src/converter/shader.rs @@ -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; @@ -89,19 +89,50 @@ 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; } @@ -109,8 +140,8 @@ vec3 read_rgb(ivec2 coord) { // 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;