├── .editorconfig ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── PKGBUILD ├── PKGBUILD.in ├── README.md ├── deps ├── README.md └── niri-ipc-25.2.0 │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── lib.rs │ ├── socket.rs │ └── state.rs ├── rustfmt.toml ├── scripts ├── cargo-tree-licences-not-mit.sh ├── ipc.bt ├── ipc.out.txt ├── msrvcheck.sh └── pkgbuild.sh └── src ├── cli.rs ├── compositors.rs ├── compositors ├── hyprland.rs ├── niri2502.rs ├── niri2505.rs └── sway.rs ├── gpu.rs ├── gpu ├── device.rs ├── instance.rs └── memory.rs ├── image.rs ├── main.rs ├── poll.rs ├── signal.rs └── wayland.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 80 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.yml] 16 | indent_size = 2 17 | 18 | [Makefile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gergo-salyi/multibg-wayland/2f80face3f8f73f6a044f97ab24010f344393eb0/.gitmodules -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.1 - 2025-06-01 4 | - Fix niri compatibility [#16](https://github.com/gergo-salyi/multibg-wayland/issues/16) 5 | - Update dependencies 6 | 7 | ## 0.2.0 - 2025-04-26 8 | 9 | ### Breaking changes 10 | - Project is being renamed to multibg-wayland to signify support for Wayland compositors other than Sway 11 | - Terminate gracefully on signals INT, HUP or TERM with exit code 0. A second of such signals still kills. USR1 and USR2 are reserved for future use 12 | - License of the source files is still MIT OR Apache-2.0 but built objects now might be under GPL-3.0-or-later. 13 | - Arch Linux package now depends on dav1d 14 | 15 | ### Other changes 16 | - Added support for the Hyprland and niri wayland compositors 17 | - Inside the wallpaper directory wallpapers symlinked to the same image are now loaded only once and shared saving memory use 18 | - Correct docs about the memory type we use. We only used CPU memory and will only use CPU memory unless the new --gpu option is given. 19 | - Add ability to store wallpapers in GPU memory with the --gpu command line option. Requires Vulkan 1.1 or newer. This might save a few milliseconds latency on wallpaper switches avoiding the use of CPU memory and PCIe bandwidth 20 | - Added support for AVIF images. Requires dav1d native dependency and the avif compile time feature (disabled by default for from source builds, enabled by default for Arch Linux package) 21 | - Update README for the new features 22 | - Update dependencies, require Rust compiler version 1.82 or newer 23 | - Lots of internal changes supporting all the above 24 | 25 | ## 0.1.10 - 2024-11-17 26 | - Fix sometimes disappearing mouse cursor above wallpapers 27 | - Add small clarifications to README 28 | - Update Arch Linux PKGBUILD to follow their Rust package guidelines 29 | - Add minimum supported Rust version so Cargo can enforce it at build time 30 | - Update dependencies 31 | 32 | ## 0.1.9 - 2024-10-09 33 | - Fix broken wallpapers on 90 degree rotated monitors [#9](https://github.com/gergo-salyi/multibg-sway/issues/9) 34 | - Update dependencies 35 | 36 | ## 0.1.8 - 2024-09-30 37 | - Try to fix crash with wayland protocol error regarding wlr_layer_surface [#8](https://github.com/gergo-salyi/multibg-sway/issues/8) 38 | - Update dependencies 39 | - Add logging messages 40 | - Code formatting with editorconfig 41 | 42 | ## 0.1.7 - 2024-05-11 43 | - Fix image corruption for certain pixel formats when output width is not a multiple of 4 [#6](https://github.com/gergo-salyi/multibg-sway/issues/6) 44 | - Add the --pixelformat cli argument. Setting --pixelformat=baseline can force wl_buffers to use the wayland default xrgb8888 pixel format if bgr888 or future others would break for any reason 45 | 46 | ## 0.1.6 - 2024-03-25 47 | - Fix displaying the wallpapers on outputs with fractional scale factor. This may help with [#5](https://github.com/gergo-salyi/multibg-sway/issues/5) 48 | 49 | ## 0.1.5 - 2024-01-02 50 | - Fix displaying the wallpapers on outputs with higher than 1 integer scale factor. This may help with [#4](https://github.com/gergo-salyi/multibg-sway/issues/4) 51 | 52 | ## 0.1.4 - 2023-08-31 53 | - Allocate/release graphics memory per output when the output is connected/disconnected. This may help with [#2](https://github.com/gergo-salyi/multibg-sway/issues/2) 54 | - Log graphics memory use (our wayland shared memory pool sizes) 55 | - Minor fix to avoid a logged error on redrawing backgrounds already being drawn 56 | - Update dependencies 57 | 58 | ## 0.1.3 - 2023-05-05 59 | - Update dependencies, including fast_image_resize which fixed a major bug 60 | 61 | ## 0.1.2 - 2023-04-27 62 | - Fix crash on suspend [#1](https://github.com/gergo-salyi/multibg-sway/issues/1) 63 | - Implement automatic image resizing 64 | 65 | ## 0.1.1 66 | - Initial release 67 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.8.12" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 16 | dependencies = [ 17 | "cfg-if", 18 | "once_cell", 19 | "version_check", 20 | "zerocopy", 21 | ] 22 | 23 | [[package]] 24 | name = "aho-corasick" 25 | version = "1.1.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 28 | dependencies = [ 29 | "memchr", 30 | ] 31 | 32 | [[package]] 33 | name = "anstream" 34 | version = "0.6.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 37 | dependencies = [ 38 | "anstyle", 39 | "anstyle-parse", 40 | "anstyle-query", 41 | "anstyle-wincon", 42 | "colorchoice", 43 | "is_terminal_polyfill", 44 | "utf8parse", 45 | ] 46 | 47 | [[package]] 48 | name = "anstyle" 49 | version = "1.0.10" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 52 | 53 | [[package]] 54 | name = "anstyle-parse" 55 | version = "0.2.6" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 58 | dependencies = [ 59 | "utf8parse", 60 | ] 61 | 62 | [[package]] 63 | name = "anstyle-query" 64 | version = "1.1.2" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 67 | dependencies = [ 68 | "windows-sys", 69 | ] 70 | 71 | [[package]] 72 | name = "anstyle-wincon" 73 | version = "3.0.8" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 76 | dependencies = [ 77 | "anstyle", 78 | "once_cell_polyfill", 79 | "windows-sys", 80 | ] 81 | 82 | [[package]] 83 | name = "anyhow" 84 | version = "1.0.98" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 87 | 88 | [[package]] 89 | name = "ash" 90 | version = "0.38.0+1.3.281" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" 93 | dependencies = [ 94 | "libloading", 95 | ] 96 | 97 | [[package]] 98 | name = "autocfg" 99 | version = "1.4.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 102 | 103 | [[package]] 104 | name = "av-data" 105 | version = "0.4.4" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "fca67ba5d317924c02180c576157afd54babe48a76ebc66ce6d34bb8ba08308e" 108 | dependencies = [ 109 | "byte-slice-cast", 110 | "bytes", 111 | "num-derive", 112 | "num-rational", 113 | "num-traits", 114 | ] 115 | 116 | [[package]] 117 | name = "bit_field" 118 | version = "0.10.2" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" 121 | 122 | [[package]] 123 | name = "bitflags" 124 | version = "1.3.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 127 | 128 | [[package]] 129 | name = "bitflags" 130 | version = "2.9.1" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 133 | 134 | [[package]] 135 | name = "bitreader" 136 | version = "0.3.11" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "886559b1e163d56c765bc3a985febb4eee8009f625244511d8ee3c432e08c066" 139 | dependencies = [ 140 | "cfg-if", 141 | ] 142 | 143 | [[package]] 144 | name = "byte-slice-cast" 145 | version = "1.2.3" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" 148 | 149 | [[package]] 150 | name = "bytemuck" 151 | version = "1.23.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" 154 | 155 | [[package]] 156 | name = "byteorder" 157 | version = "1.5.0" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 160 | 161 | [[package]] 162 | name = "byteorder-lite" 163 | version = "0.1.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" 166 | 167 | [[package]] 168 | name = "bytes" 169 | version = "1.10.1" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 172 | 173 | [[package]] 174 | name = "cc" 175 | version = "1.2.25" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" 178 | dependencies = [ 179 | "shlex", 180 | ] 181 | 182 | [[package]] 183 | name = "cfg-expr" 184 | version = "0.20.0" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "e34e221e91c7eb5e8315b5c9cf1a61670938c0626451f954a51693ed44b37f45" 187 | dependencies = [ 188 | "smallvec", 189 | "target-lexicon", 190 | ] 191 | 192 | [[package]] 193 | name = "cfg-if" 194 | version = "1.0.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 197 | 198 | [[package]] 199 | name = "clap" 200 | version = "4.5.39" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 203 | dependencies = [ 204 | "clap_builder", 205 | "clap_derive", 206 | ] 207 | 208 | [[package]] 209 | name = "clap_builder" 210 | version = "4.5.39" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 213 | dependencies = [ 214 | "anstream", 215 | "anstyle", 216 | "clap_lex", 217 | "strsim", 218 | ] 219 | 220 | [[package]] 221 | name = "clap_derive" 222 | version = "4.5.32" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 225 | dependencies = [ 226 | "heck", 227 | "proc-macro2", 228 | "quote", 229 | "syn", 230 | ] 231 | 232 | [[package]] 233 | name = "clap_lex" 234 | version = "0.7.4" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 237 | 238 | [[package]] 239 | name = "color_quant" 240 | version = "1.1.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 243 | 244 | [[package]] 245 | name = "colorchoice" 246 | version = "1.0.3" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 249 | 250 | [[package]] 251 | name = "crc32fast" 252 | version = "1.4.2" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 255 | dependencies = [ 256 | "cfg-if", 257 | ] 258 | 259 | [[package]] 260 | name = "crossbeam-deque" 261 | version = "0.8.6" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 264 | dependencies = [ 265 | "crossbeam-epoch", 266 | "crossbeam-utils", 267 | ] 268 | 269 | [[package]] 270 | name = "crossbeam-epoch" 271 | version = "0.9.18" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 274 | dependencies = [ 275 | "crossbeam-utils", 276 | ] 277 | 278 | [[package]] 279 | name = "crossbeam-utils" 280 | version = "0.8.21" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 283 | 284 | [[package]] 285 | name = "crunchy" 286 | version = "0.2.3" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" 289 | 290 | [[package]] 291 | name = "cursor-icon" 292 | version = "1.2.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" 295 | 296 | [[package]] 297 | name = "dav1d" 298 | version = "0.10.4" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "80c3f80814db85397819d464bb553268992c393b4b3b5554b89c1655996d5926" 301 | dependencies = [ 302 | "av-data", 303 | "bitflags 2.9.1", 304 | "dav1d-sys", 305 | "static_assertions", 306 | ] 307 | 308 | [[package]] 309 | name = "dav1d-sys" 310 | version = "0.8.3" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "c3c91aea6668645415331133ed6f8ddf0e7f40160cd97a12d59e68716a58704b" 313 | dependencies = [ 314 | "libc", 315 | "system-deps", 316 | ] 317 | 318 | [[package]] 319 | name = "document-features" 320 | version = "0.2.11" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 323 | dependencies = [ 324 | "litrs", 325 | ] 326 | 327 | [[package]] 328 | name = "downcast-rs" 329 | version = "1.2.1" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" 332 | 333 | [[package]] 334 | name = "env_filter" 335 | version = "0.1.3" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 338 | dependencies = [ 339 | "log", 340 | "regex", 341 | ] 342 | 343 | [[package]] 344 | name = "env_logger" 345 | version = "0.11.8" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 348 | dependencies = [ 349 | "anstream", 350 | "anstyle", 351 | "env_filter", 352 | "jiff", 353 | "log", 354 | ] 355 | 356 | [[package]] 357 | name = "equivalent" 358 | version = "1.0.2" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 361 | 362 | [[package]] 363 | name = "errno" 364 | version = "0.3.12" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 367 | dependencies = [ 368 | "libc", 369 | "windows-sys", 370 | ] 371 | 372 | [[package]] 373 | name = "exr" 374 | version = "1.73.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" 377 | dependencies = [ 378 | "bit_field", 379 | "half", 380 | "lebe", 381 | "miniz_oxide", 382 | "rayon-core", 383 | "smallvec", 384 | "zune-inflate", 385 | ] 386 | 387 | [[package]] 388 | name = "fallible_collections" 389 | version = "0.4.9" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "a88c69768c0a15262df21899142bc6df9b9b823546d4b4b9a7bc2d6c448ec6fd" 392 | dependencies = [ 393 | "hashbrown 0.13.2", 394 | ] 395 | 396 | [[package]] 397 | name = "fast_image_resize" 398 | version = "5.1.4" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "d372ab3252d8f162d858d675a3d88a8c33ba24a6238837c50c8851911c7e89cd" 401 | dependencies = [ 402 | "cfg-if", 403 | "document-features", 404 | "num-traits", 405 | "thiserror", 406 | ] 407 | 408 | [[package]] 409 | name = "fdeflate" 410 | version = "0.3.7" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 413 | dependencies = [ 414 | "simd-adler32", 415 | ] 416 | 417 | [[package]] 418 | name = "flate2" 419 | version = "1.1.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 422 | dependencies = [ 423 | "crc32fast", 424 | "miniz_oxide", 425 | ] 426 | 427 | [[package]] 428 | name = "gif" 429 | version = "0.13.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" 432 | dependencies = [ 433 | "color_quant", 434 | "weezl", 435 | ] 436 | 437 | [[package]] 438 | name = "half" 439 | version = "2.6.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" 442 | dependencies = [ 443 | "cfg-if", 444 | "crunchy", 445 | ] 446 | 447 | [[package]] 448 | name = "hashbrown" 449 | version = "0.13.2" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 452 | dependencies = [ 453 | "ahash", 454 | ] 455 | 456 | [[package]] 457 | name = "hashbrown" 458 | version = "0.15.3" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 461 | 462 | [[package]] 463 | name = "heck" 464 | version = "0.5.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 467 | 468 | [[package]] 469 | name = "image" 470 | version = "0.25.6" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" 473 | dependencies = [ 474 | "bytemuck", 475 | "byteorder-lite", 476 | "color_quant", 477 | "dav1d", 478 | "exr", 479 | "gif", 480 | "image-webp", 481 | "mp4parse", 482 | "num-traits", 483 | "png", 484 | "qoi", 485 | "tiff", 486 | "zune-core", 487 | "zune-jpeg", 488 | ] 489 | 490 | [[package]] 491 | name = "image-webp" 492 | version = "0.2.1" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" 495 | dependencies = [ 496 | "byteorder-lite", 497 | "quick-error", 498 | ] 499 | 500 | [[package]] 501 | name = "indexmap" 502 | version = "2.9.0" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 505 | dependencies = [ 506 | "equivalent", 507 | "hashbrown 0.15.3", 508 | ] 509 | 510 | [[package]] 511 | name = "is_terminal_polyfill" 512 | version = "1.70.1" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 515 | 516 | [[package]] 517 | name = "itoa" 518 | version = "1.0.15" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 521 | 522 | [[package]] 523 | name = "jiff" 524 | version = "0.2.14" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 527 | dependencies = [ 528 | "jiff-static", 529 | "log", 530 | "portable-atomic", 531 | "portable-atomic-util", 532 | "serde", 533 | ] 534 | 535 | [[package]] 536 | name = "jiff-static" 537 | version = "0.2.14" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 540 | dependencies = [ 541 | "proc-macro2", 542 | "quote", 543 | "syn", 544 | ] 545 | 546 | [[package]] 547 | name = "jpeg-decoder" 548 | version = "0.3.1" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 551 | 552 | [[package]] 553 | name = "lebe" 554 | version = "0.5.2" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" 557 | 558 | [[package]] 559 | name = "libc" 560 | version = "0.2.172" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 563 | 564 | [[package]] 565 | name = "libloading" 566 | version = "0.8.8" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 569 | dependencies = [ 570 | "cfg-if", 571 | "windows-targets 0.53.0", 572 | ] 573 | 574 | [[package]] 575 | name = "linux-raw-sys" 576 | version = "0.4.15" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 579 | 580 | [[package]] 581 | name = "litrs" 582 | version = "0.4.1" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 585 | 586 | [[package]] 587 | name = "log" 588 | version = "0.4.27" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 591 | 592 | [[package]] 593 | name = "memchr" 594 | version = "2.7.4" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 597 | 598 | [[package]] 599 | name = "memmap2" 600 | version = "0.9.5" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" 603 | dependencies = [ 604 | "libc", 605 | ] 606 | 607 | [[package]] 608 | name = "miniz_oxide" 609 | version = "0.8.8" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 612 | dependencies = [ 613 | "adler2", 614 | "simd-adler32", 615 | ] 616 | 617 | [[package]] 618 | name = "mp4parse" 619 | version = "0.17.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "63a35203d3c6ce92d5251c77520acb2e57108c88728695aa883f70023624c570" 622 | dependencies = [ 623 | "bitreader", 624 | "byteorder", 625 | "fallible_collections", 626 | "log", 627 | "num-traits", 628 | "static_assertions", 629 | ] 630 | 631 | [[package]] 632 | name = "multibg-wayland" 633 | version = "0.2.1" 634 | dependencies = [ 635 | "anyhow", 636 | "ash", 637 | "clap", 638 | "env_logger", 639 | "fast_image_resize", 640 | "image", 641 | "libc", 642 | "log", 643 | "multibg-wayland-niri-ipc", 644 | "niri-ipc", 645 | "rustix", 646 | "scopeguard", 647 | "serde", 648 | "serde_json", 649 | "smithay-client-toolkit", 650 | "swayipc", 651 | ] 652 | 653 | [[package]] 654 | name = "multibg-wayland-niri-ipc" 655 | version = "0.250200.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "57a296f0c9e420f45cc2671130f99e9291c99c644934ad948c8a2f34e75ba368" 658 | dependencies = [ 659 | "serde", 660 | "serde_json", 661 | ] 662 | 663 | [[package]] 664 | name = "niri-ipc" 665 | version = "25.5.1" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "dc3e165f7854b2f83054a2e8f7024baa49666ad25cdb95b8fb9fd17c48045605" 668 | dependencies = [ 669 | "serde", 670 | "serde_json", 671 | ] 672 | 673 | [[package]] 674 | name = "num-bigint" 675 | version = "0.4.6" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 678 | dependencies = [ 679 | "num-integer", 680 | "num-traits", 681 | ] 682 | 683 | [[package]] 684 | name = "num-derive" 685 | version = "0.4.2" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 688 | dependencies = [ 689 | "proc-macro2", 690 | "quote", 691 | "syn", 692 | ] 693 | 694 | [[package]] 695 | name = "num-integer" 696 | version = "0.1.46" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 699 | dependencies = [ 700 | "num-traits", 701 | ] 702 | 703 | [[package]] 704 | name = "num-rational" 705 | version = "0.4.2" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 708 | dependencies = [ 709 | "num-bigint", 710 | "num-integer", 711 | "num-traits", 712 | ] 713 | 714 | [[package]] 715 | name = "num-traits" 716 | version = "0.2.19" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 719 | dependencies = [ 720 | "autocfg", 721 | ] 722 | 723 | [[package]] 724 | name = "once_cell" 725 | version = "1.21.3" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 728 | 729 | [[package]] 730 | name = "once_cell_polyfill" 731 | version = "1.70.1" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 734 | 735 | [[package]] 736 | name = "pkg-config" 737 | version = "0.3.32" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 740 | 741 | [[package]] 742 | name = "png" 743 | version = "0.17.16" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 746 | dependencies = [ 747 | "bitflags 1.3.2", 748 | "crc32fast", 749 | "fdeflate", 750 | "flate2", 751 | "miniz_oxide", 752 | ] 753 | 754 | [[package]] 755 | name = "portable-atomic" 756 | version = "1.11.0" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 759 | 760 | [[package]] 761 | name = "portable-atomic-util" 762 | version = "0.2.4" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 765 | dependencies = [ 766 | "portable-atomic", 767 | ] 768 | 769 | [[package]] 770 | name = "proc-macro2" 771 | version = "1.0.95" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 774 | dependencies = [ 775 | "unicode-ident", 776 | ] 777 | 778 | [[package]] 779 | name = "qoi" 780 | version = "0.4.1" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" 783 | dependencies = [ 784 | "bytemuck", 785 | ] 786 | 787 | [[package]] 788 | name = "quick-error" 789 | version = "2.0.1" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 792 | 793 | [[package]] 794 | name = "quick-xml" 795 | version = "0.37.5" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" 798 | dependencies = [ 799 | "memchr", 800 | ] 801 | 802 | [[package]] 803 | name = "quote" 804 | version = "1.0.40" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 807 | dependencies = [ 808 | "proc-macro2", 809 | ] 810 | 811 | [[package]] 812 | name = "rayon-core" 813 | version = "1.12.1" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 816 | dependencies = [ 817 | "crossbeam-deque", 818 | "crossbeam-utils", 819 | ] 820 | 821 | [[package]] 822 | name = "regex" 823 | version = "1.11.1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 826 | dependencies = [ 827 | "aho-corasick", 828 | "memchr", 829 | "regex-automata", 830 | "regex-syntax", 831 | ] 832 | 833 | [[package]] 834 | name = "regex-automata" 835 | version = "0.4.9" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 838 | dependencies = [ 839 | "aho-corasick", 840 | "memchr", 841 | "regex-syntax", 842 | ] 843 | 844 | [[package]] 845 | name = "regex-syntax" 846 | version = "0.8.5" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 849 | 850 | [[package]] 851 | name = "rustix" 852 | version = "0.38.44" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 855 | dependencies = [ 856 | "bitflags 2.9.1", 857 | "errno", 858 | "libc", 859 | "linux-raw-sys", 860 | "windows-sys", 861 | ] 862 | 863 | [[package]] 864 | name = "ryu" 865 | version = "1.0.20" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 868 | 869 | [[package]] 870 | name = "scopeguard" 871 | version = "1.2.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 874 | 875 | [[package]] 876 | name = "serde" 877 | version = "1.0.219" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 880 | dependencies = [ 881 | "serde_derive", 882 | ] 883 | 884 | [[package]] 885 | name = "serde_derive" 886 | version = "1.0.219" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 889 | dependencies = [ 890 | "proc-macro2", 891 | "quote", 892 | "syn", 893 | ] 894 | 895 | [[package]] 896 | name = "serde_json" 897 | version = "1.0.140" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 900 | dependencies = [ 901 | "itoa", 902 | "memchr", 903 | "ryu", 904 | "serde", 905 | ] 906 | 907 | [[package]] 908 | name = "serde_spanned" 909 | version = "0.6.8" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 912 | dependencies = [ 913 | "serde", 914 | ] 915 | 916 | [[package]] 917 | name = "shlex" 918 | version = "1.3.0" 919 | source = "registry+https://github.com/rust-lang/crates.io-index" 920 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 921 | 922 | [[package]] 923 | name = "simd-adler32" 924 | version = "0.3.7" 925 | source = "registry+https://github.com/rust-lang/crates.io-index" 926 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 927 | 928 | [[package]] 929 | name = "smallvec" 930 | version = "1.15.0" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 933 | 934 | [[package]] 935 | name = "smithay-client-toolkit" 936 | version = "0.19.2" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" 939 | dependencies = [ 940 | "bitflags 2.9.1", 941 | "cursor-icon", 942 | "libc", 943 | "log", 944 | "memmap2", 945 | "rustix", 946 | "thiserror", 947 | "wayland-backend", 948 | "wayland-client", 949 | "wayland-csd-frame", 950 | "wayland-cursor", 951 | "wayland-protocols", 952 | "wayland-protocols-wlr", 953 | "wayland-scanner", 954 | "xkeysym", 955 | ] 956 | 957 | [[package]] 958 | name = "static_assertions" 959 | version = "1.1.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 962 | 963 | [[package]] 964 | name = "strsim" 965 | version = "0.11.1" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 968 | 969 | [[package]] 970 | name = "swayipc" 971 | version = "3.0.3" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "2b8c50cb2e98e88b52066a35ef791fffd8f6fa631c3a4983de18ba41f718c736" 974 | dependencies = [ 975 | "serde", 976 | "serde_json", 977 | "swayipc-types", 978 | ] 979 | 980 | [[package]] 981 | name = "swayipc-types" 982 | version = "1.4.1" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "551233c60323e87cfb8194c21cc44577ab848d00bb7fa2d324a2c7f52609eaff" 985 | dependencies = [ 986 | "serde", 987 | "serde_json", 988 | "thiserror", 989 | ] 990 | 991 | [[package]] 992 | name = "syn" 993 | version = "2.0.101" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 996 | dependencies = [ 997 | "proc-macro2", 998 | "quote", 999 | "unicode-ident", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "system-deps" 1004 | version = "7.0.5" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" 1007 | dependencies = [ 1008 | "cfg-expr", 1009 | "heck", 1010 | "pkg-config", 1011 | "toml", 1012 | "version-compare", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "target-lexicon" 1017 | version = "0.13.2" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" 1020 | 1021 | [[package]] 1022 | name = "thiserror" 1023 | version = "1.0.69" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1026 | dependencies = [ 1027 | "thiserror-impl", 1028 | ] 1029 | 1030 | [[package]] 1031 | name = "thiserror-impl" 1032 | version = "1.0.69" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1035 | dependencies = [ 1036 | "proc-macro2", 1037 | "quote", 1038 | "syn", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "tiff" 1043 | version = "0.9.1" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" 1046 | dependencies = [ 1047 | "flate2", 1048 | "jpeg-decoder", 1049 | "weezl", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "toml" 1054 | version = "0.8.22" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 1057 | dependencies = [ 1058 | "serde", 1059 | "serde_spanned", 1060 | "toml_datetime", 1061 | "toml_edit", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "toml_datetime" 1066 | version = "0.6.9" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 1069 | dependencies = [ 1070 | "serde", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "toml_edit" 1075 | version = "0.22.26" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 1078 | dependencies = [ 1079 | "indexmap", 1080 | "serde", 1081 | "serde_spanned", 1082 | "toml_datetime", 1083 | "winnow", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "unicode-ident" 1088 | version = "1.0.18" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1091 | 1092 | [[package]] 1093 | name = "utf8parse" 1094 | version = "0.2.2" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1097 | 1098 | [[package]] 1099 | name = "version-compare" 1100 | version = "0.2.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" 1103 | 1104 | [[package]] 1105 | name = "version_check" 1106 | version = "0.9.5" 1107 | source = "registry+https://github.com/rust-lang/crates.io-index" 1108 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1109 | 1110 | [[package]] 1111 | name = "wayland-backend" 1112 | version = "0.3.10" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" 1115 | dependencies = [ 1116 | "cc", 1117 | "downcast-rs", 1118 | "rustix", 1119 | "smallvec", 1120 | "wayland-sys", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "wayland-client" 1125 | version = "0.31.10" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" 1128 | dependencies = [ 1129 | "bitflags 2.9.1", 1130 | "rustix", 1131 | "wayland-backend", 1132 | "wayland-scanner", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "wayland-csd-frame" 1137 | version = "0.3.0" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" 1140 | dependencies = [ 1141 | "bitflags 2.9.1", 1142 | "cursor-icon", 1143 | "wayland-backend", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "wayland-cursor" 1148 | version = "0.31.10" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "a65317158dec28d00416cb16705934070aef4f8393353d41126c54264ae0f182" 1151 | dependencies = [ 1152 | "rustix", 1153 | "wayland-client", 1154 | "xcursor", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "wayland-protocols" 1159 | version = "0.32.8" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" 1162 | dependencies = [ 1163 | "bitflags 2.9.1", 1164 | "wayland-backend", 1165 | "wayland-client", 1166 | "wayland-scanner", 1167 | ] 1168 | 1169 | [[package]] 1170 | name = "wayland-protocols-wlr" 1171 | version = "0.3.8" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" 1174 | dependencies = [ 1175 | "bitflags 2.9.1", 1176 | "wayland-backend", 1177 | "wayland-client", 1178 | "wayland-protocols", 1179 | "wayland-scanner", 1180 | ] 1181 | 1182 | [[package]] 1183 | name = "wayland-scanner" 1184 | version = "0.31.6" 1185 | source = "registry+https://github.com/rust-lang/crates.io-index" 1186 | checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" 1187 | dependencies = [ 1188 | "proc-macro2", 1189 | "quick-xml", 1190 | "quote", 1191 | ] 1192 | 1193 | [[package]] 1194 | name = "wayland-sys" 1195 | version = "0.31.6" 1196 | source = "registry+https://github.com/rust-lang/crates.io-index" 1197 | checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" 1198 | dependencies = [ 1199 | "pkg-config", 1200 | ] 1201 | 1202 | [[package]] 1203 | name = "weezl" 1204 | version = "0.1.10" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" 1207 | 1208 | [[package]] 1209 | name = "windows-sys" 1210 | version = "0.59.0" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1213 | dependencies = [ 1214 | "windows-targets 0.52.6", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "windows-targets" 1219 | version = "0.52.6" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1222 | dependencies = [ 1223 | "windows_aarch64_gnullvm 0.52.6", 1224 | "windows_aarch64_msvc 0.52.6", 1225 | "windows_i686_gnu 0.52.6", 1226 | "windows_i686_gnullvm 0.52.6", 1227 | "windows_i686_msvc 0.52.6", 1228 | "windows_x86_64_gnu 0.52.6", 1229 | "windows_x86_64_gnullvm 0.52.6", 1230 | "windows_x86_64_msvc 0.52.6", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "windows-targets" 1235 | version = "0.53.0" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 1238 | dependencies = [ 1239 | "windows_aarch64_gnullvm 0.53.0", 1240 | "windows_aarch64_msvc 0.53.0", 1241 | "windows_i686_gnu 0.53.0", 1242 | "windows_i686_gnullvm 0.53.0", 1243 | "windows_i686_msvc 0.53.0", 1244 | "windows_x86_64_gnu 0.53.0", 1245 | "windows_x86_64_gnullvm 0.53.0", 1246 | "windows_x86_64_msvc 0.53.0", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "windows_aarch64_gnullvm" 1251 | version = "0.52.6" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1254 | 1255 | [[package]] 1256 | name = "windows_aarch64_gnullvm" 1257 | version = "0.53.0" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1260 | 1261 | [[package]] 1262 | name = "windows_aarch64_msvc" 1263 | version = "0.52.6" 1264 | source = "registry+https://github.com/rust-lang/crates.io-index" 1265 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1266 | 1267 | [[package]] 1268 | name = "windows_aarch64_msvc" 1269 | version = "0.53.0" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1272 | 1273 | [[package]] 1274 | name = "windows_i686_gnu" 1275 | version = "0.52.6" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1278 | 1279 | [[package]] 1280 | name = "windows_i686_gnu" 1281 | version = "0.53.0" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1284 | 1285 | [[package]] 1286 | name = "windows_i686_gnullvm" 1287 | version = "0.52.6" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1290 | 1291 | [[package]] 1292 | name = "windows_i686_gnullvm" 1293 | version = "0.53.0" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1296 | 1297 | [[package]] 1298 | name = "windows_i686_msvc" 1299 | version = "0.52.6" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1302 | 1303 | [[package]] 1304 | name = "windows_i686_msvc" 1305 | version = "0.53.0" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1308 | 1309 | [[package]] 1310 | name = "windows_x86_64_gnu" 1311 | version = "0.52.6" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1314 | 1315 | [[package]] 1316 | name = "windows_x86_64_gnu" 1317 | version = "0.53.0" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1320 | 1321 | [[package]] 1322 | name = "windows_x86_64_gnullvm" 1323 | version = "0.52.6" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1326 | 1327 | [[package]] 1328 | name = "windows_x86_64_gnullvm" 1329 | version = "0.53.0" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1332 | 1333 | [[package]] 1334 | name = "windows_x86_64_msvc" 1335 | version = "0.52.6" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1338 | 1339 | [[package]] 1340 | name = "windows_x86_64_msvc" 1341 | version = "0.53.0" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1344 | 1345 | [[package]] 1346 | name = "winnow" 1347 | version = "0.7.10" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 1350 | dependencies = [ 1351 | "memchr", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "xcursor" 1356 | version = "0.3.8" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" 1359 | 1360 | [[package]] 1361 | name = "xkeysym" 1362 | version = "0.2.1" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" 1365 | 1366 | [[package]] 1367 | name = "zerocopy" 1368 | version = "0.8.25" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 1371 | dependencies = [ 1372 | "zerocopy-derive", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "zerocopy-derive" 1377 | version = "0.8.25" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 1380 | dependencies = [ 1381 | "proc-macro2", 1382 | "quote", 1383 | "syn", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "zune-core" 1388 | version = "0.4.12" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" 1391 | 1392 | [[package]] 1393 | name = "zune-inflate" 1394 | version = "0.2.54" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" 1397 | dependencies = [ 1398 | "simd-adler32", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "zune-jpeg" 1403 | version = "0.4.14" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" 1406 | dependencies = [ 1407 | "zune-core", 1408 | ] 1409 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multibg-wayland" 3 | version = "0.2.1" 4 | authors = ["Gergő Sályi "] 5 | edition = "2021" 6 | rust-version = "1.82" 7 | description = "Set a different wallpaper for the background of each Sway or Hyprland or niri workspace" 8 | readme = "README.md" 9 | homepage = "https://github.com/gergo-salyi/multibg-wayland" 10 | repository = "https://github.com/gergo-salyi/multibg-wayland" 11 | license = "MIT OR Apache-2.0" 12 | keywords = ["wallpaper", "background", "desktop", "wayland", "sway"] 13 | categories = ["command-line-utilities", "multimedia::images"] 14 | exclude = ["/PKGBUILD", "/PKGBUILD.in", "/deps/", "/scripts/"] 15 | 16 | [dependencies] 17 | anyhow = "1.0.97" 18 | ash = "0.38.0" 19 | clap = { version = "4.5.3", features = ["derive"] } 20 | env_logger = "0.11.3" 21 | fast_image_resize = "5.0.0" 22 | libc = "0.2.171" 23 | log = "0.4.21" 24 | niri-ipc-25-2-0 = { package = "multibg-wayland-niri-ipc", version = "=0.250200.0" } 25 | niri-ipc-25-5-1 = { package = "niri-ipc", version = "=25.5.1" } 26 | rustix = { version = "0.38.44", features = ["event", "fs", "pipe"] } 27 | scopeguard = "1.2.0" 28 | serde = { version = "1.0.219", features = ["derive"] } 29 | serde_json = "1.0.140" 30 | smithay-client-toolkit = { version = "0.19.2", default-features = false } 31 | swayipc = "3.0.2" 32 | 33 | [dependencies.image] 34 | version = "0.25.6" 35 | default-features = false 36 | features = ["bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp"] 37 | 38 | [features] 39 | default = [] 40 | avif = ["image/avif-native"] 41 | 42 | [lints.rust] 43 | unused_must_use = "deny" 44 | 45 | [lints.clippy] 46 | uninlined_format_args = "allow" 47 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright 2023 Gergő Sályi and multibg-wayland contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Gergő Sályi 2 | # shellcheck shell=bash disable=SC2034,SC2154 3 | pkgname=multibg-wayland 4 | pkgver=0.2.1 5 | pkgrel=1 6 | pkgdesc='Set a different wallpaper for the background of each Sway or Hyprland or niri workspace' 7 | arch=('x86_64' 'i686' 'pentium4' 'armv7h' 'aarch64') 8 | url="https://github.com/gergo-salyi/multibg-wayland" 9 | # Direct source files are MIT OR Apache-2.0 but have GPL-3.0-or-later dependencies 10 | license=('GPL-3.0-or-later') 11 | depends=('dav1d>=1.3.0' 'gcc-libs' 'glibc') 12 | makedepends=('cargo') 13 | optdepends=( 14 | 'hyprland: supported window manager to set the wallpapers with' 15 | 'niri: supported window manager to set the wallpapers with' 16 | 'sway: supported window manager to set the wallpapers with' 17 | 'vulkan-driver: upload and serve wallpapers from GPU memory' 18 | 'vulkan-icd-loader: upload and serve wallpapers from GPU memory' 19 | ) 20 | conflicts=('multibg-sway') 21 | provides=('multibg-sway') 22 | source=("$pkgname-$pkgver.tar.gz::https://static.crates.io/crates/$pkgname/$pkgname-$pkgver.crate") 23 | sha256sums=('60b99e65123cf797ccee361169ea28242a8fba7f4cf0f27351ac2d4eaeaccf2e') 24 | 25 | prepare() { 26 | cd "$pkgname-$pkgver" 27 | export RUSTUP_TOOLCHAIN=stable 28 | cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" 29 | } 30 | 31 | build() { 32 | cd "$pkgname-$pkgver" 33 | export RUSTUP_TOOLCHAIN=stable 34 | export CARGO_TARGET_DIR=target 35 | cargo build --frozen --release --features avif 36 | } 37 | 38 | package() { 39 | cd "$pkgname-$pkgver" 40 | install -Dm0755 -t "$pkgdir/usr/bin/" "target/release/$pkgname" 41 | ln -rs "$pkgdir/usr/bin/$pkgname" "$pkgdir/usr/bin/multibg-sway" 42 | install -Dm644 "README.md" "$pkgdir/usr/share/doc/${pkgname}/README.md" 43 | } 44 | -------------------------------------------------------------------------------- /PKGBUILD.in: -------------------------------------------------------------------------------- 1 | # Maintainer: Gergő Sályi 2 | # shellcheck shell=bash disable=SC2034,SC2154 3 | pkgname=multibg-wayland 4 | pkgver=@pkgver@ 5 | pkgrel=1 6 | pkgdesc='Set a different wallpaper for the background of each Sway or Hyprland or niri workspace' 7 | arch=('x86_64' 'i686' 'pentium4' 'armv7h' 'aarch64') 8 | url="https://github.com/gergo-salyi/multibg-wayland" 9 | # Direct source files are MIT OR Apache-2.0 but have GPL-3.0-or-later dependencies 10 | license=('GPL-3.0-or-later') 11 | depends=('dav1d>=1.3.0' 'gcc-libs' 'glibc') 12 | makedepends=('cargo') 13 | optdepends=( 14 | 'hyprland: supported window manager to set the wallpapers with' 15 | 'niri: supported window manager to set the wallpapers with' 16 | 'sway: supported window manager to set the wallpapers with' 17 | 'vulkan-driver: upload and serve wallpapers from GPU memory' 18 | 'vulkan-icd-loader: upload and serve wallpapers from GPU memory' 19 | ) 20 | conflicts=('multibg-sway') 21 | provides=('multibg-sway') 22 | source=("$pkgname-$pkgver.tar.gz::https://static.crates.io/crates/$pkgname/$pkgname-$pkgver.crate") 23 | sha256sums=('@sha256sum@') 24 | 25 | prepare() { 26 | cd "$pkgname-$pkgver" 27 | export RUSTUP_TOOLCHAIN=stable 28 | cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" 29 | } 30 | 31 | build() { 32 | cd "$pkgname-$pkgver" 33 | export RUSTUP_TOOLCHAIN=stable 34 | export CARGO_TARGET_DIR=target 35 | cargo build --frozen --release --features avif 36 | } 37 | 38 | package() { 39 | cd "$pkgname-$pkgver" 40 | install -Dm0755 -t "$pkgdir/usr/bin/" "target/release/$pkgname" 41 | ln -rs "$pkgdir/usr/bin/$pkgname" "$pkgdir/usr/bin/multibg-sway" 42 | install -Dm644 "README.md" "$pkgdir/usr/share/doc/${pkgname}/README.md" 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multibg-wayland 2 | 3 | Set a different wallpaper for the background of each Sway / Hyprland / niri workspace 4 | 5 | ## News 6 | 7 | Project is being renamed to multibg-wayland to signify support for Wayland compositors other than Sway. The name multibg-sway remains as an alias or redirect. 8 | 9 | ## Usage 10 | 11 | $ multibg-wayland 12 | 13 | Wallpapers should be arranged in the following directory structure: 14 | 15 | wallpaper_dir/output/workspace_name.{jpg|png|...} 16 | 17 | Such as: 18 | 19 | ~/my_wallpapers/HDMI-A-1/1.jpg 20 | 21 | In more detail: 22 | 23 | - **wallpaper_dir**: A directory, this will be the command line argument 24 | 25 | - **output**: A directory with the same name as a Wayland output such as eDP-1, HDMI-A-1 26 | - For multiple outputs this can be a symlink to the directory of an other output. 27 | - Get the name of current outputs from the compositor with these Sway / Hyprland / niri commands: 28 | 29 | $ swaymsg -t get_outputs 30 | $ hyprctl monitors 31 | $ niri msg outputs 32 | 33 | - **workspace_name**: The name of the workspace, by default use the compositors assigned workspace numbers as names: 1, 2, 3, ..., 10 34 | - Can be the name of a named workspace usually defined in the config file of the compositor. (Renaming workspaces while multibg-workspace is running might not be supported yet.) 35 | - Can define a **fallback wallpaper** with the special name: **_default** 36 | - Can be a symlink to the wallpaper of an other workspace 37 | 38 | ### Example 39 | 40 | For one having a laptop with a built-in display eDP-1 and an external monitor HDMI-A-1, wallpapers can be arranged such as: 41 | 42 | ~/my_wallpapers 43 | ├─ eDP-1 44 | │ ├─ _default.jpg 45 | │ ├─ 1.jpg 46 | │ ├─ 2.png 47 | │ └─ browser.jpg 48 | └─ HDMI-A-1 49 | ├─ 1.jpg 50 | └─ 3.png 51 | 52 | Then start multibg-wayland: 53 | 54 | $ multibg-wayland ~/my_wallpapers 55 | 56 | ### Options 57 | 58 | In case of errors we log to stderr and try to continue. Redirect stderr to a log file if necessary. 59 | 60 | By default, without the `--gpu` option only CPU memory is used to store wallpapers, shared with the Wayland compositor. (All of this might be reported as memory used by the compositor process instead of our process.) 61 | 62 | With the `--gpu` option set GPU memory (again, shared with the compositor) is used. This requires Vulkan loader and driver with Vulkan 1.1 or newer, and might save a few milliseconds latency on wallpaper switches avoiding the use of CPU memory and PCIe bandwidth. (I recommend to try this out, I just can't test it with many GPUs.) 63 | 64 | The running Wayland compositor is autodetected based on environment variables. If this fails then try to set the `--compositor {sway|hyprland|niri}` command line option. 65 | 66 | It is recommended to resize the wallpapers to the resolution of the output and color adjust with dedicated tools like imagemagick or gimp. 67 | 68 | This app can do _some_ imperfect image processing at the expense of startup time. Wallpaper images with different resolution than their output are resized (with high quality filter but incorrect gamma) to _fill_ the output. Contrast and brightness (on some bad arbitrary scale) might be adjusted such as: 69 | 70 | $ multibg-wayland --contrast=-25 --brightness=-60 ~/my_wallpapers 71 | 72 | ### Resource usage 73 | 74 | For active outputs all wallpapers from the corresponding `wallpaper_dir/output` are loaded and stored uncompressed to enable fast wallpaper switching. Wallpapers with multiple symlinks pointing to it are only loaded once and shared. For example for 10 unique full HD wallpaper this means 10\*1920\*1080\*4 = 83 MB memory use. 75 | 76 | ## Installation 77 | 78 | Requires `Rust`, get it from your package manager or from the official website: [https://www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) 79 | 80 | - Latest release (from [crates.io](https://crates.io/crates/multibg-wayland)) with Cargo install provided by Rust: 81 | 82 | $ cargo install --locked multibg-wayland 83 | 84 | Run `~/.cargo/bin/multibg-wayland` 85 | 86 | - Directly from the current git source: 87 | 88 | $ git clone https://github.com/gergo-salyi/multibg-wayland.git 89 | $ cd multibg-wayland 90 | $ cargo build --release --locked 91 | 92 | Run `./target/release/multibg-wayland` 93 | 94 | - For Arch Linux from AUR: [https://aur.archlinux.org/packages/multibg-wayland](https://aur.archlinux.org/packages/multibg-wayland) 95 | - eg. with paru 96 | 97 | $ paru -S multibg-wayland 98 | 99 | ## Bug reporting 100 | 101 | Reports on any problems are appreciated, look for an existing or open a new issue at [https://github.com/gergo-salyi/multibg-wayland/issues](https://github.com/gergo-salyi/multibg-wayland/issues) 102 | 103 | Please include a verbose log from you terminal by running with `RUST_BACKTRACE=1` and `RUST_LOG=info,multibg_wayland=trace` environment variables set, such as 104 | 105 | $ export RUST_BACKTRACE=1 106 | $ export RUST_LOG=info,multibg_wayland=trace 107 | $ multibg-wayland ~/my_wallpapers 108 | 109 | If using the --gpu option also consider installing Vulkan validation layers from your distro. It which will be automatically enabled at the log levels defined above. 110 | 111 | ## Alternatives 112 | 113 | - [swaybg](https://github.com/swaywm/swaybg) 114 | - [swww](https://github.com/Horus645/swww) 115 | - [wpaperd](https://github.com/danyspin97/wpaperd) 116 | - [hyprpaper](https://github.com/hyprwm/hyprpaper) 117 | - [mpvpaper](https://github.com/GhostNaN/mpvpaper) 118 | - [oguri](https://github.com/vilhalmer/oguri) 119 | 120 | ## License 121 | 122 | Source files in this project (except vendored dependencies under `deps/`) are distributed under MIT OR Apache-2.0. 123 | 124 | Vendored dependencies under `deps/` are distributed under their respective licenses. 125 | 126 | Objects resulting from building this project might be under GPL-3.0-or-later due to licenses of statically linked dependencies. Open an issue if you need compile time features gating such dependencies. 127 | -------------------------------------------------------------------------------- /deps/README.md: -------------------------------------------------------------------------------- 1 | # Vendored dependencies 2 | 3 | We need to use multiple versions of the `niri-ipc` crate dependency to maintain compatibility with multiple version of niri. Ideally we would do this: 4 | 5 | ```toml 6 | # Cargo.toml 7 | [dependencies] 8 | niri-ipc-25-2-0 = { package = "niri-ipc", version = "=25.2.0" } 9 | niri-ipc-25-5-1 = { package = "niri-ipc", version = "=25.5.1" } 10 | ``` 11 | 12 | However for some braindamaged reasons `cargo` refuses to allow this if any of the multiple versions are semver compatible (see [cargo issue](https://github.com/rust-lang/cargo/issues/12787)) 13 | 14 | So we do a disgusting workaround here where we vendor older versions of the `niri-ipc` crate and re-publish them on crates.io under the name `multibg-wayland-niri-ipc` with semver incompatible versions e.g. `"25.2.0"` => `"0.250200.0"` 15 | 16 | ## License 17 | 18 | Vendored dependencies are included here under their respective licenses 19 | 20 | ## Workflow 21 | 22 | Example: 23 | 24 | Download the crate: 25 | ```sh 26 | curl --fail --proto '=https' --tlsv1.2 https://static.crates.io/crates/niri-ipc/niri-ipc-25.2.0.crate | tar -xz 27 | ``` 28 | 29 | Remove crates.io artifacts: 30 | ```sh 31 | rm -f .cargo_vcs_info.json Cargo.toml.orig 32 | ``` 33 | 34 | Edit `Cargo.toml`: 35 | - `name = "niri-ipc"` => `name = "multibg-wayland-niri-ipc"` 36 | - `version = "25.2.0"` => `version = "0.250200.0"` 37 | 38 | Re-publish: 39 | ```sh 40 | cargo publish 41 | ``` 42 | -------------------------------------------------------------------------------- /deps/niri-ipc-25.2.0/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.6.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is_terminal_polyfill", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.10" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.1.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 40 | dependencies = [ 41 | "windows-sys", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "3.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 49 | dependencies = [ 50 | "anstyle", 51 | "once_cell", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "clap" 57 | version = "4.5.30" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" 60 | dependencies = [ 61 | "clap_builder", 62 | "clap_derive", 63 | ] 64 | 65 | [[package]] 66 | name = "clap_builder" 67 | version = "4.5.30" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" 70 | dependencies = [ 71 | "anstream", 72 | "anstyle", 73 | "clap_lex", 74 | "strsim", 75 | ] 76 | 77 | [[package]] 78 | name = "clap_derive" 79 | version = "4.5.28" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" 82 | dependencies = [ 83 | "heck", 84 | "proc-macro2", 85 | "quote", 86 | "syn", 87 | ] 88 | 89 | [[package]] 90 | name = "clap_lex" 91 | version = "0.7.4" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 94 | 95 | [[package]] 96 | name = "colorchoice" 97 | version = "1.0.3" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 100 | 101 | [[package]] 102 | name = "dyn-clone" 103 | version = "1.0.18" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" 106 | 107 | [[package]] 108 | name = "heck" 109 | version = "0.5.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 112 | 113 | [[package]] 114 | name = "is_terminal_polyfill" 115 | version = "1.70.1" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 118 | 119 | [[package]] 120 | name = "itoa" 121 | version = "1.0.14" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 124 | 125 | [[package]] 126 | name = "memchr" 127 | version = "2.7.4" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 130 | 131 | [[package]] 132 | name = "multibg-wayland-niri-ipc" 133 | version = "0.250200.0" 134 | dependencies = [ 135 | "clap", 136 | "schemars", 137 | "serde", 138 | "serde_json", 139 | ] 140 | 141 | [[package]] 142 | name = "once_cell" 143 | version = "1.20.3" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 146 | 147 | [[package]] 148 | name = "proc-macro2" 149 | version = "1.0.93" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 152 | dependencies = [ 153 | "unicode-ident", 154 | ] 155 | 156 | [[package]] 157 | name = "quote" 158 | version = "1.0.38" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 161 | dependencies = [ 162 | "proc-macro2", 163 | ] 164 | 165 | [[package]] 166 | name = "ryu" 167 | version = "1.0.19" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 170 | 171 | [[package]] 172 | name = "schemars" 173 | version = "0.8.21" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" 176 | dependencies = [ 177 | "dyn-clone", 178 | "schemars_derive", 179 | "serde", 180 | "serde_json", 181 | ] 182 | 183 | [[package]] 184 | name = "schemars_derive" 185 | version = "0.8.21" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" 188 | dependencies = [ 189 | "proc-macro2", 190 | "quote", 191 | "serde_derive_internals", 192 | "syn", 193 | ] 194 | 195 | [[package]] 196 | name = "serde" 197 | version = "1.0.218" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 200 | dependencies = [ 201 | "serde_derive", 202 | ] 203 | 204 | [[package]] 205 | name = "serde_derive" 206 | version = "1.0.218" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 209 | dependencies = [ 210 | "proc-macro2", 211 | "quote", 212 | "syn", 213 | ] 214 | 215 | [[package]] 216 | name = "serde_derive_internals" 217 | version = "0.29.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" 220 | dependencies = [ 221 | "proc-macro2", 222 | "quote", 223 | "syn", 224 | ] 225 | 226 | [[package]] 227 | name = "serde_json" 228 | version = "1.0.139" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" 231 | dependencies = [ 232 | "itoa", 233 | "memchr", 234 | "ryu", 235 | "serde", 236 | ] 237 | 238 | [[package]] 239 | name = "strsim" 240 | version = "0.11.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 243 | 244 | [[package]] 245 | name = "syn" 246 | version = "2.0.98" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 249 | dependencies = [ 250 | "proc-macro2", 251 | "quote", 252 | "unicode-ident", 253 | ] 254 | 255 | [[package]] 256 | name = "unicode-ident" 257 | version = "1.0.17" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 260 | 261 | [[package]] 262 | name = "utf8parse" 263 | version = "0.2.2" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 266 | 267 | [[package]] 268 | name = "windows-sys" 269 | version = "0.59.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 272 | dependencies = [ 273 | "windows-targets", 274 | ] 275 | 276 | [[package]] 277 | name = "windows-targets" 278 | version = "0.52.6" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 281 | dependencies = [ 282 | "windows_aarch64_gnullvm", 283 | "windows_aarch64_msvc", 284 | "windows_i686_gnu", 285 | "windows_i686_gnullvm", 286 | "windows_i686_msvc", 287 | "windows_x86_64_gnu", 288 | "windows_x86_64_gnullvm", 289 | "windows_x86_64_msvc", 290 | ] 291 | 292 | [[package]] 293 | name = "windows_aarch64_gnullvm" 294 | version = "0.52.6" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 297 | 298 | [[package]] 299 | name = "windows_aarch64_msvc" 300 | version = "0.52.6" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 303 | 304 | [[package]] 305 | name = "windows_i686_gnu" 306 | version = "0.52.6" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 309 | 310 | [[package]] 311 | name = "windows_i686_gnullvm" 312 | version = "0.52.6" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 315 | 316 | [[package]] 317 | name = "windows_i686_msvc" 318 | version = "0.52.6" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 321 | 322 | [[package]] 323 | name = "windows_x86_64_gnu" 324 | version = "0.52.6" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 327 | 328 | [[package]] 329 | name = "windows_x86_64_gnullvm" 330 | version = "0.52.6" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 333 | 334 | [[package]] 335 | name = "windows_x86_64_msvc" 336 | version = "0.52.6" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 339 | -------------------------------------------------------------------------------- /deps/niri-ipc-25.2.0/Cargo.toml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO 2 | # 3 | # When uploading crates to the registry Cargo will automatically 4 | # "normalize" Cargo.toml files for maximal compatibility 5 | # with all versions of Cargo and also rewrite `path` dependencies 6 | # to registry (e.g., crates.io) dependencies. 7 | # 8 | # If you are reading this file be aware that the original Cargo.toml 9 | # will likely look very different (and much more reasonable). 10 | # See Cargo.toml.orig for the original contents. 11 | 12 | [package] 13 | edition = "2021" 14 | name = "multibg-wayland-niri-ipc" # name = "niri-ipc" 15 | version = "0.250200.0" # version = "25.2.0" 16 | authors = ["Ivan Molodetskikh "] 17 | build = false 18 | autolib = false 19 | autobins = false 20 | autoexamples = false 21 | autotests = false 22 | autobenches = false 23 | description = "Types and helpers for interfacing with the niri Wayland compositor." 24 | readme = "README.md" 25 | keywords = ["wayland"] 26 | categories = [ 27 | "api-bindings", 28 | "os", 29 | ] 30 | license = "GPL-3.0-or-later" 31 | repository = "https://github.com/YaLTeR/niri" 32 | 33 | [features] 34 | clap = ["dep:clap"] 35 | json-schema = ["dep:schemars"] 36 | 37 | [lib] 38 | name = "niri_ipc" 39 | path = "src/lib.rs" 40 | 41 | [dependencies.clap] 42 | version = "4.5.30" 43 | features = ["derive"] 44 | optional = true 45 | 46 | [dependencies.schemars] 47 | version = "0.8.21" 48 | optional = true 49 | 50 | [dependencies.serde] 51 | version = "1.0.218" 52 | features = ["derive"] 53 | 54 | [dependencies.serde_json] 55 | version = "1.0.139" 56 | -------------------------------------------------------------------------------- /deps/niri-ipc-25.2.0/README.md: -------------------------------------------------------------------------------- 1 | # niri-ipc 2 | 3 | Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor. 4 | 5 | ## Backwards compatibility 6 | 7 | This crate follows the niri version. 8 | It is **not** API-stable in terms of the Rust semver. 9 | In particular, expect new struct fields and enum variants to be added in patch version bumps. 10 | 11 | Use an exact version requirement to avoid breaking changes: 12 | 13 | ```toml 14 | [dependencies] 15 | niri-ipc = "=25.2.0" 16 | ``` 17 | -------------------------------------------------------------------------------- /deps/niri-ipc-25.2.0/src/socket.rs: -------------------------------------------------------------------------------- 1 | //! Helper for blocking communication over the niri socket. 2 | 3 | use std::env; 4 | use std::io::{self, BufRead, BufReader, Write}; 5 | use std::net::Shutdown; 6 | use std::os::unix::net::UnixStream; 7 | use std::path::Path; 8 | 9 | use crate::{Event, Reply, Request}; 10 | 11 | /// Name of the environment variable containing the niri IPC socket path. 12 | pub const SOCKET_PATH_ENV: &str = "NIRI_SOCKET"; 13 | 14 | /// Helper for blocking communication over the niri socket. 15 | /// 16 | /// This struct is used to communicate with the niri IPC server. It handles the socket connection 17 | /// and serialization/deserialization of messages. 18 | pub struct Socket { 19 | stream: UnixStream, 20 | } 21 | 22 | impl Socket { 23 | /// Connects to the default niri IPC socket. 24 | /// 25 | /// This is equivalent to calling [`Self::connect_to`] with the path taken from the 26 | /// [`SOCKET_PATH_ENV`] environment variable. 27 | pub fn connect() -> io::Result { 28 | let socket_path = env::var_os(SOCKET_PATH_ENV).ok_or_else(|| { 29 | io::Error::new( 30 | io::ErrorKind::NotFound, 31 | format!("{SOCKET_PATH_ENV} is not set, are you running this within niri?"), 32 | ) 33 | })?; 34 | Self::connect_to(socket_path) 35 | } 36 | 37 | /// Connects to the niri IPC socket at the given path. 38 | pub fn connect_to(path: impl AsRef) -> io::Result { 39 | let stream = UnixStream::connect(path.as_ref())?; 40 | Ok(Self { stream }) 41 | } 42 | 43 | /// Sends a request to niri and returns the response. 44 | /// 45 | /// Return values: 46 | /// 47 | /// * `Ok(Ok(response))`: successful [`Response`](crate::Response) from niri 48 | /// * `Ok(Err(message))`: error message from niri 49 | /// * `Err(error)`: error communicating with niri 50 | /// 51 | /// This method also returns a blocking function that you can call to keep reading [`Event`]s 52 | /// after requesting an [`EventStream`][Request::EventStream]. This function is not useful 53 | /// otherwise. 54 | pub fn send(self, request: Request) -> io::Result<(Reply, impl FnMut() -> io::Result)> { 55 | let Self { mut stream } = self; 56 | 57 | let mut buf = serde_json::to_string(&request).unwrap(); 58 | stream.write_all(buf.as_bytes())?; 59 | stream.shutdown(Shutdown::Write)?; 60 | 61 | let mut reader = BufReader::new(stream); 62 | 63 | buf.clear(); 64 | reader.read_line(&mut buf)?; 65 | 66 | let reply = serde_json::from_str(&buf)?; 67 | 68 | let events = move || { 69 | buf.clear(); 70 | reader.read_line(&mut buf)?; 71 | let event = serde_json::from_str(&buf)?; 72 | Ok(event) 73 | }; 74 | 75 | Ok((reply, events)) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /deps/niri-ipc-25.2.0/src/state.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for keeping track of the event stream state. 2 | //! 3 | //! 1. Create an [`EventStreamState`] using `Default::default()`, or any individual state part if 4 | //! you only care about part of the state. 5 | //! 2. Connect to the niri socket and request an event stream. 6 | //! 3. Pass every [`Event`] to [`EventStreamStatePart::apply`] on your state. 7 | //! 4. Read the fields of the state as needed. 8 | 9 | use std::collections::hash_map::Entry; 10 | use std::collections::HashMap; 11 | 12 | use crate::{Event, KeyboardLayouts, Window, Workspace}; 13 | 14 | /// Part of the state communicated via the event stream. 15 | pub trait EventStreamStatePart { 16 | /// Returns a sequence of events that replicates this state from default initialization. 17 | fn replicate(&self) -> Vec; 18 | 19 | /// Applies the event to this state. 20 | /// 21 | /// Returns `None` after applying the event, and `Some(event)` if the event is ignored by this 22 | /// part of the state. 23 | fn apply(&mut self, event: Event) -> Option; 24 | } 25 | 26 | /// The full state communicated over the event stream. 27 | /// 28 | /// Different parts of the state are not guaranteed to be consistent across every single event 29 | /// sent by niri. For example, you may receive the first [`Event::WindowOpenedOrChanged`] for a 30 | /// just-opened window *after* an [`Event::WorkspaceActiveWindowChanged`] for that window. Between 31 | /// these two events, the workspace active window id refers to a window that does not yet exist in 32 | /// the windows state part. 33 | #[derive(Debug, Default)] 34 | pub struct EventStreamState { 35 | /// State of workspaces. 36 | pub workspaces: WorkspacesState, 37 | 38 | /// State of workspaces. 39 | pub windows: WindowsState, 40 | 41 | /// State of the keyboard layouts. 42 | pub keyboard_layouts: KeyboardLayoutsState, 43 | } 44 | 45 | /// The workspaces state communicated over the event stream. 46 | #[derive(Debug, Default)] 47 | pub struct WorkspacesState { 48 | /// Map from a workspace id to the workspace. 49 | pub workspaces: HashMap, 50 | } 51 | 52 | /// The windows state communicated over the event stream. 53 | #[derive(Debug, Default)] 54 | pub struct WindowsState { 55 | /// Map from a window id to the window. 56 | pub windows: HashMap, 57 | } 58 | 59 | /// The keyboard layout state communicated over the event stream. 60 | #[derive(Debug, Default)] 61 | pub struct KeyboardLayoutsState { 62 | /// Configured keyboard layouts. 63 | pub keyboard_layouts: Option, 64 | } 65 | 66 | impl EventStreamStatePart for EventStreamState { 67 | fn replicate(&self) -> Vec { 68 | let mut events = Vec::new(); 69 | events.extend(self.workspaces.replicate()); 70 | events.extend(self.windows.replicate()); 71 | events.extend(self.keyboard_layouts.replicate()); 72 | events 73 | } 74 | 75 | fn apply(&mut self, event: Event) -> Option { 76 | let event = self.workspaces.apply(event)?; 77 | let event = self.windows.apply(event)?; 78 | let event = self.keyboard_layouts.apply(event)?; 79 | Some(event) 80 | } 81 | } 82 | 83 | impl EventStreamStatePart for WorkspacesState { 84 | fn replicate(&self) -> Vec { 85 | let workspaces = self.workspaces.values().cloned().collect(); 86 | vec![Event::WorkspacesChanged { workspaces }] 87 | } 88 | 89 | fn apply(&mut self, event: Event) -> Option { 90 | match event { 91 | Event::WorkspacesChanged { workspaces } => { 92 | self.workspaces = workspaces.into_iter().map(|ws| (ws.id, ws)).collect(); 93 | } 94 | Event::WorkspaceActivated { id, focused } => { 95 | let ws = self.workspaces.get(&id); 96 | let ws = ws.expect("activated workspace was missing from the map"); 97 | let output = ws.output.clone(); 98 | 99 | for ws in self.workspaces.values_mut() { 100 | let got_activated = ws.id == id; 101 | if ws.output == output { 102 | ws.is_active = got_activated; 103 | } 104 | 105 | if focused { 106 | ws.is_focused = got_activated; 107 | } 108 | } 109 | } 110 | Event::WorkspaceActiveWindowChanged { 111 | workspace_id, 112 | active_window_id, 113 | } => { 114 | let ws = self.workspaces.get_mut(&workspace_id); 115 | let ws = ws.expect("changed workspace was missing from the map"); 116 | ws.active_window_id = active_window_id; 117 | } 118 | event => return Some(event), 119 | } 120 | None 121 | } 122 | } 123 | 124 | impl EventStreamStatePart for WindowsState { 125 | fn replicate(&self) -> Vec { 126 | let windows = self.windows.values().cloned().collect(); 127 | vec![Event::WindowsChanged { windows }] 128 | } 129 | 130 | fn apply(&mut self, event: Event) -> Option { 131 | match event { 132 | Event::WindowsChanged { windows } => { 133 | self.windows = windows.into_iter().map(|win| (win.id, win)).collect(); 134 | } 135 | Event::WindowOpenedOrChanged { window } => { 136 | let (id, is_focused) = match self.windows.entry(window.id) { 137 | Entry::Occupied(mut entry) => { 138 | let entry = entry.get_mut(); 139 | *entry = window; 140 | (entry.id, entry.is_focused) 141 | } 142 | Entry::Vacant(entry) => { 143 | let entry = entry.insert(window); 144 | (entry.id, entry.is_focused) 145 | } 146 | }; 147 | 148 | if is_focused { 149 | for win in self.windows.values_mut() { 150 | if win.id != id { 151 | win.is_focused = false; 152 | } 153 | } 154 | } 155 | } 156 | Event::WindowClosed { id } => { 157 | let win = self.windows.remove(&id); 158 | win.expect("closed window was missing from the map"); 159 | } 160 | Event::WindowFocusChanged { id } => { 161 | for win in self.windows.values_mut() { 162 | win.is_focused = Some(win.id) == id; 163 | } 164 | } 165 | event => return Some(event), 166 | } 167 | None 168 | } 169 | } 170 | 171 | impl EventStreamStatePart for KeyboardLayoutsState { 172 | fn replicate(&self) -> Vec { 173 | if let Some(keyboard_layouts) = self.keyboard_layouts.clone() { 174 | vec![Event::KeyboardLayoutsChanged { keyboard_layouts }] 175 | } else { 176 | vec![] 177 | } 178 | } 179 | 180 | fn apply(&mut self, event: Event) -> Option { 181 | match event { 182 | Event::KeyboardLayoutsChanged { keyboard_layouts } => { 183 | self.keyboard_layouts = Some(keyboard_layouts); 184 | } 185 | Event::KeyboardLayoutSwitched { idx } => { 186 | let kb = self.keyboard_layouts.as_mut(); 187 | let kb = kb.expect("keyboard layouts must be set before a layout can be switched"); 188 | kb.current_idx = idx; 189 | } 190 | event => return Some(event), 191 | } 192 | None 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # This project doesn't use rustfmt, these are only for hints with 2 | # rustfmt --check src/file.rs 3 | 4 | doc_comment_code_block_width = 80 5 | edition = "2021" 6 | format_code_in_doc_comments = true 7 | format_macro_matchers = true 8 | format_strings = true 9 | group_imports = "StdExternalCrate" 10 | imports_granularity = "Crate" 11 | match_arm_blocks = false 12 | match_block_trailing_comma = true 13 | max_width = 80 14 | reorder_impl_items = true 15 | style_edition = "2021" 16 | trailing_semicolon = false 17 | use_field_init_shorthand = true 18 | use_small_heuristics = "Max" 19 | wrap_comments = true 20 | -------------------------------------------------------------------------------- /scripts/cargo-tree-licences-not-mit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cargo tree --prefix none --format '{p} {l}' | grep --invert-match -e 'MIT' 4 | -------------------------------------------------------------------------------- /scripts/ipc.bt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bpftrace 2 | /* 3 | * Trace the timings of wayland and sway-ipc unix socket 4 | * send and receive syscalls on multibg-wayland 5 | * 6 | * On Linux with bpftrace installed one can run this script as root 7 | * to get microsecond resolution timings printed to stdout 8 | * when on our wayland and sway-ipc sockets 9 | * - receive syscalls return 10 | * - send and write syscalls enter 11 | * 12 | * Use the obtained timestamps 13 | * to calculate our latency switching the wallpaper 14 | */ 15 | 16 | tracepoint:syscalls:sys_enter_sendto 17 | { 18 | if (comm != "multibg-wayland") { 19 | return; 20 | } 21 | 22 | printf( 23 | "%s sendto enter fd=%d\n", 24 | strftime("%H:%M:%S.%f", nsecs), 25 | args->fd 26 | ); 27 | } 28 | 29 | tracepoint:syscalls:sys_enter_sendmsg 30 | { 31 | if (comm != "multibg-wayland") { 32 | return; 33 | } 34 | 35 | printf( 36 | "%s sendmsg enter fd=%d\n", 37 | strftime("%H:%M:%S.%f", nsecs), 38 | args->fd 39 | ); 40 | } 41 | 42 | tracepoint:syscalls:sys_enter_write 43 | { 44 | if (comm != "multibg-wayland") { 45 | return; 46 | } 47 | 48 | if (args->fd == 1 && args->fd == 2) { 49 | return; 50 | } 51 | 52 | printf( 53 | "%s write enter fd=%d count=%d %r\n", 54 | strftime("%H:%M:%S.%f", nsecs), 55 | args->fd, 56 | args->count, 57 | buf(args->buf, args->count) 58 | ); 59 | } 60 | 61 | tracepoint:syscalls:sys_exit_recvfrom 62 | { 63 | if (comm != "multibg-wayland") { 64 | return; 65 | } 66 | 67 | printf( 68 | "%s recvfrom exit\n", 69 | strftime("%H:%M:%S.%f", nsecs) 70 | ); 71 | } 72 | 73 | tracepoint:syscalls:sys_exit_recvmsg 74 | { 75 | if (comm != "multibg-wayland") { 76 | return; 77 | } 78 | 79 | printf( 80 | "%s recvmsg exit\n", 81 | strftime("%H:%M:%S.%f", nsecs) 82 | ); 83 | } 84 | 85 | /* Option to trace other syscalls too */ 86 | /* 87 | tracepoint:raw_syscalls:sys_enter 88 | { 89 | if (comm != "multibg-wayland") { 90 | return; 91 | } 92 | 93 | printf( 94 | "%s syscall %d enter\n", 95 | strftime("%H:%M:%S.%f", nsecs), 96 | args->id 97 | ); 98 | } 99 | */ 100 | -------------------------------------------------------------------------------- /scripts/ipc.out.txt: -------------------------------------------------------------------------------- 1 | Attaching 4 probes... 2 | 00:52:29.501701 recvfrom exit 3 | 00:52:29.501723 recvfrom exit 4 | 00:52:29.501725 recvfrom exit 5 | 00:52:29.501740 recvfrom exit 6 | 00:52:29.501908 sendto enter fd=3 7 | 00:52:29.512243 recvmsg exit 8 | 00:52:29.512257 recvmsg exit 9 | 00:52:37.534095 recvfrom exit 10 | 00:52:37.534127 recvfrom exit 11 | 00:52:37.534129 recvfrom exit 12 | 00:52:37.534140 recvfrom exit 13 | 00:52:37.534276 sendto enter fd=3 14 | 00:52:37.546900 recvmsg exit 15 | 00:52:37.546913 recvmsg exit 16 | -------------------------------------------------------------------------------- /scripts/msrvcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # rustup toolchain uninstall 1.80-x86_64-unknown-linux-gnu 3 | # rustup toolchain install 1.82-x86_64-unknown-linux-gnu 4 | # rustup +1.82-x86_64-unknown-linux-gnu target add aarch64-unknown-linux-gnu x86_64-unknown-freebsd 5 | set -euxo pipefail 6 | cargo +1.82-x86_64-unknown-linux-gnu check --target=x86_64-unknown-linux-gnu --features=avif 7 | cargo +1.82-x86_64-unknown-linux-gnu check --target=aarch64-unknown-linux-gnu 8 | cargo +1.82-x86_64-unknown-linux-gnu check --target=x86_64-unknown-freebsd 9 | -------------------------------------------------------------------------------- /scripts/pkgbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | if [[ "$(head -2 Cargo.toml)" != '[package] 5 | name = "multibg-wayland"' ]]; then 6 | echo 'Not in crate root' 7 | exit 1 8 | fi 9 | 10 | version=$(cargo pkgid | cut -d '#' -f2) 11 | crate="target/package/multibg-wayland-$version.crate" 12 | sum=$(sha256sum "$crate" | cut -d ' ' -f1) 13 | 14 | if [[ PKGBUILD -nt "$crate" ]]; then 15 | echo 'Nothing to do' 16 | exit 1 17 | fi 18 | 19 | sed -e "s/@pkgver@/$version/" -e "s/@sha256sum@/$sum/" < PKGBUILD.in > PKGBUILD 20 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, ValueEnum}; 2 | 3 | #[derive(Parser)] 4 | #[command(author, version, long_about = None, about = "\ 5 | Set a different wallpaper for the background of each Sway / Hyprland / niri workspace 6 | 7 | $ multibg-wayland 8 | 9 | Wallpapers should be arranged in the following directory structure: 10 | 11 | wallpaper_dir/output/workspace_name.{jpg|png|...} 12 | 13 | Such as: 14 | 15 | ~/my_wallpapers 16 | ├─ eDP-1 17 | │ ├─ _default.jpg 18 | │ ├─ 1.jpg 19 | │ ├─ 2.png 20 | │ └─ browser.jpg 21 | └─ HDMI-A-1 22 | ├─ 1.jpg 23 | └─ 3.png 24 | 25 | For more details please read the README at: 26 | https://github.com/gergo-salyi/multibg-wayland/blob/master/README.md")] 27 | pub struct Cli { 28 | /// adjust contrast, eg. -c=-25 (default: 0) 29 | #[arg(short, long)] 30 | pub contrast: Option, 31 | /// adjust brightness, eg. -b=-60 (default: 0) 32 | #[arg(short, long)] 33 | pub brightness: Option, 34 | /// wl_buffer pixel format (default: auto) 35 | #[arg(long)] 36 | pub pixelformat: Option, 37 | /// Wayland compositor to connect (autodetect by default) 38 | #[arg(long)] 39 | pub compositor: Option, 40 | /// upload and serve wallpapers from GPU memory using Vulkan 41 | #[arg(long)] 42 | pub gpu: bool, 43 | /// directory with: wallpaper_dir/output/workspace_name.{jpg|png|...} 44 | pub wallpaper_dir: String, 45 | } 46 | 47 | #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] 48 | pub enum PixelFormat { 49 | Auto, 50 | Baseline, 51 | } 52 | -------------------------------------------------------------------------------- /src/compositors.rs: -------------------------------------------------------------------------------- 1 | mod hyprland; 2 | mod niri2502; 3 | mod niri2505; 4 | mod sway; 5 | 6 | use std::{ 7 | env, 8 | os::unix::ffi::OsStrExt, 9 | process::Command, 10 | sync::{mpsc::Sender, Arc}, 11 | thread, 12 | }; 13 | 14 | use anyhow::{bail, Context}; 15 | use serde::Deserialize; 16 | use log::{debug, warn}; 17 | 18 | use crate::poll::Waker; 19 | 20 | #[derive(Clone, Copy, Debug, clap::ValueEnum)] 21 | pub enum Compositor { 22 | Hyprland, 23 | Niri, 24 | Sway, 25 | } 26 | 27 | impl Compositor { 28 | pub fn from_env() -> Option { 29 | Compositor::from_xdg_desktop_var("XDG_SESSION_DESKTOP") 30 | .or_else(|| Compositor::from_xdg_desktop_var("XDG_CURRENT_DESKTOP")) 31 | .or_else(Compositor::from_ipc_socket_var) 32 | } 33 | 34 | fn from_xdg_desktop_var(xdg_desktop_var: &str) -> Option { 35 | if let Some(xdg_desktop) = env::var_os(xdg_desktop_var) { 36 | if xdg_desktop.as_bytes().starts_with(b"sway") { 37 | debug!("Selecting compositor Sway based on {xdg_desktop_var}"); 38 | Some(Compositor::Sway) 39 | } else if xdg_desktop.as_bytes().starts_with(b"Hyprland") { 40 | debug!("Selecting compositor Hyprland based on {}", 41 | xdg_desktop_var); 42 | Some(Compositor::Hyprland) 43 | } else if xdg_desktop.as_bytes().starts_with(b"niri") { 44 | debug!("Selecting compositor Niri based on {xdg_desktop_var}"); 45 | Some(Compositor::Niri) 46 | } else { 47 | warn!("Unrecognized compositor from {xdg_desktop_var} \ 48 | environment variable: {xdg_desktop:?}"); 49 | None 50 | } 51 | } else { 52 | None 53 | } 54 | } 55 | 56 | fn from_ipc_socket_var() -> Option { 57 | if env::var_os("SWAYSOCK").is_some() { 58 | debug!("Selecting compositor Sway based on SWAYSOCK"); 59 | Some(Compositor::Sway) 60 | } else if env::var_os("HYPRLAND_INSTANCE_SIGNATURE").is_some() { 61 | debug!("Selecting compositor Hyprland based on \ 62 | HYPRLAND_INSTANCE_SIGNATURE"); 63 | Some(Compositor::Hyprland) 64 | } else if env::var_os("NIRI_SOCKET").is_some() { 65 | debug!("Selecting compositor Niri based on NIRI_SOCKET"); 66 | Some(Compositor::Niri) 67 | } else { 68 | None 69 | } 70 | } 71 | } 72 | 73 | // impl From<&str> for Compositor { 74 | // fn from(s: &str) -> Self { 75 | // match s { 76 | // "sway" => Compositor::Sway, 77 | // "niri" => Compositor::Niri, 78 | // _ => panic!("Unknown compositor"), 79 | // } 80 | // } 81 | // } 82 | 83 | /// abstract 'sending back workspace change events' 84 | struct EventSender { 85 | tx: Sender, 86 | waker: Arc, 87 | } 88 | 89 | impl EventSender { 90 | fn new(tx: Sender, waker: Arc) -> Self { 91 | EventSender { tx, waker } 92 | } 93 | 94 | fn send(&self, workspace: WorkspaceVisible) { 95 | self.tx.send(workspace).unwrap(); 96 | self.waker.wake(); 97 | } 98 | } 99 | 100 | trait CompositorInterface: Send + Sync { 101 | fn request_visible_workspaces(&mut self) -> Vec; 102 | fn subscribe_event_loop(self, event_sender: EventSender); 103 | } 104 | 105 | 106 | pub struct ConnectionTask { 107 | tx: Sender, 108 | waker: Arc, 109 | interface: Box, 110 | } 111 | 112 | impl ConnectionTask { 113 | pub fn new( 114 | composer: Compositor, 115 | tx: Sender, 116 | waker: Arc, 117 | ) -> Self { 118 | let interface: Box = match composer { 119 | Compositor::Sway => Box::new(sway::SwayConnectionTask::new()), 120 | Compositor::Hyprland => Box::new( 121 | hyprland::HyprlandConnectionTask::new() 122 | ), 123 | Compositor::Niri => match get_niri_version() { 124 | Ok(niri_verison) => if niri_verison >= niri_ver(25, 5) { 125 | Box::new(niri2505::NiriConnectionTask::new()) 126 | } else { 127 | Box::new(niri2502::NiriConnectionTask::new()) 128 | }, 129 | Err(e) => { 130 | warn!("Failed to get niri version: {e:#}"); 131 | Box::new(niri2505::NiriConnectionTask::new()) 132 | } 133 | } 134 | }; 135 | 136 | ConnectionTask { 137 | tx, 138 | waker, 139 | interface, 140 | } 141 | } 142 | 143 | pub fn spawn_subscribe_event_loop( 144 | composer: Compositor, 145 | tx: Sender, 146 | waker: Arc, 147 | ) { 148 | let event_sender = EventSender::new(tx, waker); 149 | thread::Builder::new() 150 | .name("compositor".to_string()) 151 | .spawn(move || match composer { 152 | Compositor::Sway => { 153 | let composer_interface = sway::SwayConnectionTask::new(); 154 | composer_interface.subscribe_event_loop(event_sender); 155 | } 156 | Compositor::Hyprland => { 157 | let composer_interface = 158 | hyprland::HyprlandConnectionTask::new(); 159 | composer_interface.subscribe_event_loop(event_sender); 160 | } 161 | Compositor::Niri => match get_niri_version() { 162 | Ok(niri_verison) => if niri_verison >= niri_ver(25, 5) { 163 | niri2505::NiriConnectionTask::new() 164 | .subscribe_event_loop(event_sender) 165 | } else { 166 | niri2502::NiriConnectionTask::new() 167 | .subscribe_event_loop(event_sender) 168 | }, 169 | Err(e) => { 170 | warn!("Failed to get niri version: {e:#}"); 171 | niri2505::NiriConnectionTask::new() 172 | .subscribe_event_loop(event_sender) 173 | } 174 | } 175 | }) 176 | .unwrap(); 177 | } 178 | 179 | pub fn request_visible_workspace(&mut self, output: &str) { 180 | if let Some(workspace) = self 181 | .interface 182 | .request_visible_workspaces() 183 | .into_iter() 184 | .find(|w| w.output == output) 185 | { 186 | self.tx 187 | .send(WorkspaceVisible { 188 | output: workspace.output, 189 | workspace_name: workspace.workspace_name, 190 | }) 191 | .unwrap(); 192 | 193 | self.waker.wake(); 194 | } 195 | } 196 | 197 | pub fn request_visible_workspaces(&mut self) { 198 | for workspace in self.interface 199 | .request_visible_workspaces().into_iter() 200 | { 201 | self.tx 202 | .send(WorkspaceVisible { 203 | output: workspace.output, 204 | workspace_name: workspace.workspace_name, 205 | }) 206 | .unwrap(); 207 | 208 | self.waker.wake(); 209 | } 210 | } 211 | } 212 | 213 | #[derive(Debug)] 214 | pub struct WorkspaceVisible { 215 | pub output: String, 216 | pub workspace_name: String, 217 | } 218 | 219 | #[derive(Deserialize)] 220 | struct NiriVersionJson { 221 | compositor: String, 222 | } 223 | 224 | // Example: 225 | // $ niri msg --json version 226 | // {"cli":"25.02 (unknown commit)","compositor":"25.02 (unknown commit)"} 227 | fn get_niri_version() -> anyhow::Result { 228 | let out = Command::new("niri") 229 | .args(["msg", "--json", "version"]) 230 | .output().context("Command niri msg version failed")?; 231 | if !out.status.success() { 232 | bail!("Command niri msg version exited with {}: {}", 233 | out.status, String::from_utf8_lossy(&out.stderr)); 234 | } 235 | let version_json: NiriVersionJson = serde_json::from_slice(&out.stdout) 236 | .context("Failed to deserialize niri msg version json")?; 237 | debug!("Niri version: {}", version_json.compositor); 238 | let version = parse_niri_version(&version_json.compositor) 239 | .context("Failed to parse niri version")?; 240 | Ok(version) 241 | } 242 | 243 | fn parse_niri_version(version_str: &str) -> Option { 244 | // Example: "25.02 (unknown commit)" 245 | let mut iter = version_str.split(|c: char| !c.is_ascii_digit()); 246 | let major = iter.next()?.parse::().ok()?; 247 | let minor = iter.next()?.parse::().ok()?; 248 | Some(niri_ver(major, minor)) 249 | } 250 | 251 | fn niri_ver(major: u32, minor: u32) -> u64 { 252 | ((major as u64) << 32) | (minor as u64) 253 | } 254 | -------------------------------------------------------------------------------- /src/compositors/hyprland.rs: -------------------------------------------------------------------------------- 1 | // https://wiki.hyprland.org/IPC/ 2 | 3 | use std::{ 4 | env, 5 | io::{Read, Write}, 6 | os::unix::net::UnixStream, 7 | path::PathBuf, 8 | }; 9 | 10 | use log::debug; 11 | use serde::Deserialize; 12 | 13 | use super::{CompositorInterface, EventSender, WorkspaceVisible}; 14 | 15 | pub struct HyprlandConnectionTask {} 16 | 17 | impl HyprlandConnectionTask { 18 | pub fn new() -> Self { 19 | HyprlandConnectionTask {} 20 | } 21 | } 22 | 23 | impl CompositorInterface for HyprlandConnectionTask { 24 | fn request_visible_workspaces(&mut self) -> Vec { 25 | current_state().visible_workspaces 26 | } 27 | 28 | fn subscribe_event_loop(self, event_sender: EventSender) { 29 | let mut socket = socket_dir_path(); 30 | socket.push(".socket2.sock"); 31 | let mut connection = UnixStream::connect(socket) 32 | .expect("Failed to connect to Hyprland events socket"); 33 | let initial_state = current_state(); 34 | for workspace in initial_state.visible_workspaces { 35 | event_sender.send(workspace); 36 | } 37 | let mut active_monitor = initial_state.active_monitor; 38 | let mut buf = vec![0u8; 2000]; 39 | let mut filled = 0usize; 40 | let mut parsed = 0usize; 41 | loop { 42 | let read = connection.read(&mut buf[filled..]).unwrap(); 43 | if read == 0 { 44 | panic!("Hyperland events socket disconnected"); 45 | } 46 | filled += read; 47 | if filled == buf.len() { 48 | let new_len = buf.len() * 2; 49 | debug!("Growing Hyprland socket read buffer to {new_len}"); 50 | buf.resize(new_len, 0u8); 51 | } 52 | loop { 53 | let mut unparsed = &buf[parsed..filled]; 54 | let Some(gt_pos) = unparsed.iter().position(|&b| b == b'>') 55 | else { break }; 56 | let event_name = &unparsed[..gt_pos]; 57 | unparsed = &unparsed[gt_pos+2..]; 58 | let Some(lf_pos) = unparsed.iter().position(|&b| b == b'\n') 59 | else { break }; 60 | let event_data = &unparsed[..lf_pos]; 61 | unparsed = &unparsed[lf_pos+1..]; 62 | parsed = filled - unparsed.len(); 63 | debug!( 64 | "Hyprland event: {} {}", 65 | String::from_utf8_lossy(event_name), 66 | String::from_utf8_lossy(event_data), 67 | ); 68 | if event_name == b"workspace" { 69 | event_sender.send(WorkspaceVisible { 70 | output: active_monitor.clone(), 71 | workspace_name: String::from_utf8(event_data.to_vec()) 72 | .unwrap(), 73 | }); 74 | } else if event_name == b"focusedmon" { 75 | let comma_pos = event_data.iter() 76 | .position(|&b| b == b',').unwrap(); 77 | let monname = &event_data[..comma_pos]; 78 | active_monitor = String::from_utf8(monname.to_vec()) 79 | .unwrap(); 80 | } else if event_name == b"moveworkspace" 81 | || event_name == b"renameworkspace" 82 | { 83 | let current_state = current_state(); 84 | for workspace in current_state.visible_workspaces { 85 | event_sender.send(workspace); 86 | } 87 | active_monitor = current_state.active_monitor; 88 | } 89 | } 90 | if parsed == filled { 91 | filled = 0; 92 | parsed = 0; 93 | } else { 94 | buf.copy_within(parsed..filled, 0); 95 | filled -= parsed; 96 | parsed = 0; 97 | } 98 | } 99 | } 100 | } 101 | 102 | // "$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE" 103 | fn socket_dir_path() -> PathBuf { 104 | let xdg_runtime_dir = env::var_os("XDG_RUNTIME_DIR") 105 | .expect("Environment variable XDG_RUNTIME_DIR not set"); 106 | let his = env::var_os("HYPRLAND_INSTANCE_SIGNATURE") 107 | .expect("Environment variable HYPRLAND_INSTANCE_SIGNATURE not set"); 108 | let mut ret = PathBuf::with_capacity(256); 109 | ret.push(xdg_runtime_dir); 110 | ret.push("hypr"); 111 | ret.push(his); 112 | ret 113 | } 114 | 115 | fn current_state() -> CurrentState { 116 | let mut socket = socket_dir_path(); 117 | socket.push(".socket.sock"); 118 | let mut connection = UnixStream::connect(socket) 119 | .expect("Failed to connect to Hyprland requests socket"); 120 | connection.write_all(b"j/monitors") 121 | .expect("Failed to send Hyprland monitors requests"); 122 | let mut buf = Vec::with_capacity(2000); 123 | // This socket .socket.sock for hyprctl-like requests 124 | // only allows one round trip with a single or batched commands 125 | let read = connection.read_to_end(&mut buf) 126 | .expect("Failed to receive Hyprland monitors response"); 127 | let monitors: Vec = serde_json::from_slice(&buf[..read]) 128 | .expect("Failed to parse Hyprland monitors response"); 129 | let mut active_monitor = String::new(); 130 | let mut visible_workspaces = Vec::new(); 131 | for monitor in monitors { 132 | if monitor.focused { 133 | active_monitor = monitor.name.clone(); 134 | } 135 | visible_workspaces.push(WorkspaceVisible { 136 | output: monitor.name, 137 | workspace_name: monitor.active_workspace.name, 138 | }); 139 | } 140 | CurrentState { active_monitor, visible_workspaces } 141 | } 142 | 143 | struct CurrentState { 144 | active_monitor: String, 145 | visible_workspaces: Vec, 146 | } 147 | 148 | #[derive(Deserialize)] 149 | struct Monitor { 150 | name: String, 151 | #[serde(rename = "activeWorkspace")] 152 | active_workspace: ActiveWorkspace, 153 | focused: bool, 154 | } 155 | 156 | #[derive(Deserialize)] 157 | struct ActiveWorkspace { 158 | name: String, 159 | } 160 | -------------------------------------------------------------------------------- /src/compositors/niri2502.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use log::debug; 4 | use niri_ipc_25_2_0::{socket::Socket, Event, Request, Response, Workspace}; 5 | 6 | use super::{CompositorInterface, EventSender, WorkspaceVisible}; 7 | 8 | pub struct NiriConnectionTask {} 9 | 10 | impl NiriConnectionTask { 11 | pub fn new() -> Self { 12 | NiriConnectionTask {} 13 | } 14 | } 15 | 16 | impl CompositorInterface for NiriConnectionTask { 17 | fn request_visible_workspaces(&mut self) -> Vec { 18 | request_workspaces().into_iter() 19 | .filter(|w| w.is_active) 20 | .map(|workspace| WorkspaceVisible { 21 | output: workspace.output.unwrap_or_default(), 22 | workspace_name: workspace.name 23 | .unwrap_or_else(|| format!("{}", workspace.idx)), 24 | }) 25 | .collect() 26 | } 27 | 28 | fn subscribe_event_loop(self, event_sender: EventSender) { 29 | let mut workspaces_state = request_workspaces(); 30 | let mut callback = request_event_stream(); 31 | while let Ok(event) = callback() { 32 | match event { 33 | Event::WorkspaceActivated { id, focused: _ } => { 34 | debug!("Niri event: workspace id {id} activated"); 35 | let visible_workspace = 36 | find_workspace(&workspaces_state, id); 37 | event_sender.send(visible_workspace); 38 | }, 39 | Event::WorkspacesChanged { workspaces } => { 40 | debug!("Niri event: workspaces changed: {workspaces:?}"); 41 | workspaces_state = workspaces 42 | }, 43 | _ => {}, 44 | } 45 | } 46 | } 47 | } 48 | 49 | fn find_workspace(workspaces: &[Workspace], id: u64) -> WorkspaceVisible { 50 | let workspace = workspaces.iter() 51 | .find(|workspace| workspace.id == id) 52 | .unwrap_or_else(|| panic!("Unknown niri workspace id {id}")); 53 | let workspace_name = workspace.name.clone() 54 | .unwrap_or_else(|| format!("{}", workspace.idx)); 55 | let output = workspace.output.clone().unwrap_or_default(); 56 | WorkspaceVisible { output, workspace_name } 57 | } 58 | 59 | fn request_event_stream() -> impl FnMut() -> Result { 60 | let Ok((Ok(Response::Handled), callback)) = Socket::connect() 61 | .expect("failed to connect to niri socket") 62 | .send(Request::EventStream) 63 | else { 64 | panic!("failed to subscribe to event stream"); 65 | }; 66 | callback 67 | } 68 | 69 | fn request_workspaces() -> Vec { 70 | let response = Socket::connect() 71 | .expect("failed to connect to niri socket") 72 | .send(Request::Workspaces) 73 | .expect("failed to send niri ipc request") 74 | .0 75 | .expect("niri workspace query failed"); 76 | let Response::Workspaces(workspaces) = response else { 77 | panic!("unexpected response from niri"); 78 | }; 79 | workspaces 80 | } 81 | -------------------------------------------------------------------------------- /src/compositors/niri2505.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use log::debug; 4 | use niri_ipc_25_5_1::{socket::Socket, Event, Request, Response, Workspace}; 5 | 6 | use super::{CompositorInterface, EventSender, WorkspaceVisible}; 7 | 8 | pub struct NiriConnectionTask {} 9 | 10 | impl NiriConnectionTask { 11 | pub fn new() -> Self { 12 | NiriConnectionTask {} 13 | } 14 | } 15 | 16 | impl CompositorInterface for NiriConnectionTask { 17 | fn request_visible_workspaces(&mut self) -> Vec { 18 | request_workspaces().into_iter() 19 | .filter(|w| w.is_active) 20 | .map(|workspace| WorkspaceVisible { 21 | output: workspace.output.unwrap_or_default(), 22 | workspace_name: workspace.name 23 | .unwrap_or_else(|| format!("{}", workspace.idx)), 24 | }) 25 | .collect() 26 | } 27 | 28 | fn subscribe_event_loop(self, event_sender: EventSender) { 29 | let mut workspaces_state = request_workspaces(); 30 | let mut callback = request_event_stream(); 31 | while let Ok(event) = callback() { 32 | match event { 33 | Event::WorkspaceActivated { id, focused: _ } => { 34 | debug!("Niri event: workspace id {id} activated"); 35 | let visible_workspace = 36 | find_workspace(&workspaces_state, id); 37 | event_sender.send(visible_workspace); 38 | }, 39 | Event::WorkspacesChanged { workspaces } => { 40 | debug!("Niri event: workspaces changed: {workspaces:?}"); 41 | workspaces_state = workspaces 42 | }, 43 | _ => {}, 44 | } 45 | } 46 | } 47 | } 48 | 49 | fn find_workspace(workspaces: &[Workspace], id: u64) -> WorkspaceVisible { 50 | let workspace = workspaces.iter() 51 | .find(|workspace| workspace.id == id) 52 | .unwrap_or_else(|| panic!("Unknown niri workspace id {id}")); 53 | let workspace_name = workspace.name.clone() 54 | .unwrap_or_else(|| format!("{}", workspace.idx)); 55 | let output = workspace.output.clone().unwrap_or_default(); 56 | WorkspaceVisible { output, workspace_name } 57 | } 58 | 59 | fn request_event_stream() -> impl FnMut() -> Result { 60 | let mut socket = Socket::connect().expect("failed to connect to niri socket"); 61 | let Ok(Ok(Response::Handled)) = socket.send(Request::EventStream) else { 62 | panic!("failed to subscribe to event stream"); 63 | }; 64 | socket.read_events() 65 | } 66 | 67 | fn request_workspaces() -> Vec { 68 | let response = Socket::connect() 69 | .expect("failed to connect to niri socket") 70 | .send(Request::Workspaces) 71 | .expect("failed to send niri ipc request") 72 | .expect("niri workspace query failed"); 73 | let Response::Workspaces(workspaces) = response else { 74 | panic!("unexpected response from niri"); 75 | }; 76 | workspaces 77 | } 78 | -------------------------------------------------------------------------------- /src/compositors/sway.rs: -------------------------------------------------------------------------------- 1 | use super::{CompositorInterface, EventSender, WorkspaceVisible}; 2 | use swayipc::{Connection, Event, EventType, WorkspaceChange}; 3 | 4 | pub struct SwayConnectionTask { 5 | sway_conn: Connection, 6 | } 7 | 8 | impl SwayConnectionTask { 9 | pub fn new() -> Self { 10 | SwayConnectionTask { 11 | sway_conn: Connection::new().expect("Failed to connect to sway \ 12 | socket. If you're not using sway, pass the correct \ 13 | --compositor argument. Original cause"), 14 | } 15 | } 16 | } 17 | 18 | impl CompositorInterface for SwayConnectionTask { 19 | fn request_visible_workspaces(&mut self) -> Vec { 20 | self.sway_conn 21 | .get_workspaces() 22 | .unwrap() 23 | .into_iter() 24 | .filter(|w| w.visible) 25 | .map(|workspace| WorkspaceVisible { 26 | output: workspace.output, 27 | workspace_name: workspace.name, 28 | }) 29 | .collect() 30 | } 31 | 32 | fn subscribe_event_loop(self, event_sender: EventSender) { 33 | let event_stream = self.sway_conn 34 | .subscribe([EventType::Workspace]).unwrap(); 35 | for event_result in event_stream { 36 | let event = event_result.unwrap(); 37 | let Event::Workspace(workspace_event) = event else { 38 | continue; 39 | }; 40 | if let WorkspaceChange::Focus = workspace_event.change { 41 | let current_workspace = workspace_event.current.unwrap(); 42 | event_sender.send(WorkspaceVisible { 43 | output: current_workspace.output.unwrap(), 44 | workspace_name: current_workspace.name.unwrap(), 45 | }); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/gpu.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_op_in_unsafe_fn)] 2 | 3 | // https://registry.khronos.org/vulkan/specs/latest/man/html/VK_EXT_image_drm_format_modifier.html 4 | 5 | mod device; 6 | mod instance; 7 | mod memory; 8 | 9 | use std::{ 10 | ffi::CStr, 11 | os::fd::OwnedFd, 12 | rc::{Rc, Weak}, 13 | slice, 14 | }; 15 | 16 | use anyhow::Context; 17 | use ash::{ 18 | Device, Entry, Instance, 19 | ext::{debug_report, debug_utils, image_drm_format_modifier}, 20 | khr::external_memory_fd, 21 | vk::{ 22 | Buffer, 23 | CommandBuffer, 24 | CommandPool, 25 | DebugReportCallbackEXT, 26 | DebugUtilsMessengerEXT, 27 | DeviceMemory, 28 | DrmFormatModifierPropertiesEXT, 29 | ExtensionProperties, 30 | Extent2D, 31 | Image, 32 | PhysicalDevice, 33 | PhysicalDeviceMemoryProperties, 34 | Queue, 35 | } 36 | }; 37 | use log::{debug, error}; 38 | use rustix::fs::Dev; 39 | 40 | use device::device; 41 | use instance::instance; 42 | use memory::{upload, uploader}; 43 | 44 | pub struct Gpu { 45 | instance: Rc, 46 | devices: Vec>, 47 | } 48 | 49 | impl Gpu { 50 | pub fn new() -> anyhow::Result { 51 | let instance = Rc::new(unsafe { 52 | instance() 53 | }.context("Failed to create Vulkan instance")?); 54 | let devices = Vec::new(); 55 | Ok(Gpu { instance, devices }) 56 | } 57 | 58 | pub fn uploader( 59 | &mut self, 60 | dmabuf_drm_dev: Option, 61 | width: u32, 62 | height: u32, 63 | drm_format_modifiers: Vec, 64 | ) -> anyhow::Result { 65 | unsafe { 66 | let mut selected = self.select_device(dmabuf_drm_dev); 67 | if selected.is_none() { 68 | let new_device = Rc::new(device(&self.instance, dmabuf_drm_dev) 69 | .context("Failed to create new device")?); 70 | self.devices.push(Rc::downgrade(&new_device)); 71 | selected = Some(new_device); 72 | } 73 | let gpu_device = selected.unwrap(); 74 | uploader(gpu_device, width, height, drm_format_modifiers) 75 | .context("Failed to create GPU uploader") 76 | } 77 | } 78 | 79 | fn select_device( 80 | &mut self, 81 | dmabuf_drm_dev: Option, 82 | ) -> Option> { 83 | let mut ret = None; 84 | self.devices.retain(|weak_gpu_device| { 85 | if let Some(gpu_device) = weak_gpu_device.upgrade() { 86 | if ret.is_none() 87 | && gpu_device.dmabuf_drm_dev_eq(dmabuf_drm_dev) 88 | { 89 | ret = Some(gpu_device) 90 | } 91 | true 92 | } else { 93 | false 94 | } 95 | }); 96 | ret 97 | } 98 | } 99 | 100 | struct GpuInstance { 101 | _entry: Entry, 102 | instance: Instance, 103 | debug: Debug, 104 | } 105 | 106 | impl Drop for GpuInstance { 107 | fn drop(&mut self) { 108 | unsafe { 109 | match &self.debug { 110 | Debug::Utils { instance, messenger } => { 111 | instance.destroy_debug_utils_messenger(*messenger, None); 112 | }, 113 | Debug::Report { instance, callback } => { 114 | #[allow(deprecated)] 115 | instance.destroy_debug_report_callback(*callback, None); 116 | } 117 | Debug::None => (), 118 | }; 119 | self.instance.destroy_instance(None); 120 | debug!("Vulkan context has been cleaned up"); 121 | } 122 | } 123 | } 124 | 125 | enum Debug { 126 | Utils { 127 | instance: debug_utils::Instance, 128 | messenger: DebugUtilsMessengerEXT, 129 | }, 130 | Report { 131 | instance: debug_report::Instance, 132 | callback: DebugReportCallbackEXT, 133 | }, 134 | None, 135 | } 136 | 137 | struct GpuDevice { 138 | gpu_instance: Rc, 139 | physdev: PhysicalDevice, 140 | primary_drm_dev: Option, 141 | render_drm_dev: Option, 142 | dmabuf_drm_dev: Option, 143 | memory_props: PhysicalDeviceMemoryProperties, 144 | drm_format_props: Option>, 145 | device: Device, 146 | external_memory_fd_device: external_memory_fd::Device, 147 | image_drm_format_modifier_device: Option, 148 | queue_family_index: u32, 149 | queue: Queue, 150 | command_pool: CommandPool, 151 | command_buffer: CommandBuffer, 152 | } 153 | 154 | impl Drop for GpuDevice { 155 | fn drop(&mut self) { 156 | unsafe { 157 | if let Err(e) = self.device.device_wait_idle() { 158 | error!("Failed to wait device idle: {e}"); 159 | }; 160 | self.device.destroy_command_pool(self.command_pool, None); 161 | self.device.destroy_device(None); 162 | } 163 | } 164 | } 165 | 166 | impl GpuDevice { 167 | fn dmabuf_drm_dev_eq(&self, drm_dev: Option) -> bool { 168 | if drm_dev.is_some() { 169 | assert!(self.dmabuf_drm_dev.is_some()); 170 | drm_dev == self.dmabuf_drm_dev 171 | || drm_dev == self.render_drm_dev 172 | || drm_dev == self.primary_drm_dev 173 | } else { 174 | assert!(self.dmabuf_drm_dev.is_none()); 175 | true 176 | } 177 | } 178 | } 179 | 180 | pub struct GpuUploader { 181 | gpu_device: Rc, 182 | buffer: Buffer, 183 | memory: DeviceMemory, 184 | ptr: *mut u8, 185 | len: usize, 186 | extent: Extent2D, 187 | drm_format_modifiers: Vec, 188 | } 189 | 190 | impl Drop for GpuUploader { 191 | fn drop(&mut self) { 192 | unsafe { 193 | let device = &self.gpu_device.device; 194 | device.unmap_memory(self.memory); 195 | device.free_memory(self.memory, None); 196 | device.destroy_buffer(self.buffer, None); 197 | } 198 | } 199 | } 200 | 201 | impl GpuUploader { 202 | pub fn staging_buffer(&mut self) -> &mut [u8] { 203 | unsafe { slice::from_raw_parts_mut(self.ptr, self.len) } 204 | } 205 | 206 | pub fn upload(&mut self) -> anyhow::Result { 207 | unsafe { upload(self) } 208 | } 209 | } 210 | 211 | pub struct GpuWallpaper { 212 | pub drm_format_modifier: u64, 213 | pub memory_planes_len: usize, 214 | pub memory_planes: [MemoryPlane; 4], 215 | pub gpu_memory: GpuMemory, 216 | pub fd: OwnedFd, 217 | } 218 | 219 | #[derive(Clone, Copy, Default)] 220 | pub struct MemoryPlane { 221 | pub offset: u64, 222 | pub stride: u64, 223 | } 224 | 225 | pub struct GpuMemory { 226 | gpu_device: Rc, 227 | image: Image, 228 | memory: DeviceMemory, 229 | size: usize, 230 | drm_format_modifier: u64, 231 | } 232 | 233 | impl Drop for GpuMemory { 234 | fn drop(&mut self) { 235 | unsafe { 236 | self.gpu_device.device.destroy_image(self.image, None); 237 | self.gpu_device.device.free_memory(self.memory, None); 238 | } 239 | } 240 | } 241 | 242 | impl GpuMemory { 243 | pub fn gpu_uploader_eq(&self, gpu_uploader: &GpuUploader) -> bool { 244 | self.dmabuf_feedback_eq( 245 | gpu_uploader.gpu_device.dmabuf_drm_dev, 246 | gpu_uploader.drm_format_modifiers.as_slice(), 247 | ) 248 | } 249 | 250 | pub fn dmabuf_feedback_eq( 251 | &self, 252 | dmabuf_drm_dev: Option, 253 | drm_format_modifiers: &[u64], 254 | ) -> bool { 255 | self.gpu_device.dmabuf_drm_dev_eq(dmabuf_drm_dev) 256 | && drm_format_modifiers.contains(&self.drm_format_modifier) 257 | } 258 | 259 | pub fn size(&self) -> usize { 260 | self.size 261 | } 262 | } 263 | 264 | // Fourcc codes are based on libdrm drm_fourcc.h 265 | // https://gitlab.freedesktop.org/mesa/drm/-/blob/main/include/drm/drm_fourcc.h 266 | // /usr/include/libdrm/drm_fourcc.h 267 | // under license MIT 268 | pub const fn fourcc_code(a: u8, b: u8, c: u8, d: u8) -> u32 { 269 | (a as u32) | (b as u32) << 8 | (c as u32) << 16 | (d as u32) << 24 270 | } 271 | 272 | pub const fn fourcc_mod_code(vendor: u64, val: u64) -> u64 { 273 | (vendor << 56) | (val & 0x00ff_ffff_ffff_ffff) 274 | } 275 | 276 | // pub const DRM_FORMAT_INVALID: u32 = 0; 277 | pub const DRM_FORMAT_XRGB8888: u32 = fourcc_code(b'X', b'R', b'2', b'4'); 278 | // pub const DRM_FORMAT_ARGB8888: u32 = fourcc_code(b'A', b'R', b'2', b'4'); 279 | 280 | pub const DRM_FORMAT_MOD_VENDOR_NONE: u64 = 0; 281 | // pub const DRM_FORMAT_RESERVED: u64 = (1 << 56) - 1; 282 | 283 | // pub const DRM_FORMAT_MOD_INVALID: u64 = fourcc_mod_code( 284 | // DRM_FORMAT_MOD_VENDOR_NONE, 285 | // DRM_FORMAT_RESERVED, 286 | // ); 287 | pub const DRM_FORMAT_MOD_LINEAR: u64 = fourcc_mod_code( 288 | DRM_FORMAT_MOD_VENDOR_NONE, 289 | 0, 290 | ); 291 | 292 | fn has_extension(extensions: &[ExtensionProperties], name: &CStr) -> bool { 293 | extensions.iter().any(|ext| ext.extension_name_as_c_str() == Ok(name)) 294 | } 295 | 296 | pub fn fmt_modifier(drm_format_modifier: u64) -> String { 297 | format!("{drm_format_modifier:016x}") 298 | } 299 | -------------------------------------------------------------------------------- /src/gpu/device.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{c_char, CStr}, 3 | rc::Rc, 4 | }; 5 | 6 | use anyhow::{bail, Context}; 7 | use ash::{ 8 | Instance, 9 | ext::{ 10 | external_memory_dma_buf, 11 | image_drm_format_modifier, 12 | physical_device_drm, 13 | queue_family_foreign, 14 | }, 15 | khr::{driver_properties, external_memory_fd, image_format_list}, 16 | vk::{ 17 | self, 18 | api_version_variant, 19 | api_version_major, 20 | api_version_minor, 21 | api_version_patch, 22 | CommandBufferAllocateInfo, 23 | CommandBufferLevel, 24 | CommandPoolCreateFlags, 25 | CommandPoolCreateInfo, 26 | DeviceCreateInfo, 27 | DeviceQueueCreateInfo, 28 | DrmFormatModifierPropertiesEXT, 29 | DrmFormatModifierPropertiesListEXT, 30 | ExtensionProperties, 31 | Format, 32 | FormatProperties2, 33 | PhysicalDevice, 34 | PhysicalDeviceDriverProperties, 35 | PhysicalDeviceDrmPropertiesEXT, 36 | PhysicalDeviceProperties, 37 | PhysicalDeviceProperties2, 38 | PhysicalDeviceType, 39 | QueueFlags, 40 | } 41 | }; 42 | use log::{debug, error, warn}; 43 | use rustix::fs::{Dev, major, makedev, minor}; 44 | use scopeguard::{guard, ScopeGuard}; 45 | 46 | use super::{GpuDevice, GpuInstance, has_extension}; 47 | 48 | struct PhysdevInfo { 49 | physdev: PhysicalDevice, 50 | props: PhysicalDeviceProperties, 51 | extensions: Extensions, 52 | primary: Option, 53 | render: Option, 54 | dmabuf_dev: Option, 55 | score: u32, 56 | } 57 | 58 | const SCORE_MATCHES_DRM_DEV: u32 = 1 << 3; 59 | const SCORE_DISCRETE_GPU: u32 = 1 << 2; 60 | const SCORE_INTEGRATED_GPU: u32 = 1 << 1; 61 | const SCORE_VIRTUAL_GPU: u32 = 1 << 0; 62 | 63 | pub unsafe fn device( 64 | gpu_instance: &Rc, 65 | dmabuf_drm_dev: Option, 66 | ) -> anyhow::Result { 67 | let instance = &gpu_instance.instance; 68 | let physdevs = instance.enumerate_physical_devices() 69 | .context("Failed to enumerate physical devices")?; 70 | let count = physdevs.len(); 71 | if count == 0 { 72 | bail!("No physical devices found. Make sure you have a Vulkan driver \ 73 | installed for your GPU and this application has permisson to \ 74 | access graphics devices"); 75 | } 76 | let mut physdev_infos = physdevs.into_iter() 77 | .filter_map(|physdev| physdev_info(instance, dmabuf_drm_dev, physdev)) 78 | .collect::>(); 79 | physdev_infos.sort_by_key(|info| u32::MAX - info.score); 80 | let Some(max_score) = physdev_infos.first().map(|info| info.score) else { 81 | bail!("No physical devices could be probed") 82 | }; 83 | physdev_infos.retain(|info| info.score == max_score); 84 | if physdev_infos.len() == 1 { 85 | debug!("Probed {} physical device(s), max score {}", count, max_score); 86 | return device_with_physdev(gpu_instance, physdev_infos.pop().unwrap()) 87 | } 88 | warn!("Filtered multiple physical devices, {} out of {} with max score {}", 89 | physdev_infos.len(), count, max_score); 90 | let mut gpu_device_ok = None; 91 | let mut errors = Vec::new(); 92 | for physdev_info in physdev_infos { 93 | match device_with_physdev(gpu_instance, physdev_info) { 94 | Ok(gpu_device) => { 95 | gpu_device_ok = Some(gpu_device); 96 | break 97 | }, 98 | Err(e) => errors.push(e), 99 | } 100 | } 101 | if let Some(gpu_device) = gpu_device_ok { 102 | for e in errors { 103 | warn!("{e:#}"); 104 | } 105 | Ok(gpu_device) 106 | } else { 107 | for e in errors { 108 | error!("{e:#}"); 109 | } 110 | bail!("Failed to set up device with all filtered physical devices"); 111 | } 112 | } 113 | 114 | unsafe fn physdev_info( 115 | instance: &Instance, 116 | dmabuf_dev: Option, 117 | physdev: PhysicalDevice, 118 | ) -> Option { 119 | let extension_props_vec = match instance 120 | .enumerate_device_extension_properties(physdev) 121 | { 122 | Ok(ext_props) => ext_props, 123 | Err(e) => { 124 | let props = instance.get_physical_device_properties(physdev); 125 | let name = props.device_name_as_c_str().unwrap_or(c"unknown"); 126 | let typ = props.device_type; 127 | error!("Failed to enumerate device extension properties 128 | for physical device {name:?} (type {typ:?}): {e}"); 129 | return None 130 | } 131 | }; 132 | let extensions = Extensions::new(extension_props_vec); 133 | let mut props2_chain = PhysicalDeviceProperties2::default(); 134 | let has_drm_props = extensions.has(physical_device_drm::NAME); 135 | let mut drm_props = PhysicalDeviceDrmPropertiesEXT::default(); 136 | if has_drm_props { 137 | props2_chain = props2_chain.push_next(&mut drm_props); 138 | } 139 | let has_driver_props = extensions.has(driver_properties::NAME); 140 | let mut driver_props = PhysicalDeviceDriverProperties::default(); 141 | if has_driver_props { 142 | props2_chain = props2_chain.push_next(&mut driver_props); 143 | } 144 | instance.get_physical_device_properties2(physdev, &mut props2_chain); 145 | let props = props2_chain.properties; 146 | let name = props.device_name_as_c_str().unwrap_or(c"unknown"); 147 | let typ = props.device_type; 148 | debug!("Probing physical device {name:?} (type {typ:?})"); 149 | let mut score = 0u32; 150 | match typ { 151 | PhysicalDeviceType::DISCRETE_GPU => score |= SCORE_DISCRETE_GPU, 152 | PhysicalDeviceType::INTEGRATED_GPU => score |= SCORE_INTEGRATED_GPU, 153 | PhysicalDeviceType::VIRTUAL_GPU => score |= SCORE_VIRTUAL_GPU, 154 | _ => (), 155 | } 156 | if has_driver_props { 157 | debug!("Physical device driver: {:?}, {:?}", 158 | driver_props.driver_name_as_c_str().unwrap_or(c"unknown"), 159 | driver_props.driver_info_as_c_str().unwrap_or(c"unknown")); 160 | } else { 161 | debug!("VK_KHR_driver_properties unavailable"); 162 | } 163 | let (mut primary, mut render) = (None, None); 164 | if has_drm_props { 165 | if drm_props.has_primary == vk::TRUE { 166 | primary = Some(makedev( 167 | drm_props.primary_major as _, 168 | drm_props.primary_minor as _, 169 | )); 170 | } 171 | if drm_props.has_render == vk::TRUE { 172 | render = Some(makedev( 173 | drm_props.render_major as _, 174 | drm_props.render_minor as _, 175 | )); 176 | } 177 | debug!("Physical device DRM devs: primary {}, render {}", 178 | fmt_dev_option(primary), fmt_dev_option(render)); 179 | // Note [1] 180 | if dmabuf_dev.is_some() 181 | && (dmabuf_dev == primary || dmabuf_dev == render) 182 | { 183 | score |= SCORE_MATCHES_DRM_DEV; 184 | debug!("Physical device matched with the DMA-BUF feedback DRM dev"); 185 | } else { 186 | debug!("Could not match physical device with the DMA-BUF feedback \ 187 | DRM dev"); 188 | } 189 | } else { 190 | debug!("VK_EXT_physical_device_drm unavailable"); 191 | } 192 | Some(PhysdevInfo { 193 | physdev, 194 | props, 195 | extensions, 196 | primary, 197 | render, 198 | dmabuf_dev, 199 | score, 200 | }) 201 | } 202 | 203 | unsafe fn device_with_physdev( 204 | gpu_instance: &Rc, 205 | physdev_info: PhysdevInfo, 206 | ) -> anyhow::Result { 207 | let name = physdev_info.props 208 | .device_name_as_c_str().unwrap_or(c"unknown").to_owned(); 209 | let typ = physdev_info.props.device_type; 210 | let score = physdev_info.score; 211 | debug!("Setting up device with physical device {name:?} (type {typ:?})"); 212 | let gpu_device = try_device_with_physdev(gpu_instance, physdev_info) 213 | .with_context(|| format!( 214 | "Failed to set up device with physical device {:?} (type {:?})", 215 | name, typ 216 | ))?; 217 | if score & SCORE_MATCHES_DRM_DEV == 0 { 218 | // We get here if either 219 | // - using Wayland protocol Linux DMA-BUF version < 4 220 | // - device has no VK_EXT_physical_device_drm 221 | warn!("IMPORTANT: Failed to ensure that we select the same GPU where \ 222 | the compositor is running based on the DRM device numbers. About \ 223 | to use physical device {:?} (type {:?}). If this is incorrect \ 224 | then please restart without the --gpu option and open an issue", 225 | name, typ); 226 | } 227 | Ok(gpu_device) 228 | } 229 | 230 | unsafe fn try_device_with_physdev( 231 | gpu_instance: &Rc, 232 | physdev_info: PhysdevInfo, 233 | ) -> anyhow::Result { 234 | let instance = &gpu_instance.instance; 235 | let PhysdevInfo { 236 | physdev, 237 | props, 238 | mut extensions, 239 | primary, 240 | render, 241 | dmabuf_dev, 242 | .. 243 | } = physdev_info; 244 | let variant = api_version_variant(props.api_version); 245 | let major = api_version_major(props.api_version); 246 | let minor = api_version_minor(props.api_version); 247 | let patch = api_version_patch(props.api_version); 248 | if variant != 0 || major != 1 || minor < 1 { 249 | bail!("Need Vulkan device variant 0 version 1.1.0 or compatible, 250 | found variant {variant} version {major}.{minor}.{patch}"); 251 | } 252 | debug!("Vulkan device supports version {major}.{minor}.{patch}"); 253 | let memory_props = instance 254 | .get_physical_device_memory_properties(physdev); 255 | let queue_family_props = instance 256 | .get_physical_device_queue_family_properties(physdev); 257 | let queue_family_index = queue_family_props.iter() 258 | .position(|props| { 259 | props.queue_flags.contains(QueueFlags::GRAPHICS) 260 | && props.queue_count > 0 261 | }) 262 | .context("Failed to find an appropriate queue family")? as u32; 263 | // Device extension dependency chains with Vulkan 1.1 264 | // app --> EXT_external_memory_dma_buf -> KHR_external_memory_fd 265 | // \-> EXT_queue_family_foreign 266 | // \-> (optional) EXT_image_drm_format_modifier -> KHR_image_format_list 267 | // EXT_image_drm_format_modifier is notably unsupported by 268 | // - AMD GFX8 and older 269 | // - end-of-life Nvidia GPUs which never got driver version 515 270 | extensions.try_enable(external_memory_fd::NAME) 271 | .context("KHR_external_memory_fd unavailable")?; 272 | extensions.try_enable(external_memory_dma_buf::NAME) 273 | .context("EXT_external_memory_dma_buf unavailable")?; 274 | extensions.try_enable(queue_family_foreign::NAME) 275 | .context("EXT_queue_family_foreign unavailable")?; 276 | extensions.try_enable(image_format_list::NAME) 277 | .context("KHR_image_format_list unavailable")?; 278 | let has_image_drm_format_modifier = extensions 279 | .try_enable(image_drm_format_modifier::NAME).is_some(); 280 | let device = guard( 281 | instance.create_device( 282 | physdev, 283 | &DeviceCreateInfo::default() 284 | .queue_create_infos(&[DeviceQueueCreateInfo::default() 285 | .queue_family_index(queue_family_index) 286 | .queue_priorities(&[1.0])] 287 | ) 288 | .enabled_extension_names(extensions.enabled()), 289 | None 290 | ).context("Failed to create device")?, 291 | |device| device.destroy_device(None), 292 | ); 293 | let external_memory_fd_device = 294 | external_memory_fd::Device::new(instance, &device); 295 | let image_drm_format_modifier_device = if has_image_drm_format_modifier { 296 | Some(image_drm_format_modifier::Device::new(instance, &device)) 297 | } else { 298 | debug!("EXT_image_drm_format_modifier unavailable"); 299 | None 300 | }; 301 | let queue = device.get_device_queue(queue_family_index, 0); 302 | let command_pool = guard( 303 | device.create_command_pool( 304 | &CommandPoolCreateInfo::default() 305 | .flags(CommandPoolCreateFlags::RESET_COMMAND_BUFFER) 306 | .queue_family_index(queue_family_index), 307 | None 308 | ).context("Failed to create command pool")?, 309 | |command_pool| device.destroy_command_pool(command_pool, None) 310 | ); 311 | let command_buffer = guard( 312 | device.allocate_command_buffers( 313 | &CommandBufferAllocateInfo::default() 314 | .command_buffer_count(1) 315 | .command_pool(*command_pool) 316 | .level(CommandBufferLevel::PRIMARY) 317 | ).context("Failed to allocate command buffer")?[0], 318 | |command_buffer| 319 | device.free_command_buffers(*command_pool, &[command_buffer]), 320 | ); 321 | let drm_format_props = if has_image_drm_format_modifier { 322 | Some(drm_format_props_b8g8r8a8_srgb(instance, physdev)) 323 | } else { 324 | None 325 | }; 326 | Ok(GpuDevice { 327 | command_buffer: ScopeGuard::into_inner(command_buffer), 328 | command_pool: ScopeGuard::into_inner(command_pool), 329 | device: ScopeGuard::into_inner(device), 330 | external_memory_fd_device, 331 | image_drm_format_modifier_device, 332 | physdev, 333 | memory_props, 334 | drm_format_props, 335 | primary_drm_dev: primary, 336 | render_drm_dev: render, 337 | dmabuf_drm_dev: dmabuf_dev, 338 | queue, 339 | queue_family_index, 340 | gpu_instance: Rc::clone(gpu_instance), 341 | }) 342 | } 343 | 344 | struct Extensions { 345 | props: Vec, 346 | enabled: Vec<*const c_char>, 347 | } 348 | 349 | impl Extensions { 350 | fn new(props: Vec) -> Extensions { 351 | Extensions { props, enabled: Vec::new() } 352 | } 353 | 354 | fn has(&self, name: &CStr) -> bool { 355 | has_extension(&self.props, name) 356 | } 357 | 358 | fn try_enable(&mut self, extension: &CStr) -> Option<()> { 359 | if self.props.iter().any(|ext| 360 | ext.extension_name_as_c_str() == Ok(extension) 361 | ) { 362 | self.enabled.push(extension.as_ptr()); 363 | Some(()) 364 | } else { 365 | None 366 | } 367 | } 368 | 369 | fn enabled(&self) -> &[*const c_char] { 370 | &self.enabled 371 | } 372 | } 373 | 374 | fn fmt_dev_option(dev: Option) -> String { 375 | if let Some(dev) = dev { 376 | format!("{}:{}", major(dev), minor(dev)) 377 | } else { 378 | "unavailable".to_string() 379 | } 380 | } 381 | 382 | unsafe fn drm_format_props_b8g8r8a8_srgb( 383 | instance: &Instance, 384 | physdev: PhysicalDevice, 385 | ) -> Vec { 386 | let mut drm_format_props_list = 387 | DrmFormatModifierPropertiesListEXT::default(); 388 | instance.get_physical_device_format_properties2( 389 | physdev, 390 | Format::B8G8R8A8_SRGB, 391 | &mut FormatProperties2::default().push_next(&mut drm_format_props_list), 392 | ); 393 | let mut drm_format_props = vec![ 394 | DrmFormatModifierPropertiesEXT::default(); 395 | drm_format_props_list.drm_format_modifier_count as usize 396 | ]; 397 | drm_format_props_list = drm_format_props_list 398 | .drm_format_modifier_properties(&mut drm_format_props); 399 | let mut format_props_chain = FormatProperties2::default() 400 | .push_next(&mut drm_format_props_list); 401 | instance.get_physical_device_format_properties2( 402 | physdev, 403 | Format::B8G8R8A8_SRGB, 404 | &mut format_props_chain, 405 | ); 406 | drm_format_props 407 | } 408 | 409 | // [1] Wayland DMA-BUF says for the feedback main device and for the tranche 410 | // target device one must not compare the dev_t and should use drmDevicesEqual 411 | // from libdrm.so instead to find the same GPU. But neither Mesa Vulkan WSI 412 | // Wayland nor wlroots Vulkan renderer does that, they both use 413 | // PhysicalDeviceDrmPropertiesEXT the same way we do here. So this is probably 414 | // fine (because it provides both the primary and the render DRM node to 415 | // compare against not just one of them (?)). Do we need a fallback using 416 | // libdrm drmDevicesEqual? 417 | -------------------------------------------------------------------------------- /src/gpu/instance.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | backtrace::Backtrace, 3 | borrow::Cow, 4 | ffi::{c_char, c_void, CStr}, 5 | ptr, 6 | }; 7 | 8 | use anyhow::{bail, Context}; 9 | use ash::{ 10 | Entry, 11 | ext::{ 12 | debug_report, 13 | debug_utils, 14 | }, 15 | vk::{ 16 | self, 17 | api_version_variant, 18 | api_version_major, 19 | api_version_minor, 20 | api_version_patch, 21 | ApplicationInfo, 22 | Bool32, 23 | DebugReportCallbackCreateInfoEXT, 24 | DebugReportFlagsEXT, 25 | DebugReportObjectTypeEXT, 26 | DebugUtilsMessengerCallbackDataEXT, 27 | DebugUtilsMessengerCreateInfoEXT, 28 | DebugUtilsMessageSeverityFlagsEXT, 29 | DebugUtilsMessageTypeFlagsEXT, 30 | InstanceCreateInfo, 31 | LayerProperties, 32 | make_api_version, 33 | } 34 | }; 35 | use log::{debug, error, info, warn}; 36 | 37 | use super::{Debug, GpuInstance, has_extension}; 38 | 39 | const APP_VK_NAME: &CStr = match CStr::from_bytes_with_nul( 40 | concat!(env!("CARGO_PKG_NAME"), '\0').as_bytes() 41 | ) { 42 | Ok(val) => val, 43 | Err(_) => panic!(), 44 | }; 45 | const APP_VK_VERSION: u32 = make_api_version( 46 | 0, 47 | parse_decimal(env!("CARGO_PKG_VERSION_MAJOR")), 48 | parse_decimal(env!("CARGO_PKG_VERSION_MINOR")), 49 | parse_decimal(env!("CARGO_PKG_VERSION_PATCH")), 50 | ); 51 | const LAYER_VALIDATION: &CStr = c"VK_LAYER_KHRONOS_validation"; 52 | const VULKAN_VERSION_TARGET: u32 = make_api_version(0, 1, 1, 0); 53 | 54 | pub unsafe fn instance() -> anyhow::Result { 55 | let entry = Entry::load() 56 | .context("Failed to load Vulkan shared libraries. Make sure you have \ 57 | the Vulkan loader and a Vulkan driver for your GPU installed")?; 58 | let instance_version = entry.try_enumerate_instance_version() 59 | .context("Failed to enumerate instance version")? 60 | .unwrap_or_else(|| make_api_version(0, 1, 0, 0)); 61 | let variant = api_version_variant(instance_version); 62 | let major = api_version_major(instance_version); 63 | let minor = api_version_minor(instance_version); 64 | let patch = api_version_patch(instance_version); 65 | if variant != 0 || major != 1 || minor < 1 { 66 | bail!("Need Vulkan instance variant 0 version 1.1.0 or compatible, 67 | found variant {variant} version {major}.{minor}.{patch}"); 68 | } 69 | debug!("Vulkan instance supports version {major}.{minor}.{patch}"); 70 | let instance_layer_props = entry.enumerate_instance_layer_properties() 71 | .context("Failed to enumerate instance layers")?; 72 | let instance_extension_props = entry 73 | .enumerate_instance_extension_properties(None) 74 | .context("Failed to enumerate instance extensions")?; 75 | let mut instance_layers = Vec::new(); 76 | let mut extension_layers = Vec::new(); 77 | let mut has_debug_utils = false; 78 | let mut has_debug_report = false; 79 | if log::log_enabled!(log::Level::Debug) { 80 | debug!("Running with log level DEBUG or higher \ 81 | so trying to enable Vulkan validation layers"); 82 | if has_layer(&instance_layer_props, LAYER_VALIDATION) { 83 | instance_layers.push(LAYER_VALIDATION.as_ptr()); 84 | info!("Enabling VK_LAYER_KHRONOS_validation"); 85 | } else { 86 | warn!("VK_LAYER_KHRONOS_validation unavailable"); 87 | } 88 | if has_extension(&instance_extension_props, debug_utils::NAME) { 89 | debug!("Enabling VK_EXT_debug_utils"); 90 | has_debug_utils = true; 91 | extension_layers.push(debug_utils::NAME.as_ptr()); 92 | } else if has_extension(&instance_extension_props, debug_report::NAME) { 93 | debug!("Enabling VK_EXT_debug_report"); 94 | has_debug_report = true; 95 | extension_layers.push(debug_report::NAME.as_ptr()); 96 | } else { 97 | warn!("VK_EXT_debug_utils and VK_EXT_debug_report unavailable"); 98 | } 99 | } 100 | let instance = entry.create_instance( 101 | &InstanceCreateInfo::default() 102 | .application_info(&ApplicationInfo::default() 103 | .application_name(APP_VK_NAME) 104 | .application_version(APP_VK_VERSION) 105 | .engine_name(APP_VK_NAME) 106 | .engine_version(APP_VK_VERSION) 107 | .api_version(VULKAN_VERSION_TARGET) 108 | ) 109 | .enabled_layer_names(&instance_layers) 110 | .enabled_extension_names(&extension_layers), 111 | None, 112 | ).context("Failed to create instance")?; 113 | let mut debug = Debug::None; 114 | if has_debug_utils { 115 | let instance = debug_utils::Instance::new(&entry, &instance); 116 | match instance.create_debug_utils_messenger( 117 | &DebugUtilsMessengerCreateInfoEXT::default() 118 | .message_severity( 119 | DebugUtilsMessageSeverityFlagsEXT::ERROR 120 | | DebugUtilsMessageSeverityFlagsEXT::WARNING 121 | ) 122 | .message_type( 123 | DebugUtilsMessageTypeFlagsEXT::GENERAL 124 | | DebugUtilsMessageTypeFlagsEXT::VALIDATION 125 | | DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, 126 | ) 127 | .pfn_user_callback(Some(debug_utils_callback)), 128 | None 129 | ) { 130 | Ok(messenger) => 131 | debug = Debug::Utils { instance, messenger }, 132 | Err(e) => 133 | error!("Failed to create Vulkan debug utils messenger: {e}"), 134 | }; 135 | } else if has_debug_report { 136 | let instance = debug_report::Instance::new(&entry, &instance); 137 | #[allow(deprecated)] 138 | match instance.create_debug_report_callback( 139 | &DebugReportCallbackCreateInfoEXT::default() 140 | .flags(DebugReportFlagsEXT::WARNING 141 | | DebugReportFlagsEXT::PERFORMANCE_WARNING 142 | | DebugReportFlagsEXT::ERROR) 143 | .pfn_callback(Some(debug_report_callback)) 144 | .user_data(ptr::null_mut()), 145 | None, 146 | ) { 147 | Ok(callback) => 148 | debug = Debug::Report { instance, callback }, 149 | Err(e) => 150 | error!("Failed to create Vulkan debug report callback: {e}"), 151 | }; 152 | } 153 | Ok(GpuInstance { 154 | _entry: entry, 155 | instance, 156 | debug, 157 | }) 158 | } 159 | 160 | const fn parse_decimal(src: &str) -> u32 { 161 | match u32::from_str_radix(src, 10) { 162 | Ok(val) => val, 163 | Err(_) => panic!(), 164 | } 165 | } 166 | 167 | fn has_layer(layers: &[LayerProperties], name: &CStr) -> bool { 168 | layers.iter().any(|layer| layer.layer_name_as_c_str() == Ok(name)) 169 | } 170 | 171 | unsafe extern "system" fn debug_utils_callback( 172 | message_severity: DebugUtilsMessageSeverityFlagsEXT, 173 | message_type: DebugUtilsMessageTypeFlagsEXT, 174 | p_callback_data: *const DebugUtilsMessengerCallbackDataEXT<'_>, 175 | _user_data: *mut c_void, 176 | ) -> Bool32 { 177 | if p_callback_data.is_null() { 178 | error!("Vulkan: null data"); 179 | return vk::FALSE 180 | } 181 | let callback_data = *p_callback_data; 182 | let message = if callback_data.p_message.is_null() { 183 | Cow::from("null message") 184 | } else { 185 | CStr::from_ptr(callback_data.p_message).to_string_lossy() 186 | }; 187 | match message_severity { 188 | DebugUtilsMessageSeverityFlagsEXT::ERROR => { 189 | let backtrace = Backtrace::force_capture(); 190 | error!("Vulkan: {message}\nBacktrace:\n{backtrace}"); 191 | }, 192 | DebugUtilsMessageSeverityFlagsEXT::WARNING => { 193 | warn!("Vulkan: {message}"); 194 | }, 195 | severity => { 196 | error!("Unexpected Vulkan message {:?} {:?}: {}", 197 | message_type, severity, message); 198 | }, 199 | }; 200 | vk::FALSE 201 | } 202 | 203 | unsafe extern "system" fn debug_report_callback( 204 | flags: DebugReportFlagsEXT, 205 | _object_type: DebugReportObjectTypeEXT, 206 | _object: u64, 207 | _location: usize, 208 | _message_code: i32, 209 | _p_layer_prefix: *const c_char, 210 | p_message: *const c_char, 211 | _p_user_data: *mut c_void 212 | ) -> u32 { 213 | let message = if p_message.is_null() { 214 | Cow::from("null message") 215 | } else { 216 | CStr::from_ptr(p_message).to_string_lossy() 217 | }; 218 | match flags { 219 | DebugReportFlagsEXT::ERROR => { 220 | let backtrace = Backtrace::force_capture(); 221 | error!("Vulkan: {message}\nBacktrace:\n{backtrace}"); 222 | }, 223 | DebugReportFlagsEXT::WARNING 224 | | DebugReportFlagsEXT::PERFORMANCE_WARNING => { 225 | warn!("Vulkan: {message}"); 226 | }, 227 | flag => { 228 | error!("Unexpected Vulkan message {flag:?}: {message}"); 229 | } 230 | }; 231 | vk::FALSE 232 | } 233 | -------------------------------------------------------------------------------- /src/gpu/memory.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | 3 | use std::{ 4 | os::fd::{FromRawFd, OwnedFd}, 5 | rc::Rc, 6 | slice, 7 | }; 8 | 9 | use anyhow::{bail, Context}; 10 | use ash::{ 11 | Instance, 12 | vk::{ 13 | AccessFlags, 14 | BufferCreateFlags, 15 | BufferCreateInfo, 16 | BufferImageCopy, 17 | BufferUsageFlags, 18 | CommandBufferBeginInfo, 19 | CommandBufferResetFlags, 20 | DependencyFlags, 21 | DeviceSize, 22 | DrmFormatModifierPropertiesEXT, 23 | ExportMemoryAllocateInfo, 24 | Extent2D, 25 | ExternalMemoryHandleTypeFlags, 26 | ExternalMemoryImageCreateInfo, 27 | Fence, 28 | Format, 29 | FormatFeatureFlags, 30 | ImageAspectFlags, 31 | ImageCreateFlags, 32 | ImageCreateInfo, 33 | ImageDrmFormatModifierListCreateInfoEXT, 34 | ImageDrmFormatModifierPropertiesEXT, 35 | ImageFormatProperties2, 36 | ImageLayout, 37 | ImageMemoryBarrier, 38 | ImageSubresource, 39 | ImageSubresourceLayers, 40 | ImageSubresourceRange, 41 | ImageTiling, 42 | ImageType, 43 | ImageUsageFlags, 44 | MemoryAllocateInfo, 45 | MemoryGetFdInfoKHR, 46 | MemoryMapFlags, 47 | MemoryPropertyFlags, 48 | MemoryRequirements, 49 | PhysicalDevice, 50 | PhysicalDeviceImageDrmFormatModifierInfoEXT, 51 | PhysicalDeviceImageFormatInfo2, 52 | PhysicalDeviceMemoryProperties, 53 | PipelineStageFlags, 54 | QUEUE_FAMILY_FOREIGN_EXT, 55 | SampleCountFlags, 56 | SharingMode, 57 | SubmitInfo, 58 | } 59 | }; 60 | use log::debug; 61 | use scopeguard::{guard, ScopeGuard}; 62 | 63 | use super::{ 64 | DRM_FORMAT_MOD_LINEAR, fmt_modifier, 65 | GpuDevice, GpuMemory, GpuUploader, GpuWallpaper, 66 | MemoryPlane, 67 | }; 68 | 69 | pub unsafe fn uploader( 70 | gpu_device: Rc, 71 | width: u32, 72 | height: u32, 73 | drm_format_modifiers: Vec, 74 | ) -> anyhow::Result { 75 | let GpuDevice { 76 | gpu_instance, 77 | memory_props, 78 | drm_format_props, 79 | device, 80 | .. 81 | } = gpu_device.as_ref(); 82 | let instance = &gpu_instance.instance; 83 | let physdev = gpu_device.physdev; 84 | let queue_family_index = gpu_device.queue_family_index; 85 | let size = width as DeviceSize * height as DeviceSize * 4; 86 | let mut filtered_modifiers = Vec::with_capacity(drm_format_modifiers.len()); 87 | if let Some(drm_format_props) = drm_format_props { 88 | for &drm_format_modifier in drm_format_modifiers.iter() { 89 | match filter_modifier( 90 | instance, physdev, queue_family_index, drm_format_props, 91 | width, height, size, drm_format_modifier, 92 | ) { 93 | Ok(()) => filtered_modifiers.push(drm_format_modifier), 94 | Err(e) => debug!("Cannot use DRM format modifier {}: {:#}", 95 | fmt_modifier(drm_format_modifier), e), 96 | } 97 | if filtered_modifiers.is_empty() { 98 | bail!("None of the DRM format modifiers can be \ 99 | used for image creation"); 100 | } 101 | } 102 | } else if drm_format_modifiers.contains(&DRM_FORMAT_MOD_LINEAR) { 103 | debug!("Image creation can only use DRM_FORMAT_MOD_LINEAR"); 104 | let image_format_props = instance 105 | .get_physical_device_image_format_properties( 106 | physdev, 107 | Format::B8G8R8A8_SRGB, 108 | ImageType::TYPE_2D, 109 | ImageTiling::LINEAR, 110 | ImageUsageFlags::TRANSFER_DST, 111 | ImageCreateFlags::empty(), 112 | ) 113 | .context("Failed to get image format properties")?; 114 | if image_format_props.max_extent.depth < 1 115 | || image_format_props.max_mip_levels < 1 116 | || image_format_props.max_array_layers < 1 117 | || !image_format_props.sample_counts 118 | .contains(SampleCountFlags::TYPE_1) 119 | { 120 | bail!("The needed image format is unsupported") 121 | } 122 | let max_width = image_format_props.max_extent.width; 123 | let max_height = image_format_props.max_extent.height; 124 | let max_size = image_format_props.max_resource_size; 125 | if width > max_width { 126 | bail!("Needed image width {width} is greter than the max \ 127 | supported image width {max_width}") 128 | } 129 | if height > max_height { 130 | bail!("Needed image height {height} is greter than the max \ 131 | supported image height {max_height}") 132 | } 133 | if size > max_size { 134 | bail!("Needed image size {size} bytes is greter than the max \ 135 | supported image size {max_width} bytes") 136 | } 137 | } else { 138 | bail!("VK_EXT_physical_device_drm unavailable and \ 139 | no DRM_FORMAT_MOD_LINEAR was proposed for image creation"); 140 | } 141 | debug!("Image creation can use DRM format modifiers: {}", 142 | filtered_modifiers.iter() 143 | .map(|&modifier| fmt_modifier(modifier)) 144 | .collect::>().join(", ")); 145 | let buffer = guard( 146 | device.create_buffer( 147 | &BufferCreateInfo::default() 148 | .flags(BufferCreateFlags::empty()) 149 | .size(size) 150 | .usage(BufferUsageFlags::TRANSFER_SRC) 151 | .sharing_mode(SharingMode::EXCLUSIVE) 152 | .queue_family_indices(slice::from_ref(&queue_family_index)), 153 | None 154 | ).context("Failed to create staging buffer")?, 155 | |buffer| device.destroy_buffer(buffer, None), 156 | ); 157 | let buffer_memory_req = device.get_buffer_memory_requirements(*buffer); 158 | let buffer_memory_index = find_memorytype_index( 159 | &buffer_memory_req, 160 | memory_props, 161 | MemoryPropertyFlags::HOST_VISIBLE | MemoryPropertyFlags::HOST_COHERENT 162 | | MemoryPropertyFlags::HOST_CACHED, 163 | ).context("Cannot find suitable device memory type for staging buffer")?; 164 | let memory = guard( 165 | device.allocate_memory( 166 | &MemoryAllocateInfo::default() 167 | .allocation_size(buffer_memory_req.size) 168 | .memory_type_index(buffer_memory_index), 169 | None 170 | ).context("Failed to allocate memory for staging buffer")?, 171 | |memory| device.free_memory(memory, None), 172 | ); 173 | device.bind_buffer_memory(*buffer, *memory, 0) 174 | .context("Failed to bind memory to staging buffer")?; 175 | let ptr = device.map_memory( 176 | *memory, 177 | 0, 178 | buffer_memory_req.size, 179 | MemoryMapFlags::empty() 180 | ).context("Failed to map staging buffer memory")?; 181 | Ok(GpuUploader { 182 | memory: ScopeGuard::into_inner(memory), 183 | buffer: ScopeGuard::into_inner(buffer), 184 | ptr: ptr.cast(), 185 | len: buffer_memory_req.size as usize, 186 | extent: Extent2D { width, height }, 187 | drm_format_modifiers: filtered_modifiers, 188 | gpu_device, 189 | }) 190 | } 191 | 192 | unsafe fn filter_modifier( 193 | instance: &Instance, 194 | physdev: PhysicalDevice, 195 | queue_family_index: u32, 196 | drm_format_props: &[DrmFormatModifierPropertiesEXT], 197 | width: u32, 198 | height: u32, 199 | size: DeviceSize, 200 | drm_format_modifier: u64, 201 | ) -> anyhow::Result<()> { 202 | let format_props = drm_format_props.iter() 203 | .find(|props| props.drm_format_modifier == drm_format_modifier) 204 | .context("This modifier is unsupported by this Vulkan context")?; 205 | if !format_props.drm_format_modifier_tiling_features 206 | .contains(FormatFeatureFlags::TRANSFER_DST) 207 | { 208 | bail!("FormatFeatureFlag TRANSFER_DST unsupported"); 209 | } 210 | let mut image_format_props2 = ImageFormatProperties2::default(); 211 | let mut image_drm_info = 212 | PhysicalDeviceImageDrmFormatModifierInfoEXT::default() 213 | .drm_format_modifier(drm_format_modifier) 214 | .sharing_mode(SharingMode::EXCLUSIVE) 215 | .queue_family_indices(slice::from_ref(&queue_family_index)); 216 | instance.get_physical_device_image_format_properties2( 217 | physdev, 218 | &PhysicalDeviceImageFormatInfo2::default() 219 | .format(Format::B8G8R8A8_SRGB) 220 | .ty(ImageType::TYPE_2D) 221 | .tiling(ImageTiling::DRM_FORMAT_MODIFIER_EXT) 222 | .usage(ImageUsageFlags::TRANSFER_DST) 223 | .flags(ImageCreateFlags::empty()) 224 | .push_next(&mut image_drm_info), 225 | &mut image_format_props2, 226 | ).context("The needed image format is unsupported for this modifier")?; 227 | let image_format_props = image_format_props2.image_format_properties; 228 | if image_format_props.max_extent.depth < 1 229 | || image_format_props.max_mip_levels < 1 230 | || image_format_props.max_array_layers < 1 231 | || !image_format_props.sample_counts.contains(SampleCountFlags::TYPE_1) 232 | { 233 | bail!("The needed image format is unsupported for this modifier") 234 | } 235 | let max_width = image_format_props.max_extent.width; 236 | let max_height = image_format_props.max_extent.height; 237 | let max_size = image_format_props.max_resource_size; 238 | if width > max_width { 239 | bail!("Needed image width {width} is greter than the max supported \ 240 | image width {max_width}") 241 | } 242 | if height > max_height { 243 | bail!("Needed image height {height} is greter than the max supported \ 244 | image height {max_height}") 245 | } 246 | if size > max_size { 247 | bail!("Needed image size {size} bytes is greter than the max supported \ 248 | image size {max_width} bytes") 249 | } 250 | Ok(()) 251 | } 252 | 253 | // XXX: we could check if dedicated allocation is needed: 254 | // https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_dedicated_allocation.html 255 | pub unsafe fn upload( 256 | uploader: &mut GpuUploader, 257 | ) -> anyhow::Result { 258 | let GpuUploader { 259 | gpu_device, 260 | drm_format_modifiers, 261 | .. 262 | } = uploader; 263 | let GpuDevice { 264 | memory_props, 265 | drm_format_props, 266 | device, 267 | external_memory_fd_device, 268 | image_drm_format_modifier_device, 269 | .. 270 | } = gpu_device.as_ref(); 271 | let extent = uploader.extent; 272 | let buffer = uploader.buffer; 273 | let queue_family_index = gpu_device.queue_family_index; 274 | let command_buffer = gpu_device.command_buffer; 275 | let queue = gpu_device.queue; 276 | let mut external_memory_info = ExternalMemoryImageCreateInfo::default() 277 | .handle_types(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); 278 | let mut modifier_list_info = 279 | ImageDrmFormatModifierListCreateInfoEXT::default() 280 | .drm_format_modifiers(drm_format_modifiers); 281 | let mut image_create_info = ImageCreateInfo::default() 282 | .flags(ImageCreateFlags::empty()) 283 | .image_type(ImageType::TYPE_2D) 284 | .format(Format::B8G8R8A8_SRGB) 285 | .extent(extent.into()) 286 | .mip_levels(1) 287 | .array_layers(1) 288 | .samples(SampleCountFlags::TYPE_1) 289 | .usage(ImageUsageFlags::TRANSFER_DST) 290 | .sharing_mode(SharingMode::EXCLUSIVE) 291 | .queue_family_indices(slice::from_ref(&queue_family_index)) 292 | .initial_layout(ImageLayout::UNDEFINED) 293 | .push_next(&mut external_memory_info); 294 | if image_drm_format_modifier_device.is_some() { 295 | image_create_info = image_create_info 296 | .tiling(ImageTiling::DRM_FORMAT_MODIFIER_EXT) 297 | .push_next(&mut modifier_list_info); 298 | } else { 299 | image_create_info = image_create_info.tiling(ImageTiling::LINEAR); 300 | } 301 | let image = guard( 302 | device.create_image(&image_create_info, None) 303 | .context("Failed to create image")?, 304 | |image| device.destroy_image(image, None), 305 | ); 306 | let image_memory_req = device.get_image_memory_requirements(*image); 307 | let image_memory_index = find_memorytype_index( 308 | &image_memory_req, 309 | memory_props, 310 | MemoryPropertyFlags::DEVICE_LOCAL, 311 | ).context("Failed to find memorytype index for image")?; 312 | let image_memory = guard( 313 | device.allocate_memory( 314 | &MemoryAllocateInfo::default() 315 | .allocation_size(image_memory_req.size) 316 | .memory_type_index(image_memory_index) 317 | .push_next(&mut ExportMemoryAllocateInfo::default() 318 | .handle_types(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) 319 | ), 320 | None 321 | ).context("Failed to allocate memory for image")?, 322 | |memory| device.free_memory(memory, None), 323 | ); 324 | device.bind_image_memory(*image, *image_memory, 0) 325 | .context("Failed to bind image memory")?; 326 | device.reset_command_buffer( 327 | command_buffer, 328 | CommandBufferResetFlags::empty() 329 | ).context("Failed to reset command buffer")?; 330 | device.begin_command_buffer( 331 | command_buffer, 332 | &CommandBufferBeginInfo::default() 333 | ) .context("Failed to begin command buffer")?; 334 | device.cmd_pipeline_barrier( 335 | command_buffer, 336 | PipelineStageFlags::TOP_OF_PIPE, 337 | PipelineStageFlags::TRANSFER, 338 | DependencyFlags::empty(), 339 | &[], 340 | &[], 341 | &[ImageMemoryBarrier::default() 342 | .src_access_mask(AccessFlags::NONE) 343 | .dst_access_mask(AccessFlags::TRANSFER_WRITE) 344 | .old_layout(ImageLayout::UNDEFINED) 345 | .new_layout(ImageLayout::GENERAL) 346 | .src_queue_family_index(queue_family_index) 347 | .dst_queue_family_index(queue_family_index) 348 | .image(*image) 349 | .subresource_range(ImageSubresourceRange::default() 350 | .aspect_mask(ImageAspectFlags::COLOR) 351 | .level_count(1) 352 | .layer_count(1) 353 | ) 354 | ], 355 | ); 356 | device.cmd_copy_buffer_to_image( 357 | command_buffer, 358 | buffer, 359 | *image, 360 | ImageLayout::GENERAL, 361 | &[BufferImageCopy::default() 362 | .image_subresource(ImageSubresourceLayers::default() 363 | .aspect_mask(ImageAspectFlags::COLOR) 364 | .layer_count(1) 365 | ) 366 | .image_extent(extent.into()) 367 | ] 368 | ); 369 | // https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#resources-external-sharing 370 | device.cmd_pipeline_barrier( 371 | command_buffer, 372 | PipelineStageFlags::TRANSFER, 373 | PipelineStageFlags::BOTTOM_OF_PIPE, 374 | DependencyFlags::empty(), 375 | &[], 376 | &[], 377 | &[ImageMemoryBarrier::default() 378 | .src_access_mask(AccessFlags::TRANSFER_WRITE) 379 | .dst_access_mask(AccessFlags::NONE) 380 | .old_layout(ImageLayout::GENERAL) 381 | .new_layout(ImageLayout::GENERAL) 382 | .src_queue_family_index(queue_family_index) 383 | .dst_queue_family_index(QUEUE_FAMILY_FOREIGN_EXT) 384 | .image(*image) 385 | .subresource_range(ImageSubresourceRange::default() 386 | .aspect_mask(ImageAspectFlags::COLOR) 387 | .level_count(1) 388 | .layer_count(1) 389 | ) 390 | ], 391 | ); 392 | device.end_command_buffer(command_buffer) 393 | .context("Failed to end command buffer")?; 394 | device.queue_submit( 395 | queue, 396 | &[SubmitInfo::default().command_buffers(&[command_buffer])], 397 | Fence::null(), 398 | ).context("Failed to submit queue")?; 399 | device.queue_wait_idle(queue).context("Failed to wait queue idle")?; 400 | let mut drm_format_modifier = DRM_FORMAT_MOD_LINEAR; 401 | let mut memory_plane_count = 1; 402 | let mut aspect_masks = [ImageAspectFlags::COLOR; 4]; 403 | if let Some(modifier_device) = image_drm_format_modifier_device { 404 | let mut props = ImageDrmFormatModifierPropertiesEXT::default(); 405 | modifier_device 406 | .get_image_drm_format_modifier_properties(*image, &mut props) 407 | .context("Failed to get image drm format modifier properties")?; 408 | drm_format_modifier = props.drm_format_modifier; 409 | debug!("Image created with DRM format modifier {}", 410 | fmt_modifier(drm_format_modifier)); 411 | let format_prop = drm_format_props.as_ref().unwrap().iter().find(|f| 412 | f.drm_format_modifier == drm_format_modifier 413 | ).context("Failed to find DRM format modifier properties")?; 414 | memory_plane_count = format_prop 415 | .drm_format_modifier_plane_count as usize; 416 | aspect_masks = [ 417 | ImageAspectFlags::MEMORY_PLANE_0_EXT, 418 | ImageAspectFlags::MEMORY_PLANE_1_EXT, 419 | ImageAspectFlags::MEMORY_PLANE_2_EXT, 420 | ImageAspectFlags::MEMORY_PLANE_3_EXT, 421 | ]; 422 | } 423 | let mut memory_planes = [MemoryPlane::default(); 4]; 424 | for memory_plan_index in 0..memory_plane_count { 425 | let subresource_layout = device.get_image_subresource_layout( 426 | *image, 427 | ImageSubresource::default() 428 | .aspect_mask(aspect_masks[memory_plan_index]) 429 | .mip_level(0) 430 | .array_layer(0) 431 | ); 432 | memory_planes[memory_plan_index] = MemoryPlane { 433 | offset: subresource_layout.offset, 434 | stride: subresource_layout.row_pitch, 435 | }; 436 | } 437 | let raw_fd = external_memory_fd_device.get_memory_fd( 438 | &MemoryGetFdInfoKHR::default() 439 | .memory(*image_memory) 440 | .handle_type(ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) 441 | ).context("Failed to get memory fd")?; 442 | if raw_fd < 0 { 443 | bail!("Got invalid memory fd {raw_fd}") 444 | } 445 | let fd = OwnedFd::from_raw_fd(raw_fd); 446 | Ok(GpuWallpaper { 447 | drm_format_modifier, 448 | memory_planes_len: memory_plane_count, 449 | memory_planes, 450 | gpu_memory: GpuMemory { 451 | memory: ScopeGuard::into_inner(image_memory), 452 | size: uploader.len, 453 | image: ScopeGuard::into_inner(image), 454 | gpu_device: Rc::clone(&uploader.gpu_device), 455 | drm_format_modifier, 456 | }, 457 | fd, 458 | }) 459 | } 460 | 461 | fn find_memorytype_index( 462 | memory_req: &MemoryRequirements, 463 | memory_prop: &PhysicalDeviceMemoryProperties, 464 | flags: MemoryPropertyFlags, 465 | ) -> Option { 466 | memory_prop.memory_types[..memory_prop.memory_type_count as _] 467 | .iter() 468 | .enumerate() 469 | .find(|(index, memory_type)| { 470 | (1 << index) & memory_req.memory_type_bits != 0 471 | && memory_type.property_flags & flags == flags 472 | }) 473 | .map(|(index, _memory_type)| index as _) 474 | } 475 | -------------------------------------------------------------------------------- /src/image.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | 3 | use std::{ 4 | fs::read_dir, 5 | path::{Path, PathBuf}, 6 | time::UNIX_EPOCH, 7 | }; 8 | 9 | use anyhow::{bail, Context}; 10 | use fast_image_resize::{ 11 | FilterType, PixelType, Resizer, ResizeAlg, ResizeOptions, 12 | images::Image, 13 | }; 14 | use image::{ColorType, DynamicImage, ImageBuffer, ImageDecoder, ImageReader}; 15 | use log::{debug, error, warn}; 16 | use smithay_client_toolkit::reexports::client::protocol::wl_shm; 17 | 18 | #[derive(Clone, Copy, PartialEq)] 19 | pub enum ColorTransform { 20 | // Levels { input_max: u8, input_min: u8, output_max: u8, output_min: u8 }, 21 | Legacy { brightness: i32, contrast: f32 }, 22 | None, 23 | } 24 | 25 | pub struct WallpaperFile { 26 | pub path: PathBuf, 27 | pub workspace: String, 28 | pub canon_path: PathBuf, 29 | pub canon_modified: u128, 30 | } 31 | 32 | pub fn output_wallpaper_files( 33 | output_dir: &Path, 34 | ) -> anyhow::Result> { 35 | let dir = read_dir(output_dir).context("Failed to read directory")?; 36 | let mut ret = Vec::new(); 37 | for dir_entry_result in dir { 38 | let dir_entry = match dir_entry_result { 39 | Ok(dir_entry) => dir_entry, 40 | Err(e) => { 41 | error!("Failed to read directory entries: {e}"); 42 | break 43 | } 44 | }; 45 | let path = dir_entry.path(); 46 | if path.is_dir() { 47 | warn!("Skipping nested directory {path:?}"); 48 | continue 49 | } 50 | let workspace = path.file_stem().unwrap() 51 | .to_string_lossy().into_owned(); 52 | let canon_path = match path.canonicalize() { 53 | Ok(canon_path) => canon_path, 54 | Err(e) => { 55 | error!("Failed to resolve absolute path for {path:?}: {e}"); 56 | continue 57 | } 58 | }; 59 | let canon_metadata = match canon_path.metadata() { 60 | Ok(canon_metadata) => canon_metadata, 61 | Err(e) => { 62 | error!("Failed to get file metadata for {canon_path:?}: {e}"); 63 | continue 64 | } 65 | }; 66 | let canon_modified = canon_metadata.modified().unwrap() 67 | .duration_since(UNIX_EPOCH).unwrap() 68 | .as_nanos(); 69 | ret.push(WallpaperFile { path, workspace, canon_path, canon_modified }); 70 | } 71 | Ok(ret) 72 | } 73 | 74 | pub fn load_wallpaper( 75 | path: &Path, 76 | buffer: &mut [u8], 77 | surface_width: u32, 78 | surface_height: u32, 79 | surface_stride: usize, 80 | surface_format: wl_shm::Format, 81 | color_transform: ColorTransform, 82 | resizer: &mut Resizer, 83 | ) -> anyhow::Result<()> { 84 | let surface_size = surface_stride * surface_height as usize; 85 | let Some(dst) = buffer.get_mut(..surface_size) else { 86 | bail!("Provided buffer size {} smaller than wallpaper image size {}", 87 | buffer.len(), surface_size); 88 | }; 89 | let reader = ImageReader::open(path) 90 | .context("Failed to open image file")? 91 | .with_guessed_format() 92 | .context("Failed to read image file format")?; 93 | let file_format = reader.format() 94 | .context("Failed to determine image file format")?; 95 | if !file_format.can_read() { 96 | bail!("Unsupported image file format {file_format:?}") 97 | } else if !file_format.reading_enabled() { 98 | bail!("Application was compiled with support \ 99 | for image file format {file_format:?} disabled") 100 | } 101 | let mut decoder = reader.into_decoder() 102 | .context("Failed to initialize image decoder")?; 103 | let (image_width, image_height) = decoder.dimensions(); 104 | let image_size = decoder.total_bytes(); 105 | let image_color_type = decoder.color_type(); 106 | if image_width == 0 || image_height == 0 || image_size > isize::MAX as u64 { 107 | bail!("Image has invalid dimensions {image_width}x{image_height}") 108 | }; 109 | debug!("Image {image_width}x{image_height} {image_color_type:?}"); 110 | if image_color_type.has_alpha() { 111 | warn!("Image has alpha channel which will be ignored"); 112 | } 113 | if let Ok(Some(_)) = decoder.icc_profile() { 114 | debug!("Image has an embedded ICC color profile \ 115 | but ICC color profile handling is not yet implemented"); 116 | } 117 | let needs_resize = image_width != surface_width 118 | || image_height != surface_height; 119 | let surface_row_len = surface_width as usize * 3; 120 | if !needs_resize 121 | && image_color_type == ColorType::Rgb8 122 | && surface_format == wl_shm::Format::Bgr888 123 | && color_transform == ColorTransform::None 124 | && surface_row_len == surface_stride 125 | { 126 | debug!("Decoding image directly to destination buffer"); 127 | decoder.read_image(dst).context("Failed to decode image")?; 128 | return Ok(()); 129 | } 130 | let mut image = DynamicImage::from_decoder(decoder) 131 | .context("Failed to decode image")?; 132 | if let ColorTransform::Legacy { brightness, contrast } = color_transform { 133 | if contrast != 0.0 { 134 | image = image.adjust_contrast(contrast) 135 | } 136 | if brightness != 0 { 137 | image = image.brighten(brightness) 138 | } 139 | } 140 | let mut image = image.into_rgb8(); 141 | if needs_resize { 142 | debug!("Resizing image from {}x{} to {}x{}", 143 | image_width, image_height, 144 | surface_width, surface_height 145 | ); 146 | let src_image = Image::from_vec_u8( 147 | image_width, 148 | image_height, 149 | image.into_raw(), 150 | PixelType::U8x3, 151 | ).unwrap(); 152 | let mut dst_image = Image::new( 153 | surface_width, 154 | surface_height, 155 | PixelType::U8x3, 156 | ); 157 | resizer.resize( 158 | &src_image, 159 | &mut dst_image, 160 | &ResizeOptions::new() 161 | .fit_into_destination(None) 162 | .resize_alg(ResizeAlg::Convolution(FilterType::Lanczos3)) 163 | ).context("Failed to resize image")?; 164 | image = ImageBuffer::from_raw( 165 | surface_width, 166 | surface_height, 167 | dst_image.into_vec() 168 | ).unwrap(); 169 | } 170 | match surface_format { 171 | wl_shm::Format::Bgr888 => { 172 | if surface_row_len == surface_stride { 173 | dst.copy_from_slice(&image); 174 | } else { 175 | copy_pad_stride( 176 | &image, 177 | dst, 178 | surface_row_len, 179 | surface_stride, 180 | surface_height as usize, 181 | ); 182 | } 183 | }, 184 | wl_shm::Format::Xrgb8888 => { 185 | swizzle_bgra_from_rgb(&image, dst); 186 | }, 187 | _ => unreachable!(), 188 | } 189 | Ok(()) 190 | } 191 | 192 | fn copy_pad_stride( 193 | src: &[u8], 194 | dst: &mut [u8], 195 | src_stride: usize, 196 | dst_stride: usize, 197 | height: usize, 198 | ) { 199 | for row in 0..height { 200 | dst[row * dst_stride..][..src_stride] 201 | .copy_from_slice(&src[row * src_stride..][..src_stride]); 202 | } 203 | } 204 | 205 | fn swizzle_bgra_from_rgb(src: &[u8], dst: &mut [u8]) { 206 | let pixel_count = dst.len() / 4; 207 | assert_eq!(src.len(), pixel_count * 3); 208 | assert_eq!(dst.len(), pixel_count * 4); 209 | unsafe { 210 | #[cfg(target_arch = "x86_64")] 211 | if is_x86_feature_detected!("avx2") { 212 | return bgra_from_rgb_avx2(src, dst, pixel_count) 213 | } 214 | bgra_from_rgb(src, dst, pixel_count) 215 | } 216 | } 217 | 218 | #[cfg(target_arch = "x86_64")] 219 | #[target_feature(enable = "avx2")] 220 | unsafe fn bgra_from_rgb_avx2(src: &[u8], dst: &mut [u8], pixel_count: usize) { 221 | unsafe { bgra_from_rgb(src, dst, pixel_count) } 222 | } 223 | 224 | unsafe fn bgra_from_rgb(src: &[u8], dst: &mut [u8], pixel_count: usize) { 225 | unsafe { 226 | let mut src = src.as_ptr(); 227 | let mut dst = dst.as_mut_ptr(); 228 | for _ in 0..pixel_count { 229 | *dst.add(0) = *src.add(2); // B 230 | *dst.add(1) = *src.add(1); // G 231 | *dst.add(2) = *src.add(0); // R 232 | *dst.add(3) = u8::MAX; // A 233 | src = src.add(3); 234 | dst = dst.add(4); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod compositors; 3 | mod gpu; 4 | mod image; 5 | mod poll; 6 | mod signal; 7 | mod wayland; 8 | 9 | use std::{ 10 | io, 11 | os::fd::AsFd, 12 | path::{Path, PathBuf}, 13 | sync::{ 14 | Arc, 15 | mpsc::{channel, Receiver}, 16 | }, 17 | }; 18 | 19 | use clap::Parser; 20 | use log::{debug, error, info, warn}; 21 | use rustix::{ 22 | event::{poll, PollFd, PollFlags}, 23 | io::retry_on_intr, 24 | }; 25 | use smithay_client_toolkit::{ 26 | compositor::CompositorState, 27 | dmabuf::DmabufState, 28 | output::OutputState, 29 | registry::RegistryState, 30 | shell::wlr_layer::LayerShell, 31 | shm::Shm, 32 | }; 33 | use smithay_client_toolkit::reexports::client::{ 34 | Connection, EventQueue, 35 | backend::{ReadEventsGuard, WaylandError}, 36 | globals::registry_queue_init, 37 | protocol::wl_shm, 38 | }; 39 | use smithay_client_toolkit::reexports::protocols 40 | ::wp::viewporter::client::wp_viewporter::WpViewporter; 41 | 42 | use crate::{ 43 | cli::{Cli, PixelFormat}, 44 | compositors::{Compositor, ConnectionTask, WorkspaceVisible}, 45 | gpu::Gpu, 46 | image::ColorTransform, 47 | poll::{Poll, Waker}, 48 | signal::SignalPipe, 49 | wayland::BackgroundLayer, 50 | }; 51 | 52 | struct State { 53 | compositor_state: CompositorState, 54 | registry_state: RegistryState, 55 | output_state: OutputState, 56 | shm: Shm, 57 | layer_shell: LayerShell, 58 | viewporter: WpViewporter, 59 | wallpaper_dir: PathBuf, 60 | shm_format: Option, 61 | background_layers: Vec, 62 | compositor_connection_task: ConnectionTask, 63 | color_transform: ColorTransform, 64 | dmabuf_state: DmabufState, 65 | gpu: Option, 66 | } 67 | 68 | impl State { 69 | fn shm_format(&mut self) -> wl_shm::Format { 70 | *self.shm_format.get_or_insert_with(|| { 71 | let mut format = wl_shm::Format::Xrgb8888; 72 | // Consume less gpu memory by using Bgr888 if available, 73 | // fall back to the always supported Xrgb8888 otherwise 74 | if self.shm.formats().contains(&wl_shm::Format::Bgr888) { 75 | format = wl_shm::Format::Bgr888 76 | } 77 | debug!("Using shm format: {format:?}"); 78 | format 79 | }) 80 | } 81 | } 82 | 83 | fn main() -> Result<(), ()> { 84 | run().map_err(|e| { error!("{e:#}"); }) 85 | } 86 | 87 | fn run() -> anyhow::Result<()> { 88 | env_logger::Builder::from_env( 89 | env_logger::Env::default().default_filter_or( 90 | #[cfg(debug_assertions)] 91 | "info,multibg_wayland=trace", 92 | #[cfg(not(debug_assertions))] 93 | "info", 94 | ) 95 | ).init(); 96 | 97 | info!(concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"))); 98 | 99 | let cli = Cli::parse(); 100 | let wallpaper_dir = Path::new(&cli.wallpaper_dir).canonicalize().unwrap(); 101 | let brightness = cli.brightness.unwrap_or(0); 102 | let contrast = cli.contrast.unwrap_or(0.0); 103 | let color_transform = if brightness == 0 && contrast == 0.0 { 104 | ColorTransform::None 105 | } else { 106 | ColorTransform::Legacy { brightness, contrast } 107 | }; 108 | 109 | // ******************************** 110 | // Initialize wayland client 111 | // ******************************** 112 | 113 | let conn = Connection::connect_to_env().unwrap(); 114 | let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); 115 | let qh = event_queue.handle(); 116 | 117 | let compositor_state = CompositorState::bind(&globals, &qh).unwrap(); 118 | let layer_shell = LayerShell::bind(&globals, &qh).unwrap(); 119 | let shm = Shm::bind(&globals, &qh).unwrap(); 120 | let shm_format = if cli.pixelformat == Some(PixelFormat::Baseline) { 121 | debug!("Using shm format: {:?}", wl_shm::Format::Xrgb8888); 122 | Some(wl_shm::Format::Xrgb8888) 123 | } else { 124 | None 125 | }; 126 | 127 | let registry_state = RegistryState::new(&globals); 128 | 129 | let viewporter: WpViewporter = registry_state 130 | .bind_one(&qh, 1..=1, ()).expect("wp_viewporter not available"); 131 | 132 | let dmabuf_state = DmabufState::new(&globals, &qh); 133 | let mut gpu = None; 134 | if cli.gpu { 135 | if let Some(version) = dmabuf_state.version() { 136 | if version >= 4 { 137 | debug!("Using Linux DMA-BUF version {version}"); 138 | } else { 139 | warn!("Only legacy Linux DMA-BUF version {version} is \ 140 | available from the compositor where it gives no \ 141 | information about which GPU it uses."); 142 | // TODO handle this better by providing cli options 143 | // to choose DRM device by major:minor or /dev path 144 | } 145 | match Gpu::new() { 146 | Ok(val) => gpu = Some(val), 147 | Err(e) => 148 | error!("Failed to set up GPU, disabling GPU use: {e:#}"), 149 | } 150 | } else { 151 | error!("Wayland protocol Linux DMA-BUF is unavailable \ 152 | from the compositor, disabling GPU use"); 153 | } 154 | } 155 | 156 | // Sync tools for sway ipc tasks 157 | let (tx, rx) = channel(); 158 | let waker = Arc::new(Waker::new().unwrap()); 159 | 160 | let compositor = cli.compositor 161 | .or_else(Compositor::from_env) 162 | .unwrap_or(Compositor::Sway); 163 | 164 | let mut state = State { 165 | compositor_state, 166 | registry_state, 167 | output_state: OutputState::new(&globals, &qh), 168 | shm, 169 | layer_shell, 170 | viewporter, 171 | wallpaper_dir, 172 | shm_format, 173 | background_layers: Vec::new(), 174 | compositor_connection_task: ConnectionTask::new( 175 | compositor, tx.clone(), Arc::clone(&waker) 176 | ), 177 | color_transform, 178 | dmabuf_state, 179 | gpu, 180 | }; 181 | 182 | event_queue.roundtrip(&mut state).unwrap(); 183 | 184 | debug!("Initial wayland roundtrip done. Starting main event loop."); 185 | 186 | // ******************************** 187 | // Main event loop 188 | // ******************************** 189 | 190 | let mut poll = Poll::with_capacity(3); 191 | let token_wayland = poll.add_readable(&conn); 192 | ConnectionTask::spawn_subscribe_event_loop(compositor, tx, waker.clone()); 193 | let token_compositor = poll.add_readable(&waker); 194 | let signal_pipe = SignalPipe::new() 195 | .map_err(|e| error!("Failed to set up signal handling: {e}")) 196 | .ok(); 197 | let token_signal = signal_pipe.as_ref().map(|pipe| poll.add_readable(pipe)); 198 | 199 | loop { 200 | flush_blocking(&conn); 201 | let read_guard = ensure_prepare_read(&mut state, &mut event_queue); 202 | poll.poll().expect("Main event loop poll failed"); 203 | if poll.ready(token_wayland) { 204 | handle_wayland_event(&mut state, &mut event_queue, read_guard); 205 | } else { 206 | drop(read_guard); 207 | } 208 | if poll.ready(token_compositor) { 209 | waker.read(); 210 | handle_sway_event(&mut state, &rx); 211 | } 212 | if let Some(token_signal) = token_signal { 213 | if poll.ready(token_signal) { 214 | match signal_pipe.as_ref().unwrap().read() { 215 | Err(e) => error!("Failed to read the signal pipe: {e}"), 216 | Ok(signal_flags) => { 217 | if let Some(signal) = signal_flags.any_termination() { 218 | info!("Received signal {signal}, exiting"); 219 | return Ok(()); 220 | } else if signal_flags.has_usr1() 221 | || signal_flags.has_usr2() 222 | { 223 | error!("Received signal USR1 or USR2 is \ 224 | reserved for future functionality"); 225 | } 226 | }, 227 | } 228 | } 229 | } 230 | } 231 | } 232 | 233 | fn flush_blocking(connection: &Connection) { 234 | loop { 235 | let result = connection.flush(); 236 | if result.is_ok() { return } 237 | if let Err(WaylandError::Io(io_error)) = &result { 238 | if io_error.kind() == io::ErrorKind::WouldBlock { 239 | warn!("Wayland flush needs to block"); 240 | let mut poll_fds = [PollFd::from_borrowed_fd( 241 | connection.as_fd(), 242 | PollFlags::OUT, 243 | )]; 244 | retry_on_intr(|| poll(&mut poll_fds, -1)).unwrap(); 245 | continue 246 | } 247 | } 248 | result.expect("Failed to flush Wayland event queue"); 249 | } 250 | } 251 | 252 | fn ensure_prepare_read( 253 | state: &mut State, 254 | event_queue: &mut EventQueue, 255 | ) -> ReadEventsGuard { 256 | loop { 257 | if let Some(guard) = event_queue.prepare_read() { return guard } 258 | event_queue.dispatch_pending(state) 259 | .expect("Failed to dispatch pending Wayland events"); 260 | } 261 | } 262 | 263 | fn handle_wayland_event( 264 | state: &mut State, 265 | event_queue: &mut EventQueue, 266 | read_guard: ReadEventsGuard, 267 | ) { 268 | match read_guard.read() { 269 | Ok(_) => { 270 | event_queue.dispatch_pending(state) 271 | .expect("Failed to dispatch pending Wayland events"); 272 | }, 273 | Err(error) => { 274 | if let WaylandError::Io(io_error) = &error { 275 | if io_error.kind() == io::ErrorKind::WouldBlock { 276 | return 277 | } 278 | } 279 | panic!("Failed to read Wayland events: {error}"); 280 | } 281 | } 282 | } 283 | 284 | fn handle_sway_event( 285 | state: &mut State, 286 | rx: &Receiver, 287 | ) { 288 | while let Ok(workspace) = rx.try_recv() { 289 | // Find the background layer that of the output where the workspace is 290 | if let Some(affected_bg_layer) = state.background_layers.iter_mut() 291 | .find(|bg_layer| bg_layer.output_name == workspace.output) 292 | { 293 | affected_bg_layer.draw_workspace_bg(&workspace.workspace_name); 294 | } else { 295 | error!( 296 | "Workspace '{}' is on an unknown output '{}', \ 297 | known outputs were: {}", 298 | workspace.workspace_name, 299 | workspace.output, 300 | state.background_layers.iter() 301 | .map(|bg_layer| bg_layer.output_name.as_str()) 302 | .collect::>().join(", ") 303 | ); 304 | continue 305 | }; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/poll.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | marker::PhantomData, 4 | mem::MaybeUninit, 5 | os::fd::{BorrowedFd, OwnedFd}, 6 | }; 7 | 8 | use rustix::{ 9 | event::{PollFd, PollFlags, poll}, 10 | fd::AsFd, 11 | fs::{fcntl_setfl, OFlags}, 12 | io::{Errno, fcntl_setfd, FdFlags, read_uninit, retry_on_intr, write}, 13 | pipe::pipe, 14 | }; 15 | 16 | pub struct Poll<'fd> { 17 | poll_fds: Vec>, 18 | } 19 | 20 | impl<'fd> Poll<'fd> { 21 | pub fn with_capacity(capacity: usize) -> Self { 22 | Poll { poll_fds: Vec::with_capacity(capacity) } 23 | } 24 | 25 | pub fn add_readable(&mut self, fd: &'fd impl AsFd) -> Token<'fd> { 26 | let index = self.poll_fds.len(); 27 | self.poll_fds.push(PollFd::new(fd, PollFlags::IN)); 28 | Token { index, marker: PhantomData } 29 | } 30 | 31 | pub fn poll(&mut self) -> io::Result<()> { 32 | let events_count = retry_on_intr(|| poll(&mut self.poll_fds, -1))?; 33 | assert_ne!(events_count, 0); 34 | Ok(()) 35 | } 36 | 37 | pub fn ready(&mut self, token: Token) -> bool { 38 | let revents = self.poll_fds[token.index].revents(); 39 | assert!(!revents.intersects(PollFlags::NVAL)); 40 | !revents.is_empty() 41 | } 42 | } 43 | 44 | #[derive(Clone, Copy)] 45 | pub struct Token<'a> { 46 | index: usize, 47 | marker: PhantomData> 48 | } 49 | 50 | pub enum Waker { 51 | Eventfd { fd: OwnedFd }, 52 | Pipe { read_half: OwnedFd, write_half: OwnedFd }, 53 | } 54 | 55 | impl Waker { 56 | pub fn new() -> io::Result { 57 | #[cfg(any( 58 | target_os = "linux", 59 | target_os = "android", 60 | target_os = "freebsd", 61 | target_os = "illumos", 62 | ))] { 63 | use rustix::event::{EventfdFlags, eventfd}; 64 | if let Ok(fd) = eventfd( 65 | 0, 66 | EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK 67 | ) { 68 | return Ok(Waker::Eventfd { fd }); 69 | } 70 | } 71 | let (read_half, write_half) = pipe_cloexec_nonblock()?; 72 | Ok(Waker::Pipe { read_half, write_half }) 73 | } 74 | 75 | pub fn wake(&self) { 76 | match self { 77 | Waker::Eventfd { fd } => assert_ok_or_wouldblock( 78 | write(fd, &1u64.to_ne_bytes()) 79 | ), 80 | Waker::Pipe { write_half, .. } => assert_ok_or_wouldblock( 81 | write(write_half, &[0u8]) 82 | ), 83 | } 84 | } 85 | 86 | pub fn read(&self) { 87 | match self { 88 | Waker::Eventfd { fd } => assert_ok_or_wouldblock( 89 | read_uninit(fd, &mut [MaybeUninit::::uninit(); 8]) 90 | ), 91 | Waker::Pipe { read_half, .. } => assert_ok_or_wouldblock( 92 | clear_pipe(read_half) 93 | ), 94 | } 95 | } 96 | } 97 | 98 | impl AsFd for Waker { 99 | fn as_fd(&self) -> BorrowedFd { 100 | match self { 101 | Waker::Eventfd { fd } => fd.as_fd(), 102 | Waker::Pipe { read_half, .. } => read_half.as_fd(), 103 | } 104 | } 105 | } 106 | 107 | pub fn pipe_cloexec_nonblock() -> io::Result<(OwnedFd, OwnedFd)> { 108 | #[cfg(any( 109 | target_os = "linux", 110 | target_os = "android", 111 | target_os = "freebsd", 112 | target_os = "netbsd", 113 | target_os = "openbsd", 114 | target_os = "dragonfly", 115 | target_os = "illumos", 116 | target_os = "redox", 117 | ))] { 118 | use rustix::pipe::{PipeFlags, pipe_with}; 119 | if let Ok(ret) = pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK) { 120 | return Ok(ret) 121 | } 122 | } 123 | let (read_half, write_half) = pipe()?; 124 | fcntl_setfd(&read_half, FdFlags::CLOEXEC)?; 125 | fcntl_setfd(&write_half, FdFlags::CLOEXEC)?; 126 | fcntl_setfl(&read_half, OFlags::NONBLOCK)?; 127 | fcntl_setfl(&write_half, OFlags::NONBLOCK)?; 128 | Ok((read_half, write_half)) 129 | } 130 | 131 | fn clear_pipe(read_half: impl AsFd) -> Result<(), Errno> { 132 | const LEN: usize = 256; 133 | let mut buf = [MaybeUninit::::uninit(); LEN]; 134 | loop { 135 | match read_uninit(&read_half, &mut buf) { 136 | Ok((slice, _)) => if slice.len() < LEN { return Ok(()) }, 137 | Err(e) => return Err(e), 138 | } 139 | } 140 | } 141 | 142 | #[track_caller] 143 | fn assert_ok_or_wouldblock(result: Result) { 144 | match result { 145 | #[allow(unreachable_patterns)] 146 | Ok(_) | Err(Errno::AGAIN) | Err(Errno::WOULDBLOCK) => (), 147 | Err(e) => panic!("{e}"), 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/signal.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::c_int, 3 | io, 4 | mem::{ManuallyDrop, MaybeUninit}, 5 | os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd}, 6 | ptr, 7 | sync::atomic::{AtomicI32, Ordering::Relaxed}, 8 | }; 9 | 10 | use libc::{ 11 | raise, SA_RESETHAND, SA_RESTART, SIG_DFL, SIG_ERR, 12 | SIGHUP, SIGINT, SIGUSR1, SIGUSR2, SIGTERM, 13 | sigaction, sigemptyset, signal, sigset_t, write, 14 | }; 15 | use rustix::{ 16 | fd::AsFd, 17 | io::read_uninit, 18 | }; 19 | 20 | use crate::poll::pipe_cloexec_nonblock; 21 | 22 | const TERM_SIGNALS: [c_int; 3] = [SIGHUP, SIGINT, SIGTERM]; 23 | const OTHER_SIGNALS: [c_int; 2] = [SIGUSR1, SIGUSR2]; 24 | 25 | const TERM: u8 = 1 << 0; 26 | const INT: u8 = 1 << 1; 27 | const HUP: u8 = 1 << 2; 28 | const USR1: u8 = 1 << 3; 29 | const USR2: u8 = 1 << 4; 30 | 31 | static PIPE_FD: AtomicI32 = AtomicI32::new(-1); 32 | 33 | pub struct SignalPipe { 34 | read_half: OwnedFd, 35 | } 36 | 37 | impl SignalPipe { 38 | pub fn new() -> io::Result { 39 | unsafe { 40 | let (read_half, write_half) = pipe_cloexec_nonblock()?; 41 | PIPE_FD.compare_exchange( 42 | -1, 43 | write_half.as_raw_fd(), 44 | Relaxed, 45 | Relaxed, 46 | ).unwrap(); 47 | let _ = ManuallyDrop::new(write_half); 48 | let ret = SignalPipe { read_half }; 49 | let sigset_empty = sigset_empty()?; 50 | for signum in TERM_SIGNALS { 51 | sigaction_set_handler( 52 | signum, 53 | handle_termination_signals, 54 | sigset_empty, 55 | SA_RESTART | SA_RESETHAND, 56 | )?; 57 | } 58 | for signum in OTHER_SIGNALS { 59 | sigaction_set_handler( 60 | signum, 61 | handle_other_signals, 62 | sigset_empty, 63 | SA_RESTART, 64 | )?; 65 | } 66 | Ok(ret) 67 | } 68 | } 69 | 70 | pub fn read(&self) -> io::Result { 71 | let mut buf = [MaybeUninit::::uninit(); 64]; 72 | let mut flags = 0; 73 | for byte in read_uninit(&self.read_half, &mut buf)?.0 { 74 | assert_ne!(*byte, 0); 75 | flags |= *byte; 76 | } 77 | Ok(SignalFlags(flags)) 78 | } 79 | } 80 | 81 | impl Drop for SignalPipe { 82 | fn drop(&mut self) { 83 | for signum in OTHER_SIGNALS { 84 | sigaction_reset_default(signum).unwrap(); 85 | } 86 | for signum in TERM_SIGNALS { 87 | sigaction_reset_default(signum).unwrap(); 88 | } 89 | let write_half_fd = PIPE_FD.swap(-1, Relaxed); 90 | assert_ne!(write_half_fd, -1); 91 | drop(unsafe { OwnedFd::from_raw_fd(write_half_fd) }); 92 | } 93 | } 94 | 95 | impl AsFd for SignalPipe { 96 | fn as_fd(&self) -> BorrowedFd { 97 | self.read_half.as_fd() 98 | } 99 | } 100 | 101 | #[derive(Clone, Copy)] 102 | pub struct SignalFlags(u8); 103 | 104 | impl SignalFlags { 105 | pub fn any_termination(self) -> Option<&'static str> { 106 | if self.0 & TERM != 0 { 107 | Some("TERM") 108 | } else if self.0 & INT != 0 { 109 | Some("INT") 110 | } else if self.0 & HUP != 0 { 111 | Some("HUP") 112 | } else { 113 | None 114 | } 115 | } 116 | pub fn has_usr1(self) -> bool { 117 | self.0 & USR1 != 0 118 | } 119 | pub fn has_usr2(self) -> bool { 120 | self.0 & USR2 != 0 121 | } 122 | } 123 | 124 | fn sigset_empty() -> io::Result { 125 | unsafe { 126 | let mut sigset = MaybeUninit::uninit(); 127 | if sigemptyset(sigset.as_mut_ptr()) < 0 { 128 | return Err(io::Error::last_os_error()) 129 | } 130 | Ok(sigset.assume_init()) 131 | } 132 | } 133 | 134 | unsafe fn sigaction_set_handler( 135 | signum: c_int, 136 | handler: extern "C" fn(c_int), 137 | mask: sigset_t, 138 | flags: c_int, 139 | ) -> io::Result<()> { 140 | unsafe { 141 | let mut act: sigaction = MaybeUninit::zeroed().assume_init(); 142 | act.sa_sigaction = handler as _; 143 | act.sa_mask = mask; 144 | act.sa_flags = flags; 145 | if sigaction( 146 | signum, 147 | &act, 148 | ptr::null_mut(), 149 | ) < 0 { 150 | return Err(io::Error::last_os_error()) 151 | } 152 | Ok(()) 153 | } 154 | } 155 | 156 | fn sigaction_reset_default(signum: c_int) -> io::Result<()> { 157 | unsafe { 158 | if signal(signum, SIG_DFL) == SIG_ERR { 159 | return Err(io::Error::last_os_error()) 160 | } 161 | Ok(()) 162 | } 163 | } 164 | 165 | extern "C" fn handle_termination_signals(signum: c_int) { 166 | unsafe { 167 | let _errno_guard = ErrnoGuard::new(); 168 | let byte: u8 = match signum { 169 | SIGTERM => TERM, 170 | SIGINT => INT, 171 | SIGHUP => HUP, 172 | _ => 0, 173 | }; 174 | // In case of an error termination signals will have SA_RESETHAND set 175 | // so re-raise the signal to invoke the default handler 176 | if write( 177 | PIPE_FD.load(Relaxed), 178 | ptr::from_ref(&byte).cast(), 179 | 1, 180 | ) != 1 { 181 | raise(signum); 182 | } 183 | } 184 | } 185 | 186 | extern "C" fn handle_other_signals(signum: c_int) { 187 | unsafe { 188 | let _errno_guard = ErrnoGuard::new(); 189 | let byte: u8 = match signum { 190 | SIGUSR1 => USR1, 191 | SIGUSR2 => USR2, 192 | _ => 0, 193 | }; 194 | // In case of an error ignore non-termination signals 195 | let _ = write( 196 | PIPE_FD.load(Relaxed), 197 | ptr::from_ref(&byte).cast(), 198 | 1, 199 | ); 200 | } 201 | } 202 | 203 | struct ErrnoGuard(i32); 204 | 205 | impl ErrnoGuard { 206 | unsafe fn new() -> ErrnoGuard { 207 | ErrnoGuard(errno()) 208 | } 209 | } 210 | 211 | impl Drop for ErrnoGuard { 212 | fn drop(&mut self) { 213 | set_errno(self.0) 214 | } 215 | } 216 | 217 | // Based on the Rust Standard Library: 218 | // .rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/pal/unix/os.rs 219 | // with changes for stable rust from the errno crate: 220 | // https://github.com/lambda-fairy/rust-errno/blob/main/src/unix.rs 221 | // under licence MIT OR Apache-2.0 222 | 223 | #[allow(unexpected_cfgs)] 224 | extern "C" { 225 | #[cfg_attr( 226 | any( 227 | target_os = "linux", 228 | target_os = "emscripten", 229 | target_os = "fuchsia", 230 | target_os = "l4re", 231 | target_os = "hurd", 232 | target_os = "dragonfly", 233 | ), 234 | link_name = "__errno_location" 235 | )] 236 | #[cfg_attr( 237 | any( 238 | target_os = "netbsd", 239 | target_os = "openbsd", 240 | target_os = "cygwin", 241 | target_os = "android", 242 | target_os = "redox", 243 | target_os = "nuttx", 244 | target_env = "newlib", 245 | target_os = "vxworks", 246 | ), 247 | link_name = "__errno" 248 | )] 249 | #[cfg_attr( 250 | any(target_os = "solaris", target_os = "illumos"), 251 | link_name = "___errno" 252 | )] 253 | #[cfg_attr(target_os = "nto", link_name = "__get_errno_ptr")] 254 | #[cfg_attr( 255 | any(target_os = "freebsd", target_vendor = "apple"), 256 | link_name = "__error" 257 | )] 258 | #[cfg_attr(target_os = "haiku", link_name = "_errnop")] 259 | #[cfg_attr(target_os = "aix", link_name = "_Errno")] 260 | // SAFETY: this will always return the same pointer on a given thread. 261 | fn errno_location() -> *mut c_int; 262 | } 263 | 264 | /// Returns the platform-specific value of errno 265 | #[inline] 266 | fn errno() -> i32 { 267 | unsafe { (*errno_location()) as i32 } 268 | } 269 | 270 | /// Sets the platform-specific value of errno 271 | // needed for readdir and syscall! 272 | #[inline] 273 | fn set_errno(e: i32) { 274 | unsafe { *errno_location() = e as c_int } 275 | } 276 | --------------------------------------------------------------------------------