crcbl-shell 0.7.0

Crucible windowing shell: own Wayland, X11, Web/canvas and headless backends
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
//! Monitor enumeration through RandR, and the fallback when there is none.
//!
//! # Decision: enumerate CRTCs, not RandR 1.5 monitors
//!
//! RandR 1.5's `GetMonitors` is the newer and tidier request: it hands back a
//! list with names, geometry and a primary flag already assembled, and it
//! understands a monitor that spans two outputs. It is not what this backend
//! uses, for one concrete reason — **it does not carry a refresh rate.**
//!
//! [`MonitorInfo::refresh_millihertz`] exists because frame pacing needs it and
//! because 59.94 Hz is not 60, and RandR only ever states a rate as
//! `dot_clock / (htotal × vtotal)` on a *mode*, which is reached through a
//! CRTC. So the choice was `GetMonitors` plus a second CRTC walk to fill in the
//! rate, or the CRTC walk alone — and the CRTC walk alone also works on RandR
//! 1.2, which is a decade more of servers, including the `Xvfb` the end-to-end
//! suite runs against.
//!
//! What it costs is the 1.5 feature this gives up: two outputs configured as
//! one logical monitor appear as two here. That is rare, it is visible in a
//! settings screen rather than broken, and it is stated rather than hidden.
//!
//! # One coordinate space, unlike Wayland
//!
//! A CRTC's `x`/`y` are in the **screen's** pixels, and every window's position
//! is in the same space. So [`MonitorInfo::bounds`] tiles here — the thing that
//! type's own documentation says is *not* true on a Wayland desktop at mixed
//! scales, where each output has its own device-pixel space and only the
//! compositor's logical space is globally coherent.
//!
//! That is the fact behind [`ShellCaps::WINDOW_POSITION`](crate::ShellCaps) on
//! this backend, and behind
//! [`DisplayMode::Borderless`](crate::DisplayMode::Borderless) with a named
//! monitor working at all: the backend can move a window onto a chosen
//! rectangle because it and the rectangle are numbers in the same space.

use super::{Conn, MonitorId, MonitorInfo, PhysicalRect, X11Shell, ffi};

/// One CRTC's geometry, before it is turned into a [`MonitorInfo`].
struct Crtc {
    id: u32,
    x: i16,
    y: i16,
    width: u16,
    height: u16,
    mode: u32,
    output: Option<u32>,
    /// The refresh of [`mode`](Self::mode), read out of the same screen-resources
    /// reply the CRTC ids came from rather than out of one per CRTC.
    refresh_millihertz: u32,
}

impl X11Shell {
    /// Every monitor the server currently has.
    ///
    /// Called at [`open`](X11Shell::open) and again whenever RandR says the
    /// layout changed. [`MonitorId`]s are assigned through
    /// [`monitor_id_for`](Self::monitor_id_for), so a monitor that stays
    /// connected keeps its id across a hotplug of some *other* monitor — the
    /// obligation [`monitor`](crate::monitor) states, and the reason a
    /// borderless window does not jump to a different display when an
    /// unrelated cable is pulled.
    pub(super) fn enumerate_monitors(&mut self) -> Vec<MonitorInfo> {
        let scale = self.scale_factor;
        let work_area = self.conn.read_work_area();
        let Some(crtcs) = self.conn.read_crtcs() else {
            return vec![self.fallback_monitor(scale, work_area)];
        };
        if crtcs.is_empty() {
            return vec![self.fallback_monitor(scale, work_area)];
        }
        let primary_output = self.conn.read_primary_output();

        let mut monitors = Vec::with_capacity(crtcs.len());
        for crtc in crtcs {
            let bounds = PhysicalRect::new(
                i32::from(crtc.x),
                i32::from(crtc.y),
                u32::from(crtc.width),
                u32::from(crtc.height),
            );
            let (name, is_primary) = crtc.output.map_or_else(
                || (format!("CRTC-{}", crtc.id), false),
                |output| {
                    (
                        self.conn.read_output_name(output),
                        primary_output == Some(output),
                    )
                },
            );
            monitors.push(MonitorInfo {
                id: self.monitor_id_for(crtc.id),
                name,
                bounds,
                work_area: work_area.map_or(bounds, |area| intersect(bounds, area)),
                scale_factor: scale,
                refresh_millihertz: crtc.refresh_millihertz,
                is_primary,
            });
        }
        // The primary flag is a hint a consumer defaults to, so exactly one
        // monitor must carry it even on a server that never set one.
        if !monitors.iter().any(|monitor| monitor.is_primary)
            && let Some(first) = monitors.first_mut()
        {
            first.is_primary = true;
        }
        monitors
    }

    /// The single monitor a server with no usable RandR has.
    ///
    /// Not a stub: a server without RandR genuinely has one screen of a known
    /// size, and reporting that is the correct answer. What is missing is the
    /// refresh rate, and [`MonitorInfo::refresh_millihertz`] already defines
    /// zero as "the backend cannot determine it" rather than as 0 Hz.
    fn fallback_monitor(&mut self, scale: f64, work_area: Option<PhysicalRect>) -> MonitorInfo {
        let bounds = PhysicalRect::new(
            0,
            0,
            self.conn.screen_size.width,
            self.conn.screen_size.height,
        );
        MonitorInfo {
            // CRTC ids start at 1 on every server; `0` is `XCB_NONE` and can
            // never collide with a real one, so the fallback monitor keeps a
            // stable id across a RandR that appears mid-session.
            id: self.monitor_id_for(0),
            name: "screen".to_string(),
            bounds,
            work_area: work_area.map_or(bounds, |area| intersect(bounds, area)),
            scale_factor: scale,
            refresh_millihertz: 0,
            is_primary: true,
        }
    }

    /// The [`MonitorId`] for a CRTC, allocating one the first time.
    ///
    /// Never reuses an id within the session, which is exactly the obligation
    /// [`monitor`](crate::monitor) puts on a backend whose native ids *can* be
    /// recycled — and RandR CRTC ids can, after a hotplug cycle.
    fn monitor_id_for(&mut self, crtc: u32) -> MonitorId {
        if let Some((_, id)) = self.monitor_ids.iter().find(|(known, _)| *known == crtc) {
            return *id;
        }
        let id = MonitorId(self.next_monitor_id);
        self.next_monitor_id += 1;
        self.monitor_ids.push((crtc, id));
        id
    }
}

/// The refresh rate of one RandR mode, in millihertz.
///
/// `dot_clock / (htotal × vtotal)` — the calculation that produces 59.94 rather
/// than 60, which is the whole reason [`MonitorInfo::refresh_millihertz`] is an
/// integer count of thousandths. Interlaced modes report the field rate, which
/// is twice the frame rate, and double-scan modes draw each line twice for half
/// of it; both conventions are what every tool implements and what a user
/// comparing this against `xrandr` will see.
///
/// Zero for a mode with no timing — a total of zero would be a division by it,
/// and [`MonitorInfo::refresh_millihertz`] already defines zero as "the backend
/// cannot determine it".
fn refresh_millihertz(info: &ffi::RandrModeInfo) -> u32 {
    let total = u64::from(info.htotal) * u64::from(info.vtotal);
    if total == 0 {
        return 0;
    }
    /// `RR_Interlace`.
    const INTERLACE: u32 = 1 << 4;
    /// `RR_DoubleScan`.
    const DOUBLE_SCAN: u32 = 1 << 5;
    let mut millihertz = u64::from(info.dot_clock) * 1_000 / total;
    if info.mode_flags & DOUBLE_SCAN != 0 {
        millihertz /= 2;
    }
    if info.mode_flags & INTERLACE != 0 {
        millihertz *= 2;
    }
    u32::try_from(millihertz).unwrap_or(0)
}

impl Conn {
    /// Every enabled CRTC, or `None` when RandR is unusable.
    ///
    /// The two-phase shape [`atoms`](super::atoms) argues for: every
    /// `GetCrtcInfo` is fired before any reply is read, so a six-monitor
    /// desktop costs one round trip rather than six.
    fn read_crtcs(&self) -> Option<Vec<Crtc>> {
        let randr = self.ext.randr?;
        self.randr_event_base?;

        // SAFETY: the connection and root window are live; a null error
        // pointer discards the error and a null reply is handled below.
        let resources = unsafe {
            let cookie = (randr.get_screen_resources_current)(self.raw(), self.root);
            (randr.get_screen_resources_current_reply)(self.raw(), cookie, core::ptr::null_mut())
        };
        if resources.is_null() {
            return None;
        }
        // SAFETY: `resources` is a live reply this call owns, and
        // `xcb_randr_get_screen_resources_current_crtcs` returns a pointer to
        // `num_crtcs` ids inside the same allocation.
        let (config_timestamp, ids) = unsafe {
            let count = usize::from((*resources).num_crtcs);
            let crtcs = (randr.resources_crtcs)(resources);
            let ids = if count > 0 && !crtcs.is_null() {
                core::slice::from_raw_parts(crtcs, count).to_vec()
            } else {
                Vec::new()
            };
            ((*resources).config_timestamp, ids)
        };
        // The mode table, out of the *same* reply. It used to be fetched again
        // per CRTC — a synchronous round trip each, on connect and on every
        // RandR change — for a table that arrives here whole and describes every
        // mode on the screen.
        //
        // SAFETY: `resources` is still live, and
        // `xcb_randr_get_screen_resources_current_modes` points at `num_modes`
        // descriptors inside the same allocation.
        let modes: Vec<(u32, u32)> = unsafe {
            let count = usize::from((*resources).num_modes);
            let modes = (randr.resources_modes)(resources);
            if count == 0 || modes.is_null() {
                Vec::new()
            } else {
                core::slice::from_raw_parts(modes, count)
                    .iter()
                    .map(|info| (info.id, refresh_millihertz(info)))
                    .collect()
            }
        };
        // SAFETY: freed exactly once.
        unsafe { ffi::free_reply(resources) };

        // Phase one: fire every request.
        let cookies: Vec<(u32, ffi::Cookie)> = ids
            .iter()
            .map(|id| {
                // SAFETY: the connection is live and `id` came from the
                // resources reply, so it names a CRTC on this screen.
                (*id, unsafe {
                    (randr.get_crtc_info)(self.raw(), *id, config_timestamp)
                })
            })
            .collect();

        // Phase two: collect.
        let mut crtcs = Vec::new();
        for (id, cookie) in cookies {
            // SAFETY: the cookie came from `get_crtc_info` on this connection.
            let reply =
                unsafe { (randr.get_crtc_info_reply)(self.raw(), cookie, core::ptr::null_mut()) };
            if reply.is_null() {
                continue;
            }
            // SAFETY: `reply` is a live reply this call owns, and
            // `xcb_randr_get_crtc_info_outputs` points at `num_outputs` ids
            // inside it.
            let crtc = unsafe {
                let info = *reply;
                let outputs = (randr.crtc_outputs)(reply);
                let output = (info.num_outputs > 0 && !outputs.is_null()).then(|| *outputs);
                Crtc {
                    id,
                    x: info.x,
                    y: info.y,
                    width: info.width,
                    height: info.height,
                    mode: info.mode,
                    output,
                    refresh_millihertz: modes
                        .iter()
                        .find(|(id, _)| *id == info.mode)
                        .map_or(0, |(_, millihertz)| *millihertz),
                }
            };
            // SAFETY: freed exactly once.
            unsafe { ffi::free_reply(reply) };
            // A CRTC with no mode is *disabled*, not a monitor of size zero.
            // Reporting it would put a phantom display in a settings screen.
            if crtc.mode != 0 && crtc.width > 0 && crtc.height > 0 {
                crtcs.push(crtc);
            }
        }
        Some(crtcs)
    }

    /// An output's name, e.g. `"DP-2"`, or a placeholder.
    fn read_output_name(&self, output: u32) -> String {
        let Some(randr) = self.ext.randr else {
            return format!("output-{output}");
        };
        // SAFETY: the connection is live; `0` is `CurrentTime`, which
        // `GetOutputInfo` accepts.
        let reply = unsafe {
            let cookie = (randr.get_output_info)(self.raw(), output, 0);
            (randr.get_output_info_reply)(self.raw(), cookie, core::ptr::null_mut())
        };
        if reply.is_null() {
            return format!("output-{output}");
        }
        // SAFETY: `reply` is a live reply, and the name pointer names
        // `name_len` bytes inside the same allocation. The name is Latin-1 by
        // protocol and ASCII in practice.
        let name = unsafe {
            let length = usize::from((*reply).name_len);
            let bytes = (randr.output_name)(reply);
            if length == 0 || bytes.is_null() {
                None
            } else {
                Some(
                    core::slice::from_raw_parts(bytes, length)
                        .iter()
                        .map(|byte| char::from(*byte))
                        .collect::<String>(),
                )
            }
        };
        // SAFETY: freed exactly once.
        unsafe { ffi::free_reply(reply) };
        name.unwrap_or_else(|| format!("output-{output}"))
    }

    /// The primary output, or `None` when the server has no notion of one.
    fn read_primary_output(&self) -> Option<u32> {
        let randr = self.ext.randr?;
        // SAFETY: the connection and root are live.
        let reply = unsafe {
            let cookie = (randr.get_output_primary)(self.raw(), self.root);
            (randr.get_output_primary_reply)(self.raw(), cookie, core::ptr::null_mut())
        };
        if reply.is_null() {
            return None;
        }
        // SAFETY: `reply` is a live reply this call owns.
        let output = unsafe { (*reply).output };
        // SAFETY: freed exactly once.
        unsafe { ffi::free_reply(reply) };
        (output != 0).then_some(output)
    }

    /// `_NET_WORKAREA` — the desktop minus panels and docks.
    ///
    /// A window manager property, so it is `None` under bare `Xvfb`, and
    /// [`MonitorInfo::work_area`] then equals `bounds` — which that field's
    /// documentation already defines as the answer when the backend cannot
    /// find out.
    ///
    /// EWMH gives four words *per virtual desktop* and this reads the first,
    /// because the seam has no vocabulary for virtual desktops and the first is
    /// the one the current desktop almost always is. It is a rectangle over the
    /// whole screen rather than per monitor, which is why the caller intersects
    /// it with each monitor's bounds instead of using it directly.
    fn read_work_area(&self) -> Option<PhysicalRect> {
        let words = self.get_property_words(self.root, self.atoms.net_workarea);
        let [x, y, width, height, ..] = words[..] else {
            return None;
        };
        if width == 0 || height == 0 {
            return None;
        }
        Some(PhysicalRect::new(
            i32::try_from(x).unwrap_or(0),
            i32::try_from(y).unwrap_or(0),
            width,
            height,
        ))
    }
}

/// The part of `bounds` inside `area`, or `bounds` when they do not overlap.
///
/// A monitor entirely outside the work area — which happens for a moment during
/// a hotplug, before the window manager recomputes it — must not end up with a
/// zero-sized work area, because a consumer placing a window in it would then
/// place a zero-sized window.
fn intersect(bounds: PhysicalRect, area: PhysicalRect) -> PhysicalRect {
    let left = bounds.x.max(area.x);
    let top = bounds.y.max(area.y);
    let right = (bounds.x.saturating_add_unsigned(bounds.width))
        .min(area.x.saturating_add_unsigned(area.width));
    let bottom = (bounds.y.saturating_add_unsigned(bounds.height))
        .min(area.y.saturating_add_unsigned(area.height));
    if right <= left || bottom <= top {
        return bounds;
    }
    PhysicalRect::new(left, top, right.abs_diff(left), bottom.abs_diff(top))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_work_area_clips_the_monitor_it_overlaps() {
        // A 32-pixel panel across the top of a 1920×1080 primary.
        let bounds = PhysicalRect::new(0, 0, 1920, 1080);
        let area = PhysicalRect::new(0, 32, 1920, 1048);
        assert_eq!(
            intersect(bounds, area),
            PhysicalRect::new(0, 32, 1920, 1048)
        );
    }

    #[test]
    fn a_second_monitor_outside_the_work_area_keeps_its_full_bounds() {
        // `_NET_WORKAREA` is one rectangle over the whole desktop, so a monitor
        // to the right of it does not overlap at all. Clipping to nothing would
        // give a consumer a zero-sized rectangle to place windows in.
        let right = PhysicalRect::new(1920, 0, 1280, 1024);
        let area = PhysicalRect::new(0, 32, 1920, 1048);
        assert_eq!(intersect(right, area), right);
    }

    #[test]
    fn a_partial_overlap_keeps_only_the_overlapping_part() {
        let bounds = PhysicalRect::new(-800, -100, 1600, 1200);
        let area = PhysicalRect::new(0, 0, 1920, 1080);
        assert_eq!(intersect(bounds, area), PhysicalRect::new(0, 0, 800, 1080));
    }

    /// A mode descriptor with the timing fields filled and the rest zero.
    fn mode_with(dot_clock: u32, htotal: u16, vtotal: u16, mode_flags: u32) -> ffi::RandrModeInfo {
        ffi::RandrModeInfo {
            id: 1,
            width: 1920,
            height: 1080,
            dot_clock,
            hsync_start: 0,
            hsync_end: 0,
            htotal,
            hskew: 0,
            vsync_start: 0,
            vsync_end: 0,
            vtotal,
            name_len: 0,
            mode_flags,
        }
    }

    /// The same with no flags, which is every mode a monitor here reports.
    fn mode(dot_clock: u32, htotal: u16, vtotal: u16) -> ffi::RandrModeInfo {
        mode_with(dot_clock, htotal, vtotal, 0)
    }

    /// **59.94 Hz, which is the reason this is millihertz at all.**
    ///
    /// The numbers are CTA-861's 1080p59.94 timing — a 148.352 MHz pixel clock
    /// over a 2200 × 1125 total — and the answer is the one `xrandr` prints for
    /// it. A rate rounded to whole hertz would report 60 and pace a frame loop
    /// against a clock the display does not have.
    #[test]
    fn the_refresh_of_a_59_94_mode_is_not_60() {
        assert_eq!(refresh_millihertz(&mode(148_352_000, 2200, 1125)), 59_940);
        // And the mode it is 0.1% away from, so the test can tell them apart.
        assert_eq!(refresh_millihertz(&mode(148_500_000, 2200, 1125)), 60_000);
    }

    /// Interlace reports the *field* rate and double-scan halves the frame rate,
    /// which is what every tool that reads these flags does.
    #[test]
    fn interlace_doubles_the_rate_and_double_scan_halves_it() {
        /// `RR_Interlace`.
        const INTERLACE: u32 = 1 << 4;
        /// `RR_DoubleScan`.
        const DOUBLE_SCAN: u32 = 1 << 5;
        let plain = refresh_millihertz(&mode(148_500_000, 2200, 1125));
        assert_eq!(
            refresh_millihertz(&mode_with(148_500_000, 2200, 1125, INTERLACE)),
            plain * 2,
        );
        assert_eq!(
            refresh_millihertz(&mode_with(148_500_000, 2200, 1125, DOUBLE_SCAN)),
            plain / 2,
        );
        // Both at once is a mode nothing produces, and the two cancel rather
        // than one of them being ignored.
        assert_eq!(
            refresh_millihertz(&mode_with(148_500_000, 2200, 1125, INTERLACE | DOUBLE_SCAN)),
            plain,
        );
    }

    /// A mode with no timing answers zero rather than dividing by it.
    ///
    /// `MonitorInfo::refresh_millihertz` defines zero as "the backend cannot
    /// determine it", which is the honest answer for a descriptor carrying no
    /// totals — and the arithmetic above would panic in a debug build.
    #[test]
    fn a_mode_with_no_timing_reports_zero() {
        assert_eq!(refresh_millihertz(&mode(148_500_000, 0, 1125)), 0);
        assert_eq!(refresh_millihertz(&mode(148_500_000, 2200, 0)), 0);
        assert_eq!(refresh_millihertz(&mode(0, 2200, 1125)), 0);
    }
}