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.
文件
../ bininputuiconfig.rs81.2 KBcontrol.rs4.1 KBcontroller.rs43.6 KBdiagnostics.rs24.2 KBdisplay.rs29.8 KBgeometry.rs21.4 KBgpu_renderer.rs37.4 KBinput.rs79.7 KBlaunch_broker.rs10.3 KBlib.rs350 Blimits.rs714 Bmain.rs24.4 KBmapping.rs46.5 KBnetwork.rs9.2 KBrenderer.rs20.6 KBsession_gate.rs14.9 KBsunshine.rs12.2 KBui.rs68.5 KBvirtual_display.rs7.2 KBwin32_flyout.rs3.4 KBwin32_tray.rs22.3 KBwindow_migration.rs43.8 KBsrc/geometry.rs21.4 KB
1
use std::error::Error;2
use std::fmt::{Display, Formatter};3
4
use serde::de::Error as _;5
use serde::{Deserialize, Deserializer, Serialize};6
7
use 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)]13
pub 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)]20
pub 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")]27
pub 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")]34
pub 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")]44
pub enum SizingStrategy {45
#[default]46
MatchPhysicalSize,47
RoundedScale,48
IntegerScale,49
}50
51
impl<'de> Deserialize<'de> for SizingStrategy {52
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>53
where54
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)]95
pub 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)]106
pub 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)]118
pub 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)]127
pub struct PixelPoint {128
pub x: i32,129
pub y: i32,130
}131
132
#[derive(Clone, Copy, Debug, Eq, PartialEq)]133
pub 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)]141
pub struct CoordinateTransform {142
pub target: PixelRect,143
pub source: PixelRect,144
pub rotation: Rotation,145
}146
147
#[derive(Clone, Debug, Eq, PartialEq)]148
pub struct GeometryError(String);149
150
impl Display for GeometryError {151
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {152
formatter.write_str(&self.0)153
}154
}155
156
impl Error for GeometryError {}157
158
impl 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
179
impl 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
199
impl 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
234
impl 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
276
impl 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 < 0295
|| local_y < 0296
|| local_x >= self.target.width as i32297
|| local_y >= self.target.height as i32298
{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 i32310
- 1311
- scale_index(local_x, self.target.width, self.source.height),312
),313
Rotation::Deg180 => (314
self.source.width as i32315
- 1316
- scale_index(local_x, self.target.width, self.source.width),317
self.source.height as i32318
- 1319
- scale_index(local_y, self.target.height, self.source.height),320
),321
Rotation::Deg270 => (322
self.source.width as i32323
- 1324
- 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
335
impl 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
351
const fn default_alignment() -> u32 {352
2353
}354
355
fn 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
364
fn 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_width381
.checked_mul(multiplier)382
.ok_or_else(|| GeometryError("calculated dimension overflowed".into()))?;383
let height = unit_height384
.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 = multiplier395
.checked_add(1)396
.ok_or_else(|| GeometryError("calculated dimension overflowed".into()))?;397
}398
}399
400
fn 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
409
fn 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
422
fn 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 i32426
}427
428
#[cfg(test)]429
mod 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 * 16525
);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: 3840540
}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: 1350561
}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: 2160581
}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: 1080601
}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
strategy629
);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