drmkit-core 0.1.2

Core DRM/KMS device access for drmkit: device handles, property caching, and atomic requests.
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
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
// SPDX-License-Identifier: MIT

//! Parity port of `tests/unit/test_device.cpp`, `test_property_store.cpp`, and
//! `test_atomic_builder.cpp` from drm-cxx @ `dc2915b`.
//!
//! The upstream device and atomic suites skip themselves when no card is
//! present. Here the card-dependent cases are `#[ignore]` instead, so they run
//! under the vkms lane with `--include-ignored` and stay visible as skipped
//! rather than silently passing — the same reasoning as the vkms lane's own
//! health checks.

use super::*;
use std::os::fd::{AsRawFd as _, OwnedFd};

/// Serializes the card-dependent cases.
///
/// `Device::open` acquires DRM master, and master is held per open file
/// description: only one can have it at a time. Several of these cases open the
/// same card, so run in parallel they race for it -- one wins and the rest get
/// `EACCES`, which is indistinguishable from a real permission failure. The
/// mutex makes each case the only claimant while it runs.
static CARD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

/// Take the card lock, ignoring poisoning: a panicking case has already failed
/// and must not cascade into every other one.
fn card_guard() -> std::sync::MutexGuard<'static, ()> {
    CARD_LOCK
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

/// The card the ignored cases use, matching the vkms lane's environment.
fn test_card() -> String {
    std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned())
}

/// A `TEST_ONLY` commit requires DRM master. Under virtme-ng the suite runs as
/// root with no display server and always gets it; on a developer desktop a
/// compositor already holds it and every atomic call returns `EACCES`.
///
/// Rather than skip -- which would let the lane pass while testing nothing --
/// the lane sets `DRMKIT_REQUIRE_MASTER=1` and these cases then treat
/// `NotMaster` as a failure. Locally it is reported and tolerated, so
/// `--include-ignored` stays usable without a bare console.
fn check_master_dependent(outcome: Result<()>, what: &str) {
    let require = std::env::var_os("DRMKIT_REQUIRE_MASTER").is_some();
    match outcome {
        Ok(()) => {}
        Err(CoreError::NotMaster) if !require => {
            println!("note: {what} skipped -- another client holds DRM master");
        }
        Err(CoreError::NotMaster) => {
            panic!("{what}: not DRM master, but DRMKIT_REQUIRE_MASTER is set");
        }
        Err(e) => panic!("{what} failed: {e}"),
    }
}

fn open_dev_null() -> OwnedFd {
    std::fs::OpenOptions::new()
        .read(true)
        .write(true)
        .open("/dev/null")
        .expect("/dev/null")
        .into()
}

// --- test_device.cpp ---------------------------------------------------------

/// `DeviceTest.OpenInvalidPathReturnsError`
#[test]
fn open_invalid_path_returns_error() {
    let result = Device::open("/dev/dri/nonexistent_card_999");
    assert!(result.is_err());
}

/// `DeviceTest.OpenEmptyPathReturnsError`
#[test]
fn open_empty_path_returns_error() {
    assert!(Device::open("").is_err());
}

/// A node the caller may not open is a permissions problem, not DRM master.
///
/// `NotMaster` is what a scene suspends on, and its message says "not DRM
/// master" -- the wrong diagnosis for a user missing from the `video` group.
/// The reference returns the open's errno as it is, so this is `Io(EACCES)`.
#[test]
fn open_without_permission_is_io_not_master() {
    let path = std::env::temp_dir().join(format!("drmkit-no-access-{}", std::process::id()));
    std::fs::write(&path, b"").expect("create");
    std::fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o000))
        .expect("chmod");
    // Root opens it regardless, and there is nothing to check.
    let refused = std::fs::File::open(&path).is_err();
    let result = Device::open(&path);
    std::fs::remove_file(&path).expect("remove");
    if !refused {
        return;
    }
    assert!(
        matches!(result, Err(CoreError::Io(rustix::io::Errno::ACCESS))),
        "a permissions failure is not a master failure: {result:?}"
    );
}

/// `DeviceTest.MoveConstructorTransfersFd`
///
/// The C++ case name is aspirational — its body only checks that opening a
/// non-DRM node fails, because it cannot rely on a card in CI. That check is
/// the valuable part and is kept; the actual move semantics are covered below,
/// where Rust can express them.
#[test]
fn open_non_drm_node_is_rejected() {
    let result = Device::open("/dev/null");
    assert!(
        matches!(result, Err(CoreError::NotADrmDevice { .. })),
        "/dev/null is a character device but not a DRM one"
    );
}

/// `DeviceTest.FromFdDoesNotCloseOnDestruction`
///
/// A borrowed device must not close the caller's descriptor — a seat session
/// holding a revocable libseat fd depends on it.
#[test]
fn borrowed_device_does_not_close_the_descriptor() {
    let owned = open_dev_null();
    let raw = owned.as_raw_fd();

    {
        // SAFETY: `owned` outlives this scope, so the borrowed descriptor stays
        // valid for the whole life of the device.
        let device = unsafe { Device::from_borrowed_fd(raw) };
        assert_eq!(device.raw_fd(), raw);
        assert!(!device.owns_fd());
    }

    // Still open: fstat succeeds only on a live descriptor.
    assert!(
        rustix::fs::fstat(&owned).is_ok(),
        "a borrowed device must not close the caller's descriptor"
    );
}

/// `DeviceTest.FromFdMoveDoesNotCloseBorrowedFd`
///
/// Moving preserves the non-owning semantics.
#[test]
fn moving_a_borrowed_device_keeps_it_borrowed() {
    let owned = open_dev_null();
    let raw = owned.as_raw_fd();

    {
        // SAFETY: as above.
        let source = unsafe { Device::from_borrowed_fd(raw) };
        let moved = source;
        assert_eq!(moved.raw_fd(), raw);
        assert!(!moved.owns_fd());
    }

    assert!(rustix::fs::fstat(&owned).is_ok());
}

/// An owning device closes on drop. The C++ has no counterpart: `from_fd` is
/// always non-owning there, because C++ cannot express the transfer in a type.
///
/// Probing whether one specific descriptor *number* is closed does not work:
/// the harness runs cases on parallel threads, and a number freed by this drop
/// can be handed straight to another case's `open`, making the check report
/// "still open". That is exactly how an earlier version of this test passed on
/// `x86_64` and failed under `qemu-riscv64`, where the scheduling differs.
///
/// Measuring the descriptor table instead is race-tolerant in the same way the
/// `drmkit-sync` leak test is: a missing close grows the table once per
/// iteration, so the signal scales with the loop count while concurrent noise
/// stays a small constant.
#[test]
fn owned_device_closes_on_drop() {
    let open_and_drop = |times: usize| {
        for _ in 0..times {
            let device = Device::from_owned_fd(open_dev_null());
            assert!(device.owns_fd());
            drop(device);
        }
    };

    let count = || {
        std::fs::read_dir("/proc/self/fd")
            .expect("/proc/self/fd")
            .count()
    };

    open_and_drop(16);
    let after_few = count();

    open_and_drop(240);
    let after_many = count();

    // The threshold is half the leak, not a tight bound on the noise.
    //
    // A missing close grows the table by exactly 240 here; concurrent cases in
    // this binary opening their own descriptors between the two snapshots grow
    // it by however many they happen to hold at that instant. The first version
    // of this bound was 16, and a loaded workspace run exceeded it -- a false
    // failure, since nothing had leaked. Anything below 120 cannot be the
    // defect this is looking for, and anything at or above it cannot be noise
    // from a handful of sibling cases.
    let growth = after_many.saturating_sub(after_few);
    assert!(
        growth < 120,
        "240 further owning devices grew the fd table by {growth}; \
         a device that failed to close would grow it by 240"
    );
}

/// The mirror of the above: a borrowed device must *not* close, so the table
/// grows by one per iteration when the caller keeps the descriptors alive.
#[test]
fn borrowed_device_leaves_the_descriptor_to_the_caller() {
    let mut kept = Vec::new();
    for _ in 0..64 {
        let owned = open_dev_null();
        let raw = owned.as_raw_fd();
        // SAFETY: `owned` is pushed to `kept` below and outlives the device.
        let device = unsafe { Device::from_borrowed_fd(raw) };
        assert_eq!(device.raw_fd(), raw);
        drop(device);
        // Still valid because the device did not close it.
        assert!(rustix::fs::fstat(&owned).is_ok());
        kept.push(owned);
    }
    assert_eq!(kept.len(), 64);
}

// `DeviceTest.SetClientCapOnInvalidFdFails` has no Rust counterpart.
//
// The C++ passes the capability number 0xFFFFFFFF and expects rejection.
// `ClientCapability` is an enum here, so an out-of-range capability cannot be
// named -- the same reason the `fd = -1` cases upstream do not survive the
// port. Substituting a real-but-unlikely capability was tried and does not
// work either: vkms accepts `Stereo3D`, so the substitute tested nothing and
// only encoded a guess about driver support. What the case was really
// protecting -- that `set_client_cap` reports driver rejection rather than
// swallowing it -- is exercised by `open_card_and_enable_atomic_vkms`, which
// fails loudly if either capability is refused.

#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn open_card_and_enable_atomic_vkms() {
    let _guard = card_guard();
    let device = Device::open(test_card()).expect("open test card");
    device
        .enable_universal_planes()
        .expect("universal planes must be available");
    device.enable_atomic().expect("atomic must be available");
    assert!(device.owns_fd());
}

// --- test_property_store.cpp -------------------------------------------------

/// `PropertyStoreTest.LookupOnEmptyStoreReturnsError`
#[test]
fn property_id_on_empty_store_is_an_error() {
    let store = PropertyStore::new();
    assert!(matches!(
        store.property_id(42, "type"),
        Err(CoreError::NoSuchObject { object_id: 42 })
    ));
}

/// `PropertyStoreTest.PropertyValueOnEmptyStoreReturnsError`
#[test]
fn property_value_on_empty_store_is_an_error() {
    let store = PropertyStore::new();
    assert!(store.property_value(42, "type").is_err());
    assert!(store.is_immutable(42, "type").is_err());
}

/// `PropertyStoreTest.PropertiesOnEmptyStoreReturnsNull`
#[test]
fn properties_on_empty_store_is_none() {
    let store = PropertyStore::new();
    assert!(store.properties(42).is_none());
    assert!(store.is_empty());
}

/// `PropertyStoreTest.ClearEmptiesStore`
#[test]
fn clear_empties_the_store() {
    let mut store = PropertyStore::new();
    store.clear();
    assert!(store.properties(42).is_none());
    assert_eq!(store.len(), 0);
}

/// `PropertyStoreTest.CacheWithInvalidFdReturnsError`
///
/// The C++ passes `fd = -1`. That does not compile here: caching takes a
/// `&Device`, so there is no invalid descriptor to pass. The equivalent
/// reachable failure is caching an object id the kernel does not know, which
/// the vkms case below covers.
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn cache_unknown_object_is_an_error_vkms() {
    let _guard = card_guard();
    let device = Device::open(test_card()).expect("open test card");
    let mut store = PropertyStore::new();
    assert!(
        store
            .cache_properties(&device, 0xdead_beef, ObjectType::Connector)
            .is_err(),
        "an object id the kernel does not know must fail"
    );
    assert!(store.is_empty(), "a failed cache must not leave an entry");
}

/// Caching a real plane must yield the properties the allocator depends on.
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn cache_real_plane_properties_vkms() {
    use drm::control::Device as _;

    let device = Device::open(test_card()).expect("open test card");
    device.enable_universal_planes().expect("universal planes");
    device.enable_atomic().expect("atomic");

    let planes = device.plane_handles().expect("plane handles");
    let plane = *planes.first().expect("vkms exposes at least one plane");
    let plane_id: u32 = plane.into();

    let mut store = PropertyStore::new();
    store
        .cache_properties(&device, plane_id, ObjectType::Plane)
        .expect("cache plane properties");

    let props = store.properties(plane_id).expect("cached");
    assert!(!props.is_empty());

    // Every KMS plane advertises "type", and it is immutable on every driver.
    let type_id = store.property_id(plane_id, "type").expect("type property");
    assert_ne!(type_id, 0, "property ids are non-zero by construction");
    assert!(
        store.is_immutable(plane_id, "type").expect("type"),
        "plane type is immutable, so a request builder must skip it"
    );

    assert!(store.property_id(plane_id, "NOT_A_REAL_PROPERTY").is_err());
    store.forget(plane_id);
    assert!(store.properties(plane_id).is_none());
}

// --- test_atomic_builder.cpp -------------------------------------------------

/// `AtomicRequestTest.ConstructAndDestroy`
///
/// The C++ constructs from a `Device` because it allocates a
/// `drmModeAtomicReq` and needs the fd. Here a request is a plain vector, so
/// construction needs no device and cannot fail — there is no `valid()` to
/// check.
#[test]
fn construct_is_infallible_and_empty() {
    let request = AtomicRequest::new();
    assert!(request.is_empty());
    assert_eq!(request.len(), 0);
    assert!(request.writes().is_empty());
}

/// `AtomicRequestTest.AddPropertyQueuesWithoutValidation`
///
/// Non-zero ids queue without being checked against real kernel objects;
/// libdrm behaves the same, and the check happens at commit time.
#[test]
fn add_property_queues_without_validating_against_the_kernel() {
    const SENTINEL_OBJECT: u32 = 0xdead_beef;
    const SENTINEL_PROP: u32 = 0xcafe_f00d;

    let mut request = AtomicRequest::new();
    request
        .add_property(SENTINEL_OBJECT, SENTINEL_PROP, 0)
        .expect("non-zero ids queue");

    assert_eq!(request.len(), 1);
    assert_eq!(
        request.writes()[0],
        PropertyWrite {
            object_id: SENTINEL_OBJECT,
            property_id: SENTINEL_PROP,
            value: 0,
        }
    );
}

/// libdrm rejects a zero id outright, and so does this.
///
/// The rejection is load-bearing rather than cosmetic: DRM resource handles are
/// `NonZeroU32`, so a zero id has no representation to build a request from.
/// Accepting one could only mean dropping it at build time — a commit going out
/// one property short while reporting success.
#[test]
fn add_property_rejects_zero_ids() {
    let mut request = AtomicRequest::new();
    assert!(request.add_property(0, 1, 0).is_err(), "zero object id");
    assert!(request.add_property(1, 0, 0).is_err(), "zero property id");
    assert!(request.add_property(0, 0, 0).is_err());
    assert!(request.is_empty(), "a rejected write must not be queued");
}

/// Order and duplicates are preserved: the kernel applies writes in order, so a
/// later write to the same property wins. Collapsing them would change what a
/// replayed trace means.
#[test]
fn writes_preserve_order_and_duplicates() {
    let mut request = AtomicRequest::new();
    request.add_property(1, 10, 100).unwrap();
    request.add_property(2, 20, 200).unwrap();
    request.add_property(1, 10, 999).unwrap(); // same target, later value

    let writes = request.writes();
    assert_eq!(writes.len(), 3);
    assert_eq!(writes[0].value, 100);
    assert_eq!(writes[1].object_id, 2);
    assert_eq!(writes[2].value, 999, "the later write must survive");
}

/// `clear` empties the queue while keeping the allocation for the next frame.
#[test]
fn clear_empties_the_queue() {
    let mut request = AtomicRequest::with_capacity(8);
    request.add_property(1, 2, 3).unwrap();
    assert_eq!(request.len(), 1);

    request.clear();
    assert!(request.is_empty());
    assert!(request.writes().is_empty());
}

/// `AtomicRequestTest.MoveConstruct` / `MoveAssign`
///
/// A move must carry the queued writes intact.
#[test]
fn move_transfers_queued_writes() {
    let mut source = AtomicRequest::new();
    source.add_property(1, 2, 3).unwrap();
    source.add_property(4, 5, 6).unwrap();

    let moved = source;
    assert_eq!(moved.len(), 2);
    assert_eq!(moved.writes()[1].object_id, 4);
}

/// `AtomicRequestTest.TestCommitWithEmptyRequest` — needs a card.
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn test_commit_with_empty_request_vkms() {
    let _guard = card_guard();
    let device = Device::open(test_card()).expect("open test card");
    device.enable_atomic().expect("atomic");

    let request = AtomicRequest::new();
    // An empty atomic request is valid: it changes nothing.
    check_master_dependent(
        request.test(&device, AtomicCommitFlags::empty()),
        "empty request must validate",
    );
}

/// A request naming objects the kernel does not know must be rejected at
/// commit time, not at insertion.
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn test_rejects_unknown_objects_vkms() {
    let _guard = card_guard();
    let device = Device::open(test_card()).expect("open test card");
    device.enable_atomic().expect("atomic");

    let mut request = AtomicRequest::new();
    request.add_property(0xdead_beef, 0xcafe_f00d, 0).unwrap();

    match request.test(&device, AtomicCommitFlags::empty()) {
        Err(CoreError::NotMaster) => {
            // Says nothing about the ids; the commit never reached validation.
            println!("note: skipped -- another client holds DRM master");
            assert!(
                std::env::var_os("DRMKIT_REQUIRE_MASTER").is_none(),
                "not DRM master, but DRMKIT_REQUIRE_MASTER is set"
            );
        }
        Err(_) => {}
        Ok(()) => panic!("the kernel must reject ids that name nothing"),
    }
}

/// `test` masks `PAGE_FLIP_EVENT`, which the kernel rejects with `EINVAL`
/// alongside `TEST_ONLY`. Callers forward commit flags verbatim, so the mask
/// has to live here.
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn test_masks_page_flip_event_vkms() {
    let _guard = card_guard();
    let device = Device::open(test_card()).expect("open test card");
    device.enable_atomic().expect("atomic");

    let request = AtomicRequest::new();
    check_master_dependent(
        request.test(&device, AtomicCommitFlags::PAGE_FLIP_EVENT),
        "PAGE_FLIP_EVENT must be masked out rather than rejected",
    );
}

/// A signed range is reported as no range at all.
///
/// Every atomic value crosses the ioctl boundary as a `u64`, so a signed
/// range arrives as two's complement: `CRTC_X` spans `(-2^31, 2^31 - 1)`,
/// which as `u64` is a pair whose "minimum" is the larger number. A caller
/// clamping against that would turn `CRTC_X = -8` into `INT32_MAX` and push
/// the layer off the far edge of the screen -- a bug that looks like a
/// compositor fault, not a range fault. Reporting `None` makes that
/// arithmetic impossible to write by accident.
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn signed_ranges_are_not_reported_as_ranges_vkms() {
    use drm::control::Device as _;

    // Every test here that opens the card takes this first. An unguarded one
    // holds an open device while a guarded one runs, and the guarded one is
    // then not DRM master -- which surfaces as that *other* test failing,
    // intermittently, depending on how the threads interleave.
    let _guard = card_guard();
    let device = Device::open(test_card()).expect("open card");
    device.enable_universal_planes().expect("universal planes");
    device.enable_atomic().expect("atomic");

    let planes = device.plane_handles().expect("planes");
    let plane: u32 = (*planes.first().expect("at least one plane")).into();

    let mut store = PropertyStore::new();
    store
        .cache_properties(&device, plane, ObjectType::Plane)
        .expect("plane properties");

    // Unsigned: reported, and a sane interval.
    let src_w = store.range(plane, "SRC_W").expect("SRC_W exists");
    let (lo, hi) = src_w.expect("SRC_W is an unsigned range");
    assert!(
        lo <= hi,
        "an unsigned range must not be inverted: [{lo}, {hi}]"
    );

    // Signed: withheld, precisely so nobody compares those two numbers.
    assert_eq!(
        store.range(plane, "CRTC_X").expect("CRTC_X exists"),
        None,
        "CRTC_X is a signed range and must not be reported as an unsigned one"
    );
}

/// A request dump names every write, so a rejection can be read rather than
/// bisected.
///
/// An `EINVAL` from an atomic commit names no property; upstream lost three
/// sessions to one out-of-range value before it had this (`eb065b2`). An id
/// the device cannot name is shown as `?` rather than dropped, since the
/// unnameable one may be the culprit.
#[test]
fn a_request_dump_names_every_write() {
    let mut request = AtomicRequest::new();
    request.add_property(55, 10, 74).expect("write");
    request.add_property(55, 11, 66).expect("write");
    let names = |id: u32| match id {
        10 => Some("FB_ID".to_owned()),
        _ => None,
    };

    let lines = request.dump_lines("test rejected", names);

    assert_eq!(
        lines[0],
        "[atomic] test rejected: 2 properties in this request"
    );
    assert!(
        lines[1].contains("obj=55") && lines[1].contains("FB_ID") && lines[1].ends_with("= 74")
    );
    assert!(lines[2].contains('?') && lines[2].ends_with("= 66"));
    assert_eq!(
        AtomicRequest::new().dump_lines("empty", names)[0],
        "[atomic] empty: 0 properties in this request"
    );
}

/// Which modifier a framebuffer registration declares (upstream `be147be`).
///
/// `INVALID` never: it means "not saying". `LINEAR` only where the driver takes
/// modifiers -- i.MX LCDIF and tilcdc refuse the declared form even for the
/// linear layout that is their only one. Anything else always, and the
/// capability is not even asked about.
#[test]
fn which_modifier_a_framebuffer_declares() {
    use crate::device::declared_modifier;
    const INVALID: u64 = (1 << 56) - 1;
    const TILED: u64 = 0x0600_0000_0000_0001;
    let never = || -> bool { panic!("only LINEAR depends on the capability") };

    assert_eq!(declared_modifier(INVALID, never), None);
    assert_eq!(declared_modifier(TILED, never), Some(TILED));
    assert_eq!(declared_modifier(0, || true), Some(0));
    assert_eq!(declared_modifier(0, || false), None);
}