atagen

monolith

public
Star0
Watch0

Commit 0c1685f1d412

tarball

atagen <boss@atagen.co>2026-10-16 12:30 UTC

11 files changed+1065-73
monolith: add a DRM backend

Bare metal runs on a libseat session with udev and libinput, one smithay DrmOutput per connector, GBM buffers rendered by the Vulkan renderer, and each output drawn when its own vblank lands. The scene now builds per output in output-local coordinates. smithay-drm-extras is used without display-info because nixpkgs ships libdisplay-info 0.4 and the bindings require below that. Overlay planes stay off on NVIDIA, where anvil found they break composition.
Modified file: Cargo.lock+303 additions−25 deletions
@@ -82,6 +82,26 @@ dependencies = [
8282 ]
8383
8484 [[package]]
85+name = "bytemuck"
86+version = "1.25.2"
87+source = "registry+https://github.com/rust-lang/crates.io-index"
88+checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
89+dependencies = [
90+ "bytemuck_derive",
91+]
92+
93+[[package]]
94+name = "bytemuck_derive"
95+version = "1.12.1"
96+source = "registry+https://github.com/rust-lang/crates.io-index"
97+checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
98+dependencies = [
99+ "proc-macro2",
100+ "quote",
101+ "syn 3.0.9",
102+]
103+
104+[[package]]
85105 name = "bytes"
86106 version = "1.12.1"
87107 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -95,7 +115,7 @@ checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e"
95115 dependencies = [
96116 "bitflags",
97117 "polling",
98- "rustix",
118+ "rustix 1.1.5",
99119 "tracing",
100120 ]
101121
@@ -106,7 +126,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
106126 checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
107127 dependencies = [
108128 "calloop",
109- "rustix",
129+ "rustix 1.1.5",
110130 "wayland-backend",
111131 "wayland-client",
112132 ]
@@ -187,12 +207,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
187207 checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
188208
189209 [[package]]
210+name = "drm"
211+version = "0.14.1"
212+source = "registry+https://github.com/rust-lang/crates.io-index"
213+checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f"
214+dependencies = [
215+ "bitflags",
216+ "bytemuck",
217+ "drm-ffi",
218+ "drm-fourcc",
219+ "libc",
220+ "rustix 0.38.44",
221+]
222+
223+[[package]]
224+name = "drm-ffi"
225+version = "0.9.1"
226+source = "registry+https://github.com/rust-lang/crates.io-index"
227+checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690"
228+dependencies = [
229+ "drm-sys",
230+ "rustix 1.1.5",
231+]
232+
233+[[package]]
190234 name = "drm-fourcc"
191235 version = "2.2.0"
192236 source = "registry+https://github.com/rust-lang/crates.io-index"
193237 checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4"
194238
195239 [[package]]
240+name = "drm-sys"
241+version = "0.8.1"
242+source = "registry+https://github.com/rust-lang/crates.io-index"
243+checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8"
244+dependencies = [
245+ "libc",
246+ "linux-raw-sys 0.9.4",
247+]
248+
249+[[package]]
196250 name = "enumflags2"
197251 version = "0.7.12"
198252 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -225,7 +279,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
225279 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
226280 dependencies = [
227281 "libc",
228- "windows-sys",
282+ "windows-sys 0.61.2",
229283 ]
230284
231285 [[package]]
@@ -247,6 +301,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
247301 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
248302
249303 [[package]]
304+name = "gbm"
305+version = "0.18.0"
306+source = "registry+https://github.com/rust-lang/crates.io-index"
307+checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a"
308+dependencies = [
309+ "bitflags",
310+ "drm",
311+ "drm-fourcc",
312+ "gbm-sys",
313+ "libc",
314+]
315+
316+[[package]]
317+name = "gbm-sys"
318+version = "0.4.0"
319+source = "registry+https://github.com/rust-lang/crates.io-index"
320+checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f"
321+dependencies = [
322+ "libc",
323+]
324+
325+[[package]]
250326 name = "generator"
251327 version = "0.8.11"
252328 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -299,6 +375,12 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
299375
300376 [[package]]
301377 name = "hermit-abi"
378+version = "0.3.9"
379+source = "registry+https://github.com/rust-lang/crates.io-index"
380+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
381+
382+[[package]]
383+name = "hermit-abi"
302384 version = "0.5.3"
303385 source = "registry+https://github.com/rust-lang/crates.io-index"
304386 checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
@@ -314,6 +396,35 @@ dependencies = [
314396 ]
315397
316398 [[package]]
399+name = "input"
400+version = "0.10.0"
401+source = "registry+https://github.com/rust-lang/crates.io-index"
402+checksum = "f9793345a65d71317763a33066b5d8351f8760dde8d4930fe9e39b5f14a7959d"
403+dependencies = [
404+ "bitflags",
405+ "input-sys",
406+ "libc",
407+ "udev",
408+]
409+
410+[[package]]
411+name = "input-sys"
412+version = "1.19.0"
413+source = "registry+https://github.com/rust-lang/crates.io-index"
414+checksum = "36eee07d8e02bd95bf52b2e642cf13d33701b94c6e4b04fbf1d1fb07e9cb19e7"
415+
416+[[package]]
417+name = "io-lifetimes"
418+version = "1.0.11"
419+source = "registry+https://github.com/rust-lang/crates.io-index"
420+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
421+dependencies = [
422+ "hermit-abi 0.3.9",
423+ "libc",
424+ "windows-sys 0.48.0",
425+]
426+
427+[[package]]
317428 name = "itoa"
318429 version = "1.0.18"
319430 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -342,6 +453,48 @@ dependencies = [
342453 ]
343454
344455 [[package]]
456+name = "libseat"
457+version = "0.2.4"
458+source = "registry+https://github.com/rust-lang/crates.io-index"
459+checksum = "6656c69b6e0366ffb83faa232f3e4fda5fc91161722d140f2c95ce2e90b1ef31"
460+dependencies = [
461+ "errno",
462+ "libseat-sys",
463+ "log",
464+]
465+
466+[[package]]
467+name = "libseat-sys"
468+version = "0.2.0"
469+source = "registry+https://github.com/rust-lang/crates.io-index"
470+checksum = "e8a484fa48be5f62a8d3ffed767779d0ab808cfead91de98ef4362d469097dfb"
471+dependencies = [
472+ "pkg-config",
473+]
474+
475+[[package]]
476+name = "libudev-sys"
477+version = "0.1.4"
478+source = "registry+https://github.com/rust-lang/crates.io-index"
479+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
480+dependencies = [
481+ "libc",
482+ "pkg-config",
483+]
484+
485+[[package]]
486+name = "linux-raw-sys"
487+version = "0.4.15"
488+source = "registry+https://github.com/rust-lang/crates.io-index"
489+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
490+
491+[[package]]
492+name = "linux-raw-sys"
493+version = "0.9.4"
494+source = "registry+https://github.com/rust-lang/crates.io-index"
495+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
496+
497+[[package]]
345498 name = "linux-raw-sys"
346499 version = "0.12.1"
347500 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -406,11 +559,13 @@ dependencies = [
406559 "serde_json",
407560 "slotmap",
408561 "smithay",
562+ "smithay-drm-extras",
409563 "tracing",
410564 "tracing-subscriber",
411565 "tracy-client",
412566 "wayland-client",
413567 "wayland-protocols",
568+ "xcursor",
414569 ]
415570
416571 [[package]]
@@ -469,7 +624,7 @@ dependencies = [
469624 "ash",
470625 "glam",
471626 "gpu-allocator",
472- "rustix",
627+ "rustix 1.1.5",
473628 "smithay",
474629 "thiserror 2.0.21",
475630 ]
@@ -487,7 +642,7 @@ version = "0.50.3"
487642 source = "registry+https://github.com/rust-lang/crates.io-index"
488643 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
489644 dependencies = [
490- "windows-sys",
645+ "windows-sys 0.61.2",
491646 ]
492647
493648 [[package]]
@@ -525,10 +680,10 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
525680 dependencies = [
526681 "cfg-if",
527682 "concurrent-queue",
528- "hermit-abi",
683+ "hermit-abi 0.5.3",
529684 "pin-project-lite",
530- "rustix",
531- "windows-sys",
685+ "rustix 1.1.5",
686+ "windows-sys 0.61.2",
532687 ]
533688
534689 [[package]]
@@ -723,7 +878,20 @@ dependencies = [
723878 "calloop",
724879 "enumflags2",
725880 "log",
726- "rustix",
881+ "rustix 1.1.5",
882+]
883+
884+[[package]]
885+name = "rustix"
886+version = "0.38.44"
887+source = "registry+https://github.com/rust-lang/crates.io-index"
888+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
889+dependencies = [
890+ "bitflags",
891+ "errno",
892+ "libc",
893+ "linux-raw-sys 0.4.15",
894+ "windows-sys 0.59.0",
727895 ]
728896
729897 [[package]]
@@ -735,8 +903,8 @@ dependencies = [
735903 "bitflags",
736904 "errno",
737905 "libc",
738- "linux-raw-sys",
739- "windows-sys",
906+ "linux-raw-sys 0.12.1",
907+ "windows-sys 0.61.2",
740908 ]
741909
742910 [[package]]
@@ -859,23 +1027,32 @@ dependencies = [
8591027 "appendlist",
8601028 "bitflags",
8611029 "calloop",
1030+ "cc",
8621031 "cursor-icon",
8631032 "downcast-rs",
1033+ "drm",
1034+ "drm-ffi",
8641035 "drm-fourcc",
1036+ "drm-sys",
8651037 "errno",
1038+ "gbm",
8661039 "glam",
8671040 "indexmap",
1041+ "input",
8681042 "libc",
1043+ "libseat",
1044+ "pkg-config",
8691045 "portable-atomic",
8701046 "profiling",
8711047 "rand",
8721048 "reis",
873- "rustix",
1049+ "rustix 1.1.5",
8741050 "siphasher",
8751051 "smallvec",
8761052 "tempfile",
8771053 "thiserror 2.0.21",
8781054 "tracing",
1055+ "udev",
8791056 "wayland-protocols",
8801057 "wayland-protocols-misc",
8811058 "wayland-protocols-wlr",
@@ -884,6 +1061,14 @@ dependencies = [
8841061 ]
8851062
8861063 [[package]]
1064+name = "smithay-drm-extras"
1065+version = "0.1.0"
1066+source = "git+https://github.com/Smithay/smithay?rev=023b78b1c5a05ca512767cacc07efca55fae8be4#023b78b1c5a05ca512767cacc07efca55fae8be4"
1067+dependencies = [
1068+ "drm",
1069+]
1070+
1071+[[package]]
8871072 name = "syn"
8881073 version = "2.0.119"
8891074 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -914,8 +1099,8 @@ dependencies = [
9141099 "fastrand",
9151100 "getrandom",
9161101 "once_cell",
917- "rustix",
918- "windows-sys",
1102+ "rustix 1.1.5",
1103+ "windows-sys 0.61.2",
9191104 ]
9201105
9211106 [[package]]
@@ -1098,7 +1283,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
10981283 checksum = "ab27f167b093214c68413a2e0bcd318f0591af475597405ff68138ba0a433379"
10991284 dependencies = [
11001285 "cc",
1101- "windows-targets",
1286+ "windows-targets 0.52.6",
1287+]
1288+
1289+[[package]]
1290+name = "udev"
1291+version = "0.9.3"
1292+source = "registry+https://github.com/rust-lang/crates.io-index"
1293+checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f"
1294+dependencies = [
1295+ "io-lifetimes",
1296+ "libc",
1297+ "libudev-sys",
1298+ "pkg-config",
11021299 ]
11031300
11041301 [[package]]
@@ -1152,7 +1349,7 @@ checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
11521349 dependencies = [
11531350 "cc",
11541351 "downcast-rs",
1155- "rustix",
1352+ "rustix 1.1.5",
11561353 "smallvec",
11571354 "wayland-sys",
11581355 ]
@@ -1164,7 +1361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
11641361 checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
11651362 dependencies = [
11661363 "bitflags",
1167- "rustix",
1364+ "rustix 1.1.5",
11681365 "wayland-backend",
11691366 "wayland-scanner",
11701367 ]
@@ -1227,7 +1424,7 @@ checksum = "0dde9c29be0f723a573977de51ee455bf3dfa03652730a74f9dd3b337e374d75"
12271424 dependencies = [
12281425 "bitflags",
12291426 "downcast-rs",
1230- "rustix",
1427+ "rustix 1.1.5",
12311428 "wayland-backend",
12321429 "wayland-scanner",
12331430 ]
@@ -1258,6 +1455,24 @@ dependencies = [
12581455
12591456 [[package]]
12601457 name = "windows-sys"
1458+version = "0.48.0"
1459+source = "registry+https://github.com/rust-lang/crates.io-index"
1460+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1461+dependencies = [
1462+ "windows-targets 0.48.5",
1463+]
1464+
1465+[[package]]
1466+name = "windows-sys"
1467+version = "0.59.0"
1468+source = "registry+https://github.com/rust-lang/crates.io-index"
1469+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1470+dependencies = [
1471+ "windows-targets 0.52.6",
1472+]
1473+
1474+[[package]]
1475+name = "windows-sys"
12611476 version = "0.61.2"
12621477 source = "registry+https://github.com/rust-lang/crates.io-index"
12631478 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
@@ -1267,34 +1482,67 @@ dependencies = [
12671482
12681483 [[package]]
12691484 name = "windows-targets"
1485+version = "0.48.5"
1486+source = "registry+https://github.com/rust-lang/crates.io-index"
1487+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1488+dependencies = [
1489+ "windows_aarch64_gnullvm 0.48.5",
1490+ "windows_aarch64_msvc 0.48.5",
1491+ "windows_i686_gnu 0.48.5",
1492+ "windows_i686_msvc 0.48.5",
1493+ "windows_x86_64_gnu 0.48.5",
1494+ "windows_x86_64_gnullvm 0.48.5",
1495+ "windows_x86_64_msvc 0.48.5",
1496+]
1497+
1498+[[package]]
1499+name = "windows-targets"
12701500 version = "0.52.6"
12711501 source = "registry+https://github.com/rust-lang/crates.io-index"
12721502 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
12731503 dependencies = [
1274- "windows_aarch64_gnullvm",
1275- "windows_aarch64_msvc",
1276- "windows_i686_gnu",
1504+ "windows_aarch64_gnullvm 0.52.6",
1505+ "windows_aarch64_msvc 0.52.6",
1506+ "windows_i686_gnu 0.52.6",
12771507 "windows_i686_gnullvm",
1278- "windows_i686_msvc",
1279- "windows_x86_64_gnu",
1280- "windows_x86_64_gnullvm",
1281- "windows_x86_64_msvc",
1508+ "windows_i686_msvc 0.52.6",
1509+ "windows_x86_64_gnu 0.52.6",
1510+ "windows_x86_64_gnullvm 0.52.6",
1511+ "windows_x86_64_msvc 0.52.6",
12821512 ]
12831513
12841514 [[package]]
12851515 name = "windows_aarch64_gnullvm"
1516+version = "0.48.5"
1517+source = "registry+https://github.com/rust-lang/crates.io-index"
1518+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1519+
1520+[[package]]
1521+name = "windows_aarch64_gnullvm"
12861522 version = "0.52.6"
12871523 source = "registry+https://github.com/rust-lang/crates.io-index"
12881524 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
12891525
12901526 [[package]]
12911527 name = "windows_aarch64_msvc"
1528+version = "0.48.5"
1529+source = "registry+https://github.com/rust-lang/crates.io-index"
1530+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1531+
1532+[[package]]
1533+name = "windows_aarch64_msvc"
12921534 version = "0.52.6"
12931535 source = "registry+https://github.com/rust-lang/crates.io-index"
12941536 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
12951537
12961538 [[package]]
12971539 name = "windows_i686_gnu"
1540+version = "0.48.5"
1541+source = "registry+https://github.com/rust-lang/crates.io-index"
1542+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1543+
1544+[[package]]
1545+name = "windows_i686_gnu"
12981546 version = "0.52.6"
12991547 source = "registry+https://github.com/rust-lang/crates.io-index"
13001548 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -1307,24 +1555,48 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
13071555
13081556 [[package]]
13091557 name = "windows_i686_msvc"
1558+version = "0.48.5"
1559+source = "registry+https://github.com/rust-lang/crates.io-index"
1560+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1561+
1562+[[package]]
1563+name = "windows_i686_msvc"
13101564 version = "0.52.6"
13111565 source = "registry+https://github.com/rust-lang/crates.io-index"
13121566 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
13131567
13141568 [[package]]
13151569 name = "windows_x86_64_gnu"
1570+version = "0.48.5"
1571+source = "registry+https://github.com/rust-lang/crates.io-index"
1572+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1573+
1574+[[package]]
1575+name = "windows_x86_64_gnu"
13161576 version = "0.52.6"
13171577 source = "registry+https://github.com/rust-lang/crates.io-index"
13181578 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
13191579
13201580 [[package]]
13211581 name = "windows_x86_64_gnullvm"
1582+version = "0.48.5"
1583+source = "registry+https://github.com/rust-lang/crates.io-index"
1584+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1585+
1586+[[package]]
1587+name = "windows_x86_64_gnullvm"
13221588 version = "0.52.6"
13231589 source = "registry+https://github.com/rust-lang/crates.io-index"
13241590 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
13251591
13261592 [[package]]
13271593 name = "windows_x86_64_msvc"
1594+version = "0.48.5"
1595+source = "registry+https://github.com/rust-lang/crates.io-index"
1596+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1597+
1598+[[package]]
1599+name = "windows_x86_64_msvc"
13281600 version = "0.52.6"
13291601 source = "registry+https://github.com/rust-lang/crates.io-index"
13301602 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
@@ -1339,6 +1611,12 @@ dependencies = [
13391611 ]
13401612
13411613 [[package]]
1614+name = "xcursor"
1615+version = "0.3.11"
1616+source = "registry+https://github.com/rust-lang/crates.io-index"
1617+checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23"
1618+
1619+[[package]]
13421620 name = "xkbcommon"
13431621 version = "0.9.0"
13441622 source = "registry+https://github.com/rust-lang/crates.io-index"
Modified file: crates/monolith/Cargo.toml+10 additions−1 deletions
@@ -23,7 +23,16 @@ monolith-render-vk.workspace = true
2323 pound = "0.5"
2424 serde_json.workspace = true
2525 slotmap.workspace = true
26-smithay = { workspace = true, features = ["desktop"] }
26+smithay = { workspace = true, features = [
27+ "desktop",
28+ "backend_drm",
29+ "backend_gbm",
30+ "backend_libinput",
31+ "backend_udev",
32+ "backend_session_libseat",
33+] }
34+smithay-drm-extras = { git = "https://github.com/Smithay/smithay", rev = "023b78b1c5a05ca512767cacc07efca55fae8be4", default-features = false }
35+xcursor = "0.3"
2736 tracing = "0.1"
2837 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2938 tracy-client = { workspace = true, optional = true }
Added file: crates/monolith/src/cursor.rs+110 additions
@@ -0,0 +1,110 @@
1+//! The pointer image on bare metal, where nothing else draws one. A client
2+//! cursor surface wins, otherwise the xcursor theme's default arrow.
3+
4+use crate::scene::Draw;
5+use monolith_render_vk::VulkanRenderer;
6+use smithay::backend::allocator::Fourcc;
7+use smithay::backend::renderer::element::memory::{
8+ MemoryRenderBuffer, MemoryRenderBufferRenderElement,
9+};
10+use smithay::backend::renderer::element::solid::SolidColorRenderElement;
11+use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
12+use smithay::backend::renderer::element::{Id, Kind};
13+use smithay::backend::renderer::utils::CommitCounter;
14+use smithay::input::pointer::{CursorImageAttributes, CursorImageStatus};
15+use smithay::utils::{Logical, Physical, Point, Rectangle, Transform};
16+use smithay::wayland::compositor::with_states;
17+use std::sync::Mutex;
18+
19+pub struct Cursor {
20+ image: Option<(MemoryRenderBuffer, Point<i32, Logical>)>,
21+}
22+
23+impl Cursor {
24+ pub fn load() -> Self {
25+ let theme = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into());
26+ let size = std::env::var("XCURSOR_SIZE")
27+ .ok()
28+ .and_then(|s| s.parse().ok())
29+ .unwrap_or(24u32);
30+ let image = load_image(&theme, size);
31+ if image.is_none() {
32+ tracing::warn!(theme, "no xcursor theme found, drawing a plain cursor");
33+ }
34+ Self { image }
35+ }
36+
37+ /// Elements for the pointer at `pos`, in output-local coordinates.
38+ pub fn elements(
39+ &self,
40+ renderer: &mut VulkanRenderer,
41+ status: &CursorImageStatus,
42+ pos: Point<f64, Logical>,
43+ ) -> Vec<Draw> {
44+ let at = |hotspot: Point<i32, Logical>| -> Point<i32, Physical> {
45+ let p = pos - hotspot.to_f64();
46+ (p.x.round() as i32, p.y.round() as i32).into()
47+ };
48+ match status {
49+ CursorImageStatus::Hidden => Vec::new(),
50+ CursorImageStatus::Surface(surface) => {
51+ let hotspot = with_states(surface, |s| {
52+ s.data_map
53+ .get::<Mutex<CursorImageAttributes>>()
54+ .and_then(|a| a.lock().ok().map(|a| a.hotspot))
55+ .unwrap_or_default()
56+ });
57+ render_elements_from_surface_tree(
58+ renderer,
59+ surface,
60+ at(hotspot),
61+ 1.0,
62+ 1.0,
63+ Kind::Cursor,
64+ )
65+ .into_iter()
66+ .map(Draw::Surface)
67+ .collect()
68+ }
69+ CursorImageStatus::Named(_) => match &self.image {
70+ Some((buffer, hotspot)) => MemoryRenderBufferRenderElement::from_buffer(
71+ renderer,
72+ at(*hotspot).to_f64(),
73+ buffer,
74+ None,
75+ None,
76+ None,
77+ Kind::Cursor,
78+ )
79+ .map(|e| vec![Draw::Memory(e)])
80+ .unwrap_or_default(),
81+ None => vec![Draw::Border(SolidColorRenderElement::new(
82+ Id::new(),
83+ Rectangle::new(at(Point::default()), (8, 8).into()),
84+ CommitCounter::default(),
85+ [1.0, 1.0, 1.0, 1.0],
86+ Kind::Cursor,
87+ ))],
88+ },
89+ }
90+ }
91+}
92+
93+fn load_image(theme: &str, size: u32) -> Option<(MemoryRenderBuffer, Point<i32, Logical>)> {
94+ let theme = xcursor::CursorTheme::load(theme);
95+ let path = theme
96+ .load_icon("default")
97+ .or_else(|| theme.load_icon("left_ptr"))?;
98+ let bytes = std::fs::read(path).ok()?;
99+ let images = xcursor::parser::parse_xcursor(&bytes)?;
100+ let image = images.iter().min_by_key(|i| i.size.abs_diff(size))?;
101+ let buffer = MemoryRenderBuffer::from_slice(
102+ &image.pixels_rgba,
103+ Fourcc::Argb8888,
104+ (image.width as i32, image.height as i32),
105+ 1,
106+ Transform::Normal,
107+ None,
108+ );
109+ Some((buffer, (image.xhot as i32, image.yhot as i32).into()))
110+}
Added file: crates/monolith/src/drm.rs+511 additions
@@ -0,0 +1,511 @@
1+//! Bare metal. A libseat session, udev for the primary GPU and its
2+//! connectors, libinput for input, and one smithay DRM output per connector,
3+//! each rendered when its own vblank lands.
4+
5+use crate::scene::Draw;
6+use crate::state::{State, now_ns};
7+use monolith_layout::{OutputId, Rect};
8+use smithay::backend::allocator::Fourcc;
9+use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
10+use smithay::backend::drm::compositor::FrameFlags;
11+use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
12+use smithay::backend::drm::output::{DrmOutput, DrmOutputManager, DrmOutputRenderElements};
13+use smithay::backend::drm::{
14+ DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
15+};
16+use smithay::backend::input::{
17+ AbsolutePositionEvent, Axis, AxisSource, Event, InputEvent, KeyboardKeyEvent, PointerAxisEvent,
18+ PointerButtonEvent, PointerMotionEvent,
19+};
20+use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
21+use smithay::backend::session::libseat::LibSeatSession;
22+use smithay::backend::session::{Event as SessionEvent, Session};
23+use smithay::backend::udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu};
24+use smithay::input::pointer::AxisFrame;
25+use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
26+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
27+use smithay::reexports::calloop::{EventLoop, LoopHandle};
28+use smithay::reexports::drm::Device as _;
29+use smithay::reexports::drm::control::{ModeTypeFlags, connector, crtc};
30+use smithay::reexports::input::Libinput;
31+use smithay::reexports::rustix::fs::OFlags;
32+use smithay::utils::{DeviceFd, Point, Transform};
33+use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
34+use std::collections::HashMap;
35+use std::time::Duration;
36+
37+const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
38+
39+type Allocator = GbmAllocator<DrmDeviceFd>;
40+type Exporter = GbmFramebufferExporter<DrmDeviceFd>;
41+type Manager = DrmOutputManager<Allocator, Exporter, (), DrmDeviceFd>;
42+type Out = DrmOutput<Allocator, Exporter, (), DrmDeviceFd>;
43+
44+pub struct Drm {
45+ pub session: LibSeatSession,
46+ libinput: Libinput,
47+ handle: LoopHandle<'static, State>,
48+ node: DrmNode,
49+ manager: Manager,
50+ scanner: DrmScanner,
51+ nvidia: bool,
52+ surfaces: HashMap<crtc::Handle, Surface>,
53+}
54+
55+struct Surface {
56+ output: Output,
57+ id: OutputId,
58+ drm: Out,
59+ /// A frame is on its way to the screen and its vblank has not landed.
60+ queued: bool,
61+ redraw: bool,
62+ last_vblank_ns: Option<u64>,
63+ refresh_ns: u64,
64+}
65+
66+impl Surface {
67+ /// The vblank after `now` that a frame rendered now will reach.
68+ fn predict(&self, now: u64) -> u64 {
69+ match self.last_vblank_ns {
70+ Some(last) if last <= now && self.refresh_ns > 0 => {
71+ last + ((now - last) / self.refresh_ns + 1) * self.refresh_ns
72+ }
73+ _ => now,
74+ }
75+ }
76+}
77+
78+pub fn init(
79+ event_loop: &mut EventLoop<'static, State>,
80+ state: &mut State,
81+) -> Result<(), Box<dyn std::error::Error>> {
82+ let (mut session, notifier) = LibSeatSession::new()?;
83+ let seat = session.seat();
84+ let path = match primary_gpu(&seat)? {
85+ Some(p) => p,
86+ None => all_gpus(&seat)?.into_iter().next().ok_or("no GPU found")?,
87+ };
88+ let node = DrmNode::from_path(&path)?;
89+ tracing::info!(path = %path.display(), "primary GPU");
90+ let fd = session.open(
91+ &path,
92+ OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
93+ )?;
94+ let fd = DrmDeviceFd::new(DeviceFd::from(fd));
95+ let (device, drm_events) = DrmDevice::new(fd.clone(), true)?;
96+ let nvidia = device
97+ .get_driver()
98+ .map(|d| d.name().to_string_lossy().to_lowercase().contains("nvidia"))
99+ .unwrap_or(false);
100+ let gbm = GbmDevice::new(fd)?;
101+ let render_node = node
102+ .node_with_type(NodeType::Render)
103+ .and_then(Result::ok)
104+ .unwrap_or(node);
105+ let allocator = GbmAllocator::new(
106+ gbm.clone(),
107+ GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
108+ );
109+ let exporter = GbmFramebufferExporter::new(gbm.clone(), render_node.into());
110+ let render_formats: Vec<_> = state.renderer.render_formats().iter().copied().collect();
111+ let manager = DrmOutputManager::new(
112+ device,
113+ allocator,
114+ exporter,
115+ Some(gbm),
116+ [Fourcc::Xrgb8888, Fourcc::Argb8888],
117+ render_formats,
118+ );
119+
120+ let mut libinput =
121+ Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
122+ libinput
123+ .udev_assign_seat(&seat)
124+ .map_err(|()| "libinput could not take the seat")?;
125+ let handle = event_loop.handle();
126+ handle.insert_source(
127+ LibinputInputBackend::new(libinput.clone()),
128+ |event, _, state| {
129+ on_input(state, event);
130+ },
131+ )?;
132+ handle.insert_source(notifier, |event, _, state| match event {
133+ SessionEvent::PauseSession => {
134+ if let Some(drm) = state.drm.as_mut() {
135+ drm.libinput.suspend();
136+ drm.manager.pause();
137+ }
138+ }
139+ SessionEvent::ActivateSession => {
140+ if let Some(drm) = state.drm.as_mut() {
141+ if drm.libinput.resume().is_err() {
142+ tracing::warn!("libinput did not resume");
143+ }
144+ if let Err(e) = drm.manager.lock().activate(false) {
145+ tracing::warn!("could not reactivate the GPU: {e}");
146+ }
147+ for s in drm.surfaces.values_mut() {
148+ s.queued = false;
149+ s.redraw = true;
150+ }
151+ }
152+ state.needs_redraw = true;
153+ }
154+ })?;
155+ handle.insert_source(drm_events, |event, meta, state| match event {
156+ DrmEvent::VBlank(crtc) => on_vblank(state, crtc, meta),
157+ DrmEvent::Error(e) => tracing::warn!("drm: {e}"),
158+ })?;
159+ let udev = UdevBackend::new(&seat)?;
160+ handle.insert_source(udev, move |event, _, state| {
161+ if let UdevEvent::Changed { device_id } = event
162+ && state
163+ .drm
164+ .as_ref()
165+ .is_some_and(|d| d.node.dev_id() == device_id)
166+ {
167+ scan(state);
168+ }
169+ })?;
170+
171+ state.drm = Some(Drm {
172+ session,
173+ libinput,
174+ handle,
175+ node,
176+ manager,
177+ scanner: DrmScanner::new(),
178+ nvidia,
179+ surfaces: HashMap::new(),
180+ });
181+ scan(state);
182+ Ok(())
183+}
184+
185+/// Brings outputs in line with what is plugged in.
186+fn scan(state: &mut State) {
187+ let Some(drm) = state.drm.as_mut() else {
188+ return;
189+ };
190+ let events: Vec<DrmScanEvent> = match drm.scanner.scan_connectors(drm.manager.device()) {
191+ Ok(result) => result.into_iter().collect(),
192+ Err(e) => {
193+ tracing::warn!("scanning connectors failed: {e}");
194+ return;
195+ }
196+ };
197+ for event in events {
198+ match event {
199+ DrmScanEvent::Connected {
200+ connector,
201+ crtc: Some(crtc),
202+ } => connect(state, connector, crtc),
203+ DrmScanEvent::Disconnected {
204+ crtc: Some(crtc), ..
205+ } => disconnect(state, crtc),
206+ _ => {}
207+ }
208+ }
209+ state.apply_layout();
210+}
211+
212+fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
213+ let name = format!(
214+ "{}-{}",
215+ connector.interface().as_str(),
216+ connector.interface_id()
217+ );
218+ let Some(&mode) = connector
219+ .modes()
220+ .iter()
221+ .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
222+ .or_else(|| connector.modes().first())
223+ else {
224+ tracing::warn!(name, "connector has no modes");
225+ return;
226+ };
227+ let State {
228+ drm: Some(drm),
229+ renderer,
230+ layout,
231+ space,
232+ display,
233+ ..
234+ } = state
235+ else {
236+ return;
237+ };
238+ let (mm_w, mm_h) = connector.size().unwrap_or((0, 0));
239+ let output = Output::new(
240+ name.clone(),
241+ PhysicalProperties {
242+ size: (mm_w as i32, mm_h as i32).into(),
243+ subpixel: Subpixel::Unknown,
244+ make: "Unknown".into(),
245+ model: name.clone(),
246+ serial_number: String::new(),
247+ },
248+ );
249+ let wl_mode = Mode::from(mode);
250+ let x = layout
251+ .outputs()
252+ .map(|(_, o)| o.area.right())
253+ .fold(0.0, f64::max);
254+ let position = Point::from((x as i32, 0));
255+ output.change_current_state(
256+ Some(wl_mode),
257+ Some(Transform::Normal),
258+ Some(Scale::Integer(1)),
259+ Some(position),
260+ );
261+ output.set_preferred(wl_mode);
262+ output.create_global::<State>(display);
263+ let mut planes = drm.manager.device().planes(&crtc).ok();
264+ if drm.nvidia
265+ && let Some(p) = planes.as_mut()
266+ {
267+ // NVIDIA's overlay planes break composition, as anvil also found.
268+ p.overlay.clear();
269+ }
270+ let out = match drm.manager.lock().initialize_output::<_, Draw>(
271+ crtc,
272+ mode,
273+ &[connector.handle()],
274+ &output,
275+ planes,
276+ renderer,
277+ &DrmOutputRenderElements::default(),
278+ ) {
279+ Ok(out) => out,
280+ Err(e) => {
281+ tracing::warn!(name, "could not light up the output: {e}");
282+ return;
283+ }
284+ };
285+ space.map_output(&output, position);
286+ let (w, h) = mode.size();
287+ let area = Rect::new(x, 0.0, f64::from(w), f64::from(h));
288+ let id = layout.add_output(&name, area);
289+ tracing::info!(name, w, h, refresh = mode.vrefresh(), "output connected");
290+ drm.surfaces.insert(
291+ crtc,
292+ Surface {
293+ output,
294+ id,
295+ drm: out,
296+ queued: false,
297+ redraw: true,
298+ last_vblank_ns: None,
299+ refresh_ns: 1_000_000_000_000 / u64::from(wl_mode.refresh.max(1) as u32),
300+ },
301+ );
302+}
303+
304+fn disconnect(state: &mut State, crtc: crtc::Handle) {
305+ let Some(surface) = state.drm.as_mut().and_then(|d| d.surfaces.remove(&crtc)) else {
306+ return;
307+ };
308+ tracing::info!(name = surface.output.name(), "output disconnected");
309+ state.space.unmap_output(&surface.output);
310+ state.layout.remove_output(surface.id);
311+}
312+
313+/// The smithay output for a layout output, for protocols that need one.
314+pub fn output_for(drm: &Drm, id: OutputId) -> Option<Output> {
315+ drm.surfaces
316+ .values()
317+ .find(|s| s.id == id)
318+ .map(|s| s.output.clone())
319+}
320+
321+fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMetadata>) {
322+ let Some(surface) = state.drm.as_mut().and_then(|d| d.surfaces.get_mut(&crtc)) else {
323+ return;
324+ };
325+ if let Err(e) = surface.drm.frame_submitted() {
326+ tracing::warn!("frame was not submitted: {e}");
327+ }
328+ surface.queued = false;
329+ if let Some(DrmEventTime::Monotonic(t)) = meta.as_ref().map(|m| m.time) {
330+ surface.last_vblank_ns = Some(t.as_nanos() as u64);
331+ }
332+ render_surface(state, crtc);
333+}
334+
335+/// Renders every output that has something new and no frame in flight.
336+pub fn render(state: &mut State) {
337+ let Some(drm) = state.drm.as_mut() else {
338+ return;
339+ };
340+ if std::mem::take(&mut state.needs_redraw) {
341+ for s in drm.surfaces.values_mut() {
342+ s.redraw = true;
343+ }
344+ }
345+ let ready: Vec<crtc::Handle> = drm
346+ .surfaces
347+ .iter()
348+ .filter(|(_, s)| s.redraw && !s.queued)
349+ .map(|(c, _)| *c)
350+ .collect();
351+ for crtc in ready {
352+ render_surface(state, crtc);
353+ }
354+}
355+
356+fn render_surface(state: &mut State, crtc: crtc::Handle) {
357+ let State {
358+ drm: Some(drm),
359+ renderer,
360+ layout,
361+ windows,
362+ anim,
363+ borders,
364+ config,
365+ space,
366+ start,
367+ cursor,
368+ cursor_status,
369+ pointer_pos,
370+ ..
371+ } = state
372+ else {
373+ return;
374+ };
375+ if !drm.session.is_active() {
376+ return;
377+ }
378+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
379+ return;
380+ };
381+ if surface.queued {
382+ return;
383+ }
384+ let now = surface.predict(now_ns());
385+ let Some(area) = layout.output(surface.id).map(|o| o.area) else {
386+ return;
387+ };
388+ let mut elements = Vec::new();
389+ let local = *pointer_pos - Point::from((area.x, area.y));
390+ if local.x >= 0.0 && local.y >= 0.0 && local.x < area.w && local.y < area.h {
391+ elements.extend(cursor.elements(renderer, cursor_status, local));
392+ }
393+ elements.extend(crate::scene::build(
394+ layout,
395+ surface.id,
396+ windows,
397+ anim,
398+ renderer,
399+ now,
400+ &config.decoration,
401+ borders,
402+ ));
403+ let animating = anim.active(now);
404+ anim.prune(now);
405+ match surface
406+ .drm
407+ .render_frame(renderer, &elements, CLEAR, FrameFlags::DEFAULT)
408+ {
409+ Ok(result) if !result.is_empty => match surface.drm.queue_frame(()) {
410+ Ok(()) => surface.queued = true,
411+ Err(e) => tracing::warn!("queueing a frame failed: {e}"),
412+ },
413+ Ok(_) => {}
414+ Err(e) => tracing::warn!("rendering failed: {e}"),
415+ }
416+ surface.redraw = animating;
417+ if animating && !surface.queued {
418+ let wait = Duration::from_nanos(surface.refresh_ns);
419+ let _ = drm
420+ .handle
421+ .insert_source(Timer::from_duration(wait), move |_, _, state| {
422+ if let Some(s) = state.drm.as_mut().and_then(|d| d.surfaces.get_mut(&crtc)) {
423+ s.redraw = true;
424+ }
425+ render(state);
426+ TimeoutAction::Drop
427+ });
428+ }
429+ let elapsed = start.elapsed();
430+ let output = surface.output.clone();
431+ for window in space.elements_for_output(&output) {
432+ window.send_frame(&output, elapsed, Some(Duration::ZERO), |_, _| {
433+ Some(output.clone())
434+ });
435+ }
436+}
437+
438+fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {
439+ match event {
440+ InputEvent::Keyboard { event } => {
441+ let time = event.time();
442+ state.key(event.key_code(), event.state(), time);
443+ }
444+ InputEvent::PointerMotion { event } => {
445+ let pos = clamp(state, state.pointer_pos + event.delta());
446+ state.pointer_to(pos, event.time());
447+ state.pointer_frame();
448+ state.needs_redraw = true;
449+ }
450+ InputEvent::PointerMotionAbsolute { event } => {
451+ let Some(area) = state.layout.outputs().next().map(|(_, o)| o.area) else {
452+ return;
453+ };
454+ let p = event.position_transformed((area.w as i32, area.h as i32).into());
455+ let pos = Point::from((area.x + p.x, area.y + p.y));
456+ state.pointer_to(pos, event.time());
457+ state.pointer_frame();
458+ state.needs_redraw = true;
459+ }
460+ InputEvent::PointerButton { event } => {
461+ let time = event.time();
462+ state.pointer_button(event.button_code(), event.state(), time);
463+ state.pointer_frame();
464+ }
465+ InputEvent::PointerAxis { event } => {
466+ let source = event.source();
467+ let mut frame = AxisFrame::new(event.time()).source(source);
468+ for axis in [Axis::Horizontal, Axis::Vertical] {
469+ if let Some(v120) = event.amount_v120(axis) {
470+ frame = frame.v120(axis, v120 as i32);
471+ }
472+ let amount = event
473+ .amount(axis)
474+ .or_else(|| event.amount_v120(axis).map(|v| v * 15.0 / 120.0));
475+ match amount {
476+ Some(a) if a != 0.0 => frame = frame.value(axis, a),
477+ Some(_) if source == AxisSource::Finger => frame = frame.stop(axis),
478+ _ => {}
479+ }
480+ }
481+ state.pointer_axis(frame);
482+ state.pointer_frame();
483+ }
484+ _ => {}
485+ }
486+}
487+
488+/// Keeps the pointer on some output, sliding along the edge it hit.
489+fn clamp(
490+ state: &State,
491+ pos: Point<f64, smithay::utils::Logical>,
492+) -> Point<f64, smithay::utils::Logical> {
493+ let inside = |a: &Rect, p: Point<f64, smithay::utils::Logical>| {
494+ p.x >= a.x && p.y >= a.y && p.x < a.right() && p.y < a.bottom()
495+ };
496+ let areas: Vec<Rect> = state.layout.outputs().map(|(_, o)| o.area).collect();
497+ if areas.iter().any(|a| inside(a, pos)) {
498+ return pos;
499+ }
500+ let Some(a) = areas
501+ .iter()
502+ .find(|a| inside(a, state.pointer_pos))
503+ .or(areas.first())
504+ else {
505+ return pos;
506+ };
507+ Point::from((
508+ pos.x.clamp(a.x, a.right() - 1.0),
509+ pos.y.clamp(a.y, a.bottom() - 1.0),
510+ ))
511+}
Modified file: crates/monolith/src/handlers.rs+4 additions−1 deletions
@@ -160,7 +160,10 @@ impl SeatHandler for State {
160160 &mut self.seat_state
161161 }
162162
163- fn cursor_image(&mut self, _seat: &Seat<Self>, _image: CursorImageStatus) {}
163+ fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
164+ self.cursor_status = image;
165+ self.needs_redraw = true;
166+ }
164167
165168 fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
166169 let client = focused.and_then(|s| self.display.get_client(s.id()).ok());
Modified file: crates/monolith/src/input.rs+30 additions−6 deletions
@@ -5,7 +5,8 @@ use crate::state::State;
55 use monolith_config::Modifier;
66 use monolith_layout::{Rect, WindowId};
77 use smithay::backend::input::{ButtonState, InputTime, KeyState};
8-use smithay::input::keyboard::{FilterResult, Keycode};
8+use smithay::backend::session::Session;
9+use smithay::input::keyboard::{FilterResult, Keycode, xkb};
910 use smithay::input::pointer::{AxisFrame, ButtonEvent, MotionEvent};
1011 use smithay::utils::{Logical, Point, SERIAL_COUNTER};
1112
@@ -86,24 +87,47 @@ impl State {
8687 FilterResult::Forward
8788 };
8889 }
90+ let sym = handle.modified_sym().raw();
91+ if (xkb::keysyms::KEY_XF86Switch_VT_1..=xkb::keysyms::KEY_XF86Switch_VT_12)
92+ .contains(&sym)
93+ {
94+ state.suppressed.push(code);
95+ let vt = (sym - xkb::keysyms::KEY_XF86Switch_VT_1 + 1) as i32;
96+ return FilterResult::Intercept(Some(Action::Vt(vt)));
97+ }
8998 match state.binds.find(mods, &handle.raw_syms()) {
9099 Some(cmd) => {
91100 tracing::debug!(?cmd, "bind");
92101 state.suppressed.push(code);
93- FilterResult::Intercept(Some(cmd))
102+ FilterResult::Intercept(Some(Action::Run(cmd)))
94103 }
95104 None => FilterResult::Forward,
96105 }
97106 },
98107 );
99- if let Some(Some(cmd)) = action
100- && let Err(e) = self.run(cmd)
101- {
102- tracing::warn!("bind failed: {e}");
108+ match action {
109+ Some(Some(Action::Run(cmd))) => {
110+ if let Err(e) = self.run(cmd) {
111+ tracing::warn!("bind failed: {e}");
112+ }
113+ }
114+ Some(Some(Action::Vt(vt))) => {
115+ if let Some(drm) = self.drm.as_mut()
116+ && let Err(e) = drm.session.change_vt(vt)
117+ {
118+ tracing::warn!("switching to VT {vt} failed: {e}");
119+ }
120+ }
121+ _ => {}
103122 }
104123 }
105124 }
106125
126+enum Action {
127+ Run(monolith_ipc::Command),
128+ Vt(i32),
129+}
130+
107131 fn focus_under_pointer(state: &mut State) {
108132 let pos = state.pointer_pos;
109133 let id = state
Modified file: crates/monolith/src/main.rs+39 additions−7 deletions
@@ -1,5 +1,7 @@
11 mod anim;
22 mod commands;
3+mod cursor;
4+mod drm;
35 mod handlers;
46 mod input;
57 mod ipc;
@@ -32,6 +34,9 @@ enum Cmd {
3234 /// read this config instead of ~/.config/monolith/config.toml
3335 #[pound(long)]
3436 config: Option<String>,
37+ /// where to run, auto picks nested inside another compositor
38+ #[pound(long)]
39+ backend: Option<Backend>,
3540 /// a program to start inside, with its arguments
3641 #[pound(trailing)]
3742 program: Vec<String>,
@@ -47,6 +52,13 @@ enum Cmd {
4752 Subscribe,
4853 }
4954
55+#[derive(ValueEnum, Clone, Copy, PartialEq, Eq)]
56+enum Backend {
57+ Auto,
58+ Nested,
59+ Drm,
60+}
61+
5062 #[derive(ValueEnum)]
5163 enum What {
5264 State,
@@ -54,9 +66,17 @@ enum What {
5466 }
5567
5668 fn main() -> ExitCode {
57- let (config, program) = match Cli::parse().cmd {
58- None => (None, Vec::new()),
59- Some(Cmd::Run { config, program }) => (config.map(PathBuf::from), program),
69+ let (config, backend, program) = match Cli::parse().cmd {
70+ None => (None, Backend::Auto, Vec::new()),
71+ Some(Cmd::Run {
72+ config,
73+ backend,
74+ program,
75+ }) => (
76+ config.map(PathBuf::from),
77+ backend.unwrap_or(Backend::Auto),
78+ program,
79+ ),
6080 Some(Cmd::Msg { command }) => {
6181 return match Command::parse(&command) {
6282 Ok(cmd) => ipc::client(Request::Run(cmd)),
@@ -76,7 +96,12 @@ fn main() -> ExitCode {
7696 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
7797 )
7898 .init();
79- match run(config, &program) {
99+ let backend = match backend {
100+ Backend::Auto if std::env::var_os("WAYLAND_DISPLAY").is_some() => Backend::Nested,
101+ Backend::Auto => Backend::Drm,
102+ b => b,
103+ };
104+ match run(config, backend, &program) {
80105 Ok(()) => ExitCode::SUCCESS,
81106 Err(e) => {
82107 tracing::error!("{e}");
@@ -85,13 +110,20 @@ fn main() -> ExitCode {
85110 }
86111 }
87112
88-fn run(config: Option<PathBuf>, program: &[String]) -> Result<(), Box<dyn std::error::Error>> {
113+fn run(
114+ config: Option<PathBuf>,
115+ backend: Backend,
116+ program: &[String],
117+) -> Result<(), Box<dyn std::error::Error>> {
89118 let mut event_loop: EventLoop<'static, State> = EventLoop::try_new()?;
90119 let display: Display<State> = Display::new()?;
91120 let mut state = State::new(&mut event_loop, display, config)?;
92- nested::init(&mut event_loop, &mut state)?;
121+ match backend {
122+ Backend::Drm => drm::init(&mut event_loop, &mut state)?,
123+ _ => nested::init(&mut event_loop, &mut state)?,
124+ }
93125 ipc::init(&event_loop, &mut state)?;
94- tracing::info!(socket = ?state.socket_name, device = state.renderer.device_name(), "monolith running nested");
126+ tracing::info!(socket = ?state.socket_name, device = state.renderer.device_name(), "monolith running");
95127
96128 let mut startup = state.config.startup.clone();
97129 if !program.is_empty() {
Modified file: crates/monolith/src/nested/mod.rs+3 additions
@@ -178,6 +178,7 @@ pub fn render(state: &mut State) {
178178 anim,
179179 borders,
180180 config,
181+ output_id,
181182 ..
182183 } = state;
183184 let Some(host) = host.as_mut() else { return };
@@ -221,8 +222,10 @@ pub fn render(state: &mut State) {
221222 return;
222223 }
223224 };
225+ let Some(o) = *output_id else { return };
224226 let elements = crate::scene::build(
225227 layout,
228+ o,
226229 windows,
227230 anim,
228231 renderer,
Modified file: crates/monolith/src/scene.rs+33 additions−27 deletions
@@ -3,8 +3,9 @@
33
44 use crate::anim::Animator;
55 use monolith_config::Decoration;
6-use monolith_layout::{Layout, Rect, WindowId};
6+use monolith_layout::{Layout, OutputId, Rect, WindowId};
77 use monolith_render_vk::{Effect, VulkanFrame, VulkanRenderer, VulkanTexture};
8+use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
89 use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement};
910 use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
1011 use smithay::backend::renderer::element::texture::TextureRenderElement;
@@ -25,6 +26,7 @@ smithay::backend::renderer::element::render_elements! {
2526 Texture=TextureRenderElement<VulkanTexture>,
2627 Effect=EffectElement,
2728 Border=SolidColorRenderElement,
29+ Memory=MemoryRenderBufferRenderElement<VulkanRenderer>,
2830 }
2931
3032 /// Border strips per window, kept between frames so unchanged borders cause
@@ -32,9 +34,11 @@ smithay::backend::renderer::element::render_elements! {
3234 #[derive(Default)]
3335 pub struct Borders(HashMap<WindowId, [SolidColorBuffer; 4]>);
3436
35-/// Elements for every output, front to back.
37+/// Elements for output `o` in its own coordinates, front to back.
38+#[expect(clippy::too_many_arguments)]
3639 pub fn build(
3740 layout: &Layout,
41+ o: OutputId,
3842 windows: &HashMap<WindowId, Window>,
3943 anim: &Animator,
4044 renderer: &mut VulkanRenderer,
@@ -42,38 +46,40 @@ pub fn build(
4246 deco: &Decoration,
4347 borders: &mut Borders,
4448 ) -> Vec<Draw> {
49+ let Some(area) = layout.output(o).map(|out| out.area) else {
50+ return Vec::new();
51+ };
52+ let local = |r: Rect| Rect::new(r.x - area.x, r.y - area.y, r.w, r.h);
4553 let mut ghosts = Vec::new();
4654 let mut tiled = Vec::new();
4755 let mut above = Vec::new();
48- for (o, out) in layout.outputs() {
49- for g in anim.ghosts().iter().filter(|g| g.output == o) {
50- if let Some(r) = anim.tiled(o, g.id, now) {
51- ghosts.push((g, r));
52- }
53- }
54- let arr = layout.arrange(o);
55- let fullscreen_at_rest = arr
56- .fullscreen
57- .is_some_and(|w| anim.gliding(w, now).is_none());
58- for p in &arr.tiled {
59- if Some(p.window) == arr.fullscreen || fullscreen_at_rest {
60- continue;
61- }
62- match anim.gliding(p.window, now) {
63- Some(r) => above.push((p.window, r, true)),
64- None => {
65- let r = anim.tiled(o, p.window, now).unwrap_or(p.rect);
66- tiled.push((p.window, r, true));
67- }
68- }
56+ for g in anim.ghosts().iter().filter(|g| g.output == o) {
57+ if let Some(r) = anim.tiled(o, g.id, now) {
58+ ghosts.push((g, local(r)));
6959 }
70- for &(w, r) in &arr.floating {
71- above.push((w, anim.gliding(w, now).unwrap_or(r), true));
60+ }
61+ let arr = layout.arrange(o);
62+ let fullscreen_at_rest = arr
63+ .fullscreen
64+ .is_some_and(|w| anim.gliding(w, now).is_none());
65+ for p in &arr.tiled {
66+ if Some(p.window) == arr.fullscreen || fullscreen_at_rest {
67+ continue;
7268 }
73- if let Some(w) = arr.fullscreen {
74- above.push((w, anim.gliding(w, now).unwrap_or(out.area), false));
69+ match anim.gliding(p.window, now) {
70+ Some(r) => above.push((p.window, local(r), true)),
71+ None => {
72+ let r = anim.tiled(o, p.window, now).unwrap_or(p.rect);
73+ tiled.push((p.window, local(r), true));
74+ }
7575 }
7676 }
77+ for &(w, r) in &arr.floating {
78+ above.push((w, local(anim.gliding(w, now).unwrap_or(r)), true));
79+ }
80+ if let Some(w) = arr.fullscreen {
81+ above.push((w, local(anim.gliding(w, now).unwrap_or(area)), false));
82+ }
7783 borders.0.retain(|id, _| windows.contains_key(id));
7884 let focused = layout.focused();
7985 let mut out = Vec::new();
Modified file: crates/monolith/src/state.rs+17 additions−6 deletions
@@ -11,6 +11,7 @@ use smithay::backend::renderer::Renderer;
1111 use smithay::backend::renderer::utils::{import_surface_tree, with_renderer_surface_state};
1212 use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};
1313 use smithay::input::keyboard::{Keycode, XkbConfig};
14+use smithay::input::pointer::CursorImageStatus;
1415 use smithay::input::{Seat, SeatState};
1516 use smithay::reexports::calloop::generic::Generic;
1617 use smithay::reexports::calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction};
@@ -42,6 +43,9 @@ pub struct State {
4243 pub space: Space<Window>,
4344 pub popups: PopupManager,
4445 pub host: Option<Host>,
46+ pub drm: Option<crate::drm::Drm>,
47+ pub cursor: crate::cursor::Cursor,
48+ pub cursor_status: CursorImageStatus,
4549 pub needs_redraw: bool,
4650
4751 pub layout: Layout,
@@ -132,6 +136,9 @@ impl State {
132136 space: Space::default(),
133137 popups: PopupManager::default(),
134138 host: None,
139+ drm: None,
140+ cursor: crate::cursor::Cursor::load(),
141+ cursor_status: CursorImageStatus::default_named(),
135142 needs_redraw: true,
136143 layout,
137144 windows: HashMap::new(),
@@ -169,6 +176,7 @@ impl State {
169176 self.space.refresh();
170177 self.popups.cleanup();
171178 crate::nested::render(self);
179+ crate::drm::render(self);
172180 crate::ipc::broadcast(self);
173181 self.publish_workspaces();
174182 let _ = self.display.flush_clients();
@@ -411,6 +419,14 @@ impl State {
411419 }
412420 }
413421
422+ /// The smithay output behind a layout output, on whichever backend runs.
423+ pub fn smithay_output(&self, o: OutputId) -> Option<smithay::output::Output> {
424+ if let Some(host) = self.host.as_ref().filter(|_| Some(o) == self.output_id) {
425+ return Some(host.output.clone());
426+ }
427+ self.drm.as_ref().and_then(|d| crate::drm::output_for(d, o))
428+ }
429+
414430 fn publish_workspaces(&mut self) {
415431 if !self.workspaces.has_clients() {
416432 return;
@@ -419,12 +435,7 @@ impl State {
419435 let groups = l
420436 .outputs()
421437 .filter_map(|(o, _)| {
422- let output = self
423- .host
424- .as_ref()
425- .filter(|_| Some(o) == self.output_id)?
426- .output
427- .clone();
438+ let output = self.smithay_output(o)?;
428439 let workspaces = l
429440 .tags()
430441 .enumerate()
Modified file: flake.nix+5 additions
@@ -40,6 +40,11 @@
4040 buildInputs = with pkgs; [
4141 libxkbcommon
4242 vulkan-loader
43+ libinput
44+ seatd
45+ libgbm
46+ libdrm
47+ udev
4348 ];
4449 };
4550 cargoArtifacts = craneLib.buildDepsOnly commonArgs;