Files
feresaria/compositor/src/ipc/outputs.rs
T

319 lines
10 KiB
Rust

use std::sync::Arc;
use ferese_core::{OutputId, WorkspaceId};
use serde_json::Value;
use smithay::utils::{Logical, Rectangle};
use crate::backends::direct::ConnectedOutputInfo;
/// Connector data and policy borrowed for an exact, allocation-free comparison.
pub(crate) struct State<'a> {
pub connected: &'a [ConnectedOutputInfo],
pub focused: Option<OutputId>,
pub error: Option<&'a str>,
pub confirmation_pending: bool,
pub low_power: bool,
}
#[derive(Clone, Copy, PartialEq)]
pub(crate) struct Presentation {
pub id: Option<OutputId>,
pub geometry: Option<Rectangle<i32, Logical>>,
pub workspace: Option<WorkspaceId>,
pub scale: f64,
pub transform: &'static str,
}
struct Entry {
connected: Vec<ConnectedOutputInfo>,
focused: Option<OutputId>,
error: Option<String>,
confirmation_pending: bool,
low_power: bool,
presentations: Vec<Presentation>,
unlocked: Arc<Value>,
locked: Option<Arc<Value>>,
}
#[derive(Default)]
pub(crate) struct Cache {
entry: Option<Entry>,
}
impl Cache {
pub(crate) fn clear(&mut self) {
self.entry = None;
}
pub(crate) fn get_or_update(
&mut self,
state: State<'_>,
presentations: impl Iterator<Item = Presentation> + Clone,
locked: bool,
build: impl FnOnce() -> Value,
) -> Arc<Value> {
let unchanged = self.entry.as_ref().is_some_and(|entry| {
entry.connected == state.connected
&& entry.focused == state.focused
&& entry.error.as_deref() == state.error
&& entry.confirmation_pending == state.confirmation_pending
&& entry.low_power == state.low_power
&& presentations
.clone()
.eq(entry.presentations.iter().copied())
});
if !unchanged {
self.entry = Some(Entry {
connected: state.connected.to_vec(),
focused: state.focused,
error: state.error.map(str::to_owned),
confirmation_pending: state.confirmation_pending,
low_power: state.low_power,
presentations: presentations.collect(),
unlocked: Arc::new(build()),
locked: None,
});
}
let entry = self.entry.as_mut().expect("output snapshot was populated");
if locked {
entry
.locked
.get_or_insert_with(|| {
let mut value = entry.unlocked.as_ref().clone();
redact(&mut value);
Arc::new(value)
})
.clone()
} else {
entry.unlocked.clone()
}
}
}
pub(super) fn redact(value: &mut Value) {
for output in value.as_array_mut().into_iter().flatten() {
if let Some(fields) = output.as_object_mut() {
fields.remove("workspace");
fields.remove("focused");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backends::direct::ConnectedModeInfo;
use crate::config::OutputTransform;
use serde_json::json;
fn connector() -> ConnectedOutputInfo {
let mode = ConnectedModeInfo {
width: 3840,
height: 2160,
refresh_millihertz: 60_000,
preferred: true,
};
ConnectedOutputInfo {
connector: "HDMI-A-1".into(),
connected: true,
identity: "test-monitor".into(),
internal: false,
requested_enabled: true,
mirror_source: None,
enabled: true,
profile: Some("desktop".into()),
requested_profile: None,
auto_refresh: true,
physical_size: Some((600, 340)),
current_mode: Some(mode),
available_modes: vec![mode],
scale: 1.75,
transform: OutputTransform::Normal,
configured_position: Some([0, 0]),
}
}
fn state(connected: &[ConnectedOutputInfo]) -> State<'_> {
State {
connected,
focused: Some(OutputId(1)),
error: None,
confirmation_pending: false,
low_power: false,
}
}
fn presentation() -> Presentation {
Presentation {
id: Some(OutputId(1)),
geometry: Some(Rectangle::new((0, 0).into(), (2195, 1235).into())),
workspace: Some(WorkspaceId(3)),
scale: 1.75,
transform: "normal",
}
}
fn value() -> Value {
json!([{"id": 1, "workspace": 3, "focused": true, "scale": 1.75}])
}
#[test]
fn unchanged_outputs_reuse_storage_without_serializing_and_release_when_cleared() {
let connected = [connector()];
let mut cache = Cache::default();
let first = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
value,
);
for _ in 0..1000 {
let next = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
|| panic!("unchanged outputs must not rebuild JSON"),
);
assert!(Arc::ptr_eq(&first, &next));
}
cache.clear();
let next = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
value,
);
assert!(!Arc::ptr_eq(&first, &next));
assert_eq!(first, next);
}
#[test]
fn lock_redaction_is_cached_without_mutating_public_or_older_snapshots() {
let connected = [connector()];
let mut cache = Cache::default();
let public = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
value,
);
let locked = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
true,
|| panic!("lock redaction must reuse connector data"),
);
assert_eq!(*locked, json!([{"id": 1, "scale": 1.75}]));
assert_eq!(*public, value());
let again = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
true,
|| panic!("repeated locked snapshots must not rebuild"),
);
assert!(Arc::ptr_eq(&locked, &again));
let unlocked = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
|| panic!("unlock must reuse public data"),
);
assert!(Arc::ptr_eq(&public, &unlocked));
let changed = cache.get_or_update(state(&[]), [].into_iter(), true, || json!([]));
assert_eq!(*changed, json!([]));
assert_eq!(*locked, json!([{"id": 1, "scale": 1.75}]));
}
#[test]
fn connector_inventory_and_every_reported_property_invalidate_the_snapshot() {
let changes: &[fn(&mut ConnectedOutputInfo)] = &[
|info| info.connector.push('2'),
|info| info.connected = false,
|info| info.identity.push('2'),
|info| info.internal = true,
|info| info.requested_enabled = false,
|info| info.mirror_source = Some("DP-1".into()),
|info| info.enabled = false,
|info| info.profile = None,
|info| info.requested_profile = Some("mobile".into()),
|info| info.auto_refresh = false,
|info| info.physical_size = None,
|info| info.current_mode = None,
|info| info.available_modes[0].width = 1920,
|info| info.available_modes[0].height = 1080,
|info| info.available_modes[0].refresh_millihertz = 120_000,
|info| info.available_modes[0].preferred = false,
|info| info.available_modes.clear(),
|info| info.scale = 1.5,
|info| info.transform = OutputTransform::Rotate90,
|info| info.configured_position = None,
];
for (index, change) in changes.iter().enumerate() {
let mut cache = Cache::default();
let mut connected = [connector()];
let first = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
value,
);
change(&mut connected[0]);
let next = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
|| json!({"changed": index}),
);
assert_eq!(*next, json!({"changed": index}));
assert!(!Arc::ptr_eq(&first, &next));
}
let mut cache = Cache::default();
let first = cache.get_or_update(
state(&[connector()]),
[presentation()].into_iter(),
false,
value,
);
let next = cache.get_or_update(state(&[]), [].into_iter(), false, || json!([]));
assert_eq!(*next, json!([]));
assert_eq!(*first, value());
}
#[test]
fn focus_workspace_geometry_and_output_policy_changes_refresh_the_snapshot() {
let connected = [connector()];
for index in 0..10 {
let mut cache = Cache::default();
let first = cache.get_or_update(
state(&connected),
[presentation()].into_iter(),
false,
value,
);
let mut state = state(&connected);
let mut presentation = presentation();
match index {
0 => state.focused = None,
1 => state.error = Some("output rejected"),
2 => state.confirmation_pending = true,
3 => state.low_power = true,
4 => presentation.id = None,
5 => presentation.geometry = None,
6 => presentation.geometry.as_mut().unwrap().loc.x += 100,
7 => presentation.workspace = Some(WorkspaceId(4)),
8 => presentation.scale = 1.25,
9 => presentation.transform = "rotate_90",
_ => unreachable!(),
}
let next = cache.get_or_update(
state,
[presentation].into_iter(),
false,
|| json!({"changed": index}),
);
assert_eq!(*next, json!({"changed": index}));
assert!(!Arc::ptr_eq(&first, &next));
}
}
}