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