wgpu_core/device/
surface_config.rs

1//! Validation of a surface configuration against its capabilities, including
2//! resolving `SurfaceColorSpace::Auto` (and present/alpha `Auto`) to concrete
3//! values. Split out of the very large `resource.rs`.
4
5use crate::{api_log, present};
6use wgt::TextureFormat;
7
8/// The concrete color space [`SurfaceColorSpace::Auto`] resolves to for `format`,
9/// given the color spaces a surface supports for it, or `None` if `Auto` cannot
10/// be satisfied.
11///
12/// Reproduces wgpu's historical behavior: extended linear scRGB for fp16 formats
13/// when supported, sRGB otherwise. `Auto` never resolves to a wide-gamut or HDR
14/// color space (DisplayP3, ExtendedSrgb, ExtendedDisplayP3, Bt2100Pq, Bt2100Hlg),
15/// because those change how the application must encode its output, so they must
16/// be requested explicitly.
17///
18/// This is the single source of truth shared by [`validate_surface_configuration`]
19/// and the `get_capabilities` `formats` filter, so a format is listed in
20/// [`SurfaceCapabilities::formats`] exactly when `Auto` resolves for it.
21///
22/// [`SurfaceColorSpace::Auto`]: wgt::SurfaceColorSpace::Auto
23/// [`SurfaceCapabilities::formats`]: wgt::SurfaceCapabilities::formats
24pub(crate) fn resolve_auto_color_space(
25    format: TextureFormat,
26    color_spaces: wgt::SurfaceColorSpaces,
27) -> Option<wgt::SurfaceColorSpace> {
28    let fallbacks: &[_] = if format == TextureFormat::Rgba16Float {
29        &[
30            wgt::SurfaceColorSpace::ExtendedSrgbLinear,
31            wgt::SurfaceColorSpace::Srgb,
32        ]
33    } else {
34        &[wgt::SurfaceColorSpace::Srgb]
35    };
36    fallbacks
37        .iter()
38        .copied()
39        .find(|fallback| color_spaces.contains(fallback.to_color_spaces().unwrap()))
40}
41
42/// Validate `config` against `caps`, resolving the `Auto` values in
43/// `config` to concrete ones.
44pub(crate) fn validate_surface_configuration(
45    config: &mut hal::SurfaceConfiguration,
46    caps: &hal::SurfaceCapabilities,
47    max_texture_dimension_2d: u32,
48) -> Result<(), present::ConfigureSurfaceError> {
49    use present::ConfigureSurfaceError as E;
50    let width = config.extent.width;
51    let height = config.extent.height;
52
53    if width > max_texture_dimension_2d || height > max_texture_dimension_2d {
54        return Err(E::TooLarge {
55            width,
56            height,
57            max_texture_dimension_2d,
58        });
59    }
60
61    if !caps.present_modes.contains(&config.present_mode) {
62        // Automatic present mode checks.
63        //
64        // The "Automatic" modes are never supported by the backends.
65        let fallbacks = match config.present_mode {
66            wgt::PresentMode::AutoVsync => {
67                &[wgt::PresentMode::FifoRelaxed, wgt::PresentMode::Fifo][..]
68            }
69            // Always end in FIFO to make sure it's always supported
70            wgt::PresentMode::AutoNoVsync => &[
71                wgt::PresentMode::Immediate,
72                wgt::PresentMode::Mailbox,
73                wgt::PresentMode::Fifo,
74            ][..],
75            _ => {
76                return Err(E::UnsupportedPresentMode {
77                    requested: config.present_mode,
78                    available: caps.present_modes.clone(),
79                });
80            }
81        };
82
83        let new_mode = fallbacks
84            .iter()
85            .copied()
86            .find(|fallback| caps.present_modes.contains(fallback))
87            .unwrap_or_else(|| {
88                unreachable!(
89                    "Fallback system failed to choose present mode. \
90                    This is a bug. Mode: {:?}, Options: {:?}",
91                    config.present_mode, &caps.present_modes
92                );
93            });
94
95        api_log!(
96            "Automatically choosing presentation mode by rule {:?}. Chose {new_mode:?}",
97            config.present_mode
98        );
99        config.present_mode = new_mode;
100    }
101    let Some(format_caps) = caps.formats.iter().find(|fc| fc.format == config.format) else {
102        return Err(E::UnsupportedFormat {
103            requested: config.format,
104            available: caps.texture_formats().collect(),
105        });
106    };
107    if config.color_space == wgt::SurfaceColorSpace::Auto {
108        let Some(new_color_space) =
109            resolve_auto_color_space(config.format, format_caps.color_spaces)
110        else {
111            // The format is only available in color spaces that must be
112            // explicitly requested (e.g. HDR10-only on some drivers when the OS
113            // is in HDR mode).
114            return Err(E::UnsupportedColorSpace {
115                requested: config.color_space,
116                format: config.format,
117                available: format_caps.color_spaces,
118            });
119        };
120
121        api_log!(
122            "Automatically choosing color space by rule {:?}. Chose {new_color_space:?}",
123            config.color_space
124        );
125        config.color_space = new_color_space;
126    }
127    if !format_caps
128        .color_spaces
129        .contains(config.color_space.to_color_spaces().unwrap())
130    {
131        return Err(E::UnsupportedColorSpace {
132            requested: config.color_space,
133            format: config.format,
134            available: format_caps.color_spaces,
135        });
136    }
137    if !caps
138        .composite_alpha_modes
139        .contains(&config.composite_alpha_mode)
140    {
141        let new_alpha_mode = 'alpha: {
142            // Automatic alpha mode checks.
143            let fallbacks = match config.composite_alpha_mode {
144                wgt::CompositeAlphaMode::Auto => &[
145                    wgt::CompositeAlphaMode::Opaque,
146                    wgt::CompositeAlphaMode::Inherit,
147                ][..],
148                _ => {
149                    return Err(E::UnsupportedAlphaMode {
150                        requested: config.composite_alpha_mode,
151                        available: caps.composite_alpha_modes.clone(),
152                    });
153                }
154            };
155
156            for &fallback in fallbacks {
157                if caps.composite_alpha_modes.contains(&fallback) {
158                    break 'alpha fallback;
159                }
160            }
161
162            unreachable!(
163                "Fallback system failed to choose alpha mode. This is a bug. \
164                          AlphaMode: {:?}, Options: {:?}",
165                config.composite_alpha_mode, &caps.composite_alpha_modes
166            );
167        };
168
169        api_log!(
170            "Automatically choosing alpha mode by rule {:?}. Chose {new_alpha_mode:?}",
171            config.composite_alpha_mode
172        );
173        config.composite_alpha_mode = new_alpha_mode;
174    }
175    if !caps.usage.contains(config.usage) {
176        return Err(E::UnsupportedUsage {
177            requested: config.usage,
178            available: caps.usage,
179        });
180    }
181    if width == 0 || height == 0 {
182        return Err(E::ZeroArea);
183    }
184    Ok(())
185}
186
187#[cfg(test)]
188mod surface_configuration_tests {
189    use alloc::{vec, vec::Vec};
190
191    use super::validate_surface_configuration;
192    use crate::present::ConfigureSurfaceError;
193
194    fn caps(formats: Vec<wgt::SurfaceFormatCapabilities>) -> hal::SurfaceCapabilities {
195        hal::SurfaceCapabilities {
196            formats,
197            maximum_frame_latency: 1..=3,
198            current_extent: None,
199            usage: wgt::TextureUses::COLOR_TARGET,
200            present_modes: vec![wgt::PresentMode::Fifo],
201            composite_alpha_modes: vec![wgt::CompositeAlphaMode::Opaque],
202        }
203    }
204
205    fn config(
206        format: wgt::TextureFormat,
207        color_space: wgt::SurfaceColorSpace,
208    ) -> hal::SurfaceConfiguration {
209        hal::SurfaceConfiguration {
210            maximum_frame_latency: 2,
211            present_mode: wgt::PresentMode::Fifo,
212            composite_alpha_mode: wgt::CompositeAlphaMode::Opaque,
213            format,
214            color_space,
215            extent: wgt::Extent3d {
216                width: 100,
217                height: 100,
218                depth_or_array_layers: 1,
219            },
220            usage: wgt::TextureUses::COLOR_TARGET,
221            view_formats: Vec::new(),
222        }
223    }
224
225    fn format_caps(
226        format: wgt::TextureFormat,
227        color_spaces: wgt::SurfaceColorSpaces,
228    ) -> wgt::SurfaceFormatCapabilities {
229        wgt::SurfaceFormatCapabilities {
230            format,
231            color_spaces,
232        }
233    }
234
235    /// `Auto` resolves to extended linear scRGB for fp16 when supported,
236    /// reproducing the historical hardcoded behavior.
237    #[test]
238    fn auto_resolves_fp16_to_extended_srgb_linear() {
239        let caps = caps(vec![format_caps(
240            wgt::TextureFormat::Rgba16Float,
241            wgt::SurfaceColorSpaces::EXTENDED_SRGB_LINEAR | wgt::SurfaceColorSpaces::BT2100_PQ,
242        )]);
243        let mut config = config(
244            wgt::TextureFormat::Rgba16Float,
245            wgt::SurfaceColorSpace::Auto,
246        );
247        validate_surface_configuration(&mut config, &caps, 4096).unwrap();
248        assert_eq!(
249            config.color_space,
250            wgt::SurfaceColorSpace::ExtendedSrgbLinear
251        );
252    }
253
254    /// `Auto` never resolves to the encoded extended-range sRGB color space,
255    /// even for fp16 formats: it prefers extended *linear* sRGB and falls back
256    /// to plain sRGB, but `ExtendedSrgb` must be requested explicitly.
257    #[test]
258    fn auto_never_resolves_to_extended_srgb() {
259        let caps = caps(vec![format_caps(
260            wgt::TextureFormat::Rgba16Float,
261            wgt::SurfaceColorSpaces::SRGB | wgt::SurfaceColorSpaces::EXTENDED_SRGB,
262        )]);
263        let mut config = config(
264            wgt::TextureFormat::Rgba16Float,
265            wgt::SurfaceColorSpace::Auto,
266        );
267        validate_surface_configuration(&mut config, &caps, 4096).unwrap();
268        assert_eq!(config.color_space, wgt::SurfaceColorSpace::Srgb);
269    }
270
271    /// `Auto` resolves fp16 to sRGB when extended linear is unavailable
272    /// (e.g. the GLES backend).
273    #[test]
274    fn auto_resolves_fp16_to_srgb_without_extended() {
275        let caps = caps(vec![format_caps(
276            wgt::TextureFormat::Rgba16Float,
277            wgt::SurfaceColorSpaces::SRGB,
278        )]);
279        let mut config = config(
280            wgt::TextureFormat::Rgba16Float,
281            wgt::SurfaceColorSpace::Auto,
282        );
283        validate_surface_configuration(&mut config, &caps, 4096).unwrap();
284        assert_eq!(config.color_space, wgt::SurfaceColorSpace::Srgb);
285    }
286
287    /// `Auto` never resolves to an HDR color space, even if it is the only
288    /// one supported for the format: HDR output changes how the application
289    /// must encode its colors, so it must be requested explicitly.
290    #[test]
291    fn auto_refuses_hdr_only_formats() {
292        let caps = caps(vec![format_caps(
293            wgt::TextureFormat::Rgb10a2Unorm,
294            wgt::SurfaceColorSpaces::BT2100_PQ,
295        )]);
296        let mut config = config(
297            wgt::TextureFormat::Rgb10a2Unorm,
298            wgt::SurfaceColorSpace::Auto,
299        );
300        let err = validate_surface_configuration(&mut config, &caps, 4096).unwrap_err();
301        assert!(matches!(
302            err,
303            ConfigureSurfaceError::UnsupportedColorSpace { .. }
304        ));
305    }
306
307    /// `Auto` prefers sRGB for non-fp16 formats even when HDR spaces are
308    /// also supported.
309    #[test]
310    fn auto_prefers_srgb_for_non_fp16() {
311        let caps = caps(vec![format_caps(
312            wgt::TextureFormat::Rgb10a2Unorm,
313            wgt::SurfaceColorSpaces::SRGB | wgt::SurfaceColorSpaces::BT2100_PQ,
314        )]);
315        let mut config = config(
316            wgt::TextureFormat::Rgb10a2Unorm,
317            wgt::SurfaceColorSpace::Auto,
318        );
319        validate_surface_configuration(&mut config, &caps, 4096).unwrap();
320        assert_eq!(config.color_space, wgt::SurfaceColorSpace::Srgb);
321    }
322
323    /// A non-fp16 format that reports *both* sRGB and extended-linear sRGB still
324    /// resolves `Auto` deterministically to `Srgb`: the extended-linear fallback
325    /// is gated on `Rgba16Float`, so there is never ambiguity about which color
326    /// space `Auto` picks even when a format advertises both.
327    #[test]
328    fn auto_non_fp16_with_srgb_and_extended_linear_resolves_to_srgb() {
329        let caps = caps(vec![format_caps(
330            wgt::TextureFormat::Rgb10a2Unorm,
331            wgt::SurfaceColorSpaces::SRGB | wgt::SurfaceColorSpaces::EXTENDED_SRGB_LINEAR,
332        )]);
333        let mut config = config(
334            wgt::TextureFormat::Rgb10a2Unorm,
335            wgt::SurfaceColorSpace::Auto,
336        );
337        validate_surface_configuration(&mut config, &caps, 4096).unwrap();
338        assert_eq!(config.color_space, wgt::SurfaceColorSpace::Srgb);
339    }
340
341    /// Explicitly requested color spaces are honored when supported.
342    #[test]
343    fn explicit_hdr10_is_honored() {
344        let caps = caps(vec![format_caps(
345            wgt::TextureFormat::Rgb10a2Unorm,
346            wgt::SurfaceColorSpaces::SRGB | wgt::SurfaceColorSpaces::BT2100_PQ,
347        )]);
348        let mut config = config(
349            wgt::TextureFormat::Rgb10a2Unorm,
350            wgt::SurfaceColorSpace::Bt2100Pq,
351        );
352        validate_surface_configuration(&mut config, &caps, 4096).unwrap();
353        assert_eq!(config.color_space, wgt::SurfaceColorSpace::Bt2100Pq);
354    }
355
356    /// Explicitly requesting an unsupported color space fails validation.
357    #[test]
358    fn explicit_unsupported_color_space_errors() {
359        let caps = caps(vec![format_caps(
360            wgt::TextureFormat::Bgra8UnormSrgb,
361            wgt::SurfaceColorSpaces::SRGB,
362        )]);
363        let mut config = config(
364            wgt::TextureFormat::Bgra8UnormSrgb,
365            wgt::SurfaceColorSpace::Bt2100Pq,
366        );
367        let err = validate_surface_configuration(&mut config, &caps, 4096).unwrap_err();
368        assert!(matches!(
369            err,
370            ConfigureSurfaceError::UnsupportedColorSpace {
371                requested: wgt::SurfaceColorSpace::Bt2100Pq,
372                ..
373            }
374        ));
375    }
376}