Commit 9b87063809e1
tarball4 files changed+134-14
@@ -34,6 +34,14 @@ unfocused = "#3b4252"
3434
# Variable refresh rate on outputs that support it, off, on, or fullscreen.3535
vrr = "fullscreen"3636
37
+# Per connector, read when the output connects. transform is normal, 90, 180,38
+# 270, flipped, flipped-90, flipped-180 or flipped-270. Outputs without a39
+# position line up left to right.40
+# [output.DP-1]41
+# transform = "90"42
+# position = [2560, 0]43
+# mode = "1920x1080@60"44
+3745
[animation]3846
enabled = true3947
# orthogonal moves each window along one axis per phase, direct allows both.@@ -78,9 +78,47 @@ impl Default for Gaps {
7878
}7979
8080
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]81
-#[serde(default, deny_unknown_fields)]81
+#[serde(default)]8282
pub struct Outputs {8383
pub vrr: Vrr,84
+ /// Settings for one connector, by name, like `DP-1`.85
+ #[serde(flatten)]86
+ pub named: std::collections::BTreeMap<String, OutputConfig>,87
+}88
+89
+/// Applied when the output connects. Changing them needs a restart or a90
+/// replug, since a new mode or rotation means a modeset.91
+#[derive(Deserialize, Debug, Clone, PartialEq, Default)]92
+#[serde(default, deny_unknown_fields)]93
+pub struct OutputConfig {94
+ pub transform: OutputTransform,95
+ /// Top left corner in the global layout. Outputs without one are placed96
+ /// to the right of the others.97
+ pub position: Option<[i32; 2]>,98
+ /// `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`. Without a rate the highest one99
+ /// for that size wins.100
+ pub mode: Option<String>,101
+}102
+103
+/// Counterclockwise rotation, optionally after flipping horizontally.104
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]105
+#[serde(rename_all = "kebab-case")]106
+pub enum OutputTransform {107
+ #[default]108
+ Normal,109
+ #[serde(rename = "90")]110
+ Rotate90,111
+ #[serde(rename = "180")]112
+ Rotate180,113
+ #[serde(rename = "270")]114
+ Rotate270,115
+ Flipped,116
+ #[serde(rename = "flipped-90")]117
+ Flipped90,118
+ #[serde(rename = "flipped-180")]119
+ Flipped180,120
+ #[serde(rename = "flipped-270")]121
+ Flipped270,84122
}85123
86124
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]@@ -568,6 +606,11 @@ mod tests {
568606
[binds]569607
"Mod+Return" = "spawn beer"570608
"Hyper+x" = "close"609
+ [output.DP-1]610
+ transform = "90"611
+ position = [0, 0]612
+ [output.DP-2]613
+ transform = "sideways"571614
[[rules]]572615
app_id = "("573616
[[rules]]@@ -582,6 +625,8 @@ mod tests {
582625
assert_eq!(c.decoration.radius, Corners([1, 2, 3, 4]));583626
assert_eq!(c.decoration.focused, Decoration::default().focused);584627
assert_eq!((c.binds.len(), c.rules.len()), (1, 1));585
- assert_eq!(loaded.warnings.len(), 5, "{:#?}", loaded.warnings);628
+ assert_eq!(c.output.named["DP-1"].transform, OutputTransform::Rotate90);629
+ assert!(!c.output.named.contains_key("DP-2"));630
+ assert_eq!(loaded.warnings.len(), 6, "{:#?}", loaded.warnings);586631
}587632
}@@ -7,7 +7,7 @@ use crate::scene::Draw;
77
use crate::state::Services;88
use crate::state::{State, now_ns};99
use crate::wm::Wm;10
-use monolith_config::Vrr;10
+use monolith_config::{OutputTransform, Vrr};1111
use monolith_layout::{OutputId, Rect};1212
use smithay::backend::allocator::Fourcc;1313
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};@@ -31,7 +31,7 @@ use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
3131
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};3232
use smithay::reexports::calloop::{EventLoop, LoopHandle, RegistrationToken};3333
use smithay::reexports::drm::Device as _;34
-use smithay::reexports::drm::control::{ModeTypeFlags, connector, crtc};34
+use smithay::reexports::drm::control::{self, ModeTypeFlags, connector, crtc};3535
use smithay::reexports::input::Libinput;3636
use smithay::reexports::rustix::fs::OFlags;3737
use smithay::utils::{DeviceFd, Point, Transform};@@ -265,15 +265,33 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
265265
connector.interface().as_str(),266266
connector.interface_id()267267
);268
- let Some(&mode) = connector269
- .modes()270
- .iter()271
- .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))272
- .or_else(|| connector.modes().first())273
- else {268
+ let config = state269
+ .settings270
+ .config271
+ .output272
+ .named273
+ .get(&name)274
+ .cloned()275
+ .unwrap_or_default();276
+ let chosen = config.mode.as_deref().and_then(|spec| {277
+ let found = pick_mode(connector.modes(), spec);278
+ if found.is_none() {279
+ tracing::warn!(name, spec, "no such mode, using the preferred one");280
+ }281
+ found282
+ });283
+ let Some(mode) = chosen.or_else(|| {284
+ connector285
+ .modes()286
+ .iter()287
+ .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))288
+ .or_else(|| connector.modes().first())289
+ .copied()290
+ }) else {274291
tracing::warn!(name, "connector has no modes");275292
return;276293
};294
+ let transform = transform(config.transform);277295
let State {278296
backend: Backend::Drm(drm),279297
renderer,@@ -296,14 +314,17 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
296314
},297315
);298316
let wl_mode = Mode::from(mode);299
- let x = layout317
+ let right = layout300318
.outputs()301319
.map(|(_, o)| o.area.right())302320
.fold(0.0, f64::max);303
- let position = Point::from((x as i32, 0));321
+ let position = match config.position {322
+ Some([x, y]) => Point::from((x, y)),323
+ None => Point::from((right as i32, 0)),324
+ };304325
output.change_current_state(305326
Some(wl_mode),306
- Some(Transform::Normal),327
+ Some(transform),307328
Some(Scale::Integer(1)),308329
Some(position),309330
);@@ -337,13 +358,24 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
337358
);338359
space.map_output(&output, position);339360
let (w, h) = mode.size();340
- let area = Rect::new(x, 0.0, f64::from(w), f64::from(h));361
+ let size = transform.transform_size(362
+ smithay::utils::Size::<i32, smithay::utils::Logical>::from((i32::from(w), i32::from(h))),363
+ );364
+ let area = Rect::new(365
+ f64::from(position.x),366
+ f64::from(position.y),367
+ f64::from(size.w),368
+ f64::from(size.h),369
+ );341370
let id = layout.add_output(&name, area);342371
tracing::info!(343372
name,344373
w,345374
h,346375
refresh = mode.vrefresh(),376
+ ?transform,377
+ x = position.x,378
+ y = position.y,347379
vrr_capable,348380
"output connected"349381
);@@ -372,6 +404,40 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
372404
);373405
}374406
407
+/// The mode matching `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`, the highest rate408
+/// when none is given and the nearest one when it is.409
+fn pick_mode(modes: &[control::Mode], spec: &str) -> Option<control::Mode> {410
+ let (size, rate) = match spec.split_once('@') {411
+ Some((size, rate)) => (size, Some(rate.trim().parse::<f64>().ok()?)),412
+ None => (spec, None),413
+ };414
+ let (w, h) = size.trim().split_once('x')?;415
+ let size = (w.trim().parse::<u16>().ok()?, h.trim().parse::<u16>().ok()?);416
+ let fitting = modes.iter().filter(|m| m.size() == size);417
+ match rate {418
+ Some(r) => fitting419
+ .min_by(|a, b| {420
+ let off = |m: &&control::Mode| (f64::from(m.vrefresh()) - r).abs();421
+ off(a).total_cmp(&off(b))422
+ })423
+ .copied(),424
+ None => fitting.max_by_key(|m| m.vrefresh()).copied(),425
+ }426
+}427
+428
+fn transform(t: OutputTransform) -> Transform {429
+ match t {430
+ OutputTransform::Normal => Transform::Normal,431
+ OutputTransform::Rotate90 => Transform::_90,432
+ OutputTransform::Rotate180 => Transform::_180,433
+ OutputTransform::Rotate270 => Transform::_270,434
+ OutputTransform::Flipped => Transform::Flipped,435
+ OutputTransform::Flipped90 => Transform::Flipped90,436
+ OutputTransform::Flipped180 => Transform::Flipped180,437
+ OutputTransform::Flipped270 => Transform::Flipped270,438
+ }439
+}440
+375441
fn disconnect(state: &mut State, crtc: crtc::Handle) {376442
let Some(drm) = state.backend.drm_mut() else {377443
return;@@ -33,6 +33,7 @@ nix develop -c sh -c 'RUST_LOG=monolith=debug target/release/monolith run --back
3333
- [ ] **vkcube black start.** Is the half second of black at launch gone?3434
- [ ] **Fullscreen switches.** Toggle fullscreen on vkcube a few times and look for `slow frame` lines. First-time imports now run on a worker thread.3535
- [ ] **Direct scanout.** Fullscreen vkcube should log `direct scanout scanout=true`, and its `frame rate` lines should show `scanned_out` close to `presented`. If it logs `scanout=false` or never switches, the `elements`, `overlay_planes` and `cursor_plane` fields on that line show what was on screen with it.36
+- [ ] **Output rotation.** `[output.DP-1] transform = "90"` with a `position` puts DP-1 on its side where you placed it. The `output connected` log line shows the transform and position. The pointer should cross between outputs where their edges meet.3637
- [ ] **CPU target.** `cpu_p99_us` on the `frame rate` lines should stay under 500, the target in decisions.md.3738
- [ ] **NixOS module.** Add the flake as an input, import `monolith.nixosModules.default` and set `programs.monolith.enable = true`. A `monolith` session appears in the display manager. With the default `realtime-scheduling = true`, the log should show `compositor thread runs SCHED_RR` and `queue global priority priority=Some(REALTIME)`. Programs launched from monolith should not inherit CAP_SYS_NICE, which `grep Cap /proc/(pgrep beer)/status` shows as an all-zero `CapAmb`.