Skip to main content

scene_management_dso/
display_metrics.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::lib::Size;
6use fuchsia_scenic::DisplayRotation;
7use num_traits::float::FloatConst;
8
9/// Predefined viewing distances with values in millimeters.
10#[derive(Copy, Clone, PartialEq, Debug)]
11pub enum ViewingDistance {
12    Handheld = 360,
13    Close = 500,
14    Near = 720,
15    Midrange = 1200,
16    Far = 3000,
17    Unknown = 600, // Should not be used, but offers a reasonable, non-zero (and unique) default
18}
19
20/// [`DisplayMetrics`] encapsulate data associated with a display device.
21///
22/// [`DisplayMetrics`] are created from a display's width and height in pixels.
23/// Pixel density and expected viewing distance can be supplied for more accurate
24/// metrics (e.g., [`width_in_mm`] uses the display's pixel density to give the correct width).
25///
26/// If density or viewing distance is not supplied, default values are calculated based on the
27/// display dimensions.
28#[derive(Clone, Copy, Debug)]
29pub struct DisplayMetrics {
30    /// The size of the display in pixels.
31    size_in_pixels: Size,
32
33    /// The pixel density of the display. This is either supplied by the client constructing
34    /// the display metrics, or a hard-coded default is used based on the display dimensions.
35    // TODO(https://fxbug.dev/42165549)
36    density_in_pixels_per_mm: f32,
37
38    /// The expected viewing distance for the display, in millimeters. For example, a desktop
39    /// monitor may have an expected viewing distance of around 500 mm.
40    viewing_distance: ViewingDistance,
41
42    /// The screen rotation: 0 (none), 90, 180, or 270.
43    display_rotation: DisplayRotation,
44
45    /// The pip scale factor in pixels per pip in either X or Y dimension.
46    /// (Assumes square pixels.)
47    scale_in_pixels_per_pip: f32,
48
49    /// The pip density in pips per millimeter.
50    density_in_pips_per_mm: f32,
51}
52
53/// Quantizes the specified floating point number to 8 significant bits of
54/// precision in its mantissa (including the implicit leading 1 bit).
55///
56/// We quantize scale factors to reduce the likelihood of round-off errors in
57/// subsequent calculations due to excess precision.  Since IEEE 754 float
58/// has 24 significant bits, by using only 8 significant bits for the scaling
59/// factor we're guaranteed that we can multiply the factor by any integer
60/// between -65793 and 65793 without any loss of precision.  The scaled integers
61/// can likewise be added or subtracted without any loss of precision.
62fn quantize(f: f32) -> f32 {
63    let (frac, exp) = libm::frexpf(f);
64    libm::ldexpf((frac as f64 * 256.0).round() as f32, exp - 8)
65}
66
67impl DisplayMetrics {
68    /// The ideal visual angle of a pip unit in degrees, assuming default settings.
69    /// The value has been empirically determined.
70    const IDEAL_PIP_VISUAL_ANGLE_DEGREES: f32 = 0.0255;
71
72    /// Slope factor used in computing adaptation factor for perceptual differences at varying distances.
73    const ADAPTATION_SLOPE: f32 = 0.5;
74
75    /// Offset factor (in mm) used in computing adaptation factor for perceptual differences at varying distances.
76    const ADAPTATION_OFFSET: f32 = 180.0;
77
78    /// Creates a new [`DisplayMetrics`] struct.
79    ///
80    /// The width and height of the display in pixels are required to construct sensible display
81    /// metrics. Defaults can be computed for the other metrics, but they may not match expectations.
82    ///
83    /// For example, a default display pixel density can be determined based on width and height in
84    /// pixels, but it's unlikely to match the actual density of the display.
85    ///
86    /// # Parameters
87    /// - `size_in_pixels`: The size of the display, in pixels.
88    /// - `density_in_pixels_per_mm`: The density of the display, in pixels per mm. If no density is
89    /// provided, a best guess is made based on the width and height of the display.
90    /// - `viewing_distance`: The expected viewing distance for the display (i.e., how far away the
91    /// user is expected to be from the display) in mm. Defaults to [`DisplayMetrics::DEFAULT_VIEWING_DISTANCE`].
92    /// This is used to compute the ratio of pixels per pip.
93    /// - `display_rotation`: The rotation of the display, counter-clockwise, in 90-degree increments.
94    pub fn new(
95        size_in_pixels: Size,
96        density_in_pixels_per_mm: Option<f32>,
97        viewing_distance: Option<ViewingDistance>,
98        display_rotation: Option<DisplayRotation>,
99    ) -> DisplayMetrics {
100        let mut density_in_pixels_per_mm = density_in_pixels_per_mm
101            .unwrap_or_else(|| Self::default_density_in_pixels_per_mm(size_in_pixels));
102
103        if density_in_pixels_per_mm == 0.0 {
104            density_in_pixels_per_mm = Self::default_density_in_pixels_per_mm(size_in_pixels);
105        }
106
107        let mut viewing_distance =
108            viewing_distance.unwrap_or_else(|| Self::default_viewing_distance(size_in_pixels));
109        if viewing_distance == ViewingDistance::Unknown {
110            viewing_distance = Self::default_viewing_distance(size_in_pixels);
111        }
112        let viewing_distance_in_mm = viewing_distance as u32 as f32;
113
114        let display_rotation = match display_rotation {
115            Some(rotation) => rotation,
116            None => DisplayRotation::None,
117        };
118
119        assert!(density_in_pixels_per_mm != 0.0);
120        assert!(viewing_distance_in_mm != 0.0);
121
122        let scale_in_pixels_per_pip =
123            Self::compute_scale(density_in_pixels_per_mm, viewing_distance_in_mm);
124        let density_in_pips_per_mm = density_in_pixels_per_mm / scale_in_pixels_per_pip;
125        DisplayMetrics {
126            size_in_pixels,
127            density_in_pixels_per_mm,
128            viewing_distance,
129            display_rotation,
130            scale_in_pixels_per_pip,
131            density_in_pips_per_mm,
132        }
133    }
134
135    /// Computes and returns `scale_in_pixels_per_pip`.
136    ///
137    /// # Parameters
138    /// - `density_in_pixels_per_mm`: The density of the display as given, or the default (see
139    /// `new()`).
140    /// - `viewing_distance_in_mm`: The expected viewing distance for the display (i.e., how far
141    /// away the user is expected to be from the display) as given, or the default (see `new()`).
142    ///
143    /// Returns the computed scale ratio in pixels per pip.
144    fn compute_scale(density_in_pixels_per_mm: f32, viewing_distance_in_mm: f32) -> f32 {
145        // Compute the pixel visual size as a function of viewing distance in
146        // millimeters per millimeter.
147        let pvsize_in_mm_per_mm = 1.0 / (density_in_pixels_per_mm * viewing_distance_in_mm);
148
149        // The adaptation factor is an empirically determined fudge factor to take into account
150        // human perceptual differences for objects at varying distances, even if those objects
151        // are adjusted to be the same size to the eye.
152        let adaptation_factor = (viewing_distance_in_mm * Self::ADAPTATION_SLOPE
153            + Self::ADAPTATION_OFFSET)
154            / viewing_distance_in_mm;
155
156        // Compute the pip visual size as a function of viewing distance in
157        // millimeters per millimeter.
158        let pip_visual_size_in_mm_per_mm =
159            (Self::IDEAL_PIP_VISUAL_ANGLE_DEGREES * f32::PI() / 180.0).tan() * adaptation_factor;
160
161        quantize(pip_visual_size_in_mm_per_mm / pvsize_in_mm_per_mm)
162    }
163
164    /// Returns the number of pixels per pip.
165    #[inline]
166    pub fn pixels_per_pip(&self) -> f32 {
167        self.scale_in_pixels_per_pip
168    }
169
170    /// Returns the number of pips per millimeter.
171    #[inline]
172    pub fn pips_per_mm(&self) -> f32 {
173        self.density_in_pips_per_mm
174    }
175
176    /// Returns the number of millimeters per pip.
177    #[inline]
178    pub fn mm_per_pip(&self) -> f32 {
179        1.0 / self.pips_per_mm()
180    }
181
182    /// Returns the width of the display in pixels.
183    #[inline]
184    pub fn width_in_pixels(&self) -> u32 {
185        self.size_in_pixels.width as u32
186    }
187
188    /// Returns the height of the display in pixels.
189    #[inline]
190    pub fn height_in_pixels(&self) -> u32 {
191        self.size_in_pixels.height as u32
192    }
193
194    /// Returns the size of the display in pixels.
195    #[inline]
196    pub fn size_in_pixels(&self) -> Size {
197        self.size_in_pixels
198    }
199
200    /// Returns the width of the display in pips.
201    #[inline]
202    pub fn width_in_pips(&self) -> f32 {
203        self.size_in_pixels.width / self.pixels_per_pip()
204    }
205
206    /// Returns the height of the display in pips.
207    #[inline]
208    pub fn height_in_pips(&self) -> f32 {
209        self.size_in_pixels.height / self.pixels_per_pip()
210    }
211
212    /// Returns the size of the display in pips.
213    #[inline]
214    pub fn size_in_pips(&self) -> Size {
215        self.size_in_pixels / self.pixels_per_pip()
216    }
217
218    /// Returns the width of the display in millimeters.
219    #[inline]
220    pub fn width_in_mm(&self) -> f32 {
221        self.width_in_pips() * self.mm_per_pip()
222    }
223
224    /// Returns the height of the display in millimeters.
225    #[inline]
226    pub fn height_in_mm(&self) -> f32 {
227        self.height_in_pips() * self.mm_per_pip()
228    }
229
230    /// Returns the size of the display in millimeters.
231    #[inline]
232    pub fn size_in_mm(&self) -> Size {
233        self.size_in_pips() * self.mm_per_pip()
234    }
235
236    #[inline]
237    pub fn rotation(&self) -> DisplayRotation {
238        self.display_rotation
239    }
240
241    #[inline]
242    pub fn rotation_in_degrees(&self) -> u32 {
243        self.display_rotation as u32
244    }
245
246    #[inline]
247    pub fn viewing_distance(&self) -> ViewingDistance {
248        self.viewing_distance
249    }
250
251    #[inline]
252    pub fn viewing_distance_in_mm(&self) -> f32 {
253        self.viewing_distance as u32 as f32
254    }
255
256    #[inline]
257    pub fn physical_pixel_ratio(&self) -> f32 {
258        self.density_in_pixels_per_mm / Self::DEFAULT_DENSITY
259    }
260
261    /// The dimensions used to determine whether or not the device dimensions correspond to
262    /// an Acer Switch 12 Alpha. Used to set a default display pixel density.
263    const ACER_SWITCH_12_ALPHA_DIMENSIONS: (u32, u32) = (2160, 1440);
264
265    /// The dimensions used to determine whether or not the device dimensions correspond to
266    /// a Google Pixelbook. Used to set a default display pixel density.
267    const GOOGLE_PIXELBOOK_DIMENSIONS: (u32, u32) = (2400, 1600);
268
269    /// The dimensions used to determine whether or not the device dimensions correspond to
270    /// a Google Pixelbook Go with a 2K display. Used to set a default display pixel density.
271    const GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS: (u32, u32) = (1920, 1080);
272
273    /// The dimensions used to determine whether or not the device dimensions correspond to
274    /// a Google Pixelbook Go with a 4K display. Used to set a default display pixel density.
275    const GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS: (u32, u32) = (3840, 2160);
276
277    /// The dimensions used to determine whether or not the device dimensions correspond to
278    /// a 24 inch monitor. Used to set a default display pixel density.
279    const MONITOR_24_IN_DIMENSIONS: (u32, u32) = (1920, 1200);
280
281    /// The dimensions used to determine whether or not the device dimensions correspond to
282    /// a 27 inch, 2K monitor. Used to set a default display pixel density.
283    const MONITOR_27_IN_2K_DIMENSIONS: (u32, u32) = (2560, 1440);
284
285    /// Display densities are calculated by taking the pixels per inch and dividing that by 25.4
286    /// in order to convert that to pixels per millimeter. For example the Google Pixelbook Go is
287    /// 166 ppi. The result of converting that to millimeters is 6.53543307087. Rounding that to 4
288    /// decimal places is how the value of 6.5354 is calculated.
289
290    /// The display pixel density used for an Acer Switch 12 Alpha.
291    const ACER_SWITCH_12_ALPHA_DENSITY: f32 = 8.5;
292
293    /// The display pixel density used for a Google Pixelbook.
294    const GOOGLE_PIXELBOOK_DENSITY: f32 = 9.252;
295
296    /// The display pixel density used for a Google Pixelbook Go with a 2K display.
297    const GOOGLE_PIXELBOOK_GO_2K_DENSITY: f32 = 4.1725;
298
299    /// The display pixel density used for a Google Pixelbook Go with a 4K display.
300    const GOOGLE_PIXELBOOK_GO_4K_DENSITY: f32 = 8.345;
301
302    /// The display pixel density used for a 24 inch monitor.
303    const MONITOR_24_IN_DENSITY: f32 = 4.16;
304
305    // TODO(https://fxbug.dev/42119026): Allow Root Presenter clients to specify exact pixel ratio
306    /// The display pixel density used for a 27 inch monitor.
307    const MONITOR_27_IN_2K_DENSITY: f32 = 5.22;
308
309    // TODO(https://fxbug.dev/42097727): Don't lie.
310    /// The display pixel density used as default when no other default device matches.
311    /// This results in a logical to physical pixel ratio of 1.0.
312    const DEFAULT_DENSITY: f32 = 5.24;
313
314    /// Returns a default display pixel density based on the provided display dimensions.
315    ///
316    /// The pixel density is defined as pixels per millimeters.
317    ///
318    /// Clients using a `SceneManager` are expected to provide the pixel density for the display,
319    /// but this provides reasonable defaults for a few commonly used devices.
320    ///
321    /// # Parameters
322    /// - `size_in_pixels`: The size of the display in pixels.
323    fn default_density_in_pixels_per_mm(size_in_pixels: Size) -> f32 {
324        match (size_in_pixels.width as u32, size_in_pixels.height as u32) {
325            DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS => {
326                DisplayMetrics::ACER_SWITCH_12_ALPHA_DENSITY
327            }
328            DisplayMetrics::GOOGLE_PIXELBOOK_DIMENSIONS => DisplayMetrics::GOOGLE_PIXELBOOK_DENSITY,
329            DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS => {
330                DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DENSITY
331            }
332            DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS => {
333                DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DENSITY
334            }
335            DisplayMetrics::MONITOR_24_IN_DIMENSIONS => DisplayMetrics::MONITOR_24_IN_DENSITY,
336            DisplayMetrics::MONITOR_27_IN_2K_DIMENSIONS => DisplayMetrics::MONITOR_27_IN_2K_DENSITY,
337            _ => DisplayMetrics::DEFAULT_DENSITY,
338        }
339    }
340
341    fn default_viewing_distance(size_in_pixels: Size) -> ViewingDistance {
342        match (size_in_pixels.width as u32, size_in_pixels.height as u32) {
343            DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS => ViewingDistance::Close,
344            DisplayMetrics::GOOGLE_PIXELBOOK_DIMENSIONS => ViewingDistance::Close,
345            DisplayMetrics::GOOGLE_PIXELBOOK_GO_2K_DIMENSIONS => ViewingDistance::Near,
346            DisplayMetrics::GOOGLE_PIXELBOOK_GO_4K_DIMENSIONS => ViewingDistance::Near,
347            DisplayMetrics::MONITOR_24_IN_DIMENSIONS => ViewingDistance::Near,
348            DisplayMetrics::MONITOR_27_IN_2K_DIMENSIONS => ViewingDistance::Near,
349            _ => ViewingDistance::Close,
350        }
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    // Density is used as the denominator in pip calculation, so must be handled explicitly.
359    #[fuchsia::test]
360    fn test_zero_density() {
361        let metrics =
362            DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, Some(0.0), None, None);
363        let second_metrics =
364            DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, None, None, None);
365        assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
366        assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
367    }
368
369    // Viewing distance is used as the denominator in pip calculation, so must be handled explicitly.
370    #[fuchsia::test]
371    fn test_zero_distance() {
372        let metrics = DisplayMetrics::new(
373            Size { width: 100.0, height: 100.0 },
374            None,
375            Some(ViewingDistance::Unknown),
376            None,
377        );
378        let second_metrics =
379            DisplayMetrics::new(Size { width: 100.0, height: 100.0 }, None, None, None);
380        assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
381        assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
382    }
383
384    // Tests that a known default density produces the same metrics as explicitly specified.
385    #[fuchsia::test]
386    fn test_pixels_per_pip_default() {
387        let dimensions = DisplayMetrics::ACER_SWITCH_12_ALPHA_DIMENSIONS;
388        let metrics = DisplayMetrics::new(
389            Size { width: dimensions.0 as f32, height: dimensions.1 as f32 },
390            None,
391            None,
392            None,
393        );
394        let second_metrics = DisplayMetrics::new(
395            Size { width: dimensions.0 as f32, height: dimensions.1 as f32 },
396            Some(DisplayMetrics::ACER_SWITCH_12_ALPHA_DENSITY),
397            Some(ViewingDistance::Close),
398            None,
399        );
400        assert_eq!(metrics.width_in_pips(), second_metrics.width_in_pips());
401        assert_eq!(metrics.height_in_pips(), second_metrics.height_in_pips());
402
403        // The expected values here were generated and tested manually to be the expected
404        // values for the Acer Switch 12 Alpha.
405        assert_eq!(metrics.width_in_pips(), 1329.2307);
406        assert_eq!(metrics.height_in_pips(), 886.1539);
407    }
408}