Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / src
a37d6c4b· 固定源码快照
src/geometry.rs21.4 KB
1use std::error::Error;
2use std::fmt::{Display, Formatter};
3
4use serde::de::Error as _;
5use serde::{Deserialize, Deserializer, Serialize};
6
7use crate::limits::{
8 MAX_PHYSICAL_MILLIMETERS, MIN_PHYSICAL_MILLIMETERS, valid_physical_millimeters,
9};
10
11#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
12#[serde(deny_unknown_fields)]
13pub struct PixelSize {
14 pub width: u32,
15 pub height: u32,
16}
17
18#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
19#[serde(deny_unknown_fields)]
20pub struct AspectRatio {
21 pub width: u32,
22 pub height: u32,
23}
24
25#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum PhysicalMeasurement {
28 DimensionsMm { width: f64, height: f64 },
29 DiagonalMm(f64),
30}
31
32#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum Rotation {
35 #[default]
36 Deg0,
37 Deg90,
38 Deg180,
39 Deg270,
40}
41
42#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
43#[serde(rename_all = "snake_case")]
44pub enum SizingStrategy {
45 #[default]
46 MatchPhysicalSize,
47 RoundedScale,
48 IntegerScale,
49}
50
51impl<'de> Deserialize<'de> for SizingStrategy {
52 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53 where
54 D: Deserializer<'de>,
55 {
56 #[derive(Deserialize)]
57 #[serde(rename_all = "snake_case")]
58 enum Current {
59 MatchPhysicalSize,
60 RoundedScale,
61 IntegerScale,
62 }
63
64 #[derive(Deserialize)]
65 #[serde(rename_all = "snake_case")]
66 enum Legacy {
67 IntegerScale { max_scale: u8 },
68 }
69
70 #[derive(Deserialize)]
71 #[serde(untagged)]
72 enum Compatible {
73 Current(Current),
74 Legacy(Legacy),
75 }
76
77 match Compatible::deserialize(deserializer)? {
78 Compatible::Current(Current::MatchPhysicalSize) => Ok(Self::MatchPhysicalSize),
79 Compatible::Current(Current::RoundedScale) => Ok(Self::RoundedScale),
80 Compatible::Current(Current::IntegerScale) => Ok(Self::IntegerScale),
81 Compatible::Legacy(Legacy::IntegerScale { max_scale })
82 if (1..=8).contains(&max_scale) =>
83 {
84 Ok(Self::IntegerScale)
85 }
86 Compatible::Legacy(Legacy::IntegerScale { .. }) => {
87 Err(D::Error::custom("legacy max_scale must be between 1 and 8"))
88 }
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
94#[serde(deny_unknown_fields)]
95pub struct DisplayGeometry {
96 pub native_pixels: PixelSize,
97 pub physical: PhysicalMeasurement,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub aspect_ratio: Option<AspectRatio>,
100 #[serde(default)]
101 pub rotation: Rotation,
102}
103
104#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
105#[serde(deny_unknown_fields)]
106pub struct SizingRequest {
107 pub reference: DisplayGeometry,
108 pub target: DisplayGeometry,
109 #[serde(default)]
110 pub strategy: SizingStrategy,
111 #[serde(default = "default_alignment")]
112 pub alignment: u32,
113 #[serde(default)]
114 pub preferred_refresh_millihz: Option<u32>,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct SizingResult {
119 pub virtual_mode: PixelSize,
120 pub oriented_target: PixelSize,
121 pub scale_x: f64,
122 pub scale_y: f64,
123 pub preferred_refresh_millihz: Option<u32>,
124}
125
126#[derive(Clone, Copy, Debug, Eq, PartialEq)]
127pub struct PixelPoint {
128 pub x: i32,
129 pub y: i32,
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub struct PixelRect {
134 pub left: i32,
135 pub top: i32,
136 pub width: u32,
137 pub height: u32,
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
141pub struct CoordinateTransform {
142 pub target: PixelRect,
143 pub source: PixelRect,
144 pub rotation: Rotation,
145}
146
147#[derive(Clone, Debug, Eq, PartialEq)]
148pub struct GeometryError(String);
149
150impl Display for GeometryError {
151 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
152 formatter.write_str(&self.0)
153 }
154}
155
156impl Error for GeometryError {}
157
158impl PixelSize {
159 pub fn oriented(self, rotation: Rotation) -> Self {
160 match rotation {
161 Rotation::Deg0 | Rotation::Deg180 => self,
162 Rotation::Deg90 | Rotation::Deg270 => Self {
163 width: self.height,
164 height: self.width,
165 },
166 }
167 }
168
169 fn validate(self, name: &str) -> Result<(), GeometryError> {
170 if self.width == 0 || self.height == 0 {
171 return Err(GeometryError(format!(
172 "{name} pixel dimensions must be non-zero"
173 )));
174 }
175 Ok(())
176 }
177}
178
179impl AspectRatio {
180 fn validate(self) -> Result<(), GeometryError> {
181 if self.width == 0 || self.height == 0 {
182 return Err(GeometryError(
183 "aspect ratio dimensions must be non-zero".into(),
184 ));
185 }
186 Ok(())
187 }
188
189 fn physical_dimensions(self, diagonal: f64) -> Result<(f64, f64), GeometryError> {
190 self.validate()?;
191 let hypotenuse = (self.width as f64).hypot(self.height as f64);
192 Ok((
193 diagonal * self.width as f64 / hypotenuse,
194 diagonal * self.height as f64 / hypotenuse,
195 ))
196 }
197}
198
199impl DisplayGeometry {
200 fn oriented_pixels(self) -> Result<PixelSize, GeometryError> {
201 self.native_pixels.validate("display")?;
202 Ok(self.native_pixels.oriented(self.rotation))
203 }
204
205 fn oriented_physical_mm(self) -> Result<(f64, f64), GeometryError> {
206 self.oriented_pixels()?;
207 if let Some(aspect_ratio) = self.aspect_ratio {
208 aspect_ratio.validate()?;
209 }
210 let (width, height) = match self.physical {
211 PhysicalMeasurement::DimensionsMm { width, height } => match self.rotation {
212 Rotation::Deg0 | Rotation::Deg180 => (width, height),
213 Rotation::Deg90 | Rotation::Deg270 => (height, width),
214 },
215 PhysicalMeasurement::DiagonalMm(diagonal) => {
216 validate_measurement(diagonal, "diagonal")?;
217 let aspect_ratio = self.aspect_ratio.unwrap_or(AspectRatio {
218 width: self.native_pixels.width,
219 height: self.native_pixels.height,
220 });
221 let dimensions = aspect_ratio.physical_dimensions(diagonal)?;
222 match self.rotation {
223 Rotation::Deg0 | Rotation::Deg180 => dimensions,
224 Rotation::Deg90 | Rotation::Deg270 => (dimensions.1, dimensions.0),
225 }
226 }
227 };
228 validate_measurement(width, "physical width")?;
229 validate_measurement(height, "physical height")?;
230 Ok((width, height))
231 }
232}
233
234impl SizingRequest {
235 pub fn calculate(self) -> Result<SizingResult, GeometryError> {
236 if self.alignment == 0 || !self.alignment.is_power_of_two() || self.alignment > 256 {
237 return Err(GeometryError(
238 "alignment must be a power of two between 1 and 256".into(),
239 ));
240 }
241 if self.preferred_refresh_millihz == Some(0) {
242 return Err(GeometryError(
243 "preferred refresh must be non-zero when present".into(),
244 ));
245 }
246
247 let reference_pixels = self.reference.oriented_pixels()?;
248 let target_pixels = self.target.oriented_pixels()?;
249 let (reference_width_mm, _) = self.reference.oriented_physical_mm()?;
250 let (target_width_mm, _) = self.target.oriented_physical_mm()?;
251 let pixels_per_mm = reference_pixels.width as f64 / reference_width_mm;
252 let physical_width = target_width_mm * pixels_per_mm;
253 let physical_mode = aspect_aligned_mode(physical_width, target_pixels, self.alignment)?;
254
255 let virtual_mode = match self.strategy {
256 SizingStrategy::MatchPhysicalSize => physical_mode,
257 SizingStrategy::RoundedScale => {
258 snapped_scale_mode(physical_width, target_pixels, self.alignment, 4)?
259 }
260 SizingStrategy::IntegerScale => {
261 snapped_scale_mode(physical_width, target_pixels, self.alignment, 1)?
262 }
263 };
264 virtual_mode.validate("calculated virtual mode")?;
265
266 Ok(SizingResult {
267 virtual_mode,
268 oriented_target: target_pixels,
269 scale_x: virtual_mode.width as f64 / target_pixels.width as f64,
270 scale_y: virtual_mode.height as f64 / target_pixels.height as f64,
271 preferred_refresh_millihz: self.preferred_refresh_millihz,
272 })
273 }
274}
275
276impl CoordinateTransform {
277 pub fn stretch(
278 target: PixelRect,
279 source: PixelRect,
280 rotation: Rotation,
281 ) -> Result<Self, GeometryError> {
282 target.validate("target")?;
283 source.validate("source")?;
284 Ok(Self {
285 target,
286 source,
287 rotation,
288 })
289 }
290
291 pub fn map_target_point(self, point: PixelPoint) -> Option<PixelPoint> {
292 let local_x = point.x.checked_sub(self.target.left)?;
293 let local_y = point.y.checked_sub(self.target.top)?;
294 if local_x < 0
295 || local_y < 0
296 || local_x >= self.target.width as i32
297 || local_y >= self.target.height as i32
298 {
299 return None;
300 }
301
302 let (source_x, source_y) = match self.rotation {
303 Rotation::Deg0 => (
304 scale_index(local_x, self.target.width, self.source.width),
305 scale_index(local_y, self.target.height, self.source.height),
306 ),
307 Rotation::Deg90 => (
308 scale_index(local_y, self.target.height, self.source.width),
309 self.source.height as i32
310 - 1
311 - scale_index(local_x, self.target.width, self.source.height),
312 ),
313 Rotation::Deg180 => (
314 self.source.width as i32
315 - 1
316 - scale_index(local_x, self.target.width, self.source.width),
317 self.source.height as i32
318 - 1
319 - scale_index(local_y, self.target.height, self.source.height),
320 ),
321 Rotation::Deg270 => (
322 self.source.width as i32
323 - 1
324 - scale_index(local_y, self.target.height, self.source.width),
325 scale_index(local_x, self.target.width, self.source.height),
326 ),
327 };
328 Some(PixelPoint {
329 x: self.source.left.saturating_add(source_x),
330 y: self.source.top.saturating_add(source_y),
331 })
332 }
333}
334
335impl PixelRect {
336 fn validate(self, name: &str) -> Result<(), GeometryError> {
337 if self.width == 0 || self.height == 0 {
338 return Err(GeometryError(format!(
339 "{name} rectangle dimensions must be non-zero"
340 )));
341 }
342 if self.width > i32::MAX as u32 || self.height > i32::MAX as u32 {
343 return Err(GeometryError(format!(
344 "{name} rectangle dimensions exceed Win32 coordinate range"
345 )));
346 }
347 Ok(())
348 }
349}
350
351const fn default_alignment() -> u32 {
352 2
353}
354
355fn validate_measurement(value: f64, name: &str) -> Result<(), GeometryError> {
356 if !valid_physical_millimeters(value) {
357 return Err(GeometryError(format!(
358 "{name} must be finite and between {MIN_PHYSICAL_MILLIMETERS:.0} and {MAX_PHYSICAL_MILLIMETERS:.0} mm"
359 )));
360 }
361 Ok(())
362}
363
364fn aspect_aligned_mode(
365 ideal_width: f64,
366 aspect: PixelSize,
367 alignment: u32,
368) -> Result<PixelSize, GeometryError> {
369 if !ideal_width.is_finite() || ideal_width < 1.0 || ideal_width > u32::MAX as f64 {
370 return Err(GeometryError(
371 "calculated dimension is outside the supported range".into(),
372 ));
373 }
374 let divisor = greatest_common_divisor(aspect.width, aspect.height);
375 let unit_width = (aspect.width / divisor) as u64;
376 let unit_height = (aspect.height / divisor) as u64;
377 let mut multiplier = (ideal_width / unit_width as f64).ceil() as u64;
378 let alignment = alignment as u64;
379 loop {
380 let width = unit_width
381 .checked_mul(multiplier)
382 .ok_or_else(|| GeometryError("calculated dimension overflowed".into()))?;
383 let height = unit_height
384 .checked_mul(multiplier)
385 .ok_or_else(|| GeometryError("calculated dimension overflowed".into()))?;
386 if width % alignment == 0 && height % alignment == 0 {
387 return Ok(PixelSize {
388 width: u32::try_from(width)
389 .map_err(|_| GeometryError("calculated dimension overflowed".into()))?,
390 height: u32::try_from(height)
391 .map_err(|_| GeometryError("calculated dimension overflowed".into()))?,
392 });
393 }
394 multiplier = multiplier
395 .checked_add(1)
396 .ok_or_else(|| GeometryError("calculated dimension overflowed".into()))?;
397 }
398}
399
400fn greatest_common_divisor(mut left: u32, mut right: u32) -> u32 {
401 while right != 0 {
402 let remainder = left % right;
403 left = right;
404 right = remainder;
405 }
406 left.max(1)
407}
408
409fn snapped_scale_mode(
410 ideal_width: f64,
411 target: PixelSize,
412 alignment: u32,
413 steps_per_unit: u32,
414) -> Result<PixelSize, GeometryError> {
415 let target_width = target.width as f64;
416 let steps_per_unit = steps_per_unit as f64;
417 let minimum_scale = 1.0 / steps_per_unit;
418 let scale = ((ideal_width / target_width) * steps_per_unit).round() / steps_per_unit;
419 aspect_aligned_mode(target_width * scale.max(minimum_scale), target, alignment)
420}
421
422fn scale_index(value: i32, from_extent: u32, to_extent: u32) -> i32 {
423 let from_max = from_extent.saturating_sub(1).max(1) as i64;
424 let to_max = to_extent.saturating_sub(1) as i64;
425 ((value as i64 * to_max) / from_max).clamp(0, to_max) as i32
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 fn display(width: u32, height: u32, diagonal_mm: f64) -> DisplayGeometry {
433 DisplayGeometry {
434 native_pixels: PixelSize { width, height },
435 physical: PhysicalMeasurement::DiagonalMm(diagonal_mm),
436 aspect_ratio: None,
437 rotation: Rotation::Deg0,
438 }
439 }
440
441 fn assert_approximately_equal(left: f64, right: f64) {
442 assert!(
443 (left - right).abs() < 1e-9,
444 "expected {left} to approximately equal {right}"
445 );
446 }
447
448 #[test]
449 fn explicit_aspect_ratio_changes_physical_width_for_the_same_diagonal() {
450 let mut widescreen = display(2560, 1440, 600.0);
451 widescreen.aspect_ratio = Some(AspectRatio {
452 width: 16,
453 height: 9,
454 });
455 let mut square = widescreen;
456 square.aspect_ratio = Some(AspectRatio {
457 width: 1,
458 height: 1,
459 });
460
461 let (widescreen_width, _) = widescreen.oriented_physical_mm().unwrap();
462 let (square_width, _) = square.oriented_physical_mm().unwrap();
463
464 assert!(widescreen_width > square_width);
465 assert_approximately_equal(widescreen_width, 600.0 * 16.0 / 337.0_f64.sqrt());
466 assert_approximately_equal(square_width, 600.0 / 2.0_f64.sqrt());
467 }
468
469 #[test]
470 fn explicit_aspect_ratio_swaps_physical_axes_when_vertical() {
471 let mut horizontal = display(3840, 2160, 600.0);
472 horizontal.aspect_ratio = Some(AspectRatio {
473 width: 16,
474 height: 9,
475 });
476 let mut vertical = horizontal;
477 vertical.rotation = Rotation::Deg90;
478
479 let horizontal_dimensions = horizontal.oriented_physical_mm().unwrap();
480 let vertical_dimensions = vertical.oriented_physical_mm().unwrap();
481
482 assert_approximately_equal(vertical_dimensions.0, horizontal_dimensions.1);
483 assert_approximately_equal(vertical_dimensions.1, horizontal_dimensions.0);
484 }
485
486 #[test]
487 fn missing_aspect_ratio_preserves_native_pixel_ratio_behavior() {
488 let legacy = display(2560, 1600, 400.0);
489 let (width, height) = legacy.oriented_physical_mm().unwrap();
490 let hypotenuse = 2560.0_f64.hypot(1600.0);
491
492 assert_approximately_equal(width, 400.0 * 2560.0 / hypotenuse);
493 assert_approximately_equal(height, 400.0 * 1600.0 / hypotenuse);
494 }
495
496 #[test]
497 fn zero_aspect_ratio_dimension_is_rejected() {
498 let mut invalid = display(2560, 1440, 600.0);
499 invalid.aspect_ratio = Some(AspectRatio {
500 width: 0,
501 height: 9,
502 });
503
504 let error = invalid.oriented_physical_mm().unwrap_err();
505 assert!(error.to_string().contains("aspect ratio"));
506 }
507
508 #[test]
509 fn physical_strategy_matches_reference_density_and_target_aspect() {
510 let result = SizingRequest {
511 reference: display(3840, 2160, 708.0),
512 target: display(2560, 1440, 604.0),
513 strategy: SizingStrategy::MatchPhysicalSize,
514 alignment: 2,
515 preferred_refresh_millihz: Some(240_000),
516 }
517 .calculate()
518 .unwrap();
519
520 assert_eq!(result.virtual_mode.width % 2, 0);
521 assert_eq!(result.virtual_mode.height % 2, 0);
522 assert_eq!(
523 result.virtual_mode.width as u64 * 9,
524 result.virtual_mode.height as u64 * 16
525 );
526 assert_eq!(result.preferred_refresh_millihz, Some(240_000));
527 }
528
529 #[test]
530 fn rotation_swaps_oriented_extent_without_guessing_from_aspect() {
531 let size = PixelSize {
532 width: 3840,
533 height: 2160,
534 };
535 assert_eq!(
536 size.oriented(Rotation::Deg90),
537 PixelSize {
538 width: 2160,
539 height: 3840
540 }
541 );
542 assert_eq!(size.oriented(Rotation::Deg180), size);
543 }
544
545 #[test]
546 fn rounded_strategy_snaps_to_quarter_scale() {
547 let result = SizingRequest {
548 reference: display(2496, 1404, 1_000.0),
549 target: display(1920, 1080, 1_000.0),
550 strategy: SizingStrategy::RoundedScale,
551 alignment: 2,
552 preferred_refresh_millihz: None,
553 }
554 .calculate()
555 .unwrap();
556 assert_eq!(
557 result.virtual_mode,
558 PixelSize {
559 width: 2400,
560 height: 1350
561 }
562 );
563 }
564
565 #[test]
566 fn integer_strategy_snaps_to_nearest_whole_scale() {
567 let result = SizingRequest {
568 reference: display(3008, 1692, 1_000.0),
569 target: display(1920, 1080, 1_000.0),
570 strategy: SizingStrategy::IntegerScale,
571 alignment: 2,
572 preferred_refresh_millihz: None,
573 }
574 .calculate()
575 .unwrap();
576 assert_eq!(
577 result.virtual_mode,
578 PixelSize {
579 width: 3840,
580 height: 2160
581 }
582 );
583 }
584
585 #[test]
586 fn integer_strategy_never_drops_below_native_size() {
587 let result = SizingRequest {
588 reference: display(960, 540, 1_000.0),
589 target: display(1920, 1080, 1_000.0),
590 strategy: SizingStrategy::IntegerScale,
591 alignment: 2,
592 preferred_refresh_millihz: None,
593 }
594 .calculate()
595 .unwrap();
596 assert_eq!(
597 result.virtual_mode,
598 PixelSize {
599 width: 1920,
600 height: 1080
601 }
602 );
603 }
604
605 #[test]
606 fn legacy_integer_strategy_deserializes_without_exposing_its_old_limit() {
607 for max_scale in [1, 4, 8] {
608 let json = format!(r#"{{"integer_scale":{{"max_scale":{max_scale}}}}}"#);
609 let strategy: SizingStrategy = serde_json::from_str(&json).unwrap();
610 assert_eq!(strategy, SizingStrategy::IntegerScale);
611 }
612 for max_scale in [0, 9] {
613 let json = format!(r#"{{"integer_scale":{{"max_scale":{max_scale}}}}}"#);
614 assert!(serde_json::from_str::<SizingStrategy>(&json).is_err());
615 }
616 }
617
618 #[test]
619 fn current_sizing_strategies_round_trip_as_canonical_strings() {
620 for strategy in [
621 SizingStrategy::MatchPhysicalSize,
622 SizingStrategy::RoundedScale,
623 SizingStrategy::IntegerScale,
624 ] {
625 let json = serde_json::to_string(&strategy).unwrap();
626 assert_eq!(
627 serde_json::from_str::<SizingStrategy>(&json).unwrap(),
628 strategy
629 );
630 }
631 assert_eq!(
632 serde_json::to_string(&SizingStrategy::IntegerScale).unwrap(),
633 r#""integer_scale""#
634 );
635 }
636
637 #[test]
638 fn coordinate_transform_maps_edges_and_rotation() {
639 let target = PixelRect {
640 left: 0,
641 top: 0,
642 width: 100,
643 height: 50,
644 };
645 let source = PixelRect {
646 left: 1000,
647 top: -500,
648 width: 200,
649 height: 400,
650 };
651 let normal = CoordinateTransform::stretch(target, source, Rotation::Deg0).unwrap();
652 assert_eq!(
653 normal.map_target_point(PixelPoint { x: 99, y: 49 }),
654 Some(PixelPoint { x: 1199, y: -101 })
655 );
656 assert_eq!(normal.map_target_point(PixelPoint { x: 100, y: 49 }), None);
657
658 let clockwise = CoordinateTransform::stretch(target, source, Rotation::Deg90).unwrap();
659 assert_eq!(
660 clockwise.map_target_point(PixelPoint { x: 0, y: 0 }),
661 Some(PixelPoint { x: 1000, y: -101 })
662 );
663 assert_eq!(
664 clockwise.map_target_point(PixelPoint { x: 99, y: 49 }),
665 Some(PixelPoint { x: 1199, y: -500 })
666 );
667 }
668}
669