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}