Skip to main content

xgpui_gamut/
lib.rs

1//! Opt-in wide SDR content producers. Window presentation remains a host concern.
2//! Colors are explicitly linear Display P3/D65; native GPUI colors remain sRGB.
3/// Shared CPU color math, also available independently as `xgpui-color`.
4pub use xgpui_color as color;
5
6#[cfg(feature = "instanced")]
7pub mod instanced;
8
9use anyhow::{Result, ensure};
10use gpui::{CustomContent, DevicePixels, Size};
11use std::any::Any;
12
13// Counted by the Metal and DirectX 11 producers only.
14#[cfg(test)]
15#[cfg_attr(not(any(target_os = "macos", target_os = "windows")), allow(dead_code))]
16static PRODUCER_ALLOCATIONS: std::sync::atomic::AtomicUsize =
17    std::sync::atomic::AtomicUsize::new(0);
18
19#[cfg(target_os = "windows")]
20mod dx11;
21#[cfg(target_os = "macos")]
22mod metal;
23
24/// Immutable bounded-SDR color in linear Display P3/D65, with straight input alpha.
25/// Prepared textures store associated RGB/alpha and require a wide-capable host.
26///
27/// Native Metal and D3D11 window presentation accept this output when the host
28/// has enabled `WideGamutSdr`. Windows native execution/display validation remains
29/// outstanding. Other backends return an explicit preparation error.
30/// Preparation allocates a texture; output caching/pooling is not provided yet.
31#[derive(Debug)]
32pub struct WideColor {
33    // Read by the Metal, DirectX 11 and instanced producers.
34    #[cfg_attr(
35        not(any(target_os = "macos", target_os = "windows", feature = "instanced")),
36        allow(dead_code)
37    )]
38    rgba: [f32; 4],
39}
40impl WideColor {
41    /// Construct from finite straight linear P3 RGB and alpha in 0..=1.
42    /// This does not decode sRGB, load an image profile or enable a window mode.
43    pub fn linear_p3(rgba: [f32; 4]) -> Result<Self> {
44        ensure!(
45            rgba.iter().all(|c| c.is_finite() && (0. ..=1.).contains(c)),
46            "linear P3 SDR channels must be finite and in 0..=1"
47        );
48        Ok(Self { rgba })
49    }
50}
51impl CustomContent for WideColor {
52    fn prepare(&self, backend: &dyn Any, extent: Size<DevicePixels>) -> Result<Box<dyn Any>> {
53        validate_extent(extent)?;
54        #[cfg(target_os = "macos")]
55        {
56            self.prepare_metal(backend, extent)
57        }
58        #[cfg(target_os = "windows")]
59        {
60            self.prepare_dx11(backend, extent)
61        }
62        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
63        {
64            let _ = backend;
65            anyhow::bail!("WideColor requires native Metal or D3D11 preparation")
66        }
67    }
68}
69fn validate_extent(extent: Size<DevicePixels>) -> Result<()> {
70    ensure!(
71        (1..=16384).contains(&extent.width.0) && (1..=16384).contains(&extent.height.0),
72        "invalid wide color extent"
73    );
74    ensure!(
75        extent.width.0 as u64 * extent.height.0 as u64 * 8 <= 64 * 1024 * 1024,
76        "wide color exceeds 64 MiB"
77    );
78    Ok(())
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use gpui::size;
85    #[test]
86    fn rejects_invalid_color_channels() {
87        for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.01, 1.01] {
88            for i in 0..4 {
89                let mut rgba = [0.5; 4];
90                rgba[i] = bad;
91                assert!(WideColor::linear_p3(rgba).is_err());
92            }
93        }
94        assert!(WideColor::linear_p3([0.; 4]).is_ok());
95        assert!(WideColor::linear_p3([1.; 4]).is_ok());
96    }
97    #[test]
98    fn checks_float16_budget_and_extent_before_backend_work() {
99        for (w, h) in [(0, 1), (-1, 1), (1, 0), (16385, 1), (4096, 2049)] {
100            assert!(validate_extent(size(DevicePixels(w), DevicePixels(h))).is_err());
101        }
102        assert!(validate_extent(size(DevicePixels(4096), DevicePixels(2048))).is_ok());
103        let color = WideColor::linear_p3([1., 0., 0., 0.5]).unwrap();
104        assert!(
105            color
106                .prepare(&(), size(DevicePixels(0), DevicePixels(1)))
107                .unwrap_err()
108                .to_string()
109                .contains("extent")
110        );
111    }
112    #[test]
113    fn rejects_unknown_backend_without_reinterpreting_color() {
114        let color = WideColor::linear_p3([1., 0., 0., 0.5]).unwrap();
115        assert!(
116            color
117                .prepare(&(), size(DevicePixels(8), DevicePixels(8)))
118                .is_err()
119        );
120    }
121}