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/mapping.rs46.5 KB
1
use std::collections::HashSet;2
use std::error::Error;3
use std::fmt::{Display as FmtDisplay, Formatter};4
use std::sync::atomic::{AtomicBool, Ordering};5
use std::sync::{Arc, Mutex};6
use std::thread;7
use std::time::{Duration, Instant};8
9
use serde::{Deserialize, Serialize};10
use windows::core::HRESULT;11
12
use crate::display::{13
Display, active_display_topology, active_displays, apply_display_mode,14
restore_display_topology, unique_physical_display,15
};16
use crate::geometry::Rotation;17
use crate::limits::MAX_OUTPUTS;18
use crate::renderer::{Renderer, RendererEvent, RendererReporter, RendererStartError};19
use crate::session_gate::{SessionGate, VirtualDisplayConfig, VirtualMode};20
use crate::virtual_display::VirtualDisplay;21
use crate::window_migration::WindowMigration;22
23
const BASE_TOPOLOGY_TIMEOUT_SECS: u64 = 15;24
const TOPOLOGY_TIMEOUT_SECS_PER_GROUP_OVER_EIGHT: u64 = 2;25
const TOPOLOGY_SETTLE: Duration = Duration::from_millis(750);26
const POLL_INTERVAL: Duration = Duration::from_millis(100);27
const RECOVERY_STABLE_INTERVAL: Duration = Duration::from_millis(500);28
const RECOVERY_TIMEOUT: Duration = Duration::from_secs(7);29
const RECOVERY_STABLE_SAMPLES: u8 = 3;30
const ERROR_CANCELLED_HRESULT: HRESULT = HRESULT(0x800704C7_u32 as i32);31
32
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]33
pub struct MappingPlan {34
pub groups: Vec<MappingGroupRequest>,35
}36
37
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]38
pub struct MappingGroupRequest {39
/// Stable zero-based output slot. The value is also the IDD connector index.40
pub id: u32,41
pub mode: VirtualMode,42
#[serde(default)]43
pub rotation: Rotation,44
pub route: MappingRoute,45
}46
47
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]48
#[serde(tag = "kind", rename_all = "snake_case")]49
pub enum MappingRoute {50
Mirror { target: String },51
StreamOnly,52
}53
54
#[derive(Clone, Debug, Eq, PartialEq)]55
pub struct MappingGroupInfo {56
pub id: u32,57
pub mode: VirtualMode,58
pub rotation: Rotation,59
pub route: MappingRoute,60
pub source_id: String,61
pub source_device_name: String,62
pub sunshine_id: Option<String>,63
}64
65
#[derive(Clone, Debug)]66
pub enum MappingEvent {67
GroupReady(MappingGroupInfo),68
Renderer { id: u32, event: RendererEvent },69
Topology { message: String },70
}71
72
pub type MappingReporter = Arc<dyn Fn(MappingEvent) + Send + Sync + 'static>;73
74
struct ActiveGroup {75
info: MappingGroupInfo,76
target_id: Option<String>,77
renderer: Option<Renderer>,78
renderer_event_gate: Arc<RendererEventGate>,79
migration: Option<WindowMigration>,80
}81
82
enum RendererEventGateState {83
Starting(Vec<RendererEvent>),84
Active,85
Discarded,86
}87
88
struct RendererEventGate {89
id: u32,90
reporter: MappingReporter,91
state: Mutex<RendererEventGateState>,92
}93
94
impl RendererEventGate {95
fn new(id: u32, reporter: MappingReporter) -> Arc<Self> {96
Arc::new(Self {97
id,98
reporter,99
state: Mutex::new(RendererEventGateState::Starting(Vec::new())),100
})101
}102
103
fn report(&self, event: RendererEvent) {104
let mut state = self105
.state106
.lock()107
.unwrap_or_else(|poisoned| poisoned.into_inner());108
match &mut *state {109
RendererEventGateState::Starting(events) => events.push(event),110
RendererEventGateState::Active => {111
(self.reporter)(MappingEvent::Renderer { id: self.id, event })112
}113
RendererEventGateState::Discarded => {}114
}115
}116
117
fn activate(&self) {118
let mut state = self119
.state120
.lock()121
.unwrap_or_else(|poisoned| poisoned.into_inner());122
let buffered = match std::mem::replace(&mut *state, RendererEventGateState::Active) {123
RendererEventGateState::Starting(events) => events,124
RendererEventGateState::Active | RendererEventGateState::Discarded => Vec::new(),125
};126
for event in buffered {127
(self.reporter)(MappingEvent::Renderer { id: self.id, event });128
}129
}130
131
fn discard(&self) {132
let mut state = self133
.state134
.lock()135
.unwrap_or_else(|poisoned| poisoned.into_inner());136
*state = RendererEventGateState::Discarded;137
}138
}139
140
pub struct MappingSession {141
groups: Vec<ActiveGroup>,142
display: Option<VirtualDisplay>,143
session_gate: Option<SessionGate>,144
connector_indices: Vec<u32>,145
physical_topology: Vec<Display>,146
}147
148
#[derive(Debug)]149
pub struct MappingError {150
stage: &'static str,151
group_id: Option<u32>,152
message: String,153
cleanup_incomplete: bool,154
}155
156
impl FmtDisplay for MappingError {157
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {158
match self.group_id {159
Some(id) => write!(formatter, "group {id} {}: {}", self.stage, self.message),160
None => write!(formatter, "{}: {}", self.stage, self.message),161
}162
}163
}164
165
impl Error for MappingError {}166
167
impl MappingError {168
pub fn stage(&self) -> &'static str {169
self.stage170
}171
172
pub fn group_id(&self) -> Option<u32> {173
self.group_id174
}175
176
pub fn message(&self) -> &str {177
&self.message178
}179
180
pub(crate) fn is_clean_cancellation(&self) -> bool {181
self.stage == "cancelled" && !self.cleanup_incomplete182
}183
184
fn record_cleanup_failure(&mut self, error: impl FmtDisplay) {185
self.cleanup_incomplete = true;186
self.message187
.push_str(&format!("; rollback was incomplete: {error}"));188
}189
}190
191
impl MappingPlan {192
pub fn new(groups: Vec<MappingGroupRequest>) -> Result<Self, MappingError> {193
let plan = Self { groups };194
plan.validate()?;195
Ok(plan)196
}197
198
pub fn validate(&self) -> Result<(), MappingError> {199
if self.groups.is_empty() {200
return Err(MappingError {201
stage: "plan",202
group_id: None,203
message: "at least one mapping group is required".into(),204
cleanup_incomplete: false,205
});206
}207
if self.groups.len() > MAX_OUTPUTS {208
return Err(MappingError {209
stage: "plan",210
group_id: None,211
message: format!("at most {MAX_OUTPUTS} mapping groups are supported"),212
cleanup_incomplete: false,213
});214
}215
216
let mut ids = HashSet::with_capacity(self.groups.len());217
let mut targets = HashSet::with_capacity(self.groups.len());218
for group in &self.groups {219
if group.id >= MAX_OUTPUTS as u32 {220
return Err(group_stage(221
group.id,222
"plan",223
format!(224
"group id must be a connector index between 0 and {}",225
MAX_OUTPUTS - 1226
),227
));228
}229
if !ids.insert(group.id) {230
return Err(group_stage(231
group.id,232
"plan",233
"mapping group id is duplicated",234
));235
}236
group237
.mode238
.validate()239
.map_err(|error| group_stage(group.id, "mode", error))?;240
if let MappingRoute::Mirror { target } = &group.route {241
if target.trim().is_empty() {242
return Err(group_stage(243
group.id,244
"plan",245
"mirror target id must not be empty",246
));247
}248
if !targets.insert(target.to_ascii_lowercase()) {249
return Err(group_stage(250
group.id,251
"plan",252
"a physical target may only belong to one mapping group",253
));254
}255
}256
}257
Ok(())258
}259
}260
261
impl MappingSession {262
pub fn start_plan(plan: MappingPlan) -> Result<Self, MappingError> {263
Self::start_plan_with_reporter(plan, Arc::new(|_| {}))264
}265
266
pub fn start_plan_with_reporter(267
plan: MappingPlan,268
reporter: MappingReporter,269
) -> Result<Self, MappingError> {270
let cancel = AtomicBool::new(false);271
Self::start_plan_with_reporter_cancellable(plan, reporter, &cancel)272
}273
274
pub(crate) fn start_plan_with_reporter_cancellable(275
plan: MappingPlan,276
reporter: MappingReporter,277
cancel: &AtomicBool,278
) -> Result<Self, MappingError> {279
plan.validate()?;280
check_start_cancelled(cancel)?;281
let displays = active_displays().map_err(|error| stage("target", error))?;282
check_start_cancelled(cancel)?;283
for group in &plan.groups {284
if let MappingRoute::Mirror { target } = &group.route {285
mapping_target(&displays, target).map_err(|error| error.with_group(group.id))?;286
}287
}288
289
let physical_topology = displays290
.iter()291
.filter(|display| !display.virtual_display)292
.cloned()293
.collect();294
let connector_indices = plan.groups.iter().map(|group| group.id).collect();295
let gate_configs: Vec<_> = plan296
.groups297
.iter()298
.map(|group| VirtualDisplayConfig {299
connector_index: group.id,300
mode: group.mode,301
})302
.collect();303
check_start_cancelled(cancel)?;304
let session_gate = SessionGate::create_many(&gate_configs)305
.map_err(|error| stage("session-gate", error))?;306
check_start_cancelled(cancel)?;307
let display = match VirtualDisplay::create_cancellable(cancel) {308
Ok(display) => display,309
Err(error)310
if cancel.load(Ordering::Acquire) && error.code() == ERROR_CANCELLED_HRESULT =>311
{312
return Err(start_cancelled());313
}314
Err(error) => return Err(stage("device", error)),315
};316
317
let mut session = Self {318
groups: Vec::with_capacity(plan.groups.len()),319
display: Some(display),320
session_gate: Some(session_gate),321
connector_indices,322
physical_topology,323
};324
325
if let Err(error) = check_start_cancelled(cancel) {326
return Err(rollback_start(session, error));327
}328
let sources = match wait_for_sources(&plan, &reporter, cancel) {329
Ok(sources) => sources,330
Err(error) => return Err(rollback_start(session, error)),331
};332
for (request, source) in plan.groups.iter().zip(&sources) {333
let target_id = match &request.route {334
MappingRoute::Mirror { target } => Some(target.clone()),335
MappingRoute::StreamOnly => None,336
};337
session.groups.push(ActiveGroup {338
info: MappingGroupInfo {339
id: request.id,340
mode: request.mode,341
rotation: request.rotation,342
route: request.route.clone(),343
source_id: source.id.clone(),344
source_device_name: source.device_name.clone(),345
sunshine_id: source.sunshine_id.clone(),346
},347
target_id,348
renderer: None,349
renderer_event_gate: RendererEventGate::new(request.id, Arc::clone(&reporter)),350
migration: None,351
});352
}353
354
let stable_displays = match active_displays() {355
Ok(displays) => displays,356
Err(error) => return Err(rollback_start(session, stage("topology", error))),357
};358
for (index, request) in plan.groups.iter().enumerate() {359
if let Err(error) = check_start_cancelled(cancel) {360
return Err(rollback_start(session, error));361
}362
let MappingRoute::Mirror { target } = &request.route else {363
continue;364
};365
let physical = match mapping_target(&stable_displays, target) {366
Ok(target) => target,367
Err(error) => {368
return Err(rollback_start(session, error.with_group(request.id)));369
}370
};371
let migration = match WindowMigration::start(&physical, &sources[index]) {372
Ok(migration) => migration,373
Err(_) if cancel.load(Ordering::Acquire) => {374
return Err(rollback_start(session, start_cancelled()));375
}376
Err(error) => {377
return Err(rollback_start(378
session,379
group_stage(request.id, "windows", error),380
));381
}382
};383
session.groups[index].migration = Some(migration);384
if let Err(error) = check_start_cancelled(cancel) {385
return Err(rollback_start(session, error));386
}387
}388
389
for (index, request) in plan.groups.iter().enumerate() {390
if let Err(error) = check_start_cancelled(cancel) {391
return Err(rollback_start(session, error));392
}393
let MappingRoute::Mirror { target } = &request.route else {394
continue;395
};396
let physical = match mapping_target(&stable_displays, target) {397
Ok(target) => target,398
Err(error) => {399
return Err(rollback_start(session, error.with_group(request.id)));400
}401
};402
let event_gate = Arc::clone(&session.groups[index].renderer_event_gate);403
let renderer_reporter: RendererReporter = Arc::new(move |event| {404
event_gate.report(event);405
});406
let renderer = match Renderer::start_with_reporter_cancellable(407
physical,408
sources[index].clone(),409
renderer_reporter,410
cancel,411
) {412
Ok(renderer) => renderer,413
Err(RendererStartError::Cancelled { cleanup_error }) => {414
let mut cause = start_cancelled();415
if let Some(error) = cleanup_error {416
cause.record_cleanup_failure(format!("renderer startup cleanup: {error}"));417
}418
return Err(rollback_start(session, cause));419
}420
Err(RendererStartError::Failed(error)) => {421
return Err(rollback_start(422
session,423
group_stage(request.id, "first-frame", error),424
));425
}426
};427
session.groups[index].renderer = Some(renderer);428
session.groups[index].renderer_event_gate.activate();429
if let Err(error) = check_start_cancelled(cancel) {430
return Err(rollback_start(session, error));431
}432
}433
434
for group in &session.groups {435
reporter(MappingEvent::GroupReady(group.info.clone()));436
}437
Ok(session)438
}439
440
pub fn stop(&mut self) -> Result<(), MappingError> {441
if self442
.groups443
.iter()444
.all(|group| group.renderer.is_none() && group.migration.is_none())445
&& self.display.is_none()446
&& self.session_gate.is_none()447
{448
return Ok(());449
}450
451
let mut errors = Vec::new();452
for group in self.groups.iter().rev() {453
if let Some(renderer) = group.renderer.as_ref() {454
renderer.request_stop();455
}456
}457
for group in self.groups.iter_mut().rev() {458
if let Some(mut renderer) = group.renderer.take()459
&& let Err(error) = renderer.stop()460
{461
errors.push(format!("group {} renderer-stop: {error}", group.info.id));462
}463
}464
for group in self.groups.iter_mut().rev() {465
if let Some(migration) = group.migration.as_mut() {466
migration.prepare_restore();467
}468
}469
for group in self.groups.iter_mut().rev() {470
if let Some(migration) = group.migration.as_mut()471
&& let Err(error) = migration.restore()472
{473
errors.push(format!("group {} windows-restore: {error}", group.info.id));474
}475
}476
477
self.display.take();478
if let Err(error) = wait_for_sources_removed(&self.connector_indices) {479
errors.push(format!("remove: {error}"));480
}481
482
thread::sleep(TOPOLOGY_SETTLE);483
let topology_restored = match restore_display_topology(&self.physical_topology) {484
Ok(()) => true,485
Err(error) => {486
errors.push(format!("topology-restore: {error}"));487
false488
}489
};490
if topology_restored {491
thread::sleep(TOPOLOGY_SETTLE);492
}493
494
let final_displays = active_displays();495
if let Err(error) = &final_displays {496
errors.push(format!("windows-reconcile topology read: {error}"));497
}498
for group in self.groups.iter_mut().rev() {499
let Some(migration) = group.migration.as_mut() else {500
continue;501
};502
let target = group.target_id.as_ref().and_then(|target_id| {503
final_displays504
.as_ref()505
.ok()506
.and_then(|displays| unique_physical_display(displays, target_id).ok())507
});508
if let Err(error) = migration.reconcile_after_topology_change(target.as_ref()) {509
errors.push(format!(510
"group {} windows-reconcile: {error}",511
group.info.id512
));513
}514
}515
for group in &mut self.groups {516
group.migration.take();517
}518
self.session_gate.take();519
520
if errors.is_empty() {521
Ok(())522
} else {523
Err(MappingError {524
stage: "stop",525
group_id: None,526
message: errors.join("; "),527
cleanup_incomplete: false,528
})529
}530
}531
532
pub fn groups(&self) -> impl ExactSizeIterator<Item = &MappingGroupInfo> {533
self.groups.iter().map(|group| &group.info)534
}535
536
pub(crate) fn recover_group_topology(537
&mut self,538
group_id: u32,539
reporter: MappingReporter,540
cancel: &AtomicBool,541
) -> Result<MappingGroupInfo, MappingError> {542
let group_index = self543
.groups544
.iter()545
.position(|group| group.info.id == group_id)546
.ok_or_else(|| {547
group_stage(group_id, "recovery", "mapping group is no longer active")548
})?;549
let target_id = self.groups[group_index].target_id.clone().ok_or_else(|| {550
group_stage(group_id, "recovery", "stream-only group has no renderer")551
})?;552
553
self.groups[group_index].renderer_event_gate.discard();554
555
if let Some(mut renderer) = self.groups[group_index].renderer.take()556
&& let Err(error) = renderer.stop()557
{558
return Err(group_stage(group_id, "renderer-stop", error));559
}560
561
let group_info = self.groups[group_index].info.clone();562
let displays = wait_for_stable_recovery_topology(&group_info, &target_id, cancel)?;563
let target =564
mapping_target(&displays, &target_id).map_err(|error| error.with_group(group_id))?;565
let source = displays566
.iter()567
.find(|display| display.virtual_display && display.connector_index == group_id)568
.cloned()569
.ok_or_else(|| {570
group_stage(571
group_id,572
"recovery",573
"virtual source disappeared while recovering the renderer",574
)575
})?;576
577
// Windows owns the runtime desktop layout. Commit the stable physical578
// snapshot before rebuilding short-lived consumers so a renderer start579
// failure cannot make a later Stop roll the user's position change back.580
self.physical_topology = displays581
.iter()582
.filter(|display| !display.virtual_display)583
.cloned()584
.collect();585
586
if let Some(migration) = self.groups[group_index].migration.as_mut() {587
migration588
.refresh_topology(&target, &source)589
.map_err(|error| group_stage(group_id, "window-scanner", error))?;590
}591
592
let event_gate = RendererEventGate::new(group_id, reporter);593
let reporter_gate = Arc::clone(&event_gate);594
let renderer_reporter: RendererReporter = Arc::new(move |event| {595
reporter_gate.report(event);596
});597
let renderer = Renderer::start_with_reporter_cancellable(598
target,599
source.clone(),600
renderer_reporter,601
cancel,602
)603
.map_err(|error| match error {604
RendererStartError::Cancelled { cleanup_error } => {605
let mut error =606
group_stage(group_id, "cancelled", "renderer recovery was cancelled");607
if let Some(cleanup_error) = cleanup_error {608
error.record_cleanup_failure(cleanup_error);609
}610
error611
}612
RendererStartError::Failed(error) => group_stage(group_id, "first-frame", error),613
})?;614
615
self.groups[group_index].info.source_id = source.id;616
self.groups[group_index].info.source_device_name = source.device_name;617
self.groups[group_index].info.sunshine_id = source.sunshine_id;618
self.groups[group_index].renderer = Some(renderer);619
self.groups[group_index].renderer_event_gate = Arc::clone(&event_gate);620
event_gate.activate();621
Ok(self.groups[group_index].info.clone())622
}623
}624
625
impl MappingError {626
fn with_group(mut self, group_id: u32) -> Self {627
self.group_id = Some(group_id);628
self629
}630
}631
632
impl Drop for MappingSession {633
fn drop(&mut self) {634
let _ = self.stop();635
}636
}637
638
fn rollback_start(mut session: MappingSession, mut cause: MappingError) -> MappingError {639
if let Err(error) = session.stop() {640
cause.record_cleanup_failure(error);641
}642
cause643
}644
645
fn mapping_target(displays: &[Display], target_id: &str) -> Result<Display, MappingError> {646
unique_physical_display(displays, target_id).map_err(|error| stage("target", error))647
}648
649
fn wait_for_stable_recovery_topology(650
group: &MappingGroupInfo,651
target_id: &str,652
cancel: &AtomicBool,653
) -> Result<Vec<Display>, MappingError> {654
let group_id = group.id;655
let deadline = Instant::now() + RECOVERY_TIMEOUT;656
let mut stability = RecoveryStability::default();657
loop {658
check_start_cancelled(cancel)?;659
let displays =660
active_display_topology().map_err(|error| group_stage(group_id, "recovery", error))?;661
let ready = unique_physical_display(&displays, target_id).is_ok()662
&& displays.iter().any(|display| {663
display.virtual_display664
&& display.connector_index == group_id665
&& source_matches_mode(display, group.mode, group.rotation)666
});667
let signature = ready.then(|| recovery_topology_signature(&displays));668
if stability.observe(signature) {669
let hydrated =670
active_displays().map_err(|error| group_stage(group_id, "recovery", error))?;671
let hydrated_ready = unique_physical_display(&hydrated, target_id).is_ok()672
&& hydrated.iter().any(|display| {673
display.virtual_display674
&& display.connector_index == group_id675
&& source_matches_mode(display, group.mode, group.rotation)676
});677
if hydrated_ready && stability.matches(&recovery_topology_signature(&hydrated)) {678
return Ok(hydrated);679
}680
stability.reset();681
}682
if Instant::now() >= deadline {683
return Err(group_stage(684
group_id,685
"recovery",686
"display topology did not become stable within 7 seconds",687
));688
}689
thread::sleep(RECOVERY_STABLE_INTERVAL);690
}691
}692
693
type RecoveryTopologySignature = Vec<(String, String, u32, i32, i32, i32, i32, u16, u32, u32)>;694
695
#[derive(Default)]696
struct RecoveryStability {697
signature: Option<RecoveryTopologySignature>,698
samples: u8,699
}700
701
impl RecoveryStability {702
fn observe(&mut self, signature: Option<RecoveryTopologySignature>) -> bool {703
if signature.is_some() && signature == self.signature {704
self.samples = self.samples.saturating_add(1);705
} else {706
self.samples = u8::from(signature.is_some());707
self.signature = signature;708
}709
self.samples >= RECOVERY_STABLE_SAMPLES710
}711
712
fn matches(&self, signature: &RecoveryTopologySignature) -> bool {713
self.signature.as_ref() == Some(signature)714
}715
716
fn reset(&mut self) {717
self.signature = None;718
self.samples = 0;719
}720
}721
722
fn recovery_topology_signature(displays: &[Display]) -> RecoveryTopologySignature {723
let mut signature: Vec<_> = displays724
.iter()725
.map(|display| {726
(727
display.id.to_ascii_lowercase(),728
display.device_name.to_ascii_lowercase(),729
display.connector_index,730
display.rect.left,731
display.rect.top,732
display.rect.right,733
display.rect.bottom,734
rotation_degrees(display.rotation),735
display.refresh_numerator,736
display.refresh_denominator,737
)738
})739
.collect();740
signature.sort_unstable();741
signature742
}743
744
fn wait_for_sources(745
plan: &MappingPlan,746
reporter: &MappingReporter,747
cancel: &AtomicBool,748
) -> Result<Vec<Display>, MappingError> {749
let requested_ids: HashSet<_> = plan.groups.iter().map(|group| group.id).collect();750
let topology_timeout = topology_timeout(plan.groups.len());751
let deadline = Instant::now() + topology_timeout;752
let mut mode_applied = HashSet::new();753
let mut last_snapshot = None;754
loop {755
check_start_cancelled(cancel)?;756
let displays = active_display_topology().map_err(|error| stage("topology", error))?;757
let sources: Vec<_> = displays758
.iter()759
.filter(|display| display.virtual_display)760
.cloned()761
.collect();762
let snapshot = format_topology_snapshot(plan, &sources);763
if last_snapshot.as_deref() != Some(snapshot.as_str()) {764
reporter(MappingEvent::Topology {765
message: snapshot.clone(),766
});767
last_snapshot = Some(snapshot.clone());768
}769
if let Some(source) = sources770
.iter()771
.find(|source| !requested_ids.contains(&source.connector_index))772
{773
return Err(MappingError {774
stage: "topology",775
group_id: None,776
message: format!(777
"unexpected SBMS virtual connector {} is active",778
source.connector_index779
),780
cleanup_incomplete: false,781
});782
}783
let mut seen = HashSet::new();784
for source in &sources {785
if !seen.insert(source.connector_index) {786
return Err(group_stage(787
source.connector_index,788
"topology",789
"more than one virtual source reported the same connector",790
));791
}792
}793
if sources.len() == plan.groups.len() {794
let mut device_names = HashSet::with_capacity(sources.len());795
for source in &sources {796
if !device_names.insert(source.device_name.to_ascii_lowercase()) {797
return Err(group_stage(798
source.connector_index,799
"topology",800
"virtual connectors were cloned onto one GDI source",801
));802
}803
}804
}805
806
let mut ordered = Vec::with_capacity(plan.groups.len());807
let mut all_ready = true;808
for group in &plan.groups {809
let Some(source) = sources810
.iter()811
.find(|source| source.connector_index == group.id)812
else {813
all_ready = false;814
continue;815
};816
if source_matches_mode(source, group.mode, group.rotation) {817
ordered.push(source.clone());818
continue;819
}820
all_ready = false;821
if mode_applied.insert(group.id) {822
reporter(MappingEvent::Topology {823
message: format!(824
"group {} applying mode on device={} requested={}",825
group.id,826
source.device_name,827
format_requested_mode(group.mode, group.rotation)828
),829
});830
match apply_display_mode(source, group.mode) {831
Ok(()) => reporter(MappingEvent::Topology {832
message: format!(833
"group {} mode apply accepted on device={}; waiting for topology",834
group.id, source.device_name835
),836
}),837
Err(error) => {838
reporter(MappingEvent::Topology {839
message: format!(840
"group {} mode apply rejected on device={}: {}",841
group.id, source.device_name, error842
),843
});844
return Err(group_stage(group.id, "mode", error));845
}846
}847
}848
}849
if all_ready && ordered.len() == plan.groups.len() {850
let hydrated = active_displays().map_err(|error| stage("topology", error))?;851
if let Some(ordered) = ordered_ready_sources(plan, &hydrated) {852
return Ok(ordered);853
}854
}855
if Instant::now() >= deadline {856
let pending = format_pending_topology(plan, &sources);857
reporter(MappingEvent::Topology {858
message: format!(859
"timeout after {} seconds; {snapshot}",860
topology_timeout.as_secs()861
),862
});863
return Err(MappingError {864
stage: "topology",865
group_id: None,866
message: format!(867
"virtual displays were not ready after {} seconds; {pending}",868
topology_timeout.as_secs()869
),870
cleanup_incomplete: false,871
});872
}873
thread::sleep(POLL_INTERVAL);874
}875
}876
877
fn topology_timeout(group_count: usize) -> Duration {878
let extra_groups = group_count.saturating_sub(8) as u64;879
Duration::from_secs(880
BASE_TOPOLOGY_TIMEOUT_SECS + extra_groups * TOPOLOGY_TIMEOUT_SECS_PER_GROUP_OVER_EIGHT,881
)882
}883
884
fn check_start_cancelled(cancel: &AtomicBool) -> Result<(), MappingError> {885
if cancel.load(Ordering::Acquire) {886
Err(start_cancelled())887
} else {888
Ok(())889
}890
}891
892
fn start_cancelled() -> MappingError {893
stage("cancelled", "mapping startup was cancelled")894
}895
896
fn ordered_ready_sources(plan: &MappingPlan, displays: &[Display]) -> Option<Vec<Display>> {897
let sources: Vec<_> = displays898
.iter()899
.filter(|display| display.virtual_display)900
.collect();901
if sources.len() != plan.groups.len() {902
return None;903
}904
let mut connector_indices = HashSet::with_capacity(sources.len());905
let mut device_names = HashSet::with_capacity(sources.len());906
if sources.iter().any(|source| {907
!connector_indices.insert(source.connector_index)908
|| !device_names.insert(source.device_name.to_ascii_lowercase())909
}) {910
return None;911
}912
plan.groups913
.iter()914
.map(|group| {915
sources916
.iter()917
.find(|source| {918
source.connector_index == group.id919
&& source_matches_mode(source, group.mode, group.rotation)920
})921
.map(|source| (*source).clone())922
})923
.collect()924
}925
926
fn format_topology_snapshot(plan: &MappingPlan, sources: &[Display]) -> String {927
plan.groups928
.iter()929
.map(|group| format_group_topology(group, sources))930
.collect::<Vec<_>>()931
.join("; ")932
}933
934
fn format_pending_topology(plan: &MappingPlan, sources: &[Display]) -> String {935
plan.groups936
.iter()937
.filter(|group| {938
!sources.iter().any(|source| {939
source.connector_index == group.id940
&& source_matches_mode(source, group.mode, group.rotation)941
})942
})943
.map(|group| format_group_topology(group, sources))944
.collect::<Vec<_>>()945
.join("; ")946
}947
948
fn format_group_topology(group: &MappingGroupRequest, sources: &[Display]) -> String {949
let requested = format_requested_mode(group.mode, group.rotation);950
let Some(source) = sources951
.iter()952
.find(|source| source.connector_index == group.id)953
else {954
return format!("group {} absent requested={requested}", group.id);955
};956
let state = if source_matches_mode(source, group.mode, group.rotation) {957
"ready"958
} else {959
"observed"960
};961
format!(962
"group {} {} device={} actual={} requested={}",963
group.id,964
state,965
source.device_name,966
format_observed_mode(source),967
requested968
)969
}970
971
fn format_requested_mode(mode: VirtualMode, rotation: Rotation) -> String {972
format!(973
"{}x{}@{}/{} orientation={} topology_rotation={}",974
mode.width,975
mode.height,976
mode.refresh_numerator,977
mode.refresh_denominator,978
rotation_degrees(rotation),979
rotation_degrees(native_topology_rotation(rotation))980
)981
}982
983
fn format_observed_mode(source: &Display) -> String {984
format!(985
"{}x{}@{}/{} rotation={}",986
source.rect.right - source.rect.left,987
source.rect.bottom - source.rect.top,988
source.refresh_numerator,989
source.refresh_denominator,990
rotation_degrees(source.rotation)991
)992
}993
994
fn rotation_degrees(rotation: Rotation) -> u16 {995
match rotation {996
Rotation::Deg0 => 0,997
Rotation::Deg90 => 90,998
Rotation::Deg180 => 180,999
Rotation::Deg270 => 270,1000
}1001
}1002
1003
fn wait_for_sources_removed(connector_indices: &[u32]) -> Result<(), MappingError> {1004
let deadline = Instant::now() + topology_timeout(connector_indices.len());1005
loop {1006
let displays = active_display_topology().map_err(|error| stage("remove", error))?;1007
if !displays.iter().any(|display| {1008
display.virtual_display && connector_indices.contains(&display.connector_index)1009
}) {1010
return Ok(());1011
}1012
if Instant::now() >= deadline {1013
return Err(MappingError {1014
stage: "remove",1015
group_id: None,1016
message: "virtual sources stayed active after the device handle closed".into(),1017
cleanup_incomplete: false,1018
});1019
}1020
thread::sleep(POLL_INTERVAL);1021
}1022
}1023
1024
pub(crate) fn native_topology_rotation(_rotation: Rotation) -> Rotation {1025
Rotation::Deg01026
}1027
1028
fn source_matches_mode(1029
source: &Display,1030
requested: VirtualMode,1031
requested_rotation: Rotation,1032
) -> bool {1033
let width = source.rect.right - source.rect.left;1034
let height = source.rect.bottom - source.rect.top;1035
width == requested.width as i321036
&& height == requested.height as i321037
&& source.rotation == native_topology_rotation(requested_rotation)1038
&& u64::from(source.refresh_numerator) * u64::from(requested.refresh_denominator)1039
== u64::from(requested.refresh_numerator) * u64::from(source.refresh_denominator)1040
}1041
1042
fn stage(stage: &'static str, error: impl FmtDisplay) -> MappingError {1043
MappingError {1044
stage,1045
group_id: None,1046
message: error.to_string(),1047
cleanup_incomplete: false,1048
}1049
}1050
1051
fn group_stage(group_id: u32, stage: &'static str, error: impl FmtDisplay) -> MappingError {1052
MappingError {1053
stage,1054
group_id: Some(group_id),1055
message: error.to_string(),1056
cleanup_incomplete: false,1057
}1058
}1059
1060
#[cfg(test)]1061
mod tests {1062
use super::*;1063
use std::sync::atomic::AtomicBool;1064
1065
fn group(id: u32, route: MappingRoute) -> MappingGroupRequest {1066
MappingGroupRequest {1067
id,1068
mode: VirtualMode::default(),1069
rotation: Rotation::Deg0,1070
route,1071
}1072
}1073
1074
fn virtual_source(connector_index: u32) -> Display {1075
Display {1076
id: format!("virtual-{connector_index}"),1077
connector_index,1078
sunshine_id: Some(format!("sunshine-{connector_index}")),1079
name: "SBMS".into(),1080
device_name: format!(r"\\.\DISPLAY{}", connector_index + 20),1081
rect: windows::Win32::Foundation::RECT {1082
left: 0,1083
top: 0,1084
right: 3840,1085
bottom: 2160,1086
},1087
native_width: 3840,1088
native_height: 2160,1089
physical_width_mm: None,1090
physical_height_mm: None,1091
rotation: Rotation::Deg0,1092
refresh_numerator: 240,1093
refresh_denominator: 1,1094
primary: false,1095
virtual_display: true,1096
}1097
}1098
1099
#[test]1100
fn mixed_plan_preserves_stable_connector_ids() {1101
let plan = MappingPlan::new(vec![1102
group(1103
3,1104
MappingRoute::Mirror {1105
target: "physical-a".into(),1106
},1107
),1108
group(1, MappingRoute::StreamOnly),1109
])1110
.unwrap();1111
1112
assert_eq!(plan.groups[0].id, 3);1113
assert_eq!(plan.groups[1].id, 1);1114
}1115
1116
#[test]1117
fn invalid_plans_fail_before_starting_hardware() {1118
assert!(MappingPlan::new(Vec::new()).is_err());1119
assert!(1120
MappingPlan::new(vec![1121
group(0, MappingRoute::StreamOnly),1122
group(0, MappingRoute::StreamOnly),1123
])1124
.is_err()1125
);1126
assert!(1127
MappingPlan::new(vec![1128
group(1129
0,1130
MappingRoute::Mirror {1131
target: "same".into(),1132
},1133
),1134
group(1135
1,1136
MappingRoute::Mirror {1137
target: "SAME".into(),1138
},1139
),1140
])1141
.is_err()1142
);1143
assert!(1144
MappingPlan::new(vec![group(MAX_OUTPUTS as u32, MappingRoute::StreamOnly,)]).is_err()1145
);1146
}1147
1148
#[test]1149
fn cancelled_start_stops_before_hardware_and_invalid_plan_still_wins() {1150
let cancel = AtomicBool::new(true);1151
let valid = MappingPlan {1152
groups: vec![group(0, MappingRoute::StreamOnly)],1153
};1154
let Err(cancelled) =1155
MappingSession::start_plan_with_reporter_cancellable(valid, Arc::new(|_| {}), &cancel)1156
else {1157
panic!("pre-cancelled mapping unexpectedly started");1158
};1159
assert!(cancelled.is_clean_cancellation());1160
let mut message_only = start_cancelled();1161
message_only1162
.message1163
.push_str("; rollback was incomplete: cleanup failed");1164
assert!(message_only.is_clean_cancellation());1165
1166
let mut incomplete = start_cancelled();1167
incomplete.record_cleanup_failure("cleanup failed");1168
assert!(!incomplete.is_clean_cancellation());1169
assert!(incomplete.message().contains("rollback was incomplete"));1170
1171
let invalid = MappingPlan { groups: Vec::new() };1172
let Err(plan_error) = MappingSession::start_plan_with_reporter_cancellable(1173
invalid,1174
Arc::new(|_| {}),1175
&cancel,1176
) else {1177
panic!("invalid mapping unexpectedly started");1178
};1179
assert_eq!(plan_error.stage(), "plan");1180
assert!(!plan_error.is_clean_cancellation());1181
}1182
1183
#[test]1184
fn plan_json_is_an_explicit_core_interface() {1185
let json = r#"{1186
"groups": [1187
{1188
"id": 0,1189
"mode": {1190
"width": 3840,1191
"height": 2160,1192
"refresh_numerator": 240,1193
"refresh_denominator": 11194
},1195
"route": { "kind": "stream_only" }1196
}1197
]1198
}"#;1199
1200
let plan: MappingPlan = serde_json::from_str(json).unwrap();1201
plan.validate().unwrap();1202
assert_eq!(plan.groups[0].route, MappingRoute::StreamOnly);1203
assert_eq!(plan.groups[0].rotation, Rotation::Deg0);1204
}1205
1206
#[test]1207
fn native_modes_keep_final_dimensions_and_rebase_orientation() {1208
let native = VirtualMode {1209
width: 1800,1210
height: 2880,1211
refresh_numerator: 120,1212
refresh_denominator: 1,1213
};1214
assert_eq!(native.width, 1800);1215
assert_eq!(native.height, 2880);1216
assert_eq!(native_topology_rotation(Rotation::Deg0), Rotation::Deg0);1217
assert_eq!(native_topology_rotation(Rotation::Deg90), Rotation::Deg0);1218
assert_eq!(native_topology_rotation(Rotation::Deg180), Rotation::Deg0);1219
assert_eq!(native_topology_rotation(Rotation::Deg270), Rotation::Deg0);1220
}1221
1222
#[test]1223
fn native_portrait_source_satisfies_a_270_degree_request() {1224
let requested = VirtualMode {1225
width: 1800,1226
height: 2880,1227
refresh_numerator: 120,1228
refresh_denominator: 1,1229
};1230
let source = Display {1231
id: "virtual".into(),1232
connector_index: 1,1233
sunshine_id: None,1234
name: "SBMS".into(),1235
device_name: r"\\.\DISPLAY73".into(),1236
rect: windows::Win32::Foundation::RECT {1237
left: 0,1238
top: 0,1239
right: 1800,1240
bottom: 2880,1241
},1242
native_width: 1800,1243
native_height: 2880,1244
physical_width_mm: None,1245
physical_height_mm: None,1246
rotation: Rotation::Deg0,1247
refresh_numerator: 120,1248
refresh_denominator: 1,1249
primary: false,1250
virtual_display: true,1251
};1252
1253
assert!(source_matches_mode(&source, requested, Rotation::Deg270));1254
}1255
1256
#[test]1257
fn topology_timeout_scales_only_above_the_previous_eight_group_limit() {1258
assert_eq!(topology_timeout(1), Duration::from_secs(15));1259
assert_eq!(topology_timeout(8), Duration::from_secs(15));1260
assert_eq!(topology_timeout(9), Duration::from_secs(17));1261
assert_eq!(topology_timeout(16), Duration::from_secs(31));1262
}1263
1264
#[test]1265
fn topology_snapshot_explains_an_absent_connector() {1266
let plan = MappingPlan::new(vec![group(1, MappingRoute::StreamOnly)]).unwrap();1267
1268
assert_eq!(1269
format_topology_snapshot(&plan, &[]),1270
"group 1 absent requested=3840x2160@240/1 orientation=0 topology_rotation=0"1271
);1272
}1273
1274
#[test]1275
fn hydrated_sources_follow_plan_order() {1276
let plan = MappingPlan::new(vec![1277
group(3, MappingRoute::StreamOnly),1278
group(1, MappingRoute::StreamOnly),1279
])1280
.unwrap();1281
let displays = vec![virtual_source(1), virtual_source(3)];1282
1283
let ordered = ordered_ready_sources(&plan, &displays).unwrap();1284
1285
assert_eq!(1286
ordered1287
.iter()1288
.map(|display| display.connector_index)1289
.collect::<Vec<_>>(),1290
vec![3, 1]1291
);1292
assert_eq!(ordered[0].sunshine_id.as_deref(), Some("sunshine-3"));1293
}1294
1295
#[test]1296
fn hydrated_sources_reject_changed_or_cloned_topology() {1297
let plan = MappingPlan::new(vec![1298
group(0, MappingRoute::StreamOnly),1299
group(1, MappingRoute::StreamOnly),1300
])1301
.unwrap();1302
let first = virtual_source(0);1303
let mut second = virtual_source(1);1304
second.device_name = first.device_name.clone();1305
assert!(ordered_ready_sources(&plan, &[first.clone(), second]).is_none());1306
1307
let mut wrong_mode = virtual_source(1);1308
wrong_mode.rect.right = 1920;1309
assert!(ordered_ready_sources(&plan, &[first, wrong_mode]).is_none());1310
}1311
1312
#[test]1313
fn recovery_requires_three_identical_topology_samples() {1314
let signature = recovery_topology_signature(&[virtual_source(0)]);1315
let mut stability = RecoveryStability::default();1316
1317
assert!(!stability.observe(Some(signature.clone())));1318
assert!(!stability.observe(Some(signature.clone())));1319
assert!(stability.observe(Some(signature)));1320
}1321
1322
#[test]1323
fn recovery_stability_resets_when_position_changes_or_source_disappears() {1324
let first = virtual_source(0);1325
let first_signature = recovery_topology_signature(std::slice::from_ref(&first));1326
let mut moved = first;1327
moved.rect.left = -3840;1328
moved.rect.right = 0;1329
let moved_signature = recovery_topology_signature(&[moved]);1330
let mut stability = RecoveryStability::default();1331
1332
assert!(!stability.observe(Some(first_signature.clone())));1333
assert!(!stability.observe(Some(first_signature)));1334
assert!(!stability.observe(Some(moved_signature.clone())));1335
assert!(!stability.observe(None));1336
assert!(!stability.observe(Some(moved_signature.clone())));1337
assert!(!stability.observe(Some(moved_signature.clone())));1338
assert!(stability.observe(Some(moved_signature)));1339
}1340
1341
#[test]1342
fn renderer_events_are_buffered_until_the_candidate_is_installed() {1343
let (tx, rx) = std::sync::mpsc::channel();1344
let reporter: MappingReporter = Arc::new(move |event| tx.send(event).unwrap());1345
let gate = RendererEventGate::new(3, reporter);1346
1347
gate.report(RendererEvent::TopologyLost);1348
assert!(rx.try_recv().is_err());1349
gate.activate();1350
assert!(matches!(1351
rx.recv().unwrap(),1352
MappingEvent::Renderer {1353
id: 3,1354
event: RendererEvent::TopologyLost1355
}1356
));1357
}1358
1359
#[test]1360
fn renderer_events_from_discarded_candidates_stay_suppressed() {1361
let (tx, rx) = std::sync::mpsc::channel();1362
let reporter: MappingReporter = Arc::new(move |event| tx.send(event).unwrap());1363
let gate = RendererEventGate::new(4, reporter);1364
1365
gate.report(RendererEvent::TopologyLost);1366
gate.discard();1367
gate.report(RendererEvent::Failed("stale".into()));1368
1369
assert!(rx.try_recv().is_err());1370
}1371
}1372