Compare commits

..

103 Commits

Author SHA1 Message Date
Ivan Molodetskikh 8ed0da44d9 wiki: Document the release process 2026-04-25 14:19:21 +03:00
Ivan Molodetskikh 91be662ac6 Update screenshot in the README 2026-04-25 14:14:21 +03:00
Ivan Molodetskikh 4438aefc8d Update README 2026-04-25 11:19:54 +03:00
Ivan Molodetskikh 6c4dfd7772 rpkg: Update licenses 2026-04-25 11:17:16 +03:00
Ivan Molodetskikh 1ad422f0db wiki: Add screenshot to window effects 2026-04-25 11:16:48 +03:00
Ivan Molodetskikh 8fd9fb73f2 Bump Since on the wiki 2026-04-25 09:16:29 +03:00
Ivan Molodetskikh 8d83fbae67 CI: Update Fedora to 42
41 is EOL
2026-04-25 09:12:26 +03:00
Ivan Molodetskikh 414729dce5 Bump version to 26.04 2026-04-25 09:12:26 +03:00
Ivan Molodetskikh dbe79b7873 CI: Fix next release check in release workflow 2026-04-25 09:12:16 +03:00
Ivan Molodetskikh 9438f59e2b Bump Smithay (last fix was merged) 2026-04-24 18:45:29 +03:00
Ivan Molodetskikh 719255ac35 Bump Smithay to fix GTK 4.23 text-input panic 2026-04-24 18:00:34 +03:00
Ivan Molodetskikh 8a51935224 wiki: Update blur example from foot to alacritty 2026-04-24 15:38:50 +03:00
mgabor3141 8d583fe854 Preserve num lock state when loading custom keymap 2026-04-23 23:52:47 -07:00
Ivan Molodetskikh 74d2b18603 wiki: Mention Mod+M default bind 2026-04-22 13:24:01 +03:00
Ivan Molodetskikh fad02316f1 wiki: Update binds list on Getting Started 2026-04-22 13:22:57 +03:00
Ivan Molodetskikh 47385c2ecd wiki: Add missing Since next release 2026-04-22 13:18:39 +03:00
Uzlkav e430d3ab2b wiki: explicitly mention possible include methods 2026-04-22 13:18:14 +03:00
Ivan Molodetskikh e472b5b0f1 wiki: Fix typos 2026-04-21 22:35:13 +03:00
Ivan Molodetskikh efb169416d wiki: Mention these are the default blur values 2026-04-21 22:35:06 +03:00
Ivan Molodetskikh 3a3a97ec2a Remove wrong return before dlclose() 2026-04-21 17:58:59 +03:00
Ivan Molodetskikh e9c182a13c Make use of new DndGrabHandler::cancelled() 2026-04-20 20:42:21 +03:00
Ivan Molodetskikh cfe059c303 Fix clipped surface + y_invert buffer 2026-04-20 20:38:26 +03:00
Ivan Molodetskikh bd7c748a4f Upgrade Smithay (old laptop screenshot fix) 2026-04-20 20:37:33 +03:00
Ivan Molodetskikh 68bb942d21 Add test for set_fullscreen panic on removed wl_output 2026-04-19 14:35:36 +03:00
jan Lemata 04c422e43f tty: Re-evaluate ignored nodes on udev add events and session resume (#3651)
* tty: Re-evaluate ignored nodes on udev add events

When a DRM device is removed and rescanned (e.g. during a dGPU
suspend/resume cycle), the kernel may assign it a new device ID.
Re-evaluating the config paths specifically on UdevEvent::Added
ensures that symlinks like /dev/dri/by-path/... are resolved to
their new underlying IDs, preventing niri from accidentally
opening an ignored hotplugged device.

This check is restricted to device additions to avoid unnecessary
filesystem I/O on the hot path during bursts of other udev events.

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 08:50:56 +00:00
Ivan Molodetskikh d09fa2709c Guard against removed outputs in several places
Output::from_resource() succeeds even after the global has been disabled
and removed from niri. Clients operating on these disabled outputs could
cause panics in several places because niri assumed the output existed.
2026-04-19 11:19:41 +03:00
urayde 2c3315aebb wiki/getting-started: replace dms-shell references for arch 2026-04-19 10:53:55 +03:00
Indi 5a45088061 config: Print first definition in duplicate bind error message (#3536)
* improve config error messages for duplicate binds

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 07:00:18 +00:00
Ivan Molodetskikh 404d6dccc4 config: Expand ~ to home dir in includes 2026-04-19 09:41:24 +03:00
Ivan Molodetskikh 084f2cb193 tty: Skip initialization if session is inactive 2026-04-19 09:23:30 +03:00
Ivan Molodetskikh 25c88b542f tty: Add primary node first when resuming session 2026-04-19 09:19:58 +03:00
Dubakula Sai Venkata Chaitanya 6fc50a1fb8 Fix input lock when starting compositor while on a different TTY (#3593)
* fix: no longer input locks when TTY is switched before full compositor
start

* reword

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 09:14:38 +03:00
Ivan Molodetskikh 5e11b96f12 backend: Downgrade bind_wl_display() warn! to trace! 2026-04-19 08:43:31 +03:00
Austin Riba 849d26d646 backend/winit: Add DMA-BUF support (#3327)
* backend/winit: DMA-BUF setup for Nvidia support

Creates a dmabuf global in the same manner as the tty backend. This
fixes applications failing to launch on Nvidia when using the winit
backend.

This code is adapted from Smithay's Anvil compositor.
See smithay/anvil/src/winit.rs

* fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-19 05:43:11 +00:00
Benjamin Bäumler 9e5716a9db Add tablet option map-to-focused-output 2026-04-18 12:41:08 +03:00
Ivan Molodetskikh f4ebbc8017 wiki: Emphasize non-xray warnings 2026-04-18 12:02:39 +03:00
sodiboo ce9dd33213 protocols: implement ext-foreign-toplevel-list-v1
clean up foreign toplevel destruction: don't `retain` to remove one item

document why we have duplicate destruction logic in foreign toplevel

messy to have two of the same comment but like how else do i make that
information readily available to both

Co-authored-by: HigherOrderLogic <73709188+HigherOrderLogic@users.noreply.github.com>
2026-04-18 09:56:50 +03:00
Ivan Molodetskikh 10995ec62c Fix 1 new Clippy warning
I don't like the other ones
2026-04-18 09:30:28 +03:00
Ivan Molodetskikh c814c656c5 Dlsym set_default_max_buffer_size() to avoid wayland-server 1.23 requirement
Ubuntu 24.04 users rejoice
2026-04-17 16:32:10 +03:00
ArijanJ 82d4c7569e Handle trackball scrolling in overview 2026-04-16 21:17:49 +03:00
Ivan Molodetskikh 4f0db78248 wiki/FAQ: Describe hybrid GPU laptop external monitor lag 2026-04-16 21:09:39 +03:00
Ivan Molodetskikh 3e250cdc12 wiki/FAQ: Blur now exists 2026-04-16 21:09:16 +03:00
Ivan Molodetskikh a1b0bd6d1c Assume square corners for windowed fullscreen windows 2026-04-16 19:47:28 +03:00
Ivan Molodetskikh 892470afd3 Call on_maybe_dnd_ended() in two more places
This is not exhaustive and not a good solution. A proper Smithay
callback is needed: https://github.com/Smithay/smithay/issues/1996
2026-04-16 12:35:35 +03:00
Ivan Molodetskikh f1cb02cfab default-config: Bind Mod+Shift+R to switch-preset-column-width-back by default
Height presets aren't frequently needed in my experience, but switching
preset width back is very useful on 21:9 and wider monitors where you
have many more presets.
2026-04-16 10:03:47 +03:00
Ivan Molodetskikh 5dc4e83ba7 Upgrade dependencies 2026-04-16 09:54:53 +03:00
Ivan Molodetskikh 2b58e03d30 Upgrade tracing-subscriber and disable ANSI sanitization 2026-04-16 09:54:53 +03:00
Ivan Molodetskikh d4b4407236 Implement cancelling DnD with Escape
The last Smithay upgrade includes a fix that lets us do this.
2026-04-16 09:54:53 +03:00
Ivan Molodetskikh 26ff5f4bf1 Upgrade Smithay (fix disappeared connectors, fix DnD hang in Electron) 2026-04-16 09:54:53 +03:00
Ivan Molodetskikh d7905e6b74 Downgrade accesskit_unix to 0.17.0
Workaround for a 0.18 regression where accessibility doesn't work under
normal conditions.

https://github.com/niri-wm/niri/issues/3594

https://github.com/AccessKit/accesskit-c/issues/76
2026-04-16 09:54:48 +03:00
Ivan Molodetskikh 71d7fa9a61 effect_buffer: Change debug! to trace! 2026-04-15 21:35:28 +03:00
Ivan Molodetskikh 707f08559c postprocess: Replace IGN with more white-noise-like noise
IGN gives visible patterns when zooming in or increasing noise amount.
2026-04-15 21:28:22 +03:00
Ivan Molodetskikh 4d21489101 Add geometry-corner-radius and background-effect popup rules 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 5a24aae560 Add popups { opacity } window and layer rule 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 9170161a0a Implement ext-background-effect for popups 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 66d66d6030 Implement non-xray background effects 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 19866f8b0b Refactor screencopy/screencast to render via the damage tracker, fix cursorless screencopy with damage
The damage tracker stores framebuffer effect cache, so we want anything
that renders repeatedly to render through a reused damage tracker. This
way, the cache persists and is reused across renders.

This will be important for the non-xray background effects.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 4bc3ede4b7 blur: Update to use GlesRenderer in a subframe
Will be necessary for non-xray.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 250aa1f3cb Upgrade Smithay (framebuffer effect, subframes) 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 0117d6953d Document background effects 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 931123f38c Implement ext-background-effect protocol 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 73c0ce75d8 Implement blur background effect 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 1b1715fe9b Implement noise and saturation background effects 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh fee8719299 Implement xray background effect 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 7f9c7d1415 Get state in one place instead of three 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh b81cb13c2c Simplify popup position computation 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 45582ad095 render_helpers: Extract common functions 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh b3f5255bb9 layout: Narrow error! condition in update_render_elements()
As far as I can tell, the only place where we can hit this currently is
do-screen-transition when no outputs are connected. A check could be
trivially added there, but I don't think it's worth enforcing it with an
error! -- it's just an optimization (avoiding running unnecessary code)
while being rare and difficult to find if it does get logged.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 8c169b1a14 offscreen: Guard against zero size (empty elements) 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 525b33777b offscreen: Add error message 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh dec0e3bf5a layout: Improve view_rect computation for the interactively moved window 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 6bcaaf9d21 Add layer matcher to layer-rule 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh f022b3c504 damage: Remove set_size()
Changes in geometry already cause a repaint.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh ab10a260fa Rename Niri::render_inner() => render() 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 0eddd16b8a Rename Niri::render() => render_to_vec() 2026-04-15 14:35:14 +03:00
Ivan Molodetskikh d020d986ed shaders: Optimize rounding_alpha() a little
Simplified clamping exploiting half_px + half_px = 1.0 / niri_scale.

Makes the resize shader fit on a Eee PC ALU.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh 5abeb923de shaders: Extract niri_rounding_alpha() into a file
To have it in one place.
2026-04-15 14:35:14 +03:00
Ivan Molodetskikh dd1f28998f Bundle renderer and target into a RenderCtx 2026-04-15 14:35:14 +03:00
Semper_ 874e7fd70e Fix typo in switch-preset-window-height-back 2026-04-14 10:02:37 +03:00
Emir Karamehmetoglu 599db847f8 Docs: document reverse switching functionality introduced in #1670 (#3782)
* document the reverse switching functionality introduced in #1670

Toggling in reverse through preset widths & heights was added in #1670.

However, it's really difficult to find in the docs. The sole exception is a comment in the default config.kdl, but that only documents one of the new settings (width).

I had to open up `bindings.rs` on github source code to even find the right setting for the other. This information should be available in the docs or config somewhere.

## Alternatives Considered

I considered documenting the preset-toggling functionality, including reverse toggling, in `bindings` wiki, but then the original (non-reversed) toggling would be documented in multiple places. More importantly, bindings seems to be a guide on how to set bindings, not what actions are available for use with bindings.

* Update docs/wiki/Configuration:-Layout.md

* Update docs/wiki/Configuration:-Layout.md

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-04-07 11:25:55 +00:00
HigherOrderLogic d1a0380eed ci: update actions/checkout to v6 2026-04-05 14:43:37 +03:00
Jakob Hellermann 8f48f56fe1 ui/hotkey_overlay: prettify diaeresis keysyms 2026-03-27 16:00:18 +03:00
LuckShiba b07bde3ee8 nix: fix NIRI_BUILD_COMMIT when tree is dirty/shortRev is not available 2026-03-10 16:09:44 +08:00
Ivan Molodetskikh bf142e0b48 Upgrade csscolorparser to v0.8.3 (fixed MSRV) 2026-03-09 12:26:03 +03:00
Ivan Molodetskikh 8f75d171b6 CI: Switch tests to Ubuntu 26.04 2026-03-07 11:57:07 +03:00
Ivan Molodetskikh cbf4631461 Downgrade csscolorparser to fix MSRV 2026-03-07 11:36:33 +03:00
Ivan Molodetskikh a217ad6424 tty: Log on DrmScanEvent::Changed 2026-03-07 08:54:39 +03:00
Ivan Molodetskikh f4dc10e0b4 Upgrade Smithay (virtual_keyboard revert, clipboard destroy cleanup, xdg-shell v7)
The virtual keyboard is reverted to the same state as it was in v25.08, i.e.
cannot trigger compositor binds. A bigger Smithay refactor is necessary to
properly support them without breaking other things.

Fixes wtype being completely broken.
2026-03-07 08:17:52 +03:00
Ivan Molodetskikh b82d52705e Increase default Wayland buffer size
The buffer size matches the one used in other compositors, e.g.:

- https://gitlab.gnome.org/GNOME/mutter/-/blob/49.4/src/wayland/meta-wayland.c?ref_type=tags#L970-971
2026-03-07 08:03:28 +03:00
Ivan Molodetskikh c7fa5f29d6 Upgrade dependencies 2026-03-07 08:03:24 +03:00
Jon Heinritz e708f54615 winit: add an app-id when running niri in nested mode in nested mode 2026-02-27 23:53:51 +08:00
phuongdpham 2dc6f4482c chore: update git ignore
+ Exclude .idea folder
2026-02-23 20:23:06 +08:00
Ivan Molodetskikh a2a5291175 wiki: Unbreak COPR links
Accidentally changed while doing a global replacement.
2026-02-17 08:17:55 +03:00
Tobias Heider 1fa0338a17 openbsd: implement close_range workaround
Since OpenBSD doesn't have close_range we have
to use close from and close the remaining
descriptors manually.
2026-02-16 03:00:17 +08:00
Christian Meissl 8e3e93b624 bump calloop to 0.14.4
resolves an issue where cancelling
timers can result in performance degradation
over time and memory buildup
2026-02-14 13:50:16 +08:00
Christian Meissl c1146c0bef bump smithay: fix zxdg_exported crash 2026-02-13 15:16:38 +08:00
Ivan Molodetskikh 41b5de8769 Change all links from YaLTeR/niri to niri-wm/niri 2026-02-10 17:59:14 +03:00
blue linden 8d9bc2a5c9 replace yalter/ and vortriz/ with niri-wm/ in repo links 2026-02-10 21:52:58 +08:00
Christian Meissl 6d5c5f12b2 Fix dead surface hook VRAM leak (#3404)
* remove pre-commit hook when surface is destroyed

this re-uses the already existing remove_default_dmabuf_pre_commit_hook
during surface destruction instead of just removing the hook from the
stored list of hooks.

* do not add dmabuf pre-commit hook for destroyed surfaces

this prevents surfaces getting stored indefinitely in case
some logic tries to add the hook for an already
destroyed surface.

* align surface/toplevel destruction order for client destruction

resource destruction has undefined order in case
the client does not explicitly destroy the resourced
and wait for destruction to complete.
the same applies for clients exiting unexpectedly.

* rearrange some things

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-02-10 05:55:49 +00:00
Ivan Molodetskikh 42b2aeb6e6 wiki: Add missing Since 2026-02-10 08:25:51 +03:00
Ivan Molodetskikh ab47f5cec4 Remove obsolete FIXME 2026-02-06 19:22:39 +03:00
ジムワルド 549148d277 Add load-config-file --path to load a different config (#3395)
* ipc: allow load-config to relocate the path of the config

* doc: add info about alternative configuration paths and relocating

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>

* Update docs/wiki/Integrating-niri.md

* Update niri-ipc/src/lib.rs

* Update src/ipc/server.rs

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2026-02-06 19:22:16 +03:00
Manuel Mendez 189917c933 Add trackball to Configuration:-Input overview 2026-02-03 12:29:25 +08:00
123 changed files with 6494 additions and 1697 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
contact_links:
- name: Feature request
url: https://github.com/YaLTeR/niri/discussions/new?category=ideas
url: https://github.com/niri-wm/niri/discussions/new?category=ideas
about: Ideas for new features and functionality (start a Discussion)
- name: Ask a question
url: https://github.com/YaLTeR/niri/discussions/new?category=q-a
url: https://github.com/niri-wm/niri/discussions/new?category=q-a
about: Question about niri (start a Discussion)
- name: Matrix room
url: https://matrix.to/#/#niri:matrix.org
+14 -14
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -91,7 +91,7 @@ jobs:
runs-on: ubuntu-24.04
container: alpine:3
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -121,7 +121,7 @@ jobs:
PROPTEST_MAX_SHRINK_ITERS: 200000
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -148,7 +148,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -172,7 +172,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -195,7 +195,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -217,7 +217,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -230,10 +230,10 @@ jobs:
fedora:
runs-on: ubuntu-24.04
container: fedora:41
container: fedora:42
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -251,7 +251,7 @@ jobs:
CARGO_HOME: /home/runner/work/niri/niri/cargo-home
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -289,7 +289,7 @@ jobs:
dotnet: false
large-packages: false
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
@@ -308,7 +308,7 @@ jobs:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
lfs: true
show-progress: false
@@ -325,7 +325,7 @@ jobs:
contents: write
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
lfs: true
show-progress: false
+7 -4
View File
@@ -22,15 +22,18 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
show-progress: false
- name: Check for unreplaced "Since:" in the wiki
run: |
if grep --recursive 'Since: next release' wiki; then
exit 1
fi
# Fail if a match is found (exit code 0)
grep --recursive 'Since: next release' docs/wiki && exit 1
# Fail if grep failed (exit code 2)
status=$?
if [ $status -ne 1 ]; then exit $status; fi
- name: Install dependencies
run: |
+2
View File
@@ -1,2 +1,4 @@
/target
/result
.idea
+2 -2
View File
@@ -31,7 +31,7 @@ I would really appreciate help with testing and reviewing them.
### Testing
Pick a pull request you like, then build it and give it a go.
The [Developing niri wiki page](https://yalter.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
The [Developing niri wiki page](https://niri-wm.github.io/niri/Development:-Developing-niri) has guidance on running niri test builds.
Be really thorough with your testing.
We're striving for polished features in niri, so point out any issues and bugs, even small ones like animation jank.
@@ -84,7 +84,7 @@ When creating pull requests, please keep the following in mind.
- When working on bigger features, I usually start with a big messy commit, then gradually split out smaller self-contained changes from it as the code gets into shape.
- [git-rebase.io](https://git-rebase.io/) is a helpful guide for splitting commits and cleaning up history in git.
- When you address a review comment, mark it as resolved.
- Remember to [run tests](https://yalter.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
- Remember to [run tests](https://niri-wm.github.io/niri/Development:-Developing-niri#tests) and format the code with `cargo +nightly fmt --all`.
- For new layout actions, remember to add them to the randomized tests. For weird Wayland handling, adding client-server tests in `src/tests/` could be very useful.
- Test your changes by hand thoroughly, including for edge cases and weird interactions. See the Testing section above for some tips.
- Remember to document new config options on the wiki.
Generated
+708 -523
View File
File diff suppressed because it is too large Load Diff
+40 -37
View File
@@ -6,37 +6,35 @@ members = [
]
[workspace.package]
version = "25.11.0"
version = "26.4.0"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"
repository = "https://github.com/YaLTeR/niri"
repository = "https://github.com/niri-wm/niri"
rust-version = "1.85"
[workspace.dependencies]
anyhow = "1.0.100"
bitflags = "2.10.0"
clap = { version = "4.5.54", features = ["derive"] }
insta = "1.46.0"
anyhow = "1.0.102"
bitflags = "2.11.1"
clap = { version = "4.6.1", features = ["derive"] }
insta = "1.47.2"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tracing = { version = "0.1.44", features = ["max_level_trace", "release_max_level_debug"] }
# 0.3.20 filters out all ANSI codes to "fix a security issue" while also breaking
# everyone who relied on them for color output, with no fallback available.
# https://github.com/tokio-rs/tracing/issues/3378
tracing-subscriber = { version = "=0.3.19", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
tracy-client = { version = "0.18.4", default-features = false }
[workspace.dependencies.smithay]
# version = "0.4.1"
git = "https://github.com/Smithay/smithay.git"
# path = "../smithay"
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
default-features = false
[workspace.dependencies.smithay-drm-extras]
# version = "0.1.0"
git = "https://github.com/Smithay/smithay.git"
rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef"
# path = "../smithay/smithay-drm-extras"
[package]
@@ -53,49 +51,53 @@ readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
accesskit = { version = "0.22.0", optional = true }
accesskit_unix = { version = "0.18.0", optional = true }
# accesskit_unix 0.18 has a regression where it doesn't work in normal configurations.
# accesskit 0.21 is its correct dependent version.
# https://github.com/niri-wm/niri/issues/3594
accesskit = { version = "0.21", optional = true }
accesskit_unix = { version = "0.17", optional = true }
anyhow.workspace = true
arrayvec = "0.7.6"
async-channel = "2.5.0"
async-io = { version = "2.6.0", optional = true }
atomic = "0.6.1"
bitflags.workspace = true
bytemuck = { version = "1.24.0", features = ["derive"] }
calloop = { version = "0.14.3", features = ["executor", "futures-io", "signals"] }
bytemuck = { version = "1.25.0", features = ["derive"] }
calloop = { version = "0.14.4", features = ["executor", "futures-io", "signals"] }
clap = { workspace = true, features = ["string"] }
clap_complete = "4.5.65"
clap_complete_nushell = "4.5.10"
clap_complete = "4.6.2"
clap_complete_nushell = "4.6.0"
directories = "6.0.0"
drm-ffi = "0.9.0"
fastrand = "2.3.0"
futures-util = { version = "0.3.31", default-features = false, features = ["std", "io"] }
drm-ffi = "0.9.1"
fastrand = "2.4.1"
futures-util = { version = "0.3.32", default-features = false, features = ["std", "io"] }
git-version = "0.3.9"
glam = "0.30.10"
input = { version = "0.9.1", features = ["libinput_1_21"] }
glam = "0.32.1"
input = { version = "0.10.0", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.180"
libc = "0.2.185"
libdisplay-info = "0.3.0"
log = { version = "0.4.29", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "25.11.0", path = "niri-config" }
niri-ipc = { version = "25.11.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.1.0"
niri-config = { version = "26.4.0", path = "niri-config" }
niri-ipc = { version = "26.4.0", path = "niri-ipc", features = ["clap"] }
ordered-float = "5.3.0"
pango = { version = "0.21.5", features = ["v1_44"] }
pangocairo = "0.21.5"
pipewire = { version = "0.9.2", optional = true, features = ["v0_3_33"] }
png = "0.18.0"
png = "0.18.1"
profiling = "1.0.17"
sd-notify = "0.4.5"
sd-notify = "0.5.0"
serde.workspace = true
serde_json.workspace = true
smithay-drm-extras.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
wayland-backend = "0.3.12"
wayland-scanner = "0.31.8"
wayland-backend = "0.3.15"
wayland-scanner = "0.31.10"
wayland-server = "0.31.13"
xcursor = "0.3.10"
zbus = { version = "5.13.0", optional = true }
zbus = { version = "5.13.2", optional = true }
[dependencies.smithay]
workspace = true
@@ -119,14 +121,14 @@ features = [
approx = "0.5.1"
calloop-wayland-source = "0.4.1"
insta.workspace = true
proptest = "1.9.0"
proptest-derive = { version = "0.7.0", features = ["boxed_union"] }
rayon = "1.11.0"
wayland-client = "0.31.12"
proptest = "1.11.0"
proptest-derive = { version = "0.8.0", features = ["boxed_union"] }
rayon = "1.12.0"
wayland-client = "0.31.14"
xshell = "0.2.7"
[build-dependencies]
pkg-config = "0.3.32"
pkg-config = "0.3.33"
[features]
default = ["dbus", "systemd", "xdp-gnome-screencast"]
@@ -147,6 +149,7 @@ dinit = []
[lints.clippy]
new_without_default = "allow"
collapsible_match = "allow"
[profile.release]
debug = "line-tables-only"
@@ -162,7 +165,7 @@ insta.opt-level = 3
similar.opt-level = 3
[package.metadata.generate-rpm]
version = "25.11"
version = "26.04"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+24 -22
View File
@@ -2,15 +2,15 @@
<p align="center">A scrollable-tiling Wayland compositor.</p>
<p align="center">
<a href="https://matrix.to/#/#niri:matrix.org"><img alt="Matrix" src="https://img.shields.io/badge/matrix-%23niri-blue?logo=matrix"></a>
<a href="https://github.com/YaLTeR/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/YaLTeR/niri"></a>
<a href="https://github.com/YaLTeR/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/YaLTeR/niri?logo=github"></a>
<a href="https://github.com/niri-wm/niri/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/niri-wm/niri"></a>
<a href="https://github.com/niri-wm/niri/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/niri-wm/niri?logo=github"></a>
</p>
<p align="center">
<a href="https://yalter.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://yalter.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/YaLTeR/niri/discussions/325">Setup&nbsp;Showcase</a>
<a href="https://niri-wm.github.io/niri/Getting-Started.html">Getting Started</a> | <a href="https://niri-wm.github.io/niri/Configuration%3A-Introduction.html">Configuration</a> | <a href="https://github.com/niri-wm/niri/discussions/325">Setup&nbsp;Showcase</a>
</p>
![niri with a few windows open](https://github.com/user-attachments/assets/535e6530-2f44-4b84-a883-1240a3eee6e9)
<img width="1280" height="720" alt="niri with a few windows open" src="https://github.com/user-attachments/assets/dea5909e-1859-4aaa-9d88-d37f9663e00b" />
## About
@@ -29,23 +29,24 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Features
- Built from the ground up for scrollable tiling
- [Dynamic workspaces](https://yalter.github.io/niri/Workspaces.html) like in GNOME
- [Dynamic workspaces](https://niri-wm.github.io/niri/Workspaces.html) like in GNOME
- An [Overview](https://github.com/user-attachments/assets/379a5d1f-acdb-4c11-b36c-e85fd91f0995) that zooms out workspaces and windows
- Built-in screenshot UI
- Monitor and window screencasting through xdg-desktop-portal-gnome
- You can [block out](https://yalter.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://yalter.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/YaLTeR/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/YaLTeR/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://yalter.github.io/niri/Tabs.html)
- You can [block out](https://niri-wm.github.io/niri/Configuration%3A-Window-Rules.html#block-out-from) sensitive windows from screencasts
- [Dynamic cast target](https://niri-wm.github.io/niri/Screencasting.html#dynamic-screencast-target) that can change what it shows on the go
- [Touchpad](https://github.com/niri-wm/niri/assets/1794388/946a910e-9bec-4cd1-a923-4a9421707515) and [mouse](https://github.com/niri-wm/niri/assets/1794388/8464e65d-4bf2-44fa-8c8e-5883355bd000) gestures
- Group windows into [tabs](https://niri-wm.github.io/niri/Tabs.html)
- Configurable layout: gaps, borders, struts, window sizes
- [Gradient borders](https://yalter.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
- [Animations](https://github.com/YaLTeR/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/YaLTeR/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- [Gradient borders](https://niri-wm.github.io/niri/Configuration%3A-Layout.html#gradients) with Oklab and Oklch support
- [Background blur](https://niri-wm.github.io/niri/Window-Effects.html) for windows and layer-shell surfaces
- [Animations](https://github.com/niri-wm/niri/assets/1794388/ce178da2-af9e-4c51-876f-8709c241d95e) with support for [custom shaders](https://github.com/niri-wm/niri/assets/1794388/27a238d6-0a22-4692-b794-30dc7a626fad)
- Live-reloading config
- Works with [screen readers](https://yalter.github.io/niri/Accessibility.html)
- Works with [screen readers](https://niri-wm.github.io/niri/Accessibility.html)
## Video Demo
https://github.com/YaLTeR/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
https://github.com/niri-wm/niri/assets/1794388/bce834b0-f205-434e-a027-b373495f9729
Also check out this video from Brodie Robertson that showcases a lot of the niri functionality: [Niri Is My New Favorite Wayland Compositor](https://youtu.be/DeYx2exm04M)
@@ -55,8 +56,8 @@ Niri is stable for day-to-day use and does most things expected of a Wayland com
Many people are daily-driving niri, and are happy to help in our [Matrix channel].
Give it a try!
Follow the instructions on the [Getting Started](https://yalter.github.io/niri/Getting-Started.html) page.
Have your [waybar]s and [fuzzel]s ready: niri is not a complete desktop environment.
Follow the instructions on the [Getting Started](https://niri-wm.github.io/niri/Getting-Started.html) page.
Grab a desktop shell like [DankMaterialShell] or [Noctalia] (or build a more traditional setup): niri by itself is not a complete desktop environment.
Also check out [awesome-niri], a list of niri-related links and projects.
Here are some points you may have questions about:
@@ -72,7 +73,7 @@ We have touchpad gestures, but no touchscreen gestures yet.
You can check on [wayland.app](https://wayland.app) at the bottom of each protocol's page.
- **Performance**: while I run niri on beefy machines, I try to stay conscious of performance.
I've seen someone use it fine on an Eee PC 900 from 2008, of all things.
- **Xwayland**: [integrated](https://yalter.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
- **Xwayland**: [integrated](https://niri-wm.github.io/niri/Xwayland.html#using-xwayland-satellite) via xwayland-satellite starting from niri 25.08.
## Media
@@ -93,7 +94,7 @@ An LWN article with a nice overview and introduction to niri.
## Contributing
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
See [CONTRIBUTING.md](https://github.com/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
## Inspiration
@@ -109,8 +110,8 @@ Here are some other projects which implement a similar workflow:
- [PaperWM]: scrollable tiling on top of GNOME Shell.
- [karousel]: scrollable tiling on top of KDE.
- [scroll](https://github.com/dawsers/scroll) and [papersway]: scrollable tiling on top of sway/i3.
- [hyprscrolling] and [hyprslidr]: scrollable tiling on top of Hyprland.
- [PaperWM.spoon]: scrollable tiling on top of macOS.
- Hyprland has a built-in [scrolling layout](https://wiki.hypr.land/Configuring/Scrolling-Layout/).
- [Paneru] and [PaperWM.spoon]: scrollable tiling on top of macOS.
## Contact
@@ -121,11 +122,12 @@ We also have a community Discord server: https://discord.gg/vT8Sfjy7sx
[PaperWM]: https://github.com/paperwm/PaperWM
[waybar]: https://github.com/Alexays/Waybar
[fuzzel]: https://codeberg.org/dnkl/fuzzel
[awesome-niri]: https://github.com/Vortriz/awesome-niri
[awesome-niri]: https://github.com/niri-wm/awesome-niri
[karousel]: https://github.com/peterfajdiga/karousel
[papersway]: https://spwhitton.name/tech/code/papersway/
[hyprscrolling]: https://github.com/hyprwm/hyprland-plugins/tree/main/hyprscrolling
[hyprslidr]: https://gitlab.com/magus/hyprslidr
[Paneru]: https://github.com/karinushka/paneru
[PaperWM.spoon]: https://github.com/mogenson/PaperWM.spoon
[Matrix channel]: https://matrix.to/#/#niri:matrix.org
[OpenTabletDriver]: https://opentabletdriver.net/
[DankMaterialShell]: https://danklinux.com/
[Noctalia]: https://noctalia.dev/
+1 -1
View File
@@ -40,5 +40,5 @@ def _badge_for_version(preposition: str, version: str):
# we might fail to make real links to release notes on other cases too, but for now this is the one i've found
return f"<span class=\"badge\">{preposition}: {version}</span>"
else:
path = f"https://github.com/YaLTeR/niri/releases/tag/v{version}"
path = f"https://github.com/niri-wm/niri/releases/tag/v{version}"
return f"<span class=\"badge\">[{preposition}: {version}]({path})</span>"
+4 -2
View File
@@ -1,7 +1,7 @@
site_name: niri
docs_dir: wiki
site_url: https://yalter.github.io/niri
repo_url: https://github.com/YaLTeR/niri
site_url: https://niri-wm.github.io/niri
repo_url: https://github.com/niri-wm/niri
edit_uri: edit/main/docs/wiki/
use_directory_urls: false
@@ -85,6 +85,7 @@ nav:
- Xwayland: Xwayland.md
- Gestures: Gestures.md
- Fullscreen and Maximize: Fullscreen-and-Maximize.md
- Window Effects: Window-Effects.md
- Packaging niri: Packaging-niri.md
- Integrating niri: Integrating-niri.md
- Accessibility: Accessibility.md
@@ -110,6 +111,7 @@ nav:
- Design Principles: Development:-Design-Principles.md
- Developing niri: Development:-Developing-niri.md
- Documenting niri: Development:-Documenting-niri.md
- Releasing niri: Development:-Releasing-niri.md
- Fractional Layout: Development:-Fractional-Layout.md
- Redraw Loop: Development:-Redraw-Loop.md
- Animation Timing: Development:-Animation-Timing.md
@@ -107,6 +107,8 @@ debug {
### `force-disable-connectors-on-resume`
<sup>Since: 26.04</sup>
Force-disables all outputs upon resuming niri (TTY switch or waking up from suspend).
This causes a modeset/screen blank on all outputs.
+7 -1
View File
@@ -16,6 +16,12 @@ Settings from included files will be merged with the settings from the main conf
Included config files can in turn include more files.
All included files are watched for changes, and the config live-reloads when any of them change.
You can include by filename or path.
* Relative to the current file: `other.kdl` or `./other.kdl`
* By absolute path: `/path/to/file.kdl`
* <sup>Since: 26.04</sup> Home dir paths: `~/file.kdl` expands to `/home/user/file.kdl`
Includes work only at the top level of the config:
```kdl,must-fail
@@ -116,7 +122,7 @@ window-rule {
### Optional includes
<sup>Since: next release</sup>
<sup>Since: 26.04</sup>
By default, including a nonexistent file will cause an error.
You can allow nonexistent includes by setting `optional=true`:
+6 -1
View File
@@ -2,7 +2,7 @@
In this section you can configure input devices like keyboard and mouse, and some input-related options.
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `tablet`, `touch`.
There's a section for each device type: `keyboard`, `touchpad`, `mouse`, `trackpoint`, `trackball`, `tablet`, `touch`.
Settings in those sections will apply to every device of that type.
Currently, there's no way to configure specific devices individually (but that is planned).
@@ -89,6 +89,7 @@ input {
tablet {
// off
map-to-output "eDP-1"
// map-to-focused-output
// left-handed
// calibration-matrix 1.0 0.0 0.0 0.0 1.0 0.0
}
@@ -281,6 +282,10 @@ Valid output names are the same as the ones used for output configuration.
<sup>Since: 0.1.7</sup> When a tablet is not mapped to any output, it will map to the union of all connected outputs, without aspect ratio correction.
Setting specific to `tablet`:
- `map-to-focused-output`: <sup>Since: 26.04</sup> will map the tablet to the focused output, takes precedence over `map-to-output`.
### General Settings
These settings are not specific to a particular input device.
+1 -1
View File
@@ -19,7 +19,7 @@ You can find documentation for various sections of the config on these wiki page
### Loading
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
Please use the default configuration file as the starting point for your custom configuration.
The configuration is live-reloaded.
+1 -1
View File
@@ -382,7 +382,7 @@ binds {
}
```
<sup>Since: next release</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
<sup>Since: 26.04</sup> You can show the mouse pointer on window screenshots with the `show-pointer=true` property.
The pointer will be included only if the window is currently receiving pointer input (usually this means the pointer is on top of the window).
```kdl
+101
View File
@@ -14,6 +14,7 @@ Here are all matchers and properties that a layer rule could have:
layer-rule {
match namespace="waybar"
match at-startup=true
match layer="top"
// Properties that apply continuously.
opacity 0.5
@@ -34,6 +35,25 @@ layer-rule {
geometry-corner-radius 12
place-within-backdrop true
baba-is-float true
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
popups {
opacity 0.5
geometry-corner-radius 6
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
}
}
```
@@ -69,6 +89,22 @@ layer-rule {
}
```
#### `layer`
<sup>Since: 26.04</sup>
Matches surfaces on this layer-shell layer.
Can be `"background"`, `"bottom"`, `"top"`, or `"overlay"`.
```kdl
// Make all overlay-layer surfaces FLOAT.
layer-rule {
match layer="overlay"
baba-is-float true
}
```
### Dynamic Properties
These properties apply continuously to open layer-shell surfaces.
@@ -191,3 +227,68 @@ layer-rule {
baba-is-float true
}
```
#### `background-effect`
<sup>Since: 26.04</sup>
Override the background effect options for this surface.
- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
- `blur`: set to `true` to enable blur behind this surface, or `false` to force-disable it.
- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
See the [window effects page](./Window-Effects.md) for an overview of background effects.
```kdl
// Make top and overlay layers use the regular blur (if enabled),
// while bottom and background layers keep using the efficient xray blur.
layer-rule {
match layer="top"
match layer="overlay"
background-effect {
xray false
}
}
```
#### `popups`
<sup>Since: 26.04</sup>
Override properties for this layer surface's pop-ups (e.g. a menu opened by clicking an item in Waybar).
The properties work the same way as the corresponding layer-rule properties, except that they apply to the layer surface's pop-ups rather than to the layer surface itself.
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
Other properties apply independently.
> [!NOTE]
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
>
> Some desktop shells will emulate pop-ups by drawing something that looks like a pop-up inside a regular layer surface.
> As far as niri is concerned, those are just layer surfaces and not pop-ups, so this block won't apply to them.
>
> This block also does not affect input-method pop-ups, such as Fcitx.
```kdl
// Blur the background behind Waybar popup menus.
layer-rule {
match namespace="^waybar$"
popups {
// Match the default GTK 3 popup corner radius.
geometry-corner-radius 6
opacity 0.85
background-effect {
blur true
}
}
}
```
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the layer surface correctly sets its Wayland geometry to exclude any shadows.
Pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
+3 -1
View File
@@ -177,6 +177,7 @@ layout {
### `preset-column-widths`
Set the widths that the `switch-preset-column-width` action (Mod+R) toggles between.
<sup>Since: 25.08</sup> You can use the `switch-preset-column-width-back` action (Mod+Shift+R) to toggle in reverse.
`proportion` sets the width as a fraction of the output width, taking gaps into account.
For example, you can perfectly fit four windows sized `proportion 0.25` on an output, regardless of the gaps setting.
@@ -228,7 +229,8 @@ layout {
<sup>Since: 0.1.9</sup>
Set the heights that the `switch-preset-window-height` action (Mod+Shift+R) toggles between.
Set the heights that the `switch-preset-window-height` action (Mod+Ctrl+Shift+R) toggles between.
<sup>Since: 25.08</sup> You can use the `switch-preset-window-height-back` action (not bound by default) to toggle in reverse.
`proportion` sets the height as a fraction of the output height, taking gaps into account.
The default preset heights are <sup>1</sup>&frasl;<sub>3</sub>, <sup>1</sup>&frasl;<sub>2</sub> and <sup>2</sup>&frasl;<sub>3</sub> of the output.
+87
View File
@@ -54,6 +54,14 @@ hotkey-overlay {
config-notification {
disable-failed
}
blur {
// off
passes 3
offset 3.0
noise 0.02
saturation 1.5
}
```
### `spawn-at-startup`
@@ -320,3 +328,82 @@ config-notification {
disable-failed
}
```
### `blur`
<sup>Since: 26.04</sup>
Blur configuration that affects all background blur.
See the [window effects page](./Window-Effects.md) for an overview of background effects.
```kdl
// These are the default values:
blur {
// off
passes 3
offset 3
noise 0.02
saturation 1.5
}
```
#### `off`
By default, blur is available on request by a window or layer surface (via the `ext-background-effect` protocol).
You can also enable it manually with the `blur true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
Setting the `off` flag will disable all blur, both requested by the window, and configured in window rules.
```kdl
blur {
off
}
```
#### `passes` and `offset`
`passes` controls the number of downsample/upsample passes for dual kawase blur.
More passes produce a larger, smoother blur, but cost more GPU resources.
`offset` is the pixel offset multiplier for each pass.
Offset `1` is the original dual kawase blur.
Larger values produce a smoother blur, at no additional GPU cost.
However, setting `offset` too big will produce visual artifacts.
You will need to increase `passes` to be able to use a bigger `offset` without artifacts.
When configuring blur, try increasing `offset` first (since it doesn't cause any extra GPU load) until you start getting artifacts.
Then, if you still need smoother blur, increase `passes` by 1.
Keep doing this until you get the desired visuals.
```kdl
blur {
passes 3
offset 3.0
}
```
#### `noise`
Amount of noise to add on top of the blur.
This is helpful to reduce color banding artifacts.
```kdl
blur {
noise 0.02
}
```
#### `saturation`
Color saturation applied to the blurred background.
Values above `1` increase saturation; values below `1` reduce it.
```kdl
blur {
saturation 1.5
}
```
+108
View File
@@ -100,6 +100,25 @@ window-rule {
tiled-state true
baba-is-float true
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
popups {
opacity 0.5
geometry-corner-radius 15
background-effect {
xray true
blur true
noise 0.05
saturation 3
}
}
min-width 100
max-width 200
min-height 300
@@ -909,6 +928,95 @@ https://github.com/user-attachments/assets/3f4cb1a4-40b2-4766-98b7-eec014c19509
</video>
#### `background-effect`
<sup>Since: 26.04</sup>
Override the background effect options for this window.
- `xray`: set to `true` to enable the xray effect, or `false` to disable it.
- `blur`: set to `true` to enable blur behind this window, or `false` to force-disable it.
- `noise`: amount of pixel noise added to the background (helps with color banding from blur).
- `saturation`: color saturation of the background (`0` is desaturated, `1` is normal, `2` is 200% saturation).
See the [window effects page](./Window-Effects.md) for an overview of background effects.
```kdl
// Make floating windows use the regular blur (if enabled),
// while tiled windows keep using the efficient xray blur.
//
// Warning: non-xray blur is currently experimental and has known limitations.
// In particular, it doesn't work during window opening and closing animations.
window-rule {
match is-floating=true
background-effect {
xray false
}
}
```
#### `popups`
<sup>Since: 26.04</sup>
Override properties for this window's pop-ups (menus and tooltips).
The properties work the same way as the corresponding window-rule properties, except that they apply to the window's pop-ups rather than to the window itself.
`opacity` is applied *on top* of the layer surface's own opacity rule, so setting both will make pop-ups more transparent than the surface.
Other properties apply independently.
> [!NOTE]
> This block affects only pop-ups created by the app via Wayland's [xdg-popup](https://wayland.app/protocols/xdg-shell#xdg_popup) (which should be most of them).
>
> Examples of things that look like pop-ups that won't work:
>
> - Fully emulated by the client, i.e. not a pop-up at all, the client just draws something that looks like a pop-up inside its window.
> These are common in game engines and in web apps, e.g. the right click menu in Google Docs or in Electron apps like Discord.
>
> - Uses a wl-subsurface instead of an xdg-popup.
> Common in older apps using GTK 3, notably Firefox still uses these for some menus.
> Subsurfaces are an indivisible part of a surface and they aren't usually pop-ups, so it wouldn't make sense for niri to apply these rules to them.
>
> These emulated pop-ups come with other downsides: they cannot reliably extend outside their window, and if the app tries to do that, they will be clipped by rules such as `clip-to-geometry`.
> So most modern apps will correctly use xdg-popup, which is the intended way to show pop-ups on Wayland.
>
> This block also does not affect input-method pop-ups, such as Fcitx.
>
> For pop-ups created by your desktop shell or desktop components, use the corresponding [layer rule](./Configuration:-Layer-Rules.md#popups).
```kdl
// Blur the background behind pop-up menus in Nautilus.
window-rule {
match app-id="Nautilus"
popups {
// Matches the default libadwaita pop-up corner radius.
geometry-corner-radius 15
// Note: it'll look better to set background opacity
// through your GTK theme CSS and not here.
// This is just an example that makes it look obvious.
opacity 0.5
background-effect {
blur true
}
}
}
```
Keep in mind that the background effect will look right only if the pop-up is shaped like a (rounded) rectangle, and the window correctly sets its Wayland geometry to exclude any shadows.
For example, GTK 4 pop-ups with pointing arrows (`has-arrow=true` property) are *not* rounded rectangles—the arrow sticks out—so if you enable blur, it will also stick out of the pop-up.
| Correct | Wrong |
|-----------------------------------------------------|--------------------------------------------------------------------------------|
| The pop-up is a rounded rectangle. Blur looks fine. | The pop-up is not a rounded rectangle. Blur extends above, where the arrow is. |
| ![](./img/popup-no-arrow.png) | ![](./img/popup-arrow.png) |
These pop-ups with custom shapes will need the app to implement the [ext-background-effect protocol](https://wayland.app/protocols/ext-background-effect-v1) to work properly.
#### Size Overrides
You can amend the window's minimum and maximum size in logical pixels.
+1 -1
View File
@@ -74,7 +74,7 @@ Here are some design considerations for the window layout logic.
## Default config
The [default config](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
The [default config](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl) is intended to give a familiar, helpful, and not too jarring experience to new niri users.
Importantly, it is not a "suggested rice config"; we don't want to startle people with full-on rainbow borders and crazy shaders.
Since we're not a complete desktop environment (and don't have the contributor base to become one), we cannot provide a fully integrated experience—distro spins are better positioned to do this.
+2 -2
View File
@@ -1,8 +1,8 @@
niri's documentation files are found in `docs/wiki/` and should be viewable and browsable in at least three systems:
- The GitHub repo's markdown file preview
- [The GitHub repo's wiki](https://github.com/YaLTeR/niri/wiki)
- [The documentation site](https://yalter.github.io/niri/)
- [The GitHub repo's wiki](https://github.com/niri-wm/niri/wiki)
- [The documentation site](https://niri-wm.github.io/niri/)
## The GitHub repo's wiki
+130
View File
@@ -0,0 +1,130 @@
This is a checklist of things to release a new niri version.
We'll use `26.04` as the example new version.
When making a patch release, append the patch number like `26.04.1`.
## Prepare the release notes
Plan for a few days of work, this usually takes a while.
During this process, also check:
- that all additions are marked with "next release" on the wiki,
- if anything needs updating in `README.md`.
## Bump version
We use `year.month.patch` versioning.
If the month contains a leading zero, drop it from the crate version (Cargo requirement).
You can use the command from [cargo-edit](https://github.com/killercup/cargo-edit):
```
cargo set-version 26.4.0
```
Then, manually update version in:
- `[package.metadata.generate-rpm]` in Cargo.toml
- Dependency example in `niri-ipc/README.md`
- Dependency example in `niri-ipc/src/lib.rs`
Do a full text search for the old version to make sure there are no other places.
## Replace all "Since: next release" mentions
Do a full text search for `next release`, replace everything with the new version number.
## Build, test, push, and have the CI run
Run all tests:
```
RUN_SLOW_TESTS=1 cargo test --release --all
```
- Run `cargo package -p niri-ipc` and make sure it succeeds.
- Make sure the CI passes.
- Make sure the niri-git COPR build passes.
## Trigger the "Prepare release" workflow on GitHub Actions
Set the "Public version" input to a version like `26.04`.
This workflow will:
- do some pre-release checks like grepping the wiki for "next version",
- make a vendored dependency archive,
- build and test niri with that dependency archive,
- draft a new GitHub release with the archive attached.
It will NOT override an existing draft release with the same name so the release notes are safe.
Make sure it succeeds and grab the vendored dependency archive that it produces.
## Update the niri COPR spec, update licenses in .spec.rpkg
You can grab the previous spec from [the last build](https://copr.fedorainfracloud.org/coprs/yalter/niri/builds/) in the COPR.
- Update version global to `26.04`.
- Update commit global to the commit hash corresponding to the release commit.
You can use `git rev-parse HEAD`.
- Reset the `Release:` number to 1 if it was higher.
To run a test build, you can download the vendored dependency archive from the last step.
Comment/uncomment `Source:` and `%autosetup` lines accordingly.
Download the source files:
```
spectool -g niri.spec
```
Build RPMs:
```
fedpkg --release 44 mockbuild
```
During the build, it will print the list of licenses.
Update it in both the COPR spec and in `niri.spec.rpkg` accordingly.
If you had to update `niri.spec.rpkg` and therefore make another commit to the niri repo, make sure to update the commit hash in the COPR spec again.
Revert any temporary changes that you did to the COPR spec for local testing.
## Create and push the release git tag
The tag starts with a `v`:
```
git tag -am "v26.04 release" v26.04
git push origin v26.04
```
While you can let GitHub create the tag automatically upon creating the release, this is not recommended.
GitHub creates a *lightweight* tag, but we want an annotated tag that plays better with various tooling.
## Publish the release on GitHub
- Either upload the vendored dependencies file to your draft release with the release notes, or move the release notes to the GitHub-created release (the difference is that it's attributed to github-actions).
- Set the tag to `v26.04`.
- Set the release title to `v26.04`.
- Check "Create a discussion for this release".
## Publish the niri-ipc crate
```
cargo publish -p niri-ipc
```
## Kick off the COPR build
Upload on the web or:
```
copr-cli build niri niri.spec
```
## Announce the release
Chat rooms, social media, etc.
+19 -7
View File
@@ -40,6 +40,20 @@ hotkey-overlay {
}
```
### How to fix lag on external monitors connected to a hybrid GPU laptop?
Hybrid GPU laptops (which have both an integrated and a discrete GPU) generally connect the external monitor port to the discrete GPU.
Meanwhile, the built-in monitor is connected to the integrated GPU, and the integrated GPU is used for rendering by default.
This is good and expected because the integrated GPU uses significantly less battery compared to the discrete GPU.
However, this means that niri has to render the external monitor contents on the integrated GPU, then copy them over to the discrete GPU for display.
On some laptops this can cause lag and stuttering (it gets worse with monitor resolution and refresh rate).
If your laptop has a MUX switch—usually a GPU toggle in the UEFI settings—then you can switch it to use the discrete GPU, then niri will render on the discrete GPU, and the external monitor won't lag.
Otherwise, you can try configuring niri to render on the discrete GPU via the [`render-drm-device`](./Configuration:-Debug-Options.md#render-drm-device) debug option.
Keep in mind that using the discrete GPU for rendering will make the laptop's battery deplete much faster.
### How to run X11 apps like Steam or Discord?
To run X11 apps, you can use [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite).
@@ -66,14 +80,12 @@ I wouldn't be too surprised if, down the road, xwayland-satellite becomes the st
### Can I enable blur behind semitransparent windows?
Not yet, follow/upvote [this issue](https://github.com/YaLTeR/niri/issues/54).
There's also [a PR](https://github.com/YaLTeR/niri/pull/1634) adding blur to niri which you can build and run manually.
Keep in mind that it's an experimental implementation that may have problems and performance concerns.
<sup>Since: 26.04</sup> Yes.
See the [window effects](./Window-Effects.md) wiki page.
### Can I make a window sticky / pinned / always on top / appear on all workspaces?
Not yet, follow/upvote [this issue](https://github.com/YaLTeR/niri/issues/932).
Not yet, follow/upvote [this issue](https://github.com/niri-wm/niri/issues/932).
You can emulate this with a script that uses the niri IPC.
For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature (`toggle-follow-mode`).
@@ -82,7 +94,7 @@ For example, [nirius](https://git.sr.ht/~tsdh/nirius) seems to have this feature
Firefox seems to first open the Bitwarden window with a generic Firefox title, and only later change the window title to Bitwarden, so you can't effectively target it with an `open-floating` window rule.
You'll need to use a script, for example [this one](https://github.com/YaLTeR/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
You'll need to use a script, for example [this one](https://github.com/niri-wm/niri/discussions/1599) or other ones (search niri issues and discussions for Bitwarden).
### Can I open a window directly in the current column / in the same column as another window?
@@ -92,7 +104,7 @@ Listen to the event stream for a new window opening, then call an action like `c
Adding this directly to niri is challenging:
- The act of "opening a window directly in some column" by itself is quite involved. Niri will have to compute the exact initial window size provided how other windows in a column would resize in response. This logic exists, but it isn't directly pluggable to the code computing a size for a new window. Then, it'll need to handle all sorts of edge cases like the column disappearing, or new windows getting added to the column, before the target window had a chance to appear.
- How do you indicate if a new window should spawn in an existing column (and in which one), as opposed to a new column? Different people seem to have different needs here (including very complex rules based on parent PID, etc.), and it's very unclear design-wise what kind of (simple) setting is actually needed and would be useful. See also https://github.com/YaLTeR/niri/discussions/1125.
- How do you indicate if a new window should spawn in an existing column (and in which one), as opposed to a new column? Different people seem to have different needs here (including very complex rules based on parent PID, etc.), and it's very unclear design-wise what kind of (simple) setting is actually needed and would be useful. See also https://github.com/niri-wm/niri/discussions/1125.
### Why does moving the mouse against a monitor edge focus the next window, but only sometimes?
+1 -1
View File
@@ -16,7 +16,7 @@ You can make a window open in a maximized column with the [`open-maximized true`
<sup>Since: 25.11</sup>
You can maximize an individual window via `maximize-window-to-edges`.
You can maximize an individual window via `maximize-window-to-edges` (bound to <kbd>Mod</kbd><kbd>M</kbd> by default).
This is the same maximize as you can find on other desktop environments and operating systems: it expands a window to the edges of the available screen area.
You will still see your bar, but not struts, gaps, or borders.
+4 -8
View File
@@ -9,10 +9,9 @@ sudo dnf install niri dms
systemctl --user add-wants niri.service dms
```
Arch Linux (via [paru](https://github.com/morganamilo/paru)):
Arch Linux:
```
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty
paru -S dms-shell-bin matugen cava qt6-multimedia-ffmpeg
sudo pacman -Syu niri xwayland-satellite xdg-desktop-portal-gnome xdg-desktop-portal-gtk alacritty dms-shell-niri matugen cava qt6-multimedia-ffmpeg
systemctl --user add-wants niri.service dms
```
@@ -147,13 +146,10 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>PageUp</kbd> | Move the focused column to the workspace above |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>U</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageDown</kbd> | Move the focused workspace down |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>I</kbd> or <kbd>Mod</kbd><kbd>Shift</kbd><kbd>PageUp</kbd> | Move the focused workspace up |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the bottom window in the focused column into its own column |
| <kbd>Mod</kbd><kbd>[</kbd> | Consume or expel the focused window to the left |
| <kbd>Mod</kbd><kbd>]</kbd> | Consume or expel the focused window to the right |
| <kbd>Mod</kbd><kbd>R</kbd> | Toggle between preset column widths |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column heights |
| <kbd>Mod</kbd><kbd>F</kbd> | Maximize column |
| <kbd>Mod</kbd><kbd>R</kbd> and <kbd>Mod</kbd><kbd>Shift</kbd><kbd>R</kbd> | Toggle between preset column widths forward and back |
| <kbd>Mod</kbd><kbd>M</kbd> | Maximize window |
| <kbd>Mod</kbd><kbd>C</kbd> | Center column within view |
| <kbd>Mod</kbd><kbd>-</kbd> | Decrease column width by 10% |
| <kbd>Mod</kbd><kbd>=</kbd> | Increase column width by 10% |
+2 -2
View File
@@ -26,7 +26,7 @@ To get a taste of the events, run `niri msg event-stream`.
Though, this is more of a debug function than anything.
You can get raw events from `niri msg --json event-stream`, or by connecting to the niri socket and requesting an event stream manually.
You can find the full list of events along with documentation [here](https://yalter.github.io/niri/niri_ipc/enum.Event.html).
You can find the full list of events along with documentation [here](https://niri-wm.github.io/niri/niri_ipc/enum.Event.html).
### Programmatic Access
@@ -57,7 +57,7 @@ $ env NIRI_SOCKET=./temp.sock niri msg action focus-workspace 2
{"Action":{"FocusWorkspace":{"reference":{"Index":2}}}}
```
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://yalter.github.io/niri/niri_ipc/).
You can find all available requests and response types in the [niri-ipc sub-crate documentation](https://niri-wm.github.io/niri/niri_ipc/).
### Backwards Compatibility
+7 -3
View File
@@ -4,14 +4,18 @@ First, for creating a niri package, see the [Packaging](./Packaging-niri.md) pag
### Configuration
Niri will load configuration from `$XDG_CONFIG_HOME/niri/config.kdl` or `~/.config/niri/config.kdl`, falling back to `/etc/niri/config.kdl`.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
If both of these files are missing, niri will create `$XDG_CONFIG_HOME/niri/config.kdl` with the contents of [the default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl), which are embedded into the niri binary at build time.
This means that you can customize your distribution defaults by creating `/etc/niri/config.kdl`.
When this file is present, niri *will not* automatically create a config at `~/.config/niri/`, so you'll need to direct your users how to do it themselves.
Keep in mind that we update the default config in new releases, so if you have a custom `/etc/niri/config.kdl`, you likely want to inspect and apply the relevant changes too.
You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
The default configuration locations can be overridden with the `NIRI_CONFIG` environment variable.
<sup>Since: 26.04</sup> You can also change the configuration path at runtime via the niri IPC or using the command `niri msg action load-config-file --path <path-to-config.kdl>`.
<sup>Since: 25.11</sup> You can split the niri config file into multiple files using [`include`](./Configuration:-Include.md).
### Xwayland
@@ -32,7 +36,7 @@ Make sure your system installer sets the keyboard layout via systemd-localed, an
### Autostart
Niri works with the normal systemd autostart.
The default [niri.service](https://github.com/YaLTeR/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
The default [niri.service](https://github.com/niri-wm/niri/blob/main/resources/niri.service) brings up `graphical-session.target` as well as `xdg-desktop-autostart.target`.
To make a program run at niri startup without editing the niri config, you can either link its .desktop to `~/.config/autostart/`, or use a .service file with `WantedBy=graphical-session.target`.
See the [example systemd setup](./Example-systemd-Setup.md) page for some examples.
+1 -1
View File
@@ -47,7 +47,7 @@ Set them as automatically-installed optional dependencies, if possible.
Working hardware acceleration is required for running niri.
- Some notification daemon like `mako`, generally required for apps to work correctly.
Finally, you may want to auto-install some of the applications bound in niri's [default configuration file](https://github.com/YaLTeR/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
Finally, you may want to auto-install some of the applications bound in niri's [default configuration file](https://github.com/niri-wm/niri/blob/main/resources/default-config.kdl) (search for `spawn`), such as `alacritty` and `fuzzel`.
### Running tests
+2 -2
View File
@@ -4,9 +4,9 @@ Feel free to look through usage and [Getting started](./Getting-Started.md).
If you're looking for ways to configure niri, check out the [introduction to configuration](./Configuration:-Introduction.md).
If you'd like to help with niri, there are plenty of both coding- and non-coding-related ways to do so.
See [CONTRIBUTING.md](https://github.com/YaLTeR/niri/blob/main/CONTRIBUTING.md) for an overview.
See [CONTRIBUTING.md](https://github.com/niri-wm/niri/blob/main/CONTRIBUTING.md) for an overview.
If you're not already here, check out our new wiki website! https://yalter.github.io/niri/
If you're not already here, check out our new wiki website! https://niri-wm.github.io/niri/
---
+87
View File
@@ -0,0 +1,87 @@
### Overview
<sup>Since: 26.04</sup>
You can apply background effects to windows and layer-shell surfaces.
These include blur, xray, saturation, and noise.
They can be enabled in the `background-effect {}` section of [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rules.
![Screenshot with blur](./img/blur.png)
The window needs to be semitransparent for you to see the background effect (otherwise it's fully covered by the opaque window).
Focus ring and border can also cover the background effect, see [this FAQ entry](./FAQ.md#why-are-transparent-windows-tinted-why-is-the-borderfocus-ring-showing-up-through-semitransparent-windows) for how to change this.
### Blur
Windows and layer surfaces can request their background to be blurred via the [`ext-background-effect` protocol](https://wayland.app/protocols/ext-background-effect-v1).
In this case, the application will usually offer some "background blur" setting that you'll need to enable in its configuration.
You can also enable blur on the niri side with the `blur true` background effect window rule:
```kdl
// Enable blur behind the Alacritty terminal.
window-rule {
match app-id="^Alacritty$"
background-effect {
blur true
}
}
// Enable blur behind the fuzzel launcher.
layer-rule {
match namespace="^launcher$"
background-effect {
blur true
}
}
```
Blur enabled via the window rule will follow the window corner radius set via [`geometry-corner-radius`](./Configuration:-Window-Rules.md#geometry-corner-radius).
On the other hand, blur enabled through `ext-background-effect` will exactly follow the shape requested by the window.
If the window or layer has clientside rounded corners or other complex shape, it should set a corresponding blur shape through `ext-background-effect`, then it will get correctly shaped background blur without any manual niri configuration.
Windows can also blur their pop-up menus using `ext-background-effect`.
On the niri side, you can do it with a `popups` block inside [`window-rule`](./Configuration:-Window-Rules.md#popups) and [`layer-rule`](./Configuration:-Layer-Rules.md#popups).
See those wiki pages for examples and limitations.
Global blur settings are configured in the [`blur {}` config section](./Configuration:-Miscellaneous.md#blur) and apply to all background blur.
### Xray
Xray makes the window background "see through" to your wallpaper, ignoring any other windows below.
You can enable it with `xray true` background effect [window](./Configuration:-Window-Rules.md#background-effect) or [layer](./Configuration:-Layer-Rules.md#background-effect) rule.
Xray is automatically enabled by default if any other background effect (like blur) is active.
This is because it's much more efficient: with xray active, niri only needs to blur the background once, and then can reuse this blurred version with no extra work (since the wallpaper changes very rarely).
If you have an animated wallpaper, xray will still have to recompute blur every frame, but that happens once and shared among all windows, rather than recomputed separately for each window.
#### Non-xray effects (experimental)
You can disable xray with `xray false` background effect window rule.
This gives you the normal kind of blur where everything below a window is blurred.
Keep in mind that non-xray blur and other non-xray effects are more expensive as niri has to recompute them any time you move the window, or the contents underneath change.
> [!WARNING]
> Non-xray effects are currently experimental because they have some known limitations.
>
> - They disappear during window open/close animations and while dragging a tiled window.
> Fixing this requires a refactor to the niri rendering code to defer offscreen rendering, and possibly other refactors.
### Implementation notes
The `ext-background-effect` protocol supports any wl_surface.
We currently implement it only for toplevels, layer surfaces, and pop-ups, which should cover the vast majority of what's actually used by applications.
For pop-ups, effects default to *non-xray* because pop-ups generally appear on top of windows.
In particular, the following surface types don't support `ext-background-effect`.
They can be implemented as the need arises.
- Subsurfaces. Would require implementing `clip-to-geometry` support for background effects.
- Lock surfaces. Not useful as it would just show our red locked session background.
- Cursor and drag-and-drop icon.
The main challenge here will be screencasts where the cursor is rendered separately.
This is problematic because non-xray effects require rendering the whole scene in one go rather than separately.
+1 -1
View File
@@ -47,7 +47,7 @@ It will open as a new window.
This method involves invoking XWayland directly and running it as its own window, it also requires an extra X11 window manager running inside it.
![Xwayland running in rootful mode.](https://github.com/YaLTeR/niri/assets/1794388/b64e96c4-a0bb-4316-94a0-ff445d4c7da7)
![Xwayland running in rootful mode.](https://github.com/niri-wm/niri/assets/1794388/b64e96c4-a0bb-4316-94a0-ff445d4c7da7)
Here's how you do it:
+2
View File
@@ -14,6 +14,7 @@
* [Xwayland](./Xwayland.md)
* [Gestures](./Gestures.md)
* [Fullscreen and Maximize](./Fullscreen-and-Maximize.md)
* [Window Effects](./Window-Effects.md)
* [Packaging niri](./Packaging-niri.md)
* [Integrating niri](./Integrating-niri.md)
* [Accessibility](./Accessibility.md)
@@ -41,6 +42,7 @@
* [Design Principles](./Development:-Design-Principles.md)
* [Developing niri](./Development:-Developing-niri.md)
* [Documenting niri](./Development:-Documenting-niri.md)
* [Releasing niri](./Development:-Releasing-niri.md)
* [Fractional Layout](./Development:-Fractional-Layout.md)
* [Redraw Loop](./Development:-Redraw-Loop.md)
* [Animation Timing](./Development:-Animation-Timing.md)
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:44cbf3144d7add741762a8034779602f6a94b2b99a6d62774c9d75f3606b98b1
size 1582702
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5a63ea3cc2f158e175c00dd058988a2bbf676e2a2aac5c2ef1603bd983589d5
size 166777
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bef0c57d617916bf6014fe08e268c8201d7f6ef682e3aea3395e76116b1d0400
size 56936
+5 -4
View File
@@ -20,6 +20,7 @@
rust-overlay,
}:
let
revision = self.shortRev or self.dirtyShortRev or "unknown";
niri-package =
{
lib,
@@ -46,7 +47,7 @@
rustPlatform.buildRustPackage {
pname = "niri";
version = self.shortRev or self.dirtyShortRev or "unknown";
version = revision;
src = lib.fileset.toSource {
root = ./.;
@@ -107,7 +108,7 @@
buildNoDefaultFeatures = true;
# ever since this commit:
# https://github.com/YaLTeR/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# https://github.com/niri-wm/niri/commit/771ea1e81557ffe7af9cbdbec161601575b64d81
# niri now runs an actual instance of the real compositor (with a mock backend) during tests
# and thus creates a real socket file in the runtime dir.
# this is fine for our build, we just need to make sure it has a directory to write to.
@@ -148,7 +149,7 @@
"-Wl,--pop-state"
]
);
NIRI_BUILD_COMMIT = self.shortRev;
NIRI_BUILD_COMMIT = revision;
};
passthru = {
@@ -157,7 +158,7 @@
meta = {
description = "Scrollable-tiling Wayland compositor";
homepage = "https://github.com/YaLTeR/niri";
homepage = "https://github.com/niri-wm/niri";
license = lib.licenses.gpl3Only;
mainProgram = "niri";
platforms = lib.platforms.linux;
+3 -3
View File
@@ -9,11 +9,11 @@ repository.workspace = true
[dependencies]
bitflags.workspace = true
csscolorparser = "0.8.1"
csscolorparser = "0.8.3"
knuffel = "3.2.0"
miette = { version = "5.10.0", features = ["fancy-no-backtrace"] }
niri-ipc = { version = "25.11.0", path = "../niri-ipc" }
regex = "1.12.2"
niri-ipc = { version = "26.4.0", path = "../niri-ipc" }
regex = "1.12.3"
smithay = { workspace = true, features = ["backend_libinput"] }
tracing.workspace = true
tracy-client.workspace = true
+97
View File
@@ -1006,6 +1006,103 @@ where
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Blur {
pub off: bool,
pub passes: u8,
pub offset: f64,
pub noise: f64,
pub saturation: f64,
}
impl Default for Blur {
fn default() -> Self {
Self {
off: false,
passes: 3,
offset: 3.,
noise: 0.02,
saturation: 1.5,
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct BlurPart {
#[knuffel(child)]
pub off: bool,
#[knuffel(child)]
pub on: bool,
#[knuffel(child, unwrap(argument))]
pub passes: Option<u8>,
#[knuffel(child, unwrap(argument))]
pub offset: Option<FloatOrInt<0, 100>>,
#[knuffel(child, unwrap(argument))]
pub noise: Option<FloatOrInt<0, 1000>>,
#[knuffel(child, unwrap(argument))]
pub saturation: Option<FloatOrInt<0, 1000>>,
}
impl MergeWith<BlurPart> for Blur {
fn merge_with(&mut self, part: &BlurPart) {
self.off |= part.off;
if part.on {
self.off = false;
}
merge_clone!((self, part), passes);
merge!((self, part), offset, noise, saturation);
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct BackgroundEffectRule {
#[knuffel(child, unwrap(argument))]
pub xray: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub blur: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub noise: Option<FloatOrInt<0, 1000>>,
#[knuffel(child, unwrap(argument))]
pub saturation: Option<FloatOrInt<0, 1000>>,
}
/// Resolved background effect rule.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct BackgroundEffect {
/// Whether to render with xray effect (see through).
///
/// - `None`: xray if any background effect is active
/// - `Some(false)`: no xray
/// - `Some(true)`: xray even if no other background effect is active
pub xray: Option<bool>,
/// Whether to blur the background.
///
/// - `None`: blur when the window/layer requests it (e.g. through ext-background-effect
/// protocol)
/// - `Some(false)`: never blur
/// - `Some(true)`: always blur
pub blur: Option<bool>,
pub noise: Option<f64>,
pub saturation: Option<f64>,
}
impl MergeWith<BackgroundEffectRule> for BackgroundEffect {
fn merge_with(&mut self, part: &BackgroundEffectRule) {
merge_clone_opt!((self, part), xray, blur);
if let Some(x) = part.noise {
self.noise = Some(x.0);
}
if let Some(x) = part.saturation {
self.saturation = Some(x.0);
}
}
}
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, assert_snapshot};
+26 -38
View File
@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::str::FromStr;
use std::time::Duration;
@@ -368,7 +369,7 @@ pub enum Action {
#[knuffel(skip)]
UnsetWindowUrgent(u64),
#[knuffel(skip)]
LoadConfigFile,
LoadConfigFile(#[knuffel(argument)] Option<String>),
#[knuffel(skip)]
MruAdvance {
direction: MruDirection,
@@ -699,7 +700,7 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ToggleWindowUrgent { id } => Self::ToggleWindowUrgent(id),
niri_ipc::Action::SetWindowUrgent { id } => Self::SetWindowUrgent(id),
niri_ipc::Action::UnsetWindowUrgent { id } => Self::UnsetWindowUrgent(id),
niri_ipc::Action::LoadConfigFile {} => Self::LoadConfigFile,
niri_ipc::Action::LoadConfigFile { path } => Self::LoadConfigFile(path),
}
}
}
@@ -769,7 +770,7 @@ where
) -> Result<Self, DecodeError<S>> {
expect_only_children(node, ctx);
let mut seen_keys = HashSet::new();
let mut seen_keys: HashMap<Key, &knuffel::ast::SpannedNode<S>> = HashMap::new();
let mut binds = Vec::new();
@@ -779,39 +780,26 @@ where
ctx.emit_error(e);
}
Ok(bind) => {
if seen_keys.insert(bind.key) {
binds.push(bind);
} else {
// ideally, this error should point to the previous instance of this keybind
//
// i (sodiboo) have tried to implement this in various ways:
// miette!(), #[derive(Diagnostic)]
// DecodeError::Custom, DecodeError::Conversion
// nothing seems to work, and i suspect it's not possible.
//
// DecodeError is fairly restrictive.
// even DecodeError::Custom just wraps a std::error::Error
// and this erases all rich information from miette. (why???)
//
// why does knuffel do this?
// from what i can tell, it doesn't even use DecodeError for much.
// it only ever converts them to a Report anyways!
// https://github.com/tailhook/knuffel/blob/c44c6b0c0f31ea6d1174d5d2ed41064922ea44ca/src/wrappers.rs#L55-L58
//
// besides like, allowing downstream users (such as us!)
// to match on parse failure, i don't understand why
// it doesn't just use a generic error type
//
// even the matching isn't consistent,
// because errors can also be omitted as ctx.emit_error.
// why does *that one* especially, require a DecodeError?
//
// anyways if you can make it format nicely, definitely do fix this
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"keybind",
"duplicate keybind",
));
match seen_keys.entry(bind.key) {
Entry::Occupied(entry) => {
// Even though it's technically incorrect, we use
// `DecodeError::Missing` here because it labels the bind with
// "node starts here", which is the least bad option
ctx.emit_error(DecodeError::missing(
entry.get(),
"keybind first defined here",
));
ctx.emit_error(DecodeError::unexpected(
&child.node_name,
"keybind",
"duplicate keybind later defined here",
));
}
Entry::Vacant(entry) => {
entry.insert(child);
binds.push(bind);
}
}
}
}
@@ -1034,7 +1022,7 @@ impl FromStr for Key {
// [0]: https://github.com/xkbcommon/libxkbcommon/blob/45a118d5325b051343b4b174f60c1434196fa7d4/src/keysym.c#L276
// [1]: https://docs.rs/xkbcommon/latest/xkbcommon/xkb/keysyms/index.html#:~:text=KEY%5FXF86ScreenSaver
//
// See https://github.com/YaLTeR/niri/issues/1969
// See https://github.com/niri-wm/niri/issues/1969
if keysym == Keysym::XF86_Screensaver {
keysym = keysym_from_name(key, KEYSYM_NO_FLAGS);
if keysym.raw() == KEY_NoSymbol {
+2
View File
@@ -364,6 +364,8 @@ pub struct Tablet {
#[knuffel(child, unwrap(argument))]
pub map_to_output: Option<String>,
#[knuffel(child)]
pub map_to_focused_output: bool,
#[knuffel(child)]
pub left_handed: bool,
}
+8 -1
View File
@@ -1,5 +1,6 @@
use crate::appearance::{BlockOutFrom, CornerRadius, ShadowRule};
use crate::appearance::{BackgroundEffectRule, BlockOutFrom, CornerRadius, ShadowRule};
use crate::utils::RegexEq;
use crate::window_rule::PopupsRule;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct LayerRule {
@@ -20,6 +21,10 @@ pub struct LayerRule {
pub place_within_backdrop: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub baba_is_float: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
#[knuffel(child, default)]
pub popups: PopupsRule,
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -28,4 +33,6 @@ pub struct Match {
pub namespace: Option<RegexEq>,
#[knuffel(property)]
pub at_startup: Option<bool>,
#[knuffel(property, str)]
pub layer: Option<niri_ipc::Layer>,
}
+64 -4
View File
@@ -59,7 +59,9 @@ use crate::recent_windows::RecentWindowsPart;
pub use crate::recent_windows::{MruDirection, MruFilter, MruPreviews, MruScope, RecentWindows};
pub use crate::utils::FloatOrInt;
use crate::utils::{Flag, MergeWith as _};
pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule};
pub use crate::window_rule::{
FloatingPosition, PopupsRule, RelativeTo, ResolvedPopupsRules, WindowRule,
};
pub use crate::workspace::{Workspace, WorkspaceLayoutPart};
const RECURSION_LIMIT: u8 = 10;
@@ -78,6 +80,7 @@ pub struct Config {
pub hotkey_overlay: HotkeyOverlay,
pub config_notification: ConfigNotification,
pub animations: Animations,
pub blur: Blur,
pub gestures: Gestures,
pub overview: Overview,
pub environment: Environment,
@@ -194,6 +197,7 @@ where
"hotkey-overlay" => m_merge!(hotkey_overlay),
"config-notification" => m_merge!(config_notification),
"animations" => m_merge!(animations),
"blur" => m_merge!(blur),
"gestures" => m_merge!(gestures),
"overview" => m_merge!(overview),
"xwayland-satellite" => m_merge!(xwayland_satellite),
@@ -336,12 +340,26 @@ where
));
}
let base = ctx.get::<BasePath>().unwrap();
let path = base.0.join(path);
// We use DecodeError::Missing throughout this block because it results in the
// least confusing error messages while still allowing to provide a span.
// Expand ~ into the home dir
let path = if let Ok(rest) = path.strip_prefix("~") {
let Some(home) = std::env::home_dir() else {
ctx.emit_error(DecodeError::missing(
node,
format!("error retrieving home directory to expand {path:?}"),
));
continue;
};
home.join(rest)
} else {
// Otherwise, use the current include base dir
let base = ctx.get::<BasePath>().unwrap();
base.0.join(path)
};
let recursion = ctx.get::<Recursion>().unwrap().0 + 1;
if recursion == RECURSION_LIMIT {
ctx.emit_error(DecodeError::missing(
@@ -701,6 +719,7 @@ mod tests {
tablet {
map-to-output "eDP-1"
map-to-focused-output
calibration-matrix 1.0 2.0 3.0 \
4.0 5.0 6.0
}
@@ -1093,6 +1112,7 @@ mod tests {
map_to_output: Some(
"eDP-1",
),
map_to_focused_output: true,
left_handed: false,
},
touch: Touch {
@@ -1616,6 +1636,13 @@ mod tests {
},
),
},
blur: Blur {
off: false,
passes: 3,
offset: 3.0,
noise: 0.02,
saturation: 1.5,
},
gestures: Gestures {
dnd_edge_view_scroll: DndEdgeViewScroll {
trigger_width: 10.0,
@@ -1845,6 +1872,22 @@ mod tests {
),
scroll_factor: None,
tiled_state: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
popups: PopupsRule {
opacity: None,
geometry_corner_radius: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
},
},
],
layer_rules: [
@@ -1859,6 +1902,7 @@ mod tests {
),
),
at_startup: None,
layer: None,
},
],
excludes: [],
@@ -1879,6 +1923,22 @@ mod tests {
geometry_corner_radius: None,
place_within_backdrop: None,
baba_is_float: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
popups: PopupsRule {
opacity: None,
geometry_corner_radius: None,
background_effect: BackgroundEffectRule {
xray: None,
blur: None,
noise: None,
saturation: None,
},
},
},
],
binds: Binds(
+45 -2
View File
@@ -1,8 +1,11 @@
use niri_ipc::ColumnDisplay;
use crate::appearance::{BlockOutFrom, BorderRule, CornerRadius, ShadowRule, TabIndicatorRule};
use crate::appearance::{
BackgroundEffect, BackgroundEffectRule, BlockOutFrom, BorderRule, CornerRadius, ShadowRule,
TabIndicatorRule,
};
use crate::layout::DefaultPresetSize;
use crate::utils::RegexEq;
use crate::utils::{MergeWith, RegexEq};
use crate::FloatOrInt;
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
@@ -72,6 +75,46 @@ pub struct WindowRule {
pub scroll_factor: Option<FloatOrInt<0, 100>>,
#[knuffel(child, unwrap(argument))]
pub tiled_state: Option<bool>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
#[knuffel(child, default)]
pub popups: PopupsRule,
}
/// Rules for popup surfaces.
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
pub struct PopupsRule {
#[knuffel(child, unwrap(argument))]
pub opacity: Option<f32>,
#[knuffel(child)]
pub geometry_corner_radius: Option<CornerRadius>,
#[knuffel(child, default)]
pub background_effect: BackgroundEffectRule,
}
/// Resolved popup-specific rules.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct ResolvedPopupsRules {
/// Extra opacity to draw popups with.
pub opacity: Option<f32>,
/// Corner radius to assume the popups have.
pub geometry_corner_radius: Option<CornerRadius>,
/// Background effect configuration for popups.
pub background_effect: BackgroundEffect,
}
impl MergeWith<PopupsRule> for ResolvedPopupsRules {
fn merge_with(&mut self, part: &PopupsRule) {
if let Some(x) = part.opacity {
self.opacity = Some(x);
}
if let Some(x) = part.geometry_corner_radius {
self.geometry_corner_radius = Some(x);
}
self.background_effect.merge_with(&part.background_effect);
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq)]
+1 -1
View File
@@ -13,7 +13,7 @@ readme = "README.md"
[dependencies]
clap = { workspace = true, optional = true }
schemars = { version = "1.2.0", optional = true }
schemars = { version = "1.2.1", optional = true }
serde.workspace = true
serde_json.workspace = true
+2 -2
View File
@@ -1,6 +1,6 @@
# niri-ipc
Types and helpers for interfacing with the [niri](https://github.com/YaLTeR/niri) Wayland compositor.
Types and helpers for interfacing with the [niri](https://github.com/niri-wm/niri) Wayland compositor.
## Backwards compatibility
@@ -12,5 +12,5 @@ Use an exact version requirement to avoid breaking changes:
```toml
[dependencies]
niri-ipc = "=25.11.0"
niri-ipc = "=26.4.0"
```
+22 -2
View File
@@ -41,7 +41,7 @@
//!
//! ```toml
//! [dependencies]
//! niri-ipc = "=25.11.0"
//! niri-ipc = "=26.4.0"
//! ```
//!
//! ## Features
@@ -936,7 +936,13 @@ pub enum Action {
///
/// Can be useful for scripts changing the config file, to avoid waiting the small duration for
/// niri's config file watcher to notice the changes.
LoadConfigFile {},
LoadConfigFile {
/// Path of a new config file to load.
///
/// If unset, reloads the current config file.
#[cfg_attr(feature = "clap", arg(long))]
path: Option<String>,
},
}
/// Change in window or column size.
@@ -1862,6 +1868,20 @@ impl FromStr for Transform {
}
}
impl FromStr for Layer {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"background" => Ok(Self::Background),
"bottom" => Ok(Self::Bottom),
"top" => Ok(Self::Top),
"overlay" => Ok(Self::Overlay),
_ => Err("invalid layer, can be \"background\", \"bottom\", \"top\" or \"overlay\""),
}
}
}
impl FromStr for ModeToSet {
type Err = &'static str;
+2 -2
View File
@@ -11,8 +11,8 @@ repository.workspace = true
adw = { version = "0.8.1", package = "libadwaita", features = ["v1_4"] }
anyhow.workspace = true
gtk = { version = "0.10.3", package = "gtk4", features = ["v4_12"] }
niri = { version = "25.11.0", path = ".." }
niri-config = { version = "25.11.0", path = "../niri-config" }
niri = { version = "26.4.0", path = ".." }
niri-config = { version = "26.4.0", path = "../niri-config" }
smithay.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
+7 -4
View File
@@ -3,7 +3,7 @@ use std::time::Duration;
use niri::animation::Clock;
use niri::layout::{ActivateWindow, AddWindowTarget, LayoutElement as _, Options, SizingMode};
use niri::render_helpers::RenderTarget;
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::{Color, OutputName, PresetSize};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -270,12 +270,15 @@ impl TestCase for Layout {
self.layout.update_render_elements(Some(&self.output));
let mut rv = Vec::new();
let ctx = RenderCtx {
renderer,
target: RenderTarget::Output,
xray: None,
};
self.layout
.monitor_for_output(&self.output)
.unwrap()
.render_workspaces(renderer, RenderTarget::Output, true, &mut |elem| {
rv.push(Box::new(elem) as _)
});
.render_workspaces(ctx, true, &mut |elem| rv.push(Box::new(elem) as _));
rv
}
}
+11 -7
View File
@@ -2,7 +2,8 @@ use std::rc::Rc;
use std::time::Duration;
use niri::layout::Options;
use niri::render_helpers::RenderTarget;
use niri::render_helpers::xray::XrayPos;
use niri::render_helpers::{RenderCtx, RenderTarget};
use niri_config::Color;
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -121,13 +122,16 @@ impl TestCase for Tile {
);
let mut rv = Vec::new();
self.tile.render(
let ctx = RenderCtx {
renderer,
location,
true,
RenderTarget::Output,
&mut |elem| rv.push(Box::new(elem) as _),
);
target: RenderTarget::Output,
xray: None,
};
let xray_pos = XrayPos::new(location, 1.);
self.tile
.render(ctx, location, xray_pos, true, &mut |elem| {
rv.push(Box::new(elem) as _)
});
rv
}
}
+9 -8
View File
@@ -1,5 +1,5 @@
use niri::layout::{LayoutElement, SizingMode};
use niri::render_helpers::RenderTarget;
use niri::render_helpers::{RenderCtx, RenderTarget};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Physical, Point, Scale, Size};
@@ -53,14 +53,15 @@ impl TestCase for Window {
.downscale(2.);
let mut rv = Vec::new();
self.window.render_normal(
let ctx = RenderCtx {
renderer,
location,
Scale::from(1.),
1.,
RenderTarget::Output,
&mut |elem| rv.push(Box::new(elem) as _),
);
target: RenderTarget::Output,
xray: None,
};
self.window
.render_normal(ctx, location, Scale::from(1.), 1., &mut |elem| {
rv.push(Box::new(elem) as _)
});
rv
}
}
+9 -1
View File
@@ -20,6 +20,7 @@ mod imp {
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer};
use smithay::reexports::gbm::Format as Fourcc;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Physical, Rectangle, Scale, Transform};
use super::*;
@@ -206,8 +207,15 @@ mod imp {
if let Some(mut damage) = rect.intersection(dst) {
damage.loc -= dst.loc;
let cache = UserDataMap::new();
if element.is_framebuffer_effect() {
element
.capture_framebuffer(&mut frame, src, dst, &cache)
.context("error in capture_framebuffer()")?;
}
element
.draw(&mut frame, src, dst, &[damage], &[])
.draw(&mut frame, src, dst, &[damage], &[], Some(&cache))
.context("error drawing element")?;
}
}
+2 -3
View File
@@ -9,7 +9,7 @@ use niri::layout::{
use niri::render_helpers::offscreen::OffscreenData;
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::RenderTarget;
use niri::render_helpers::RenderCtx;
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::Kind;
@@ -151,11 +151,10 @@ impl LayoutElement for TestWindow {
fn render_normal<R: NiriRenderer>(
&self,
_renderer: &mut R,
_ctx: RenderCtx<R>,
location: Point<f64, Logical>,
_scale: Scale<f64>,
alpha: f32,
_target: RenderTarget,
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
) {
let inner = self.inner.borrow();
+2 -4
View File
@@ -38,7 +38,6 @@ SourceLicense: GPL-3.0-or-later
# 0BSD OR MIT OR Apache-2.0
# Apache-2.0
# Apache-2.0 AND MIT
# Apache-2.0 OR BSL-1.0
# Apache-2.0 OR MIT
# Apache-2.0 OR MIT OR Unlicense
# Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT
@@ -53,14 +52,13 @@ SourceLicense: GPL-3.0-or-later
# MIT OR Apache-2.0 OR Zlib
# MIT OR Zlib OR Apache-2.0
# MPL-2.0
# Unicode-3.0
# Unlicense OR MIT
# Zlib
# Zlib OR Apache-2.0 OR MIT
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND (Apache-2.0) AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (BSD-2-Clause) AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND (GPL-3.0-or-later) AND (ISC) AND (MIT) AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND (MPL-2.0) AND (Unicode-3.0) AND (Unlicense OR MIT) AND (Zlib) AND (Zlib OR Apache-2.0 OR MIT)
License: ((MIT OR Apache-2.0) AND BSD-3-Clause) AND ((MIT OR Apache-2.0) AND Unicode-3.0) AND (0BSD OR MIT OR Apache-2.0) AND Apache-2.0 AND (Apache-2.0 AND MIT) AND (Apache-2.0 OR MIT) AND (Apache-2.0 OR MIT OR Unlicense) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND BSD-2-Clause AND (BSD-2-Clause OR Apache-2.0 OR MIT) AND (BSD-3-Clause OR MIT OR Apache-2.0) AND GPL-3.0-or-later AND ISC AND MIT AND (MIT OR Apache-2.0) AND (MIT OR Apache-2.0 OR LGPL-2.1-or-later) AND (MIT OR Apache-2.0 OR Zlib) AND (MIT OR Zlib OR Apache-2.0) AND MPL-2.0 AND (Unlicense OR MIT) AND Zlib AND (Zlib OR Apache-2.0 OR MIT)
# LICENSE.dependencies contains a full license breakdown
URL: https://github.com/YaLTeR/niri
URL: https://github.com/niri-wm/niri
VCS: {{{ git_dir_vcs }}}
Source: {{{ git_dir_pack }}}
+12 -9
View File
@@ -1,11 +1,11 @@
// This config is in the KDL format: https://kdl.dev
// "/-" comments out the following node.
// Check the wiki for a full description of the configuration:
// https://yalter.github.io/niri/Configuration:-Introduction
// https://niri-wm.github.io/niri/Configuration:-Introduction
// Input device configuration.
// Find the full list of options on the wiki:
// https://yalter.github.io/niri/Configuration:-Input
// https://niri-wm.github.io/niri/Configuration:-Input
input {
keyboard {
xkb {
@@ -73,7 +73,7 @@ input {
// by running `niri msg outputs` while inside a niri instance.
// The built-in laptop monitor is usually called "eDP-1".
// Find more information on the wiki:
// https://yalter.github.io/niri/Configuration:-Outputs
// https://niri-wm.github.io/niri/Configuration:-Outputs
// Remember to uncomment the node by removing "/-"!
/-output "eDP-1" {
// Uncomment this line to disable this output.
@@ -108,7 +108,7 @@ input {
// Settings that influence how windows are positioned and sized.
// Find more information on the wiki:
// https://yalter.github.io/niri/Configuration:-Layout
// https://niri-wm.github.io/niri/Configuration:-Layout
layout {
// Set gaps around windows in logical pixels.
gaps 16
@@ -134,7 +134,7 @@ layout {
// fixed 1920
}
// You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between.
// You can also customize the heights that "switch-preset-window-height" (Mod+Ctrl+Shift+R) toggles between.
// preset-window-heights { }
// You can change the default width of the new windows.
@@ -295,7 +295,7 @@ screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
// Animation settings.
// The wiki explains how to configure individual animations:
// https://yalter.github.io/niri/Configuration:-Animations
// https://niri-wm.github.io/niri/Configuration:-Animations
animations {
// Uncomment to turn off all animations.
// off
@@ -306,7 +306,7 @@ animations {
// Window rules let you adjust behavior for individual windows.
// Find more information on the wiki:
// https://yalter.github.io/niri/Configuration:-Window-Rules
// https://niri-wm.github.io/niri/Configuration:-Window-Rules
// Work around WezTerm's initial configure bug
// by setting an empty default-column-width.
@@ -550,11 +550,14 @@ binds {
// Expel the bottom window from the focused column to the right.
Mod+Period { expel-window-from-column; }
// Cycle through widths set in preset-column-widths.
Mod+R { switch-preset-column-width; }
// Cycling through the presets in reverse order is also possible.
// Mod+R { switch-preset-column-width-back; }
Mod+Shift+R { switch-preset-window-height; }
Mod+Shift+R { switch-preset-column-width-back; }
Mod+Ctrl+Shift+R { switch-preset-window-height; }
Mod+Ctrl+R { reset-window-height; }
Mod+F { maximize-column; }
Mod+Shift+F { fullscreen-window; }
+89 -39
View File
@@ -67,7 +67,7 @@ use crate::frame_clock::FrameClock;
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation};
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
@@ -97,9 +97,6 @@ pub struct Tty {
dmabuf_global: Option<DmabufGlobal>,
// The output config had changed, but the session is paused, so we need to update it on resume.
update_output_config_on_resume: bool,
// The ignored nodes have changed, but the session is paused, so we need to update it on
// resume.
update_ignored_nodes_on_resume: bool,
// Whether the debug tinting is enabled.
debug_tint: bool,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
@@ -441,6 +438,14 @@ impl Tty {
}
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
// If the session is not active at startup (e.g. niri was launched from a different TTY),
// suspend libinput now so that when ActivateSession fires, libinput.resume() performs a
// full re-enumeration of input devices instead of being a no-op.
if !session.is_active() {
debug!("session is not active, starting libinput in paused state");
libinput.suspend();
}
let input_backend = LibinputInputBackend::new(libinput.clone());
event_loop
.insert_source(input_backend, |mut event, _, state| {
@@ -487,11 +492,6 @@ impl Tty {
}
info!("using as the render node: {node_path}");
let mut ignored_nodes = ignored_nodes_from_config(&config.borrow());
if ignored_nodes.remove(&primary_node) || ignored_nodes.remove(&primary_render_node) {
warn!("ignoring the primary node or render node is not allowed");
}
Ok(Self {
config,
session,
@@ -500,17 +500,27 @@ impl Tty {
gpu_manager,
primary_node,
primary_render_node,
ignored_nodes,
ignored_nodes: HashSet::new(),
devices: HashMap::new(),
dmabuf_global: None,
update_output_config_on_resume: false,
update_ignored_nodes_on_resume: false,
debug_tint: false,
ipc_outputs: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn init(&mut self, niri: &mut Niri) {
// If the session is inactive, skip initialization because we won't be able to do much with
// the devices anyway. We'll get ActivateSession and add the devices there instead.
//
// This can happen when starting niri while having a different TTY active (e.g. via tmux).
if !self.session.is_active() {
return;
}
// Initialize the ignored nodes.
self.ignored_nodes = self.compute_ignored_nodes();
let udev = self.udev_dispatcher.clone();
let udev = udev.as_source_ref();
@@ -550,6 +560,10 @@ impl Tty {
return;
}
// Recompute ignored nodes to resolve symlinks (like /dev/dri/by-path/...) to their
// new underlying device IDs.
self.ignored_nodes = self.compute_ignored_nodes();
if let Err(err) = self.device_added(device_id, &path, niri) {
warn!("error adding device: {err:?}");
}
@@ -597,16 +611,9 @@ impl Tty {
warn!("error resuming libinput");
}
if self.update_ignored_nodes_on_resume {
self.update_ignored_nodes_on_resume = false;
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
if ignored_nodes.remove(&self.primary_node)
|| ignored_nodes.remove(&self.primary_render_node)
{
warn!("ignoring the primary node or render node is not allowed");
}
self.ignored_nodes = ignored_nodes;
}
// While the session was suspended, GPUs could have been added, so
// /dev/dri/by-path/... symlinks need to be re-resolved.
self.ignored_nodes = self.compute_ignored_nodes();
let mut device_list = self
.udev_dispatcher
@@ -649,7 +656,7 @@ impl Tty {
let device = self.devices.get_mut(&node).unwrap();
// Someone on an old device hit what seems to be a driver bug without this:
// https://github.com/YaLTeR/niri/issues/3048
// https://github.com/niri-wm/niri/issues/3048
let force_disable = self
.config
.borrow()
@@ -699,7 +706,14 @@ impl Tty {
}
// Add new devices.
for (device_id, path) in device_list.into_iter() {
//
// Add the primary node first as later nodes might depend on the primary render
// node being available.
let primary_device_id = self.primary_node.dev_id();
let primary_device_path = device_list.remove(&primary_device_id);
let primary = primary_device_path.map(|path| (primary_device_id, path));
for (device_id, path) in primary.into_iter().chain(device_list) {
if let Err(err) = self.device_added(device_id, &path, niri) {
warn!("error adding device: {err:?}");
}
@@ -809,7 +823,10 @@ impl Tty {
.context("error creating renderer")?;
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding wl-display in EGL: {err:?}");
// wl_drm is on its way out so this is expected on most modern distros.
trace!("error binding legacy EGL to wl_display: {err}");
} else {
debug!("bound legacy EGL to wl_display");
}
let gles_renderer = renderer.as_gles_renderer();
@@ -979,6 +996,32 @@ impl Tty {
} => {
removed.push(crtc);
}
// Emitted when the list of connector modes changes at runtime.
//
// Some devices, notably USB-C docks with DP-MST/alt-mode, report Connected before
// the EDID has been read, with an empty mode list. Then, at a later point, the
// modes will be populated, at which point we'll get this Changed event.
DrmScanEvent::Changed {
connector,
crtc: Some(crtc),
} => {
let connector_name = format_connector_name(&connector);
let name = make_output_name(&device.drm, connector.handle(), connector_name);
debug!(
"connector changed: {} \"{}\"",
&name.connector,
name.format_make_model_serial(),
);
if !device.known_crtcs.contains_key(&crtc) {
// I guess this can happen if the connector initially wasn't mapped to a
// CRTC but then got mapped before being changed.
warn!("changed connector missing from known crtcs");
}
// We don't actually need to do anything here; on_output_config_changed() will
// take care of picking a new mode if needed.
}
_ => (),
}
}
@@ -1393,7 +1436,7 @@ impl Tty {
// Create the compositor.
let res = DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
OutputModeSource::Auto(output.downgrade()),
surface,
None,
device.allocator.clone(),
@@ -1423,7 +1466,7 @@ impl Tty {
.create_surface(crtc, mode, &[connector.handle()])?;
DrmCompositor::new(
OutputModeSource::Auto(output.clone()),
OutputModeSource::Auto(output.downgrade()),
surface,
None,
device.allocator.clone(),
@@ -1677,8 +1720,8 @@ impl Tty {
// This is an error!() because it shouldn't happen, but on some systems it somehow
// does. Kernel sending rogue vblank events?
//
// https://github.com/YaLTeR/niri/issues/556
// https://github.com/YaLTeR/niri/issues/615
// https://github.com/niri-wm/niri/issues/556
// https://github.com/niri-wm/niri/issues/615
error!(
"unexpected redraw state for output {name} (should be WaitingForVBlank); \
can happen when resuming from sleep or powering on monitors: {state:?}"
@@ -1839,8 +1882,12 @@ impl Tty {
};
// Render the elements.
let mut elements =
niri.render::<TtyRenderer>(&mut renderer, output, true, RenderTarget::Output);
let ctx = RenderCtx {
renderer: &mut renderer,
target: RenderTarget::Output,
xray: None,
};
let mut elements = niri.render_to_vec(ctx, output, true);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
@@ -2236,22 +2283,25 @@ impl Tty {
}
}
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
// If we're inactive, we can't do anything, so just set a flag for later.
if !self.session.is_active() {
self.update_ignored_nodes_on_resume = true;
return;
}
fn compute_ignored_nodes(&self) -> HashSet<DrmNode> {
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
if ignored_nodes.remove(&self.primary_node)
|| ignored_nodes.remove(&self.primary_render_node)
{
warn!("ignoring the primary node or render node is not allowed");
}
ignored_nodes
}
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
// If we're inactive, we can't do anything, but we'll recompute in ActivateSession.
if !self.session.is_active() {
return;
}
let ignored_nodes = self.compute_ignored_nodes();
if ignored_nodes == self.ignored_nodes {
return;
}
+54 -9
View File
@@ -4,8 +4,10 @@ use std::mem;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use anyhow::Context as _;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::egl::EGLDevice;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer};
@@ -14,13 +16,15 @@ use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::LoopHandle;
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
use smithay::reexports::winit::dpi::LogicalSize;
use smithay::reexports::winit::platform::wayland::WindowAttributesExtWayland;
use smithay::reexports::winit::window::Window;
use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal};
use smithay::wayland::presentation::Refresh;
use super::{IpcOutputMap, OutputId, RenderResult};
use crate::niri::{Niri, RedrawState, State};
use crate::render_helpers::debug::draw_damage;
use crate::render_helpers::{resources, shaders, RenderTarget};
use crate::render_helpers::{resources, shaders, RenderCtx, RenderTarget};
use crate::utils::{get_monotonic_time, logical_output};
pub struct Winit {
@@ -28,6 +32,7 @@ pub struct Winit {
output: Output,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
dmabuf_global: Option<DmabufGlobal>,
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
}
@@ -41,7 +46,8 @@ impl Winit {
let builder = Window::default_attributes()
.with_inner_size(LogicalSize::new(1280.0, 800.0))
// .with_resizable(false)
.with_title("niri");
.with_title("niri")
.with_name("niri", "");
let (backend, winit) = winit::init_from_attributes(builder)?;
let output = Output::new(
@@ -135,6 +141,7 @@ impl Winit {
output,
backend,
damage_tracker,
dmabuf_global: None,
ipc_outputs,
})
}
@@ -142,7 +149,10 @@ impl Winit {
pub fn init(&mut self, niri: &mut Niri) {
let renderer = self.backend.renderer();
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
warn!("error binding renderer wl_display: {err}");
// wl_drm is on its way out so this is expected on most modern distros.
trace!("error binding legacy EGL to wl_display: {err}");
} else {
debug!("bound legacy EGL to wl_display");
}
resources::init(renderer);
@@ -162,9 +172,44 @@ impl Winit {
niri.update_shaders();
self.create_dmabuf_global(niri);
niri.add_output(self.output.clone(), None, false);
}
pub fn create_dmabuf_global(&mut self, niri: &mut Niri) {
let renderer = self.backend.renderer();
let default_feedback = || {
let display = renderer.egl_context().display();
let device =
EGLDevice::device_for_display(display).context("error getting EGL device")?;
let node = device
.try_get_render_node()
.context("error getting EGL device render node")?
.context("failed to query EGL device render node")?;
let primary_formats = renderer.dmabuf_formats();
DmabufFeedbackBuilder::new(node.dev_id(), primary_formats)
.build()
.context("error building dmabuf feedback")
};
// Fallback to dmabuf v3 if we failed to build feedback.
let dmabuf_global = match default_feedback() {
Ok(feedback) => niri
.dmabuf_state
.create_global_with_default_feedback::<State>(&niri.display_handle, &feedback),
Err(err) => {
debug!("failed building default dmabuf feedback, falling back to v3: {err:?}");
let primary_formats = renderer.dmabuf_formats();
niri.dmabuf_state
.create_global::<State>(&niri.display_handle, primary_formats)
}
};
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
}
pub fn seat_name(&self) -> String {
"winit".to_owned()
}
@@ -180,12 +225,12 @@ impl Winit {
let _span = tracy_client::span!("Winit::render");
// Render the elements.
let mut elements = niri.render::<GlesRenderer>(
self.backend.renderer(),
output,
true,
RenderTarget::Output,
);
let ctx = RenderCtx {
renderer: self.backend.renderer(),
target: RenderTarget::Output,
xray: None,
};
let mut elements = niri.render_to_vec(ctx, output, true);
// Visualize the damage, if enabled.
if niri.debug_draw_damage {
+123
View File
@@ -0,0 +1,123 @@
use std::sync::{Arc, Mutex};
use smithay::delegate_background_effect;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Rectangle};
use smithay::wayland::background_effect::{
self, BackgroundEffectSurfaceCachedState, ExtBackgroundEffectHandler,
};
use smithay::wayland::compositor::{
add_post_commit_hook, with_states, RegionAttributes, SurfaceData,
};
use crate::niri::State;
use crate::utils::region::region_to_non_overlapping_rects;
/// Per-surface cache for processed blur region (non-overlapping rects).
#[derive(Default)]
struct CachedBlurRegionUserData(Mutex<CachedBlurRegionInner>);
#[derive(Default)]
struct CachedBlurRegionInner {
/// Whether a region change is pending to be committed.
pending_dirty: bool,
/// Whether the region must be recomputed.
dirty: bool,
/// Whether the post-commit hook has been registered for this surface.
hook_registered: bool,
/// Cached non-overlapping rects in surface-local coordinates.
///
/// `None` means there's no blur region.
rects: Option<Arc<Vec<Rectangle<i32, Logical>>>>,
}
/// Gets the cached blur region for a surface, lazily recomputing if dirty.
pub fn get_cached_blur_region(states: &SurfaceData) -> Option<Arc<Vec<Rectangle<i32, Logical>>>> {
let cache = states
.data_map
.get_or_insert_threadsafe(CachedBlurRegionUserData::default);
let mut guard = cache.0.lock().unwrap();
if guard.dirty {
guard.dirty = false;
recompute_blur_region(states, &mut guard);
}
guard.rects.clone()
}
fn recompute_blur_region(states: &SurfaceData, inner: &mut CachedBlurRegionInner) {
let cached = &states.cached_state;
let rects = if let Some(arc) = &mut inner.rects {
if Arc::strong_count(arc) > 1 {
debug!("cloning rects due to non-unique reference");
}
arc
} else {
inner.rects.insert(Arc::new(Vec::new()))
};
let rects = Arc::make_mut(rects);
if cached.has::<BackgroundEffectSurfaceCachedState>() {
let mut guard = cached.get::<BackgroundEffectSurfaceCachedState>();
if let Some(region) = &guard.current().blur_region {
region_to_non_overlapping_rects(region, rects);
} else {
inner.rects = None;
}
return;
}
inner.rects = None;
}
fn mark_blur_region_pending_dirty(wl_surface: &WlSurface) {
let register_hook = with_states(wl_surface, |states| {
let cache = states
.data_map
.get_or_insert_threadsafe(CachedBlurRegionUserData::default);
let mut guard = cache.0.lock().unwrap();
guard.pending_dirty = true;
if guard.hook_registered {
false
} else {
guard.hook_registered = true;
true
}
});
if register_hook {
add_post_commit_hook::<State, _>(wl_surface, |_state, _dh, surface| {
with_states(surface, |states| {
if let Some(cache) = states.data_map.get::<CachedBlurRegionUserData>() {
let mut guard = cache.0.lock().unwrap();
if guard.pending_dirty {
guard.pending_dirty = false;
guard.dirty = true;
crate::render_helpers::background_effect::damage_surface(states);
}
} else {
error!("unexpected missing CachedBlurRegionUserData");
}
});
});
}
}
impl ExtBackgroundEffectHandler for State {
fn capabilities(&self) -> background_effect::Capability {
background_effect::Capability::Blur
}
fn set_blur_region(&mut self, wl_surface: WlSurface, _region: RegionAttributes) {
mark_blur_region_pending_dirty(&wl_surface);
}
fn unset_blur_region(&mut self, wl_surface: WlSurface) {
mark_blur_region_pending_dirty(&wl_surface);
}
}
delegate_background_effect!(State);
+24 -8
View File
@@ -195,7 +195,10 @@ impl CompositorHandler for State {
// The mapped pre-commit hook deals with dma-bufs on its own.
self.remove_default_dmabuf_pre_commit_hook(surface);
let hook = add_mapped_toplevel_pre_commit_hook(toplevel);
let mapped = Mapped::new(window, rules, hook);
let mapped = {
let config = self.niri.config.borrow();
Mapped::new(window, rules, hook, &config)
};
let window = mapped.window.clone();
let target = if let Some(p) = &parent {
@@ -486,11 +489,10 @@ impl CompositorHandler for State {
// subsurface is destroyed; in the case of alacritty, this is the top CSD shadow. But, it
// gets most of the job done.
if let Some(root) = self.niri.root_surface.get(surface) {
if let Some((mapped, _)) = self.niri.layout.find_window_and_output(root) {
if let Some((mapped, output)) = self.niri.layout.find_window_and_output(root) {
let window = mapped.window.clone();
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
let output = output.cloned();
self.store_unmap_snapshot(&window, output.as_ref());
}
}
@@ -498,7 +500,16 @@ impl CompositorHandler for State {
.root_surface
.retain(|k, v| k != surface && v != surface);
self.niri.dmabuf_pre_commit_hook.remove(surface);
// The object destruction order is not guaranteed to follow the logical role order. So for
// example when a client disconnects unexpectedly, WlSurface::destroyed() may be called
// before XdgShellHandler::toplevel_destroyed(). In this case, the surface will *not* have
// the default dmabuf pre-commit hook: it will still have the toplevel pre-commit hook.
//
// So, this may come out empty, and then the toplevel pre-commit hook will be removed in the
// subsequent toplevel_destroyed() call.
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
remove_pre_commit_hook(surface, &hook);
}
}
}
@@ -517,6 +528,11 @@ delegate_shm!(State);
impl State {
pub fn add_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
if !surface.is_alive() {
error!("tried to add dmabuf pre-commit hook for a dead surface");
return;
}
let hook = add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
let maybe_dmabuf = with_states(surface, |surface_data| {
surface_data
@@ -556,13 +572,13 @@ impl State {
let s = surface.clone();
if let Some(prev) = self.niri.dmabuf_pre_commit_hook.insert(s, hook) {
error!("tried to add dmabuf pre-commit hook when there was already one");
remove_pre_commit_hook(surface, prev);
remove_pre_commit_hook(surface, &prev);
}
}
pub fn remove_default_dmabuf_pre_commit_hook(&mut self, surface: &WlSurface) {
if let Some(hook) = self.niri.dmabuf_pre_commit_hook.remove(surface) {
remove_pre_commit_hook(surface, hook);
remove_pre_commit_hook(surface, &hook);
} else {
error!("tried to remove dmabuf pre-commit hook but there was none");
}
+42 -6
View File
@@ -1,12 +1,11 @@
use smithay::delegate_layer_shell;
use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::wayland::compositor::{get_parent, with_states};
use smithay::wayland::compositor::{add_pre_commit_hook, get_parent, with_states, HookId};
use smithay::wayland::shell::wlr_layer::{
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
WlrLayerShellState,
self, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceCachedState, LayerSurfaceData,
WlrLayerShellHandler, WlrLayerShellState,
};
use smithay::wayland::shell::xdg::PopupSurface;
@@ -27,7 +26,7 @@ impl WlrLayerShellHandler for State {
namespace: String,
) {
let output = if let Some(wl_output) = &wl_output {
Output::from_resource(wl_output)
self.niri.output_from_resource(wl_output)
} else {
self.niri.layout.active_output().cloned()
};
@@ -126,8 +125,10 @@ impl State {
let output_size = output_size(&output);
let scale = output.current_scale().fractional_scale();
let hook = add_mapped_layer_pre_commit_hook(layer);
let mapped = MappedLayer::new(
layer.clone(),
hook,
rules,
output_size,
scale,
@@ -142,6 +143,21 @@ impl State {
if prev.is_some() {
error!("MappedLayer was present for an unmapped surface");
}
} else {
// The surface remains mapped.
if let Some(mapped) = self.niri.mapped_layer_surfaces.get_mut(layer) {
// Check if the layer changed.
if mapped.take_recompute_rules_on_commit() {
let config = self.niri.config.borrow();
if mapped
.recompute_layer_rules(&config.layer_rules, self.niri.is_at_startup)
{
mapped.update_config(&config);
}
}
} else {
error!("MappedLayer missing for a mapped surface");
}
}
// Give focus to newly mapped on-demand surfaces. Some launchers like lxqt-runner rely
@@ -155,7 +171,7 @@ impl State {
// 2) Same-layer exclusive layer surfaces are already preferred to on-demand surfaces in
// update_keyboard_focus(), so we don't need to check for that here.
//
// https://github.com/YaLTeR/niri/issues/641
// https://github.com/niri-wm/niri/issues/641
let on_demand = layer.cached_state().keyboard_interactivity
== wlr_layer::KeyboardInteractivity::OnDemand;
if was_unmapped && on_demand {
@@ -204,3 +220,23 @@ impl State {
true
}
}
fn add_mapped_layer_pre_commit_hook(layer: &LayerSurface) -> HookId {
add_pre_commit_hook::<State, _>(layer.wl_surface(), move |state, _dh, surface| {
let layer_changed = with_states(surface, |states| {
let mut guard = states.cached_state.get::<LayerSurfaceCachedState>();
let pending_layer = guard.pending().layer;
let current_layer = guard.current().layer;
pending_layer != current_layer
});
if layer_changed {
for mapped in state.niri.mapped_layer_surfaces.values_mut() {
if mapped.surface().wl_surface() == surface {
mapped.set_recompute_rules_on_commit();
break;
}
}
}
})
}
+27 -6
View File
@@ -1,3 +1,4 @@
pub mod background_effect;
mod compositor;
mod layer_shell;
mod xdg_shell;
@@ -68,7 +69,7 @@ use smithay::{
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock,
delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager,
delegate_viewporter, delegate_xdg_activation,
delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation,
};
pub use crate::handlers::xdg_shell::KdeDecorationsModeState;
@@ -279,6 +280,7 @@ impl KeyboardShortcutsInhibitHandler for State {
delegate_input_method_manager!(State);
delegate_keyboard_shortcuts_inhibit!(State);
delegate_virtual_keyboard_manager!(State);
impl SelectionHandler for State {
type SelectionUserData = Arc<[u8]>;
@@ -359,7 +361,7 @@ impl DndGrabHandler for State {
trace!("dnd dropped, target: {target:?}, validated: {validated}");
// End DnD before activating a specific window below so that it takes precedence.
self.niri.layout.dnd_end();
self.niri.on_maybe_dnd_ended();
// Activate the target output, since that's how Firefox drag-tab-into-new-window works for
// example. On successful drop, additionally activate the target window.
@@ -381,10 +383,21 @@ impl DndGrabHandler for State {
self.niri.layout.focus_output(&output);
}
}
}
self.niri.dnd_icon = None;
fn cancelled(&mut self, _seat: Seat<Self>, _location: Point<f64, Logical>) {
trace!("dnd cancelled");
self.niri.on_maybe_dnd_ended();
}
}
impl crate::niri::Niri {
fn on_maybe_dnd_ended(&mut self) {
self.layout.dnd_end();
self.dnd_icon = None;
// FIXME: more granular
self.niri.queue_redraw_all();
self.queue_redraw_all();
}
}
@@ -459,7 +472,7 @@ impl SessionLockHandler for State {
}
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
let Some(output) = Output::from_resource(&output) else {
let Some(output) = self.niri.output_from_resource(&output) else {
warn!("no Output matching WlOutput");
return;
};
@@ -544,7 +557,9 @@ impl ForeignToplevelHandler for State {
{
let window = mapped.window.clone();
if let Some(requested_output) = wl_output.as_ref().and_then(Output::from_resource) {
if let Some(requested_output) =
wl_output.and_then(|o| self.niri.output_from_resource(&o))
{
if Some(&requested_output) != current_output {
self.niri.layout.move_to_output(
Some(&window),
@@ -620,6 +635,12 @@ delegate_ext_workspace!(State);
impl ScreencopyHandler for State {
fn frame(&mut self, manager: &ZwlrScreencopyManagerV1, screencopy: Screencopy) {
// This can happen if the output was removed before this was called.
if !self.niri.output_exists(screencopy.output()) {
trace!("screencopy output no longer exists");
return;
}
// If with_damage then push it onto the queue for redraw of the output,
// otherwise render it immediately.
if screencopy.with_damage() {
+14 -9
View File
@@ -612,7 +612,7 @@ impl XdgShellHandler for State {
toplevel: ToplevelSurface,
wl_output: Option<wl_output::WlOutput>,
) {
let requested_output = wl_output.as_ref().and_then(Output::from_resource);
let requested_output = wl_output.and_then(|o| self.niri.output_from_resource(&o));
if let Some((mapped, current_output)) = self
.niri
@@ -846,9 +846,7 @@ impl XdgShellHandler for State {
self.niri
.stop_casts_for_target(CastTarget::Window { id: id.get() });
self.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window);
});
self.store_unmap_snapshot(&window, output.as_ref());
let transaction = Transaction::new();
let blocker = transaction.blocker();
@@ -863,7 +861,15 @@ impl XdgShellHandler for State {
self.niri.window_mru_ui.remove_window(id);
self.niri.layout.remove_window(&window, transaction.clone());
self.add_default_dmabuf_pre_commit_hook(surface.wl_surface());
let surface = surface.wl_surface();
// This check is necessary because implicit resource destruction is done with
// undefined order, so the surface might get destroyed before toplevel_destroyed() is
// called. In this case, adding the default pre-commit hook here would leak it, since the
// place that removes it is WlSurface::destroyed(), which had already been called by now.
if surface.is_alive() {
self.add_default_dmabuf_pre_commit_hook(surface);
}
// If this is the only instance, then this transaction will complete immediately, so no
// need to set the timer.
@@ -1437,7 +1443,7 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let span =
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
let Some((mapped, output)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
return;
};
@@ -1539,9 +1545,8 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
let window = mapped.window.clone();
if got_unmapped {
state.backend.with_primary_renderer(|renderer| {
state.niri.layout.store_unmap_snapshot(renderer, &window);
});
let output = output.cloned();
state.store_unmap_snapshot(&window, output.as_ref());
} else {
if animate {
state.backend.with_primary_renderer(|renderer| {
-7
View File
@@ -4,7 +4,6 @@ use smithay::backend::winit::WinitVirtualDevice;
use smithay::output::Output;
use crate::niri::State;
use crate::protocols::virtual_keyboard::VirtualKeyboard;
use crate::protocols::virtual_pointer::VirtualPointer;
pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> {
@@ -45,12 +44,6 @@ impl NiriInputDevice for WinitVirtualDevice {
}
}
impl NiriInputDevice for VirtualKeyboard {
fn output(&self, _: &State) -> Option<Output> {
None
}
}
impl NiriInputDevice for VirtualPointer {
fn output(&self, _: &State) -> Option<Output> {
self.output().cloned()
+33 -47
View File
@@ -7,7 +7,7 @@ use std::time::Duration;
use calloop::timer::{TimeoutAction, Timer};
use input::event::gesture::GestureEventCoordinates as _;
use niri_config::{
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger, Xkb,
Action, Bind, Binds, Config, Key, ModKey, Modifiers, MruDirection, SwitchBinds, Trigger,
};
use niri_ipc::LayoutSwitchTarget;
use smithay::backend::input::{
@@ -41,6 +41,8 @@ use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use touch_overview_grab::TouchOverviewGrab;
use self::move_grab::MoveGrab;
use self::pick_color_grab::PickColorGrab;
use self::pick_window_grab::PickWindowGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
#[cfg(feature = "dbus")]
@@ -48,7 +50,6 @@ use crate::dbus::freedesktop_a11y::KbMonBlock;
use crate::layout::scrolling::ScrollDirection;
use crate::layout::{ActivateWindow, LayoutElement as _};
use crate::niri::{CastTarget, PointerVisibility, State};
use crate::protocols::virtual_keyboard::VirtualKeyboard;
use crate::ui::mru::{WindowMru, WindowMruUi};
use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::{spawn, spawn_sh};
@@ -294,6 +295,7 @@ impl State {
I::Device: 'static,
{
let device_output = event.device().output(self);
let device_output = device_output.filter(|output| self.niri.output_exists(output));
let device_output = device_output.as_ref();
let (target_geo, keep_ratio, px, transform) =
if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) {
@@ -361,36 +363,11 @@ impl State {
.is_some_and(KeyboardShortcutsInhibitor::is_active)
}
fn on_keyboard<I: InputBackend + 'static>(
fn on_keyboard<I: InputBackend>(
&mut self,
event: I::KeyboardKeyEvent,
consumed_by_a11y: &mut bool,
) where
I::Device: 'static,
{
// Reset the keymap when handling a physical keyboard after a virtual one.
if self.niri.reset_keymap {
let device = event.device();
let is_virtual_keyboard = (&device as &dyn Any)
.downcast_ref::<VirtualKeyboard>()
.is_some();
if !is_virtual_keyboard {
self.niri.reset_keymap = false;
let config = self.niri.config.borrow();
let xkb_config = config.input.keyboard.xkb.clone();
std::mem::drop(config);
if xkb_config != Xkb::default() {
self.set_xkb_config(xkb_config.to_xkb_config());
} else {
// Use locale1 settings if xkb config is unset.
let xkb = self.niri.xkb_from_locale1.clone().unwrap_or_default();
self.set_xkb_config(xkb.to_xkb_config());
}
}
}
) {
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
let serial = SERIAL_COUNTER.next_serial();
@@ -514,19 +491,17 @@ impl State {
}
}
if pressed
&& raw == Some(Keysym::Escape)
&& (this.niri.pick_window.is_some() || this.niri.pick_color.is_some())
{
// We window picking state so the pick window grab must be active.
// Unsetting it cancels window picking.
this.niri
.seat
.get_pointer()
.unwrap()
.unset_grab(this, serial, time);
this.niri.suppressed_keys.insert(key_code);
return FilterResult::Intercept(None);
if pressed && raw == Some(Keysym::Escape) {
// Cancel certain grabs on Escape.
let pointer = this.niri.seat.get_pointer().unwrap();
if pointer
.with_grab(|_, grab| Self::grab_can_be_cancelled_with_esc(grab))
.unwrap_or(false)
{
pointer.unset_grab(this, serial, time);
this.niri.suppressed_keys.insert(key_code);
return FilterResult::Intercept(None);
}
}
if let Some(Keysym::space) = raw {
@@ -2318,9 +2293,9 @@ impl State {
}
self.niri.queue_redraw_all();
}
Action::LoadConfigFile => {
Action::LoadConfigFile(path) => {
if let Some(watcher) = &self.niri.config_file_watcher {
watcher.load_config();
watcher.load_config(path);
}
}
Action::MruConfirm => {
@@ -3286,8 +3261,8 @@ impl State {
let horizontal_amount = event.amount(Axis::Horizontal);
let vertical_amount = event.amount(Axis::Vertical);
// Handle touchpad scroll bindings.
if source == AxisSource::Finger {
// Handle touchpad and continuous scroll bindings.
if source == AxisSource::Finger || source == AxisSource::Continuous {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let modifiers = modifiers_from_state(mods);
@@ -4060,6 +4035,7 @@ impl State {
fallback_output: Option<&Output>,
) -> Option<Point<f64, Logical>> {
let output = evt.device().output(self);
let output = output.filter(|output| self.niri.output_exists(output));
let output = output.as_ref().or(fallback_output)?;
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let transform = output.current_transform();
@@ -4322,6 +4298,12 @@ impl State {
// Null-source DnD: weston-dnd --self-only
|| grab.is::<DnDGrab<Self, WlSurface, WlSurface>>()
}
fn grab_can_be_cancelled_with_esc(grab: &(dyn PointerGrab<State> + 'static)) -> bool {
let grab = grab.as_any();
grab.is::<PickWindowGrab>() || grab.is::<PickColorGrab>() || Self::is_dnd_grab(grab)
}
}
/// Check whether the key should be intercepted and mark intercepted
@@ -4704,7 +4686,11 @@ pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::
let _ = device.config_tap_set_enabled(c.tap);
let _ = device.config_dwt_set_enabled(c.dwt);
let _ = device.config_dwtp_set_enabled(c.dwtp);
let _ = device.config_tap_set_drag_lock_enabled(c.drag_lock);
let _ = device.config_tap_set_drag_lock_enabled(if c.drag_lock {
input::DragLockState::EnabledTimeout
} else {
input::DragLockState::Disabled
});
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed.0);
let _ = device.config_left_handed_set(c.left_handed);
+6 -6
View File
@@ -13,7 +13,7 @@ use smithay::input::SeatHandler;
use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
use crate::niri::State;
use crate::render_helpers::{render_and_download, RenderTarget};
use crate::render_helpers::{render_and_download, RenderCtx, RenderTarget};
pub struct PickColorGrab {
start_data: PointerGrabStartData<State>,
@@ -49,13 +49,13 @@ impl PickColorGrab {
let pos = pos_within_output.to_physical_precise_floor(scale);
let size = Size::<i32, Physical>::from((1, 1));
let elements = data.niri.render(
let ctx = RenderCtx {
renderer,
&output,
false,
// This is an interactive operation so we can render without blocking out.
RenderTarget::Output,
);
target: RenderTarget::Output,
xray: None,
};
let elements = data.niri.render_to_vec(ctx, &output, false);
let mapping = match render_and_download(
renderer,
+1 -1
View File
@@ -193,7 +193,7 @@ pub fn handle_msg(mut msg: Msg, json: bool) -> anyhow::Result<()> {
return Ok(());
}
windows.sort_unstable_by(|a, b| a.id.cmp(&b.id));
windows.sort_unstable_by_key(|a| a.id);
for window in windows {
print_window(&window);
+9 -1
View File
@@ -463,7 +463,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
fn validate_action(action: &Action) -> Result<(), String> {
if let Action::Screenshot { path, .. }
| Action::ScreenshotScreen { path, .. }
| Action::ScreenshotWindow { path, .. } = action
| Action::ScreenshotWindow { path, .. }
| Action::LoadConfigFile { path } = action
{
if let Some(path) = path {
// Relative paths are resolved against the niri compositor's working directory, which
@@ -474,6 +475,13 @@ fn validate_action(action: &Action) -> Result<(), String> {
}
}
if let Action::LoadConfigFile { path: Some(path) } = action {
let p = Path::new(path);
if !p.is_file() {
return Err(format!("path does not point to a file: {path}"));
}
}
Ok(())
}
+122 -25
View File
@@ -2,19 +2,22 @@ use niri_config::utils::MergeWith as _;
use niri_config::{Config, LayerRule};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::Kind;
use smithay::desktop::{LayerSurface, PopupManager};
use smithay::utils::{Logical, Point, Scale, Size};
use smithay::desktop::{LayerSurface, PopupKind, PopupManager};
use smithay::utils::{Logical, Point, Rectangle, Scale, Size};
use smithay::wayland::compositor::{remove_pre_commit_hook, HookId};
use smithay::wayland::shell::wlr_layer::{ExclusiveZone, Layer};
use super::ResolvedLayerRules;
use crate::animation::Clock;
use crate::layout::shadow::Shadow;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::surface::push_elements_from_surface_tree;
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::{background_effect, RenderCtx};
use crate::utils::{baba_is_float_offset, round_logical_in_physical};
#[derive(Debug)]
@@ -22,15 +25,26 @@ pub struct MappedLayer {
/// The surface itself.
surface: LayerSurface,
/// Pre-commit hook that we have on all mapped layer surfaces.
pre_commit_hook: HookId,
/// Up-to-date rules.
rules: ResolvedLayerRules,
/// Whether to recompute layer rules on the next commit.
///
/// Set in the pre-commit hook when the layer changes; consumed in the commit handler.
recompute_rules_on_commit: bool,
/// Buffer to draw instead of the surface when it should be blocked out.
block_out_buffer: SolidColorBuffer,
/// The shadow around the surface.
shadow: Shadow,
/// The blur config, passed for background effect rendering.
blur_config: niri_config::Blur,
/// The view size for the layer surface's output.
view_size: Size<f64, Logical>,
@@ -46,12 +60,14 @@ niri_render_elements! {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
Shadow = ShadowRenderElement,
BackgroundEffect = BackgroundEffectElement,
}
}
impl MappedLayer {
pub fn new(
surface: LayerSurface,
pre_commit_hook: HookId,
rules: ResolvedLayerRules,
view_size: Size<f64, Logical>,
scale: f64,
@@ -65,11 +81,14 @@ impl MappedLayer {
Self {
surface,
pre_commit_hook,
rules,
recompute_rules_on_commit: false,
block_out_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
view_size,
scale,
shadow: Shadow::new(shadow_config),
blur_config: config.blur,
clock,
}
}
@@ -80,6 +99,8 @@ impl MappedLayer {
shadow_config.on = false;
shadow_config.merge_with(&self.rules.shadow);
self.shadow.update_config(shadow_config);
self.blur_config = config.blur;
}
pub fn update_shaders(&mut self) {
@@ -128,6 +149,14 @@ impl MappedLayer {
true
}
pub fn set_recompute_rules_on_commit(&mut self) {
self.recompute_rules_on_commit = true;
}
pub fn take_recompute_rules_on_commit(&mut self) -> bool {
std::mem::take(&mut self.recompute_rules_on_commit)
}
pub fn place_within_backdrop(&self) -> bool {
if !self.rules.place_within_backdrop {
return false;
@@ -157,16 +186,23 @@ impl MappedLayer {
pub fn render_normal<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
ns: Option<usize>,
location: Point<f64, Logical>,
target: RenderTarget,
xray_pos: XrayPos,
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
) {
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
if target.should_block_out(self.rules.block_out_from) {
let bob_offset = self.bob_offset();
let location = location + bob_offset;
let xray_pos = xray_pos.offset(bob_offset);
let surface = self.surface.wl_surface();
let should_block_out = ctx.target.should_block_out(self.rules.block_out_from);
if should_block_out {
// Round to physical pixels.
let location = location.to_physical_precise_round(scale).to_logical(scale);
@@ -182,9 +218,8 @@ impl MappedLayer {
// Layer surfaces don't have extra geometry like windows.
let buf_pos = location;
let surface = self.surface.wl_surface();
push_elements_from_surface_tree(
renderer,
ctx.renderer,
surface,
buf_pos.to_physical_precise_round(scale),
scale,
@@ -196,41 +231,103 @@ impl MappedLayer {
let location = location.to_physical_precise_round(scale).to_logical(scale);
self.shadow
.render(renderer, location, &mut |elem| push(elem.into()));
.render(ctx.renderer, location, &mut |elem| push(elem.into()));
let geometry = Rectangle::new(location, self.block_out_buffer.size());
let surface_off = Point::new(0., 0.); // No geometry on layer surfaces.
let surface_anim_scale = Scale::from(1.);
let radius = self.rules.geometry_corner_radius.unwrap_or_default();
background_effect::render_for_tile(
ctx.as_gles(),
ns,
geometry,
self.scale,
false,
surface,
surface_off,
surface_anim_scale,
self.blur_config,
radius,
self.rules.background_effect,
should_block_out,
xray_pos,
&mut |elem| push(elem.into()),
);
}
pub fn render_popups<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
ns: Option<usize>,
location: Point<f64, Logical>,
target: RenderTarget,
xray_pos: XrayPos,
push: &mut dyn FnMut(LayerSurfaceRenderElement<R>),
) {
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let location = location + self.bob_offset();
if target.should_block_out(self.rules.block_out_from) {
if ctx.target.should_block_out(self.rules.block_out_from) {
return;
}
// Layer surfaces don't have extra geometry like windows.
let buf_pos = location;
let scale = Scale::from(self.scale);
let alpha = self.rules.opacity.unwrap_or(1.).clamp(0., 1.);
let bob_offset = self.bob_offset();
let location = location + bob_offset;
let xray_pos = xray_pos.offset(bob_offset);
let surface = self.surface.wl_surface();
for (popup, popup_offset) in PopupManager::popups_for_surface(surface) {
// Layer surfaces don't have extra geometry like windows.
let offset = popup_offset - popup.geometry().loc;
for (popup, offset) in PopupManager::popups_for_surface(surface) {
let popup_rules = match popup {
PopupKind::Xdg(_) => self.rules.popups,
// IME popups aren't affected by rules for regular popups.
PopupKind::InputMethod(_) => niri_config::ResolvedPopupsRules::default(),
};
let alpha = alpha * popup_rules.opacity.unwrap_or(1.).clamp(0., 1.);
let surface = popup.wl_surface();
let popup_geo = popup.geometry();
let surface_loc = location + (offset - popup_geo.loc).to_f64();
push_elements_from_surface_tree(
renderer,
popup.wl_surface(),
(buf_pos + offset.to_f64()).to_physical_precise_round(scale),
ctx.renderer,
surface,
surface_loc.to_physical_precise_round(scale),
scale,
alpha,
Kind::ScanoutCandidate,
&mut |elem| push(elem.into()),
);
let geometry = Rectangle::new(location + offset.to_f64(), popup_geo.size.to_f64());
let surface_off = popup_geo.loc.upscale(-1).to_f64();
let surface_anim_scale = Scale::from(1.);
let mut effect = popup_rules.background_effect;
// Default xray to false for pop-ups since they're always on top of something.
if effect.xray.is_none() {
effect.xray = Some(false);
}
let xray_pos = xray_pos.offset(offset.to_f64());
background_effect::render_for_tile(
ctx.as_gles(),
ns,
geometry,
self.scale,
false,
surface,
surface_off,
surface_anim_scale,
self.blur_config,
popup_rules.geometry_corner_radius.unwrap_or_default(),
effect,
false,
xray_pos,
&mut |elem| push(elem.into()),
);
}
}
}
impl Drop for MappedLayer {
fn drop(&mut self) {
remove_pre_commit_hook(self.surface.wl_surface(), &self.pre_commit_hook);
}
}
+26 -1
View File
@@ -1,7 +1,8 @@
use niri_config::layer_rule::{LayerRule, Match};
use niri_config::utils::MergeWith as _;
use niri_config::{BlockOutFrom, CornerRadius, ShadowRule};
use niri_config::{BackgroundEffect, BlockOutFrom, CornerRadius, ResolvedPopupsRules, ShadowRule};
use smithay::desktop::LayerSurface;
use smithay::wayland::shell::wlr_layer::Layer;
pub mod mapped;
pub use mapped::MappedLayer;
@@ -26,6 +27,12 @@ pub struct ResolvedLayerRules {
/// Whether to bob this window up and down.
pub baba_is_float: bool,
/// Background effect configuration.
pub background_effect: BackgroundEffect,
/// Rules for this layer surface's popups.
pub popups: ResolvedPopupsRules,
}
impl ResolvedLayerRules {
@@ -70,6 +77,12 @@ impl ResolvedLayerRules {
}
resolved.shadow.merge_with(&rule.shadow);
resolved
.background_effect
.merge_with(&rule.background_effect);
resolved.popups.merge_with(&rule.popups);
}
resolved
@@ -83,5 +96,17 @@ fn surface_matches(surface: &LayerSurface, m: &Match) -> bool {
}
}
if let Some(layer) = m.layer {
let surface_layer = match surface.layer() {
Layer::Background => niri_ipc::Layer::Background,
Layer::Bottom => niri_ipc::Layer::Bottom,
Layer::Top => niri_ipc::Layer::Top,
Layer::Overlay => niri_ipc::Layer::Overlay,
};
if layer != surface_layer {
return false;
}
}
true
}
+31 -5
View File
@@ -21,7 +21,7 @@ use crate::render_helpers::shader_element::ShaderRenderElement;
use crate::render_helpers::shaders::{mat3_uniform, ProgramType, Shaders};
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
use crate::render_helpers::{render_to_encompassing_texture, RenderCtx, RenderTarget};
use crate::utils::transaction::TransactionBlocker;
#[derive(Debug)]
@@ -29,6 +29,12 @@ pub struct ClosingWindow {
/// Contents of the window.
buffer: TextureBuffer<GlesTexture>,
/// Contents that are not blocked out, but the background is blocked out.
///
/// If `None` then the background doesn't have any blocked-out surfaces, and normal `buffer`
/// can be used instead.
buffer_with_blocked_out_bg: Option<TextureBuffer<GlesTexture>>,
/// Blocked-out contents of the window.
blocked_out_buffer: TextureBuffer<GlesTexture>,
@@ -44,6 +50,9 @@ pub struct ClosingWindow {
/// How much the texture should be offset.
buffer_offset: Point<f64, Logical>,
/// How much the texture with blocked-out bg should be offset.
buffer_with_blocked_out_bg_offset: Point<f64, Logical>,
/// How much the blocked-out texture should be offset.
blocked_out_buffer_offset: Point<f64, Logical>,
@@ -121,17 +130,27 @@ impl ClosingWindow {
let (buffer, buffer_offset) =
render_to_texture(snapshot.contents).context("error rendering contents")?;
let (buffer_with_blocked_out_bg, buffer_with_blocked_out_bg_offset) =
if let Some(contents) = snapshot.contents_with_blocked_out_bg {
let (buffer, offset) = render_to_texture(contents)
.context("error rendering contents with blocked-out bg")?;
(Some(buffer), offset)
} else {
(None, Point::default())
};
let (blocked_out_buffer, blocked_out_buffer_offset) =
render_to_texture(snapshot.blocked_out_contents)
.context("error rendering blocked-out contents")?;
Ok(Self {
buffer,
buffer_with_blocked_out_bg,
blocked_out_buffer,
block_out_from: snapshot.block_out_from,
geo_size,
pos,
buffer_offset,
buffer_with_blocked_out_bg_offset,
blocked_out_buffer_offset,
anim_state: AnimationState::new(blocker, anim),
random_seed: fastrand::f32(),
@@ -159,13 +178,17 @@ impl ClosingWindow {
pub fn render(
&self,
renderer: &mut GlesRenderer,
ctx: RenderCtx<GlesRenderer>,
view_rect: Rectangle<f64, Logical>,
scale: Scale<f64>,
target: RenderTarget,
) -> ClosingWindowRenderElement {
let (buffer, offset) = if target.should_block_out(self.block_out_from) {
let (buffer, offset) = if ctx.target.should_block_out(self.block_out_from) {
(&self.blocked_out_buffer, self.blocked_out_buffer_offset)
} else if ctx.target != RenderTarget::Output && self.buffer_with_blocked_out_bg.is_some() {
(
self.buffer_with_blocked_out_bg.as_ref().unwrap(),
self.buffer_with_blocked_out_bg_offset,
)
} else {
(&self.buffer, self.buffer_offset)
};
@@ -200,7 +223,10 @@ impl ClosingWindow {
let progress = anim.value();
let clamped_progress = anim.clamped_value().clamp(0., 1.);
if Shaders::get(renderer).program(ProgramType::Close).is_some() {
if Shaders::get(ctx.renderer)
.program(ProgramType::Close)
.is_some()
{
let area_loc = Vec2::new(view_rect.loc.x as f32, view_rect.loc.y as f32);
let area_size = Vec2::new(view_rect.size.w as f32, view_rect.size.h as f32);
+8 -5
View File
@@ -18,7 +18,8 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::utils::transaction::TransactionBlocker;
use crate::utils::{
center_preferring_top_left_in_area, clamp_preferring_top_left_in_area, ensure_min_max_size,
@@ -489,6 +490,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
// Now, descendants is in back-to-front order, and repositioning them in the front-to-back
// order will preserve the subsequent indices and work out right.
let mut idx = idx;
#[allow(clippy::explicit_counter_loop)]
for descendant_idx in descendants.into_iter().rev() {
self.raise_window(descendant_idx, idx);
idx += 1;
@@ -1055,9 +1057,9 @@ impl<W: LayoutElement> FloatingSpace<W> {
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
xray_pos: XrayPos,
view_rect: Rectangle<f64, Logical>,
target: RenderTarget,
focus_ring: bool,
push: &mut dyn FnMut(FloatingSpaceRenderElement<R>),
) {
@@ -1067,7 +1069,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
//
// FIXME: I guess this should rather preserve the stacking order when the window is closed.
for closing in self.closing_windows.iter().rev() {
let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
let elem = closing.render(ctx.as_gles(), view_rect, scale);
push(elem.into());
}
@@ -1076,7 +1078,8 @@ impl<W: LayoutElement> FloatingSpace<W> {
// For the active tile, draw the focus ring.
let focus_ring = focus_ring && Some(tile.window().id()) == active.as_ref();
tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| {
let xray_pos = xray_pos.offset(tile_pos);
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
push(elem.into())
});
}
+117 -29
View File
@@ -59,12 +59,14 @@ use crate::animation::{Animation, Clock};
use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::scrolling::ScrollDirection;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::offscreen::OffscreenData;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::texture::TextureBuffer;
use crate::render_helpers::{BakedBuffer, RenderTarget};
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::{BakedBuffer, RenderCtx};
use crate::rubber_band::RubberBand;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{
@@ -112,6 +114,7 @@ niri_render_elements! {
LayoutElementRenderElement<R> => {
Wayland = WaylandSurfaceRenderElement<R>,
SolidColor = SolidColorRenderElement,
BackgroundEffect = BackgroundEffectElement,
}
}
@@ -132,6 +135,11 @@ pub trait LayoutElement {
/// Unique ID of this element.
fn id(&self) -> &Self::Id;
/// Updates the config for the element.
fn update_config(&mut self, blur_config: niri_config::Blur) {
let _ = blur_config;
}
/// Visual size of the element.
///
/// This is what the user would consider the size, i.e. excluding CSD shadows and whatnot.
@@ -154,41 +162,55 @@ pub trait LayoutElement {
/// location.
fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
xray_pos: XrayPos,
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
) {
self.render_popups(renderer, location, scale, alpha, target, push);
self.render_normal(renderer, location, scale, alpha, target, push);
self.render_popups(ctx.r(), location, scale, alpha, xray_pos, push);
self.render_normal(ctx.r(), location, scale, alpha, push);
}
/// Renders the non-popup parts of the element.
fn render_normal<R: NiriRenderer>(
&self,
renderer: &mut R,
ctx: RenderCtx<R>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
) {
let _ = (renderer, location, scale, alpha, target, push);
let _ = (ctx, location, scale, alpha, push);
}
/// Renders the popups of the element.
fn render_popups<R: NiriRenderer>(
&self,
renderer: &mut R,
ctx: RenderCtx<R>,
location: Point<f64, Logical>,
scale: Scale<f64>,
alpha: f32,
target: RenderTarget,
xray_pos: XrayPos,
push: &mut dyn FnMut(LayoutElementRenderElement<R>),
) {
let _ = (renderer, location, scale, alpha, target, push);
let _ = (ctx, location, scale, alpha, xray_pos, push);
}
/// Renders the background effect behind the main surface of the element.
#[allow(clippy::too_many_arguments)]
fn render_background_effect(
&self,
_ctx: RenderCtx<GlesRenderer>,
_geometry: Rectangle<f64, Logical>,
_scale: f64,
_clip_to_geometry: bool,
_surface_anim_scale: Scale<f64>,
_radius: CornerRadius,
_xray_pos: XrayPos,
_push: &mut dyn FnMut(BackgroundEffectElement),
) {
}
/// Requests the element to change its size.
@@ -268,6 +290,9 @@ pub trait LayoutElement {
Some(requested)
}
fn is_windowed_fullscreen(&self) -> bool {
false
}
fn is_pending_windowed_fullscreen(&self) -> bool {
false
}
@@ -275,6 +300,22 @@ pub trait LayoutElement {
let _ = value;
}
/// The effective geometry corner radius for this element.
///
/// Returns zero when the element is in windowed fullscreen, since fullscreen windows have
/// square corners.
///
/// This method only handles windowed fullscreen and not maximized/real fullscreen. This is
/// because windowed fullscreen is handled by the element itself, whereas other sizing modes
/// are handled externally by the Tile, so the corner radius changes for those modes is also
/// handled externally.
fn geometry_corner_radius(&self) -> CornerRadius {
if self.is_windowed_fullscreen() {
return CornerRadius::default();
}
self.rules().geometry_corner_radius.unwrap_or_default()
}
fn is_child_of(&self, parent: &Self) -> bool;
fn rules(&self) -> &ResolvedWindowRules;
@@ -351,6 +392,7 @@ pub struct Options {
pub animations: niri_config::Animations,
pub gestures: niri_config::Gestures,
pub overview: niri_config::Overview,
pub blur: niri_config::Blur,
// Debug flags.
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
@@ -611,6 +653,7 @@ impl Options {
animations: config.animations.clone(),
gestures: config.gestures,
overview: config.overview,
blur: config.blur,
disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions,
deactivate_unfocused_windows: config.debug.deactivate_unfocused_windows,
@@ -2732,8 +2775,18 @@ impl<W: LayoutElement> Layout<W> {
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if output.is_none_or(|output| move_.output == *output) {
let pos_within_output = move_.tile_render_location(zoom);
// We're not on any specific workspace so we can't compute a "workspace view" rect.
// Let's instead compute a rect relative to the output.
//
// FIXME: we could make the colors match up better in the overview by figuring out
// where a centered workspace would currently be, and computing the view rect
// against that. Since most of the time the dragged window will be on a centered
// workspace.
let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output))
.downscale(zoom);
move_.tile.update_render_elements(true, view_rect);
}
}
@@ -2746,7 +2799,9 @@ impl<W: LayoutElement> Layout<W> {
..
} = &mut self.monitor_set
else {
error!("update_render_elements called with no monitors");
if output.is_some() {
error!("update_render_elements called with no monitors but Some output");
}
return;
};
@@ -2818,13 +2873,12 @@ impl<W: LayoutElement> Layout<W> {
ws.scrolling_insert_position(pos_within_workspace)
};
let rules = move_.tile.window().rules();
let border_width = move_.tile.effective_border_width().unwrap_or(0.);
let corner_radius = rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
});
let corner_radius = move_
.tile
.window()
.geometry_corner_radius()
.expanded_by(border_width as f32);
mon.insert_hint = Some(InsertHint {
workspace: insert_ws,
position,
@@ -4596,12 +4650,33 @@ impl<W: LayoutElement> Layout<W> {
}
}
pub fn store_unmap_snapshot(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
pub fn store_unmap_snapshot(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
window: &W::Id,
) {
let _span = tracy_client::span!("Layout::store_unmap_snapshot");
let zoom = self.overview_zoom();
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
if move_.tile.window().id() == window {
move_.tile.store_unmap_snapshot_if_empty(renderer);
let pos_within_output = move_.tile_render_location(zoom);
// Computation matches update_render_elements().
let view_rect =
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output))
.downscale(zoom);
move_.tile.update_render_elements(false, view_rect);
move_.tile.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::new(pos_within_output, zoom),
);
return;
}
}
@@ -4609,9 +4684,15 @@ impl<W: LayoutElement> Layout<W> {
match &mut self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mut mon.workspaces {
for (ws, geo) in mon.workspaces_with_render_geo_mut(false) {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
ws.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::new(geo.loc, zoom),
window,
);
return;
}
}
@@ -4620,7 +4701,13 @@ impl<W: LayoutElement> Layout<W> {
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
if ws.has_window(window) {
ws.store_unmap_snapshot_if_empty(renderer, window);
ws.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
XrayPos::default(),
window,
);
return;
}
}
@@ -4721,9 +4808,8 @@ impl<W: LayoutElement> Layout<W> {
pub fn render_interactive_move_for_output<R: NiriRenderer>(
&self,
renderer: &mut R,
ctx: RenderCtx<R>,
output: &Output,
target: RenderTarget,
push: &mut dyn FnMut(RescaleRenderElement<TileRenderElement<R>>),
) {
if self.update_render_elements_time != self.clock.now() {
@@ -4740,13 +4826,15 @@ impl<W: LayoutElement> Layout<W> {
let scale = Scale::from(move_.output.current_scale().fractional_scale());
let zoom = self.overview_zoom();
let location = move_.tile_render_location(zoom);
let pos_in_backdrop = move_.tile_render_location(zoom);
let xray_pos = XrayPos::new(pos_in_backdrop, zoom);
move_
.tile
.render(renderer, location, true, target, &mut |elem| {
.render(ctx, pos_in_backdrop, xray_pos, true, &mut |elem| {
push(RescaleRenderElement::from_element(
elem,
location.to_physical_precise_round(scale),
pos_in_backdrop.to_physical_precise_round(scale),
zoom,
));
});
+8 -6
View File
@@ -24,7 +24,8 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::SolidColorRenderElement;
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::rubber_band::RubberBand;
use crate::utils::transaction::Transaction;
use crate::utils::{
@@ -1669,8 +1670,7 @@ impl<W: LayoutElement> Monitor<W> {
pub fn render_workspaces<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
mut ctx: RenderCtx<R>,
focus_ring: bool,
push: &mut dyn FnMut(MonitorRenderElement<R>),
) {
@@ -1734,16 +1734,18 @@ impl<W: LayoutElement> Monitor<W> {
}};
}
ws.render_floating(renderer, target, focus_ring, push!());
let xray_pos = XrayPos::new(geo.loc, zoom);
ws.render_floating(ctx.r(), xray_pos, focus_ring, push!());
if let Some(loc) = insert_hint_render_loc {
if loc.workspace == InsertWorkspace::Existing(ws.id()) {
self.insert_hint_element
.render(renderer, loc.location, push!());
.render(ctx.renderer, loc.location, push!());
}
}
ws.render_scrolling(renderer, target, focus_ring, push!());
ws.render_scrolling(ctx.r(), xray_pos, focus_ring, push!());
}
}
+8 -6
View File
@@ -21,7 +21,8 @@ use crate::input::swipe_tracker::SwipeTracker;
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::XrayPos;
use crate::render_helpers::RenderCtx;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::ResizeEdge;
use crate::window::ResolvedWindowRules;
@@ -2899,8 +2900,8 @@ impl<W: LayoutElement> ScrollingSpace<W> {
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
mut ctx: RenderCtx<R>,
xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(ScrollingSpaceRenderElement<R>),
) {
@@ -2909,7 +2910,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
// Draw the closing windows on top of the other windows.
let view_rect = Rectangle::new(Point::from((self.view_pos(), 0.)), self.view_size);
for closing in self.closing_windows.iter().rev() {
let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
let elem = closing.render(ctx.as_gles(), view_rect, scale);
push(elem.into());
}
@@ -2930,7 +2931,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let pos = view_off + col_off + col_render_off;
let pos = pos.to_physical_precise_round(scale).to_logical(scale);
col.tab_indicator
.render(renderer, pos, &mut |elem| push(elem.into()));
.render(ctx.renderer, pos, &mut |elem| push(elem.into()));
}
for (tile, tile_off, visible) in col.tiles_in_render_order() {
@@ -2955,7 +2956,8 @@ impl<W: LayoutElement> ScrollingSpace<W> {
continue;
}
tile.render(renderer, tile_pos, focus_ring, target, &mut |elem| {
let xray_pos = xray_pos.offset(tile_pos);
tile.render(ctx.r(), tile_pos, xray_pos, focus_ring, &mut |elem| {
push(elem.into())
});
}
+6
View File
@@ -116,10 +116,12 @@ impl TestWindow {
if self.0.animate_next_configure.get() {
self.0.animation_snapshot.replace(Some(RenderSnapshot {
contents: Vec::new(),
contents_with_blocked_out_bg: None,
blocked_out_contents: Vec::new(),
block_out_from: None,
size: self.0.bbox.get().size.to_f64(),
texture: OnceCell::new(),
texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: OnceCell::new(),
}));
}
@@ -241,6 +243,10 @@ impl LayoutElement for TestWindow {
self.0.requested_size.get()
}
fn is_windowed_fullscreen(&self) -> bool {
self.0.is_windowed_fullscreen.get()
}
fn is_pending_windowed_fullscreen(&self) -> bool {
self.0.is_pending_windowed_fullscreen.get()
}
+195 -77
View File
@@ -18,6 +18,7 @@ use super::{
use crate::animation::{Animation, Clock};
use crate::layout::SizingMode;
use crate::niri_render_elements;
use crate::render_helpers::background_effect::BackgroundEffectElement;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::clipped_surface::{ClippedSurfaceRenderElement, RoundedCornerDamage};
use crate::render_helpers::damage::ExtraDamage;
@@ -27,7 +28,8 @@ use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::{RenderCtx, RenderTarget};
use crate::utils::transaction::Transaction;
use crate::utils::{
baba_is_float_offset, round_logical_in_physical, round_logical_in_physical_max1,
@@ -130,6 +132,7 @@ niri_render_elements! {
ClippedSurface = ClippedSurfaceRenderElement<R>,
Offscreen = OffscreenRenderElement,
ExtraDamage = ExtraDamage,
BackgroundEffect = BackgroundEffectElement,
}
}
@@ -248,6 +251,8 @@ impl<W: LayoutElement> Tile<W> {
let shadow_config = self.options.layout.shadow.merged_with(&rules.shadow);
self.shadow.update_config(shadow_config);
self.window.update_config(self.options.blur);
}
pub fn update_shaders(&mut self) {
@@ -398,12 +403,11 @@ impl<W: LayoutElement> Tile<W> {
self.shadow.update_config(shadow_config);
let window_size = self.window_size();
let radius = rules
.geometry_corner_radius
.unwrap_or_default()
let radius = self
.window
.geometry_corner_radius()
.fit_to(window_size.w as f32, window_size.h as f32);
self.rounded_corner_damage.set_corner_radius(radius);
self.rounded_corner_damage.set_size(window_size);
}
pub fn advance_animations(&mut self) {
@@ -469,11 +473,22 @@ impl<W: LayoutElement> Tile<W> {
border_window_size.w -= border_width * 2.;
border_window_size.h -= border_width * 2.;
let radius = rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
})
// FIXME: this takes into account the animation from normal sizing mode to
// maximized/fullscreen, but it doesn't take into account the corner radius animation from
// the window itself.
//
// Currently, an easy way to see the problem is to start from a window with a nonzero
// radius, then go from windowed fullscreen (that forces 0 radius) to regular fullscreen.
// At the start of the animation, windowed fullscreen becomes false, but the window hasn't
// animated to the normal fullscreen yet, so the radius here jumps to its nonzero value,
// even though it should remain zero throughout.
//
// Later, when windows get the surface shape protocol with radii, this issue will happen
// when that changes between animated commits.
let radius = self
.window
.geometry_corner_radius()
.expanded_by(border_width as f32)
.scaled_by(1. - expanded_progress as f32);
self.border.update_render_elements(
border_window_size,
@@ -492,9 +507,8 @@ impl<W: LayoutElement> Tile<W> {
let radius = if self.visual_border_width().is_some() {
radius
} else {
rules
.geometry_corner_radius
.unwrap_or_default()
self.window
.geometry_corner_radius()
.scaled_by(1. - expanded_progress as f32)
};
self.shadow.update_render_elements(
@@ -1009,10 +1023,10 @@ impl<W: LayoutElement> Tile<W> {
fn render_inner<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
mut xray_pos: XrayPos,
focus_ring: bool,
target: RenderTarget,
push: &mut dyn FnMut(TileRenderElement<R>),
) {
let _span = tracy_client::span!("Tile::render_inner");
@@ -1039,67 +1053,69 @@ impl<W: LayoutElement> Tile<W> {
//
// This isn't to say that adding it here is perfect; indeed, it kind of breaks view_rect
// passed to update_render_elements(). But, it works well enough for what it is.
let location = location + self.bob_offset();
let bob_offset = self.bob_offset();
let location = location + bob_offset;
xray_pos = xray_pos.offset(bob_offset);
let window_loc = self.window_loc();
let window_size = self.window_size();
let animated_window_size = self.animated_window_size();
let window_render_loc = location + window_loc;
let area = Rectangle::new(window_render_loc, animated_window_size);
xray_pos = xray_pos.offset(window_loc);
let rules = self.window.rules();
// Clip to geometry including during the fullscreen animation to help with buggy clients
// that submit a full-sized buffer before acking the fullscreen state (Firefox).
let clip_to_geometry = fullscreen_progress < 1. && rules.clip_to_geometry == Some(true);
let radius = rules
.geometry_corner_radius
.unwrap_or_default()
let radius = self
.window
.geometry_corner_radius()
.scaled_by(1. - expanded_progress as f32);
// Popups go on top, whether it's resize or not.
self.window.render_popups(
renderer,
ctx.r(),
window_render_loc,
scale,
win_alpha,
target,
xray_pos,
&mut |elem| push(elem.into()),
);
// If we're resizing, try to render a shader, or a fallback.
let mut pushed_resize = false;
if let Some(resize) = &self.resize_animation {
if ResizeRenderElement::has_shader(renderer) {
let gles_renderer = renderer.as_gles_renderer();
if ResizeRenderElement::has_shader(ctx.renderer) {
let mut ctx = ctx.as_gles();
if let Some(texture_from) = resize.snapshot.texture(gles_renderer, scale, target) {
if let Some(texture_from) = resize.snapshot.texture(ctx.r(), scale) {
let mut window_elements = Vec::new();
self.window.render_normal(
gles_renderer,
ctx.r(),
Point::from((0., 0.)),
scale,
1.,
target,
&mut |elem| window_elements.push(elem),
);
let current = resize
.offscreen
.render(gles_renderer, scale, &window_elements)
.render(ctx.renderer, scale, &window_elements)
.map_err(|err| warn!("error rendering window to texture: {err:?}"))
.ok();
// Clip blocked-out resizes unconditionally because they use solid color render
// elements.
let clip_to_geometry = if target
.should_block_out(resize.snapshot.block_out_from)
&& target.should_block_out(rules.block_out_from)
{
true
} else {
clip_to_geometry
};
let clip_to_geometry =
if ctx.target.should_block_out(resize.snapshot.block_out_from)
&& ctx.target.should_block_out(rules.block_out_from)
{
true
} else {
clip_to_geometry
};
if let Some((elem_current, _sync_point, mut data)) = current {
let texture_current = elem_current.texture().clone();
@@ -1148,12 +1164,12 @@ impl<W: LayoutElement> Tile<W> {
}
// If we're not resizing, render the window itself.
let has_border_shader = BorderRenderElement::has_shader(renderer);
let has_border_shader = BorderRenderElement::has_shader(ctx.renderer);
if !pushed_resize {
let geo = Rectangle::new(window_render_loc, window_size);
let radius = radius.fit_to(window_size.w as f32, window_size.h as f32);
let clip_shader = ClippedSurfaceRenderElement::shader(renderer).cloned();
let clip_shader = ClippedSurfaceRenderElement::shader(ctx.renderer).cloned();
let clip = |elem| match elem {
LayoutElementRenderElement::Wayland(elem) => {
// If we should clip to geometry, render a clipped window.
@@ -1203,21 +1219,23 @@ impl<W: LayoutElement> Tile<W> {
// Otherwise, render the solid color as is.
LayoutElementRenderElement::SolidColor(elem).into()
}
elem @ LayoutElementRenderElement::BackgroundEffect(_) => {
// This is only used on popups for now. If subsurface blur is implemented, this
// will need to be handled somehow.
error!("background effect clipping is unimplemented");
elem.into()
}
};
if clip_to_geometry && clip_shader.is_some() {
let damage = self.rounded_corner_damage.element();
push(damage.with_location(window_render_loc).into());
let damage = self.rounded_corner_damage.render(geo);
push(damage.into());
}
self.window.render_normal(
renderer,
window_render_loc,
scale,
win_alpha,
target,
&mut |elem| push(clip(elem)),
);
self.window
.render_normal(ctx.r(), window_render_loc, scale, win_alpha, &mut |elem| {
push(clip(elem))
});
}
if fullscreen_progress > 0. {
@@ -1227,11 +1245,10 @@ impl<W: LayoutElement> Tile<W> {
// animated corner radius.
if fullscreen_progress < 1. && has_border_shader {
let border_width = self.visual_border_width().unwrap_or(0.);
let radius = rules
.geometry_corner_radius
.map_or(CornerRadius::default(), |radius| {
radius.expanded_by(border_width as f32)
})
let radius = self
.window
.geometry_corner_radius()
.expanded_by(border_width as f32)
.scaled_by(1. - expanded_progress as f32);
let size = self.fullscreen_backdrop.size();
@@ -1264,7 +1281,7 @@ impl<W: LayoutElement> Tile<W> {
if let Some(width) = self.visual_border_width() {
self.border.render(
renderer,
ctx.renderer,
location + Point::from((width, width)),
&mut |elem| push(elem.into()),
);
@@ -1276,21 +1293,33 @@ impl<W: LayoutElement> Tile<W> {
// a bit weird).
if focus_ring && expanded_progress < 1. {
self.focus_ring
.render(renderer, location, &mut |elem| push(elem.into()));
.render(ctx.renderer, location, &mut |elem| push(elem.into()));
}
if expanded_progress < 1. {
self.shadow
.render(renderer, location, &mut |elem| push(elem.into()));
.render(ctx.renderer, location, &mut |elem| push(elem.into()));
}
let surface_anim_scale = animated_window_size / window_size;
self.window.render_background_effect(
ctx.as_gles(),
area,
self.scale,
clip_to_geometry,
surface_anim_scale,
radius,
xray_pos,
&mut |elem| push(elem.into()),
);
}
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut R,
mut ctx: RenderCtx<R>,
location: Point<f64, Logical>,
xray_pos: XrayPos,
focus_ring: bool,
target: RenderTarget,
push: &mut dyn FnMut(TileRenderElement<R>),
) {
let _span = tracy_client::span!("Tile::render");
@@ -1306,17 +1335,17 @@ impl<W: LayoutElement> Tile<W> {
self.window().set_offscreen_data(None);
if let Some(open) = &self.open_animation {
let renderer = renderer.as_gles_renderer();
let mut ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(
renderer,
Point::from((0., 0.)),
ctx.r(),
Point::new(0., 0.),
xray_pos,
focus_ring,
target,
&mut |elem| elements.push(elem),
);
match open.render(
renderer,
ctx.renderer,
&elements,
self.animated_tile_size(),
location,
@@ -1333,16 +1362,16 @@ impl<W: LayoutElement> Tile<W> {
}
}
} else if let Some(alpha) = &self.alpha_animation {
let renderer = renderer.as_gles_renderer();
let mut ctx = ctx.as_gles();
let mut elements = Vec::new();
self.render_inner(
renderer,
Point::from((0., 0.)),
ctx.r(),
Point::new(0., 0.),
xray_pos,
focus_ring,
target,
&mut |elem| elements.push(elem),
);
match alpha.offscreen.render(renderer, scale, &elements) {
match alpha.offscreen.render(ctx.renderer, scale, &elements) {
Ok((elem, _sync, data)) => {
let offset = elem.offset();
let elem = elem.with_alpha(tile_alpha).with_offset(location + offset);
@@ -1358,48 +1387,137 @@ impl<W: LayoutElement> Tile<W> {
}
if !pushed {
self.render_inner(renderer, location, focus_ring, target, &mut |elem| {
push(elem)
});
self.render_inner(ctx, location, xray_pos, focus_ring, &mut |elem| push(elem));
}
}
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer) {
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
) {
if self.unmap_snapshot.is_some() {
return;
}
self.unmap_snapshot = Some(self.render_snapshot(renderer));
self.unmap_snapshot =
Some(self.render_snapshot(renderer, xray, xray_has_blocked_out_layers, xray_pos));
}
fn render_snapshot(&self, renderer: &mut GlesRenderer) -> TileRenderSnapshot {
fn render_snapshot(
&self,
renderer: &mut GlesRenderer,
mut xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
) -> TileRenderSnapshot {
let _span = tracy_client::span!("Tile::render_snapshot");
let mut contents = Vec::new();
self.render(
renderer,
RenderCtx {
target: RenderTarget::Output,
renderer,
xray: xray.as_deref(),
},
Point::from((0., 0.)),
xray_pos,
false,
RenderTarget::Output,
&mut |elem| contents.push(elem),
);
let mut contents_with_blocked_out_bg = None;
// Do a bit of pointer surgery on Xray.
//
// The idea is to avoid the combinatorial combination of rendering snapshots for target
// (Output, Screencast) × Xray target (Output, Screencast, ScreenCapture).
//
// Our main goals:
// - Everything must look unblocked for RenderTarget::Output.
// - If anything is potentially blocked-out, it must not show up on any screen capture.
//
// Right above we rendered a fully-unblocked snapshot for the Output, so that's covered.
//
// Next, *only if Xray has any blocked-out surfaces* (which is a rare case), we will render
// a snapshot where the window itself is unblocked, but the Xray background is blocked. To
// do this, we swap the Output target buffers in Xray with the Screencast target buffers
// (which were prepared for us higher up the stack).
//
// Finally, we render a fully blocked-out snapshot. If Xray has blocked-out surfaces, then
// Xray's Screencast buffers are already filled-in, but if not, then we swap in the Output
// buffers, to avoid an extra render. This is safe since we know there are no blocked
// surfaces there.
let output_idx = RenderTarget::Output as usize;
let screencast_idx = RenderTarget::Screencast as usize;
let mut screencast_background = None;
let mut screencast_backdrop = None;
let mut output_background = None;
let mut output_backdrop = None;
if let Some(xray) = &mut xray {
screencast_background = Some(Rc::clone(&xray.background[screencast_idx]));
screencast_backdrop = Some(Rc::clone(&xray.backdrop[screencast_idx]));
output_background = Some(Rc::clone(&xray.background[output_idx]));
output_backdrop = Some(Rc::clone(&xray.backdrop[output_idx]));
if xray_has_blocked_out_layers {
xray.background[output_idx] = screencast_background.clone().unwrap();
xray.backdrop[output_idx] = screencast_backdrop.clone().unwrap();
let mut contents = Vec::new();
self.render(
RenderCtx {
target: RenderTarget::Output,
renderer,
xray: Some(xray),
},
Point::from((0., 0.)),
xray_pos,
false,
&mut |elem| contents.push(elem),
);
contents_with_blocked_out_bg = Some(contents);
} else {
xray.background[screencast_idx] = output_background.clone().unwrap();
xray.backdrop[screencast_idx] = output_backdrop.clone().unwrap();
}
}
// A bit of a hack to render blocked out as for screencast, but I think it's fine here.
let mut blocked_out_contents = Vec::new();
self.render(
renderer,
RenderCtx {
target: RenderTarget::Screencast,
renderer,
xray: xray.as_deref(),
},
Point::from((0., 0.)),
xray_pos,
false,
RenderTarget::Screencast,
&mut |elem| blocked_out_contents.push(elem),
);
// Put everything back to normal.
if let Some(xray) = &mut xray {
if xray_has_blocked_out_layers {
xray.background[output_idx] = output_background.take().unwrap();
xray.backdrop[output_idx] = output_backdrop.take().unwrap();
} else {
xray.background[screencast_idx] = screencast_background.take().unwrap();
xray.backdrop[screencast_idx] = screencast_backdrop.take().unwrap();
}
}
RenderSnapshot {
contents,
contents_with_blocked_out_bg,
blocked_out_contents,
block_out_from: self.window.rules().block_out_from,
size: self.animated_tile_size(),
texture: Default::default(),
texture_with_blocked_out_bg: Default::default(),
blocked_out_texture: Default::default(),
}
}
+26 -15
View File
@@ -32,7 +32,8 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::shadow::ShadowRenderElement;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::render_helpers::xray::{Xray, XrayPos};
use crate::render_helpers::RenderCtx;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{
@@ -1626,22 +1627,22 @@ impl<W: LayoutElement> Workspace<W> {
pub fn render_scrolling<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
ctx: RenderCtx<R>,
xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
) {
let scrolling_focus_ring = focus_ring && !self.floating_is_active();
self.scrolling
.render(renderer, target, scrolling_focus_ring, &mut |elem| {
.render(ctx, xray_pos, scrolling_focus_ring, &mut |elem| {
push(elem.into())
});
}
pub fn render_floating<R: NiriRenderer>(
&self,
renderer: &mut R,
target: RenderTarget,
ctx: RenderCtx<R>,
xray_pos: XrayPos,
focus_ring: bool,
push: &mut dyn FnMut(WorkspaceRenderElement<R>),
) {
@@ -1651,13 +1652,10 @@ impl<W: LayoutElement> Workspace<W> {
let view_rect = Rectangle::from_size(self.view_size);
let floating_focus_ring = focus_ring && self.floating_is_active();
self.floating.render(
renderer,
view_rect,
target,
floating_focus_ring,
&mut |elem| push(elem.into()),
);
self.floating
.render(ctx, xray_pos, view_rect, floating_focus_ring, &mut |elem| {
push(elem.into())
});
}
pub fn render_shadow<R: NiriRenderer>(
@@ -1689,14 +1687,27 @@ impl<W: LayoutElement> Workspace<W> {
) || !self.render_above_top_layer()
}
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
pub fn store_unmap_snapshot_if_empty(
&mut self,
renderer: &mut GlesRenderer,
xray: Option<&mut Xray>,
xray_has_blocked_out_layers: bool,
xray_pos: XrayPos,
window: &W::Id,
) {
let view_size = self.view_size();
for (tile, tile_pos) in self.tiles_with_render_positions_mut(false) {
if tile.window().id() == window {
let view_pos = Point::from((-tile_pos.x, -tile_pos.y));
let view_rect = Rectangle::new(view_pos, view_size);
tile.update_render_elements(false, view_rect);
tile.store_unmap_snapshot_if_empty(renderer);
let xray_pos = xray_pos.offset(tile_pos);
tile.store_unmap_snapshot_if_empty(
renderer,
xray,
xray_has_blocked_out_layers,
xray_pos,
);
return;
}
}
+40 -1
View File
@@ -55,6 +55,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.compact()
.with_writer(io::stderr)
.with_env_filter(env_filter)
.with_ansi_sanitization(false)
.init();
if env::var_os("NOTIFY_SOCKET").is_some() {
@@ -169,6 +170,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create the compositor.
let display = Display::new().unwrap();
// Increase the buffer size so that it's harder to crash a frozen client with a 1000 Hz mouse.
set_default_max_buffer_size(&display, 1024 * 1024);
let mut state = State::new(
config,
event_loop.handle(),
@@ -230,7 +235,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if env::var_os("NIRI_DISABLE_SYSTEM_MANAGER_NOTIFY").is_none_or(|x| x != "1") {
// Notify systemd we're ready.
if let Err(err) = sd_notify::notify(true, &[NotifyState::Ready]) {
if let Err(err) = sd_notify::notify(&[NotifyState::Ready]) {
warn!("error notifying systemd: {err:?}");
};
@@ -369,3 +374,37 @@ fn notify_fd() -> anyhow::Result<()> {
notif.write_all(b"READY=1\n")?;
Ok(())
}
// The wayland-server crate has set_default_max_buffer_size() under a libwayland_1_23 feature, but
// this hard-requires libwayland-server >= 1.23 which is not present on e.g. Ubuntu 24.04. Since
// calling this is an optional enhancement, do it optionally at runtime.
fn set_default_max_buffer_size(display: &Display<State>, size: usize) {
use std::ffi::c_void;
unsafe {
// RTLD_NOLOAD ensures we only get a handle to the libwayland-server that wayland-rs has
// already loaded into this process, rather than potentially pulling in a different copy.
let lib = libc::dlopen(
c"libwayland-server.so.0".as_ptr(),
libc::RTLD_LAZY | libc::RTLD_NOLOAD,
);
if lib.is_null() {
// It's not really expected that this can happen, maybe if some distro changes the
// library name?
warn!("cannot set default max buffer size: libwayland-server.so.0 is not loaded");
return;
}
let sym = libc::dlsym(lib, c"wl_display_set_default_max_buffer_size".as_ptr());
if sym.is_null() {
// Expected on libwayland-server < 1.23.
trace!("wl_display_set_default_max_buffer_size is missing; skipping");
} else {
let func: unsafe extern "C" fn(*mut c_void, libc::size_t) = std::mem::transmute(sym);
let display_ptr = display.handle().backend_handle().display_ptr();
func(display_ptr.cast(), size);
}
libc::dlclose(lib);
}
}
+525 -166
View File
File diff suppressed because it is too large Load Diff
+238 -36
View File
@@ -1,10 +1,16 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use arrayvec::ArrayVec;
use smithay::output::Output;
use smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::{
ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1},
};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_protocols_wlr;
use smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1}, zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
};
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
@@ -12,22 +18,20 @@ use smithay::reexports::wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
};
use smithay::wayland::shell::xdg::{
ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes,
ToplevelState, ToplevelStateSet, XdgToplevelSurfaceRoleAttributes
};
use wayland_protocols_wlr::foreign_toplevel::v1::server::{
zwlr_foreign_toplevel_handle_v1, zwlr_foreign_toplevel_manager_v1,
};
use zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1;
use zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
use crate::niri::State;
use crate::window::mapped::MappedId;
use crate::utils::with_toplevel_role_and_current;
const VERSION: u32 = 3;
const EXT_LIST_VERSION: u32 = 1;
const WLR_MANAGEMENT_VERSION: u32 = 3;
pub struct ForeignToplevelManagerState {
display: DisplayHandle,
instances: Vec<ZwlrForeignToplevelManagerV1>,
ext_list_instances: HashSet<ExtForeignToplevelListV1>,
wlr_management_instances: HashSet<ZwlrForeignToplevelManagerV1>,
toplevels: HashMap<WlSurface, ToplevelData>,
}
@@ -42,33 +46,45 @@ pub trait ForeignToplevelHandler {
}
struct ToplevelData {
identifier: MappedId,
title: Option<String>,
app_id: Option<String>,
states: ArrayVec<u32, 3>,
output: Option<Output>,
instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
ext_list_instances: HashSet<ExtForeignToplevelHandleV1>,
wlr_management_instances: HashMap<ZwlrForeignToplevelHandleV1, Vec<WlOutput>>,
// FIXME: parent.
}
#[derive(Clone)]
pub struct ForeignToplevelGlobalData {
filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
filter: Arc<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>,
}
impl ForeignToplevelManagerState {
pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self
where
D: GlobalDispatch<ZwlrForeignToplevelManagerV1, ForeignToplevelGlobalData>,
D: GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData>,
D: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
D: Dispatch<ExtForeignToplevelListV1, ()>,
D: 'static,
F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static,
{
let global_data = ForeignToplevelGlobalData {
filter: Box::new(filter),
filter: Arc::new(filter),
};
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(VERSION, global_data);
display
.create_global::<D, ExtForeignToplevelListV1, _>(EXT_LIST_VERSION, global_data.clone());
display.create_global::<D, ZwlrForeignToplevelManagerV1, _>(
WLR_MANAGEMENT_VERSION,
global_data,
);
Self {
display: display.clone(),
instances: Vec::new(),
ext_list_instances: HashSet::new(),
wlr_management_instances: HashSet::new(),
toplevels: HashMap::new(),
}
}
@@ -85,7 +101,11 @@ pub fn refresh(state: &mut State) {
return true;
}
for instance in data.instances.keys() {
for instance in data.ext_list_instances.iter() {
instance.closed();
}
for instance in data.wlr_management_instances.keys() {
instance.closed();
}
@@ -107,15 +127,23 @@ pub fn refresh(state: &mut State) {
};
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((mapped.window.clone(), output.cloned()));
focused = Some((mapped.id(), mapped.window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, role, cur, output, false);
refresh_toplevel(
protocol_state,
wl_surface,
mapped.id(),
role,
cur,
output,
false,
);
}
});
});
// Finally, refresh the focused window.
if let Some((window, output)) = focused {
if let Some((identifier, window, output)) = focused {
let toplevel = window.toplevel().expect("no X11 support");
let wl_surface = toplevel.wl_surface();
with_toplevel_role_and_current(toplevel, |role, cur| {
@@ -124,7 +152,15 @@ pub fn refresh(state: &mut State) {
return;
};
refresh_toplevel(protocol_state, wl_surface, role, cur, output.as_ref(), true);
refresh_toplevel(
protocol_state,
wl_surface,
identifier,
role,
cur,
output.as_ref(),
true,
);
});
}
}
@@ -142,7 +178,7 @@ pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput)
continue;
}
for (instance, outputs) in &mut data.instances {
for (instance, outputs) in &mut data.wlr_management_instances {
if instance.client().as_ref() != Some(&client) {
continue;
}
@@ -157,6 +193,7 @@ pub fn on_output_bound(state: &mut State, output: &Output, wl_output: &WlOutput)
fn refresh_toplevel(
protocol_state: &mut ForeignToplevelManagerState,
wl_surface: &WlSurface,
identifier: MappedId,
role: &XdgToplevelSurfaceRoleAttributes,
current: &ToplevelState,
output: Option<&Output>,
@@ -201,11 +238,24 @@ fn refresh_toplevel(
output_changed = true;
}
let something_changed =
let something_changed_for_ext = new_title.is_some() || new_app_id.is_some();
let something_changed_for_wlr =
new_title.is_some() || new_app_id.is_some() || states_changed || output_changed;
if something_changed {
for (instance, outputs) in &mut data.instances {
if something_changed_for_ext {
for instance in &data.ext_list_instances {
if let Some(new_title) = new_title {
instance.title(new_title.to_owned());
}
if let Some(new_app_id) = new_app_id {
instance.app_id(new_app_id.to_owned());
}
instance.done();
}
}
if something_changed_for_wlr {
for (instance, outputs) in &mut data.wlr_management_instances {
if let Some(new_title) = new_title {
instance.title(new_title.to_owned());
}
@@ -232,7 +282,7 @@ fn refresh_toplevel(
}
}
for outputs in data.instances.values_mut() {
for outputs in data.wlr_management_instances.values_mut() {
// Clean up dead wl_outputs.
outputs.retain(|x| x.is_alive());
}
@@ -240,16 +290,24 @@ fn refresh_toplevel(
Entry::Vacant(entry) => {
// New window, start tracking it.
let mut data = ToplevelData {
identifier,
title: role.title.clone(),
app_id: role.app_id.clone(),
states,
output: output.cloned(),
instances: HashMap::new(),
ext_list_instances: HashSet::new(),
wlr_management_instances: HashMap::new(),
};
for manager in &protocol_state.instances {
for manager in &protocol_state.ext_list_instances {
if let Some(client) = manager.client() {
data.add_instance::<State>(&protocol_state.display, &client, manager);
data.add_ext_instance::<State>(&protocol_state.display, &client, manager);
}
}
for manager in &protocol_state.wlr_management_instances {
if let Some(client) = manager.client() {
data.add_wlr_instance::<State>(&protocol_state.display, &client, manager);
}
}
@@ -259,7 +317,35 @@ fn refresh_toplevel(
}
impl ToplevelData {
fn add_instance<D>(
fn add_ext_instance<D>(
&mut self,
handle: &DisplayHandle,
client: &Client,
manager: &ExtForeignToplevelListV1,
) where
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
D: 'static,
{
let toplevel = client
.create_resource::<ExtForeignToplevelHandleV1, _, D>(handle, manager.version(), ())
.unwrap();
manager.toplevel(&toplevel);
toplevel.identifier(self.identifier.to_protocol_identifier());
if let Some(title) = &self.title {
toplevel.title(title.clone());
}
if let Some(app_id) = &self.app_id {
toplevel.app_id(app_id.clone());
}
toplevel.done();
self.ext_list_instances.insert(toplevel);
}
fn add_wlr_instance<D>(
&mut self,
handle: &DisplayHandle,
client: &Client,
@@ -292,7 +378,111 @@ impl ToplevelData {
toplevel.done();
self.instances.insert(toplevel, outputs);
self.wlr_management_instances.insert(toplevel, outputs);
}
}
impl<D> GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData, D>
for ForeignToplevelManagerState
where
D: GlobalDispatch<ExtForeignToplevelListV1, ForeignToplevelGlobalData>,
D: Dispatch<ExtForeignToplevelListV1, ()>,
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn bind(
state: &mut D,
handle: &DisplayHandle,
client: &Client,
resource: New<ExtForeignToplevelListV1>,
_global_data: &ForeignToplevelGlobalData,
data_init: &mut DataInit<'_, D>,
) {
let manager = data_init.init(resource, ());
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.add_ext_instance::<D>(handle, client, &manager);
}
state.ext_list_instances.insert(manager);
}
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
(global_data.filter)(&client)
}
}
impl<D> Dispatch<ExtForeignToplevelListV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ExtForeignToplevelListV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
state: &mut D,
_client: &Client,
resource: &ExtForeignToplevelListV1,
request: <ExtForeignToplevelListV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
ext_foreign_toplevel_list_v1::Request::Stop => {
resource.finished();
// remove the instance here so we won't send any more events.
let state = state.foreign_toplevel_manager_state();
state.ext_list_instances.remove(resource);
}
ext_foreign_toplevel_list_v1::Request::Destroy => {}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ExtForeignToplevelListV1,
_data: &(),
) {
// also remove the instance here, in case `stop` was never sent, e.g. sudden disconnect.
let state = state.foreign_toplevel_manager_state();
state.ext_list_instances.remove(resource);
}
}
impl<D> Dispatch<ExtForeignToplevelHandleV1, (), D> for ForeignToplevelManagerState
where
D: Dispatch<ExtForeignToplevelHandleV1, ()>,
D: ForeignToplevelHandler,
{
fn request(
_state: &mut D,
_client: &Client,
_resource: &ExtForeignToplevelHandleV1,
request: <ExtForeignToplevelHandleV1 as Resource>::Request,
_data: &(),
_dhandle: &DisplayHandle,
_data_init: &mut DataInit<'_, D>,
) {
match request {
ext_foreign_toplevel_handle_v1::Request::Destroy => {}
_ => unreachable!(),
}
}
fn destroyed(
state: &mut D,
_client: ClientId,
resource: &ExtForeignToplevelHandleV1,
_data: &(),
) {
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.ext_list_instances.remove(resource);
}
}
}
@@ -317,10 +507,10 @@ where
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.add_instance::<D>(handle, client, &manager);
data.add_wlr_instance::<D>(handle, client, &manager);
}
state.instances.push(manager);
state.wlr_management_instances.insert(manager);
}
fn can_view(client: Client, global_data: &ForeignToplevelGlobalData) -> bool {
@@ -346,8 +536,9 @@ where
zwlr_foreign_toplevel_manager_v1::Request::Stop => {
resource.finished();
// remove the instance here so we won't send any more events.
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
state.wlr_management_instances.remove(resource);
}
_ => unreachable!(),
}
@@ -359,8 +550,9 @@ where
resource: &ZwlrForeignToplevelManagerV1,
_data: &(),
) {
// also remove the instance here, in case `stop` was never sent, e.g. sudden disconnect.
let state = state.foreign_toplevel_manager_state();
state.instances.retain(|x| x != resource);
state.wlr_management_instances.remove(resource);
}
}
@@ -383,7 +575,7 @@ where
let Some((surface, _)) = protocol_state
.toplevels
.iter()
.find(|(_, data)| data.instances.contains_key(resource))
.find(|(_, data)| data.wlr_management_instances.contains_key(resource))
else {
return;
};
@@ -422,7 +614,7 @@ where
) {
let state = state.foreign_toplevel_manager_state();
for data in state.toplevels.values_mut() {
data.instances.retain(|instance, _| instance != resource);
data.wlr_management_instances.remove(resource);
}
}
}
@@ -454,6 +646,16 @@ fn to_state_vec(states: &ToplevelStateSet, has_focus: bool) -> ArrayVec<u32, 3>
#[macro_export]
macro_rules! delegate_foreign_toplevel {
($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols::ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1: ()
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1: $crate::protocols::foreign_toplevel::ForeignToplevelGlobalData
] => $crate::protocols::foreign_toplevel::ForeignToplevelManagerState);
-1
View File
@@ -4,7 +4,6 @@ pub mod gamma_control;
pub mod mutter_x11_interop;
pub mod output_management;
pub mod screencopy;
pub mod virtual_keyboard;
pub mod virtual_pointer;
pub mod raw;
-132
View File
@@ -1,132 +0,0 @@
use smithay::backend::input::{
Device, DeviceCapability, Event, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, Keycode,
UnusedEvent,
};
use smithay::delegate_virtual_keyboard_manager;
use smithay::input::keyboard::xkb::ModMask;
use smithay::input::keyboard::KeyboardHandle;
use smithay::wayland::virtual_keyboard::VirtualKeyboardHandler;
use crate::niri::State;
pub struct VirtualKeyboardInputBackend;
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct VirtualKeyboard;
impl Device for VirtualKeyboard {
fn id(&self) -> String {
String::from("virtual keyboard")
}
fn name(&self) -> String {
String::from("virtual keyboard")
}
fn has_capability(&self, capability: DeviceCapability) -> bool {
matches!(capability, DeviceCapability::Keyboard)
}
fn usb_id(&self) -> Option<(u32, u32)> {
None
}
fn syspath(&self) -> Option<std::path::PathBuf> {
None
}
}
pub struct VirtualKeyboardKeyEvent {
pub keycode: Keycode,
pub state: KeyState,
pub time: u32,
}
impl Event<VirtualKeyboardInputBackend> for VirtualKeyboardKeyEvent {
fn time(&self) -> u64 {
self.time as u64 * 1000 // millis to micros
}
fn device(&self) -> VirtualKeyboard {
VirtualKeyboard
}
}
impl KeyboardKeyEvent<VirtualKeyboardInputBackend> for VirtualKeyboardKeyEvent {
fn key_code(&self) -> Keycode {
self.keycode
}
fn state(&self) -> KeyState {
self.state
}
fn count(&self) -> u32 {
0 // Not used by niri
}
}
impl InputBackend for VirtualKeyboardInputBackend {
type Device = VirtualKeyboard;
type KeyboardKeyEvent = VirtualKeyboardKeyEvent;
type PointerAxisEvent = UnusedEvent;
type PointerButtonEvent = UnusedEvent;
type PointerMotionEvent = UnusedEvent;
type PointerMotionAbsoluteEvent = UnusedEvent;
type GestureSwipeBeginEvent = UnusedEvent;
type GestureSwipeUpdateEvent = UnusedEvent;
type GestureSwipeEndEvent = UnusedEvent;
type GesturePinchBeginEvent = UnusedEvent;
type GesturePinchUpdateEvent = UnusedEvent;
type GesturePinchEndEvent = UnusedEvent;
type GestureHoldBeginEvent = UnusedEvent;
type GestureHoldEndEvent = UnusedEvent;
type TouchDownEvent = UnusedEvent;
type TouchUpEvent = UnusedEvent;
type TouchMotionEvent = UnusedEvent;
type TouchCancelEvent = UnusedEvent;
type TouchFrameEvent = UnusedEvent;
type TabletToolAxisEvent = UnusedEvent;
type TabletToolProximityEvent = UnusedEvent;
type TabletToolTipEvent = UnusedEvent;
type TabletToolButtonEvent = UnusedEvent;
type SwitchToggleEvent = UnusedEvent;
type SpecialEvent = UnusedEvent;
}
impl VirtualKeyboardHandler for State {
fn on_keyboard_event(
&mut self,
keycode: Keycode,
state: KeyState,
time: u32,
_keyboard: KeyboardHandle<Self>,
) {
// The virtual keyboard impl in Smithay changes the keymap, so we'll need to reset it on
// the next real keyboard event.
self.niri.reset_keymap = true;
let event = VirtualKeyboardKeyEvent {
keycode,
state,
time,
};
self.process_input_event(InputEvent::<VirtualKeyboardInputBackend>::Keyboard { event });
}
// We handle modifiers when the key event is sent.
fn on_keyboard_modifiers(
&mut self,
_depressed_mods: ModMask,
_latched_mods: ModMask,
_locked_mods: ModMask,
_keyboard: KeyboardHandle<Self>,
) {
}
}
delegate_virtual_keyboard_manager!(State);
+332
View File
@@ -0,0 +1,332 @@
use std::sync::{Arc, Mutex};
use niri_config::CornerRadius;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale};
use smithay::wayland::compositor::{with_states, SurfaceData};
use wayland_server::protocol::wl_surface::WlSurface;
use crate::handlers::background_effect::get_cached_blur_region;
use crate::niri_render_elements;
use crate::render_helpers::blur::BlurOptions;
use crate::render_helpers::damage::ExtraDamage;
use crate::render_helpers::framebuffer_effect::{FramebufferEffect, FramebufferEffectElement};
use crate::render_helpers::xray::{XrayElement, XrayPos};
use crate::render_helpers::RenderCtx;
use crate::utils::region::TransformedRegion;
use crate::utils::surface_geo;
#[derive(Debug)]
pub struct BackgroundEffect {
nonxray: FramebufferEffect,
/// Damage when options change.
damage: ExtraDamage,
/// Corner radius for clipping.
///
/// Stored here in addition to `RenderParams` to damage when it changes.
// FIXME: would be good to remove this duplication of radius.
corner_radius: CornerRadius,
blur_config: niri_config::Blur,
options: Options,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Options {
pub blur: bool,
pub xray: bool,
pub noise: Option<f64>,
pub saturation: Option<f64>,
}
impl Options {
fn is_visible(&self) -> bool {
self.xray
|| self.blur
|| self.noise.is_some_and(|x| x > 0.)
|| self.saturation.is_some_and(|x| x != 1.)
}
}
/// Render-time parameters.
#[derive(Debug)]
pub struct RenderParams {
/// Geometry of the background effect.
pub geometry: Rectangle<f64, Logical>,
/// Effect subregion, will be clipped to `geometry`.
///
/// `subregion.iter()` should return `geometry`-relative rectangles.
pub subregion: Option<TransformedRegion>,
/// Geometry and radius for clipping in the same coordinate space as `geometry`.
pub clip: Option<(Rectangle<f64, Logical>, CornerRadius)>,
/// Scale to use for rounding to physical pixels.
pub scale: f64,
}
impl RenderParams {
fn fit_clip_radius(&mut self) {
if let Some((geo, radius)) = &mut self.clip {
// HACK: increase radius to avoid slight bleed on rounded corners.
*radius = radius.expanded_by(1.);
*radius = radius.fit_to(geo.size.w as f32, geo.size.h as f32);
}
}
}
niri_render_elements! {
BackgroundEffectElement => {
FramebufferEffect = FramebufferEffectElement,
Xray = XrayElement,
ExtraDamage = ExtraDamage,
}
}
impl BackgroundEffect {
pub fn new() -> Self {
Self {
nonxray: FramebufferEffect::new(),
damage: ExtraDamage::new(),
corner_radius: CornerRadius::default(),
blur_config: niri_config::Blur::default(),
options: Options::default(),
}
}
/// Damage the background effect, for example when a blur subregion changes.
pub fn damage(&mut self) {
self.damage.damage_all();
self.nonxray.damage();
}
pub fn update_config(&mut self, config: niri_config::Blur) {
if self.blur_config == config {
return;
}
self.blur_config = config;
self.damage.damage_all();
self.nonxray.damage();
}
pub fn update_render_elements(
&mut self,
corner_radius: CornerRadius,
effect: niri_config::BackgroundEffect,
has_blur_region: bool,
) {
// If the surface explicitly requests a blur region, default blur to true.
let blur = if has_blur_region {
effect.blur != Some(false)
} else {
effect.blur == Some(true)
};
let mut options = Options {
blur,
xray: effect.xray == Some(true),
noise: effect.noise,
saturation: effect.saturation,
};
// If we have some background effect but xray wasn't explicitly set, default it to true
// since it's cheaper.
if options.is_visible() && effect.xray.is_none() {
options.xray = true;
}
if self.options == options && self.corner_radius == corner_radius {
return;
}
self.options = options;
self.corner_radius = corner_radius;
self.damage.damage_all();
self.nonxray.damage();
}
pub fn is_visible(&self) -> bool {
self.options.is_visible()
}
pub fn render(
&self,
ctx: RenderCtx<GlesRenderer>,
ns: Option<usize>,
mut params: RenderParams,
xray_pos: XrayPos,
push: &mut dyn FnMut(BackgroundEffectElement),
) {
if !self.is_visible() {
return;
}
if let Some(clip) = &mut params.clip {
clip.1 = self.corner_radius;
}
params.fit_clip_radius();
let damage = self.damage.render(params.geometry);
// Use noise/saturation from options, falling back to blur defaults if blurred, and
// to no effect if not blurred.
let blur = self.options.blur && !self.blur_config.off;
let blur_options = blur.then_some(BlurOptions::from(self.blur_config));
let noise = if blur { self.blur_config.noise } else { 0. };
let noise = self.options.noise.unwrap_or(noise) as f32;
let saturation = if blur {
self.blur_config.saturation
} else {
1.
};
let saturation = self.options.saturation.unwrap_or(saturation) as f32;
if self.options.xray {
let Some(xray) = ctx.xray else {
return;
};
push(damage.into());
xray.render(
ctx,
params,
xray_pos,
blur,
noise,
saturation,
&mut |elem| push(elem.into()),
);
} else {
// Render non-xray effect.
let elem = self
.nonxray
.render(ns, params, blur_options, noise, saturation);
push(elem.into());
}
}
}
fn render_params_for_tile(
geometry: Rectangle<f64, Logical>,
scale: f64,
clip_to_geometry: bool,
block_out: bool,
blur_region: Option<Arc<Vec<Rectangle<i32, Logical>>>>,
surface_geo: Rectangle<f64, Logical>,
surface_anim_scale: Scale<f64>,
) -> Option<RenderParams> {
// Effects not requested by the surface itself are drawn to match the geometry.
let mut clip = true;
let mut effect_geometry = geometry;
let mut subregion = None;
if let Some(rects) = blur_region {
if rects.is_empty() {
// Surface has a set, but empty blur region.
return None;
} else {
// If the surface itself requests the effects, apply different defaults.
clip = clip_to_geometry;
// Use geometry-shaped blur for blocked-out windows to avoid unintentionally
// leaking any surface shapes. We render those windows as geometry-shaped solid
// rectangles anyway.
if block_out {
clip = true;
} else {
let mut surface_geo = surface_geo.upscale(surface_anim_scale);
surface_geo.loc += geometry.loc;
subregion = Some(TransformedRegion {
rects,
scale: surface_anim_scale,
offset: surface_geo.loc,
});
surface_geo = surface_geo
.to_physical_precise_round(scale)
.to_logical(scale);
effect_geometry = surface_geo;
}
}
}
// This corner radius is reset to self.corner_radius in render().
let clip = clip.then_some((geometry, CornerRadius::default()));
Some(RenderParams {
geometry: effect_geometry,
subregion,
clip,
scale,
})
}
/// Per-surface background effect stored in its data map.
struct SurfaceBackgroundEffect(Mutex<BackgroundEffect>);
impl SurfaceBackgroundEffect {
fn get(states: &SurfaceData) -> &Self {
states
.data_map
.get_or_insert(|| SurfaceBackgroundEffect(Mutex::new(BackgroundEffect::new())))
}
}
pub fn damage_surface(states: &SurfaceData) {
if let Some(effect) = states.data_map.get::<SurfaceBackgroundEffect>() {
effect.0.lock().unwrap().damage();
}
}
// Silence, Clippy
// A Smithay user is talking
#[allow(clippy::too_many_arguments)]
pub fn render_for_tile(
ctx: RenderCtx<GlesRenderer>,
ns: Option<usize>,
geometry: Rectangle<f64, Logical>,
scale: f64,
clip_to_geometry: bool,
surface: &WlSurface,
surface_off: Point<f64, Logical>,
surface_anim_scale: Scale<f64>,
blur_config: niri_config::Blur,
radius: CornerRadius,
effect: niri_config::BackgroundEffect,
should_block_out: bool,
xray_pos: XrayPos,
push: &mut dyn FnMut(BackgroundEffectElement),
) {
with_states(surface, |states| {
let background_effect = SurfaceBackgroundEffect::get(states);
let mut background_effect = background_effect.0.lock().unwrap();
let blur_region = get_cached_blur_region(states);
let has_blur_region = blur_region.as_ref().is_some_and(|r| !r.is_empty());
background_effect.update_config(blur_config);
background_effect.update_render_elements(radius, effect, has_blur_region);
if !background_effect.is_visible() {
return;
}
let mut surface_geo = surface_geo(states).unwrap_or_default().to_f64();
surface_geo.loc += surface_off;
let Some(params) = render_params_for_tile(
geometry,
scale,
clip_to_geometry,
should_block_out,
blur_region,
surface_geo,
surface_anim_scale,
) else {
return;
};
let xray_pos = xray_pos.offset(params.geometry.loc - geometry.loc);
background_effect.render(ctx, ns, params, xray_pos, push);
});
}
+342
View File
@@ -0,0 +1,342 @@
use std::cmp::max;
use std::iter::{once, zip};
use std::rc::Rc;
use anyhow::{ensure, Context as _};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::gles::{ffi, link_program, GlesError, GlesRenderer, GlesTexture};
use smithay::backend::renderer::{ContextId, Renderer as _, Texture as _};
use smithay::gpu_span_location;
use smithay::utils::{Buffer, Size};
use crate::render_helpers::shaders::Shaders;
#[derive(Debug)]
pub struct Blur {
program: BlurProgram,
/// Context ID of the renderer that created the program and the textures.
renderer_context_id: ContextId<GlesTexture>,
/// Output texture followed by intermediate textures, large to small.
///
/// Created lazily and stored here to avoid recreating blur textures frequently.
textures: Vec<GlesTexture>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct BlurOptions {
pub passes: u8,
pub offset: f64,
}
impl From<niri_config::Blur> for BlurOptions {
fn from(config: niri_config::Blur) -> Self {
Self {
passes: config.passes,
offset: config.offset,
}
}
}
#[derive(Debug, Clone)]
pub struct BlurProgram(Rc<BlurProgramInner>);
#[derive(Debug)]
struct BlurProgramInner {
down: BlurProgramInternal,
up: BlurProgramInternal,
}
#[derive(Debug)]
struct BlurProgramInternal {
program: ffi::types::GLuint,
uniform_tex: ffi::types::GLint,
uniform_half_pixel: ffi::types::GLint,
uniform_offset: ffi::types::GLint,
attrib_vert: ffi::types::GLint,
}
unsafe fn compile_program(gl: &ffi::Gles2, src: &str) -> Result<BlurProgramInternal, GlesError> {
let program = unsafe { link_program(gl, include_str!("shaders/blur.vert"), src)? };
let vert = c"vert";
let tex = c"tex";
let half_pixel = c"half_pixel";
let offset = c"offset";
Ok(BlurProgramInternal {
program,
uniform_tex: gl.GetUniformLocation(program, tex.as_ptr()),
uniform_half_pixel: gl.GetUniformLocation(program, half_pixel.as_ptr()),
uniform_offset: gl.GetUniformLocation(program, offset.as_ptr()),
attrib_vert: gl.GetAttribLocation(program, vert.as_ptr()),
})
}
impl BlurProgram {
pub fn compile(renderer: &mut GlesRenderer) -> anyhow::Result<Self> {
renderer
.with_context(move |gl| unsafe {
let down = compile_program(gl, include_str!("shaders/blur_down.frag"))
.context("error compiling blur_down shader")?;
let up = compile_program(gl, include_str!("shaders/blur_up.frag"))
.context("error compiling blur_up shader")?;
Ok(Self(Rc::new(BlurProgramInner { down, up })))
})
.context("error making GL context current")?
}
pub fn destroy(self, renderer: &mut GlesRenderer) -> Result<(), GlesError> {
renderer.with_context(move |gl| unsafe {
gl.DeleteProgram(self.0.down.program);
gl.DeleteProgram(self.0.up.program);
})
}
}
impl Blur {
pub fn new(renderer: &mut GlesRenderer) -> Option<Self> {
let program = Shaders::get(renderer).blur.clone()?;
Some(Self {
program,
renderer_context_id: renderer.context_id(),
textures: Vec::new(),
})
}
pub fn context_id(&self) -> ContextId<GlesTexture> {
self.renderer_context_id.clone()
}
pub fn prepare_textures(
&mut self,
mut create_texture: impl FnMut(Fourcc, Size<i32, Buffer>) -> Result<GlesTexture, GlesError>,
source: &GlesTexture,
options: BlurOptions,
) -> anyhow::Result<()> {
let _span = tracy_client::span!("Blur::prepare_textures");
let passes = options.passes.clamp(1, 31) as usize;
let size = source.size();
if let Some(output) = self.textures.first_mut() {
let old_size = output.size();
if old_size != size {
trace!(
"recreating textures: output size changed from {} × {} to {} × {}",
old_size.w,
old_size.h,
size.w,
size.h
);
self.textures.clear();
} else if !output.is_unique_reference() {
debug!("recreating textures: not unique",);
// We only need to recreate the output texture here, but this case shouldn't really
// happen anyway, and this is simpler.
self.textures.clear();
}
}
// Create any missing textures.
let mut w = size.w;
let mut h = size.h;
for i in 0..=passes {
let size = Size::new(w, h);
w = max(1, w / 2);
h = max(1, h / 2);
if self.textures.len() > i {
// This texture already exists.
continue;
}
// debug!("creating texture for step {i} sized {w} × {h}");
let texture: GlesTexture =
create_texture(Fourcc::Abgr8888, size).context("error creating texture")?;
self.textures.push(texture);
}
// Drop any no longer needed textures.
self.textures.drain(passes + 1..);
Ok(())
}
pub fn render(
&mut self,
renderer: &mut GlesRenderer,
source: &GlesTexture,
options: BlurOptions,
) -> anyhow::Result<GlesTexture> {
let _span = tracy_client::span!("Blur::render");
trace!("rendering blur");
ensure!(
renderer.context_id() == self.renderer_context_id,
"wrong renderer"
);
let passes = options.passes.clamp(1, 31) as usize;
let size = source.size();
ensure!(
self.textures.len() == passes + 1,
"wrong textures len: expected {}, got {}",
passes + 1,
self.textures.len()
);
let output = &mut self.textures[0];
ensure!(
output.size() == size,
"wrong output texture size: expected {size:?}, got {:?}",
output.size()
);
ensure!(
output.is_unique_reference(),
"output texture has a non-unique reference"
);
renderer.with_profiled_context(gpu_span_location!("Blur::render"), |gl| unsafe {
while gl.GetError() != ffi::NO_ERROR {}
gl.Disable(ffi::BLEND);
gl.Disable(ffi::SCISSOR_TEST);
gl.ActiveTexture(ffi::TEXTURE0);
let mut fbos = [0; 2];
gl.GenFramebuffers(fbos.len() as _, fbos.as_mut_ptr());
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbos[0]);
let program = &self.program.0.down;
gl.UseProgram(program.program);
gl.Uniform1i(program.uniform_tex, 0);
gl.Uniform1f(program.uniform_offset, options.offset as f32);
let vertices: [f32; 12] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0];
gl.EnableVertexAttribArray(program.attrib_vert as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
gl.VertexAttribPointer(
program.attrib_vert as u32,
2,
ffi::FLOAT,
ffi::FALSE,
0,
vertices.as_ptr().cast(),
);
let src = once(source).chain(&self.textures[1..]);
let dst = &self.textures[1..];
for (src, dst) in zip(src, dst) {
let dst_size = dst.size();
let w = dst_size.w;
let h = dst_size.h;
gl.Viewport(0, 0, w, h);
// During downsampling, half_pixel is half of the destination pixel.
gl.Uniform2f(program.uniform_half_pixel, 0.5 / w as f32, 0.5 / h as f32);
let src = src.tex_id();
let dst = dst.tex_id();
trace!("drawing down {src} to {dst}");
gl.FramebufferTexture2D(
ffi::DRAW_FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
dst,
0,
);
gl.BindTexture(ffi::TEXTURE_2D, src);
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32);
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_WRAP_S,
ffi::CLAMP_TO_EDGE as i32,
);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_WRAP_T,
ffi::CLAMP_TO_EDGE as i32,
);
gl.DrawArrays(ffi::TRIANGLES, 0, 6);
}
gl.DisableVertexAttribArray(program.attrib_vert as u32);
// Up
let program = &self.program.0.up;
gl.UseProgram(program.program);
gl.Uniform1i(program.uniform_tex, 0);
gl.Uniform1f(program.uniform_offset, options.offset as f32);
let vertices: [f32; 12] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0];
gl.EnableVertexAttribArray(program.attrib_vert as u32);
gl.BindBuffer(ffi::ARRAY_BUFFER, 0);
gl.VertexAttribPointer(
program.attrib_vert as u32,
2,
ffi::FLOAT,
ffi::FALSE,
0,
vertices.as_ptr().cast(),
);
let src = self.textures.iter().rev();
let dst = self.textures.iter().rev().skip(1);
for (src, dst) in zip(src, dst) {
let dst_size = dst.size();
let w = dst_size.w;
let h = dst_size.h;
gl.Viewport(0, 0, w, h);
// During upsampling, half_pixel is half of the source pixel.
let src_size = src.size();
let src_w = src_size.w as f32;
let src_h = src_size.h as f32;
gl.Uniform2f(program.uniform_half_pixel, 0.5 / src_w, 0.5 / src_h);
let src = src.tex_id();
let dst = dst.tex_id();
trace!("drawing up {src} to {dst}");
gl.FramebufferTexture2D(
ffi::DRAW_FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
dst,
0,
);
gl.BindTexture(ffi::TEXTURE_2D, src);
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MIN_FILTER, ffi::LINEAR as i32);
gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_MAG_FILTER, ffi::LINEAR as i32);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_WRAP_S,
ffi::CLAMP_TO_EDGE as i32,
);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_WRAP_T,
ffi::CLAMP_TO_EDGE as i32,
);
gl.DrawArrays(ffi::TRIANGLES, 0, 6);
}
gl.DisableVertexAttribArray(program.attrib_vert as u32);
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, 0);
gl.DeleteFramebuffers(fbos.len() as _, fbos.as_ptr());
})?;
Ok(self.textures[0].clone())
}
}
+5 -1
View File
@@ -9,6 +9,7 @@ use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, Unde
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, Uniform};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::gpu_span_location;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::renderer::NiriRenderer;
@@ -285,6 +286,7 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
let _span = tracy_client::span!("BorderRenderElement::draw");
frame.with_gpu_span(gpu_span_location!("BorderRenderElement::draw"), |frame| {
@@ -295,6 +297,7 @@ impl RenderElement<GlesRenderer> for BorderRenderElement {
dst,
damage,
opaque_regions,
cache,
)
})
}
@@ -312,9 +315,10 @@ impl<'render> RenderElement<TtyRenderer<'render>> for BorderRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions, cache)?;
Ok(())
}
+24 -10
View File
@@ -1,11 +1,13 @@
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
use smithay::backend::renderer::buffer_y_inverted;
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::damage::ExtraDamage;
@@ -74,12 +76,18 @@ impl<R: NiriRenderer> ClippedSurfaceRenderElement<R> {
* Mat3::from_cols_array(transform.matrix().as_ref())
* Mat3::from_translation(-Vec2::new(0.5, 0.5));
// FIXME: y_inverted
let y_invert = if buffer_y_inverted(self.inner.buffer()).unwrap_or(false) {
Mat3::from_scale(Vec2::new(1., -1.))
} else {
Mat3::IDENTITY
};
let input_to_geo = transform_matrix * Mat3::from_scale(elem_geo_size / geo_size)
* Mat3::from_translation((elem_geo_loc - geo_loc) / elem_geo_size)
// Apply viewporter src.
* Mat3::from_scale(buf_size / src_size)
* Mat3::from_translation(-src_loc / buf_size);
* Mat3::from_translation(-src_loc / buf_size)
* y_invert;
let geo_size = (self.geometry.size.w as f32, self.geometry.size.h as f32);
@@ -228,9 +236,18 @@ impl RenderElement<GlesRenderer> for ClippedSurfaceRenderElement<GlesRenderer> {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
frame.override_default_tex_program(self.program.clone(), self.compute_uniforms());
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(
&self.inner,
frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
frame.clear_tex_program_override();
Ok(())
}
@@ -252,11 +269,12 @@ impl<'render> RenderElement<TtyRenderer<'render>>
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
frame
.as_gles_frame()
.override_default_tex_program(self.program.clone(), self.compute_uniforms());
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
RenderElement::draw(&self.inner, frame, src, dst, damage, opaque_regions, cache)?;
frame.as_gles_frame().clear_tex_program_override();
Ok(())
}
@@ -272,10 +290,6 @@ impl<'render> RenderElement<TtyRenderer<'render>>
}
impl RoundedCornerDamage {
pub fn set_size(&mut self, size: Size<f64, Logical>) {
self.damage.set_size(size);
}
pub fn set_corner_radius(&mut self, corner_radius: CornerRadius) {
if self.corner_radius == corner_radius {
return;
@@ -286,7 +300,7 @@ impl RoundedCornerDamage {
self.damage.damage_all();
}
pub fn element(&self) -> ExtraDamage {
self.damage.clone()
pub fn render(&self, geometry: Rectangle<f64, Logical>) -> ExtraDamage {
self.damage.render(geometry)
}
}
+7 -13
View File
@@ -1,7 +1,8 @@
use smithay::backend::renderer::element::{Element, Id, RenderElement};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::Renderer;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size};
#[derive(Debug, Clone)]
pub struct ExtraDamage {
@@ -19,22 +20,14 @@ impl ExtraDamage {
}
}
pub fn set_size(&mut self, size: Size<f64, Logical>) {
if self.geometry.size == size {
return;
}
self.geometry.size = size;
self.commit.increment();
}
pub fn damage_all(&mut self) {
self.commit.increment();
}
pub fn with_location(mut self, location: Point<f64, Logical>) -> Self {
self.geometry.loc = location;
self
pub fn render(&self, geometry: Rectangle<f64, Logical>) -> Self {
let mut this = self.clone();
this.geometry = geometry;
this
}
}
@@ -70,6 +63,7 @@ impl<R: Renderer> RenderElement<R> for ExtraDamage {
_dst: Rectangle<i32, Physical>,
_damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
_cache: Option<&UserDataMap>,
) -> Result<(), R::Error> {
Ok(())
}
+325
View File
@@ -0,0 +1,325 @@
use std::mem;
use anyhow::{ensure, Context as _};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::{Id, RenderElementStates};
use smithay::backend::renderer::gles::{GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{
Bind as _, Color32F, ContextId, FrameContext as _, Offscreen as _, Renderer as _, Texture,
};
use smithay::utils::{Buffer, Logical, Physical, Scale, Size, Transform};
use crate::niri::OutputRenderElements;
use crate::render_helpers::blur::{Blur, BlurOptions};
#[derive(Debug)]
pub struct EffectBuffer {
/// Id to be used for this effect buffer's elements.
id: Id,
/// Size of the effect buffer.
size: Size<i32, Buffer>,
/// Scale of the effect buffer.
scale: Scale<f64>,
/// Options for blurring.
blur_options: BlurOptions,
/// Elements to be rendered on demand.
elements: Elements,
/// Offscreen buffer where elements get rendered.
offscreen: Option<Offscreen>,
/// Blurring program, if available.
blur: Option<Blur>,
/// Commit counter that takes into account both original and blurred texture changes.
commit_counter: CommitCounter,
}
#[derive(Debug)]
enum Elements {
/// Contents remain unchanged.
Unchanged(
// Storage to avoid reallocating it every time.
Vec<OutputRenderElements<GlesRenderer>>,
),
/// New contents, need to check damage and render.
New(Vec<OutputRenderElements<GlesRenderer>>),
}
#[derive(Debug)]
struct Offscreen {
/// The texture with the offscreen contents.
texture: GlesTexture,
/// Id of the renderer context that the texture comes from.
renderer_context_id: ContextId<GlesTexture>,
/// Scale of the texture.
scale: Scale<f64>,
/// Damage tracker for drawing to the texture.
damage: OutputDamageTracker,
/// Render element states from the last render into the offscreen.
states: RenderElementStates,
/// Rendered blurred version of the texture.
///
/// When texture needs to be reblurred, this field must be reset to `None`.
blurred: Option<GlesTexture>,
}
impl Default for Elements {
fn default() -> Self {
Self::Unchanged(Vec::new())
}
}
impl EffectBuffer {
pub fn new() -> Self {
Self {
id: Id::new(),
size: Size::default(),
scale: Scale::from(1.),
blur_options: BlurOptions::default(),
elements: Elements::default(),
offscreen: None,
blur: None,
commit_counter: CommitCounter::default(),
}
}
pub fn id(&self) -> &Id {
&self.id
}
pub fn commit(&self) -> CommitCounter {
self.commit_counter
}
pub fn logical_size(&self) -> Size<f64, Logical> {
self.size.to_f64().to_logical(self.scale, Transform::Normal)
}
pub fn scale(&self) -> Scale<f64> {
self.scale
}
pub fn render_element_states(&self) -> Option<&RenderElementStates> {
self.offscreen.as_ref().map(|o| &o.states)
}
pub fn update_size(&mut self, size: Size<i32, Physical>, scale: Scale<f64>) {
self.size = size.to_logical(1).to_buffer(1, Transform::Normal);
self.scale = scale;
}
pub fn update_blur_options(&mut self, options: BlurOptions) {
if self.blur_options == options {
return;
}
self.blur_options = options;
if let Some(offscreen) = &mut self.offscreen {
if offscreen.blurred.is_some() {
offscreen.blurred = None;
self.commit_counter.increment();
}
}
}
pub fn elements(&mut self) -> &mut Vec<OutputRenderElements<GlesRenderer>> {
// Assume we're going to insert new elements, switch to New.
match mem::take(&mut self.elements) {
Elements::Unchanged(elements) | Elements::New(elements) => {
self.elements = Elements::New(elements);
}
}
let Elements::New(elements) = &mut self.elements else {
unreachable!();
};
elements
}
pub fn prepare(&mut self, renderer: &mut GlesRenderer, blur: bool) -> bool {
if let Err(err) = self.prepare_offscreen(renderer) {
warn!("error preparing offscreen: {err:?}");
return false;
};
if blur {
if let Err(err) = self.prepare_blur(renderer) {
warn!("error preparing blur: {err:?}");
return false;
}
}
true
}
fn prepare_offscreen(&mut self, renderer: &mut GlesRenderer) -> anyhow::Result<()> {
let _span = tracy_client::span!("EffectBuffer::prepare_offscreen");
// Check if we need to create or recreate the texture.
let size_string;
let mut reason = "";
if let Some(Offscreen {
texture,
renderer_context_id,
..
}) = &mut self.offscreen
{
let old_size = texture.size();
if old_size != self.size {
size_string = format!(
"size changed from {} × {} to {} × {}",
old_size.w, old_size.h, self.size.w, self.size.h
);
reason = &size_string;
self.offscreen = None;
} else if !texture.is_unique_reference() {
reason = "not unique";
self.offscreen = None;
} else if *renderer_context_id != renderer.context_id() {
reason = "renderer id changed";
self.offscreen = None;
}
} else {
reason = "first render";
}
let offscreen = if let Some(offscreen) = &mut self.offscreen {
offscreen
} else {
trace!("creating new offscreen texture: {reason}");
let span = tracy_client::span!("creating effect offscreen texture");
span.emit_text(reason);
let texture: GlesTexture = renderer
.create_buffer(Fourcc::Abgr8888, self.size)
.context("error creating texture")?;
let buffer_size = self.size.to_logical(1, Transform::Normal).to_physical(1);
let damage = OutputDamageTracker::new(buffer_size, self.scale, Transform::Normal);
self.offscreen.insert(Offscreen {
texture,
renderer_context_id: renderer.context_id(),
scale: self.scale,
damage,
states: RenderElementStates::default(),
blurred: None,
})
};
// Recreate the damage tracker if the scale changes. We already recreate it for buffer size
// changes, and transform is always Normal.
if offscreen.scale != self.scale {
offscreen.scale = self.scale;
trace!("recreating damage tracker due to scale change");
let buffer_size = self.size.to_logical(1, Transform::Normal).to_physical(1);
offscreen.damage = OutputDamageTracker::new(buffer_size, self.scale, Transform::Normal);
self.commit_counter.increment();
offscreen.blurred = None;
}
// Render the elements if any.
let mut elements = match mem::take(&mut self.elements) {
Elements::New(elements) => elements,
x @ Elements::Unchanged(_) => {
// No redrawing necessary.
self.elements = x;
return Ok(());
}
};
let res = {
let mut target = renderer
.bind(&mut offscreen.texture)
.context("error binding texture")?;
offscreen
.damage
.render_output(renderer, &mut target, 1, &elements, Color32F::TRANSPARENT)
.context("error rendering")?
};
offscreen.states = res.states;
if res.damage.is_some() {
self.commit_counter.increment();
// Original texture changed; reset the blurred texture.
offscreen.blurred = None;
}
// Clear and put the storage back.
elements.clear();
self.elements = Elements::Unchanged(elements);
Ok(())
}
fn prepare_blur(&mut self, renderer: &mut GlesRenderer) -> anyhow::Result<()> {
let offscreen = self.offscreen.as_mut().context("missing offscreen")?;
if offscreen.blurred.is_some() {
// Already rendered.
return Ok(());
}
if let Some(blur) = &self.blur {
if blur.context_id() != renderer.context_id() {
debug!("recreating blur: renderer changed");
self.blur = None;
}
}
let blur = if let Some(blur) = &mut self.blur {
blur
} else {
let Some(blur) = Blur::new(renderer) else {
// Missing blur shader.
return Ok(());
};
self.blur.insert(blur)
};
ensure!(
offscreen.renderer_context_id == renderer.context_id(),
"wrong renderer context id"
);
blur.prepare_textures(
|fourcc, size| renderer.create_buffer(fourcc, size),
&offscreen.texture,
self.blur_options,
)
.context("error preparing blur textures")?;
Ok(())
}
pub fn render(&mut self, frame: &mut GlesFrame, blur: bool) -> anyhow::Result<GlesTexture> {
let offscreen = self.offscreen.as_mut().context("offscreen is missing")?;
if !blur {
return Ok(offscreen.texture.clone());
}
let texture = if let Some(texture) = &offscreen.blurred {
texture.clone()
} else {
let blur = self.blur.as_mut().context("blur is missing")?;
let mut guard = frame.renderer();
let renderer = guard.as_mut();
let blurred = blur
.render(renderer, &offscreen.texture, self.blur_options)
.context("error rendering blur")?;
offscreen.blurred.insert(blurred).clone()
};
Ok(texture)
}
}
+457
View File
@@ -0,0 +1,457 @@
use std::cell::RefCell;
use glam::{Mat3, Vec2};
use niri_config::CornerRadius;
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::{Element, Id, RenderElement};
use smithay::backend::renderer::gles::{
ffi, GlesError, GlesFrame, GlesRenderer, GlesTexture, Uniform,
};
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{Frame as _, FrameContext, Offscreen, Texture as _};
use smithay::gpu_span_location;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform};
use crate::backend::tty::{TtyFrame, TtyRenderer, TtyRendererError};
use crate::render_helpers::background_effect::RenderParams;
use crate::render_helpers::blur::{Blur, BlurOptions};
use crate::render_helpers::renderer::AsGlesFrame as _;
use crate::render_helpers::shaders::{mat3_uniform, Shaders};
use crate::utils::region::TransformedRegion;
#[derive(Debug)]
pub struct FramebufferEffect {
id: Id,
commit: CommitCounter,
}
#[derive(Debug)]
pub struct FramebufferEffectElement {
id: Id,
commit: CommitCounter,
geometry: Rectangle<f64, Logical>,
clip_geo: Rectangle<f64, Logical>,
corner_radius: CornerRadius,
subregion: Option<TransformedRegion>,
scale: f32,
blur_options: Option<BlurOptions>,
noise: f32,
saturation: f32,
}
#[derive(Debug)]
struct Inner {
framebuffer: Option<GlesTexture>,
blur: Option<Blur>,
intermediate: Option<GlesTexture>,
/// Reusable storage for subregion-filtered damage rects.
subregion_damage: Vec<Rectangle<i32, Physical>>,
}
impl FramebufferEffect {
pub fn new() -> Self {
Self {
id: Id::new(),
commit: CommitCounter::default(),
}
}
pub fn damage(&mut self) {
self.commit.increment();
}
pub fn render(
&self,
ns: Option<usize>,
params: RenderParams,
blur_options: Option<BlurOptions>,
noise: f32,
saturation: f32,
) -> FramebufferEffectElement {
let (clip_geo, corner_radius) = params
.clip
.unwrap_or((params.geometry, CornerRadius::default()));
let mut id = self.id.clone();
if let Some(ns) = ns {
id = id.namespaced(ns);
}
FramebufferEffectElement {
id,
commit: self.commit,
geometry: params.geometry,
clip_geo,
corner_radius,
subregion: params.subregion,
scale: params.scale as f32,
blur_options,
noise,
saturation,
}
}
}
impl FramebufferEffectElement {
fn compute_uniforms(
&self,
crop: Rectangle<f64, Logical>,
transform: Transform,
) -> [Uniform<'static>; 7] {
let offset = crop.loc - (self.clip_geo.loc - self.geometry.loc);
let offset = Vec2::new(offset.x as f32, offset.y as f32);
let crop_size = Vec2::new(crop.size.w as f32, crop.size.h as f32);
let clip_size = Vec2::new(self.clip_geo.size.w as f32, self.clip_geo.size.h as f32);
// Our v_coords are [0, 1] inside crop. We want them to be [0, 1] inside clip_geo.
let input_to_clip_geo =
Mat3::from_scale(crop_size / clip_size) * Mat3::from_translation(offset / crop_size);
// Revert the effect of the texture transform.
let transform_mat = Mat3::from_translation(Vec2::new(0.5, 0.5))
* Mat3::from_cols_array(transform.matrix().as_ref())
* Mat3::from_translation(Vec2::new(-0.5, -0.5));
let input_to_clip_geo = input_to_clip_geo * transform_mat;
let clip_geo_size = (self.clip_geo.size.w as f32, self.clip_geo.size.h as f32);
[
Uniform::new("niri_scale", self.scale),
Uniform::new("geo_size", clip_geo_size),
Uniform::new("corner_radius", <[f32; 4]>::from(self.corner_radius)),
mat3_uniform("input_to_geo", input_to_clip_geo),
Uniform::new("noise", self.noise),
Uniform::new("saturation", self.saturation),
Uniform::new("bg_color", [0f32, 0., 0., 0.]),
]
}
}
impl Element for FramebufferEffectElement {
fn id(&self) -> &Id {
&self.id
}
fn current_commit(&self) -> CommitCounter {
self.commit
}
fn src(&self) -> Rectangle<f64, Buffer> {
// We don't use src for drawing but we can use it to figure out how we were cropped.
let size = self.geometry.size.to_buffer(1., Transform::Normal);
Rectangle::from_size(size)
}
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry.to_physical_precise_round(scale)
}
fn is_framebuffer_effect(&self) -> bool {
true
}
}
impl RenderElement<GlesRenderer> for FramebufferEffectElement {
fn capture_framebuffer(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
cache: &UserDataMap,
) -> Result<(), GlesError> {
let _span = tracy_client::span!("FramebufferEffectElement::capture_framebuffer");
let location = gpu_span_location!("FramebufferEffectElement::capture_framebuffer");
frame.with_gpu_span(location, |frame| {
let output_rect = Rectangle::from_size(frame.output_size());
let transform = frame.transformation();
let mut guard = frame.renderer();
let inner = cache
.get_or_insert::<RefCell<Inner>, _>(|| RefCell::new(Inner::new(guard.as_mut())));
let mut inner = inner.borrow_mut();
let inner = &mut *inner;
inner.intermediate = None;
// We want clamp-to-edge behavior for out-of-bounds pixels. However, glBlitFramebuffer
// seems to skip out-of-bounds pixels, even though my reading of the docs suggests
// otherwise (we use GL_LINEAR filter). So, clamp dst to the framebuffer bounds
// ourselves.
let clamped_dst = match dst.intersection(output_rect) {
Some(clamped) => clamped,
None => return Ok(()),
};
let clamp_scale = clamped_dst.size.to_f64() / dst.size.to_f64();
let dst = transform.transform_rect_in(clamped_dst, &output_rect.size);
// Compute size from our geometry and scale.
//
// The "correct" size is always dst.size since that's the pixel region we're actually
// blitting. However, using dst.size causes two undesirable things when zooming out for
// the overview:
// 1. dst.size shrinks every frame, causing a texture realloaction for every fb effect
// element every frame.
// 2. The underlying blur visually expands. This is technically correct, since the
// underlying contents shrink, but it's not what you visually expect: you expect the
// blur to also shrink as the windows zoom out, to give the zooming out effect.
//
// Using size computed from geometry and scale solves both of those problems (even
// though there's a bit of a cost in that zoomed-out elements still blur the entire
// unzoomed texture size, and even though the blur ends up slightly wrong as there's two
// layers of texture resampling, up and back down).
//
// Here we use src.size rather than geometry directly because src takes into account
// cropping.
let size = src
.size
.to_logical(1., Transform::Normal)
.upscale(clamp_scale)
.to_physical_precise_round(self.scale);
let size = transform.transform_size(size);
let size = size.to_logical(1).to_buffer(1, Transform::Normal);
// Recreate framebuffer if needed.
if inner
.framebuffer
.as_ref()
.is_some_and(|fb| fb.size() != size)
{
inner.framebuffer = None;
}
let framebuffer = if let Some(fb) = &inner.framebuffer {
fb
} else {
trace!("creating framebuffer texture sized {} × {}", size.w, size.h);
let renderer = guard.as_mut();
let texture = renderer.create_buffer(Fourcc::Abgr8888, size)?;
inner.framebuffer.insert(texture)
};
// Prepare blur textures.
let mut blur = Option::zip(inner.blur.as_mut(), self.blur_options);
if let Some((b, options)) = &mut blur {
let renderer = guard.as_mut();
if let Err(err) = b.prepare_textures(
|fourcc, size| renderer.create_buffer(fourcc, size),
framebuffer,
*options,
) {
warn!("error preparing blur textures: {err:?}");
blur = None;
}
}
// We can't use renderer.with_context() as that will reset the GlesFrame binding that we
// want to blit from.
drop(guard);
// Blit the framebuffer contents.
frame.with_context(|gl| unsafe {
while gl.GetError() != ffi::NO_ERROR {}
let mut current_fbo = 0i32;
gl.GetIntegerv(ffi::DRAW_FRAMEBUFFER_BINDING, &mut current_fbo as *mut _);
// BlitFramebuffer is affected by the scissor test, we don't want that.
gl.Disable(ffi::SCISSOR_TEST);
let mut fbo = 0;
gl.GenFramebuffers(1, &mut fbo as *mut _);
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, fbo);
gl.FramebufferTexture2D(
ffi::DRAW_FRAMEBUFFER,
ffi::COLOR_ATTACHMENT0,
ffi::TEXTURE_2D,
framebuffer.tex_id(),
0,
);
gl.BlitFramebuffer(
dst.loc.x,
dst.loc.y,
dst.loc.x + dst.size.w,
dst.loc.y + dst.size.h,
0,
0,
size.w,
size.h,
ffi::COLOR_BUFFER_BIT,
ffi::LINEAR,
);
// Restore state set by GlesFrame that we just modified.
gl.BindFramebuffer(ffi::DRAW_FRAMEBUFFER, current_fbo as u32);
gl.Enable(ffi::SCISSOR_TEST);
gl.DeleteFramebuffers(1, &mut fbo as *mut _);
if gl.GetError() != ffi::NO_ERROR {
Err(GlesError::BlitError)
} else {
Ok(())
}
})??;
// If blur is off, use the unblurred texture.
if self.blur_options.is_none() {
inner.intermediate = Some(framebuffer.clone());
return Ok(());
}
if let Some((blur, options)) = blur {
let mut guard = frame.renderer();
let renderer = guard.as_mut();
match blur.render(renderer, framebuffer, options) {
Ok(blurred) => inner.intermediate = Some(blurred),
Err(err) => {
warn!("error rendering blur: {err:?}");
}
}
}
Ok(())
})
}
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
let Some(cache) = cache else {
return Ok(());
};
let Some(inner) = cache.get::<RefCell<Inner>>() else {
return Ok(());
};
let mut inner = inner.borrow_mut();
let inner = &mut *inner;
let Some(texture) = &inner.intermediate else {
return Ok(());
};
// Clamp the same way as in capture_framebuffer().
let output_rect = Rectangle::from_size(frame.output_size());
let clamped_dst = match dst.intersection(output_rect) {
Some(clamped) => clamped,
None => return Ok(()),
};
let clamp_offset = clamped_dst.loc - dst.loc;
// Filter damage by subregion, reusing the stored Vec to avoid allocation.
let filtered = &mut inner.subregion_damage;
filtered.clear();
if let Some(subregion) = &self.subregion {
// Convert to subregion coordinates.
let mut crop = src.to_logical(1., Transform::Normal, &src.size);
crop.loc += self.geometry.loc;
subregion.filter_damage(crop, dst, damage, filtered);
} else {
filtered.extend(damage.iter());
};
// Adjust for clamped dst.
if clamped_dst != dst {
let r = Rectangle::new(clamp_offset, clamped_dst.size);
filtered.retain_mut(|d| {
if let Some(mut crop) = d.intersection(r) {
crop.loc -= clamp_offset;
*d = crop;
true
} else {
false
}
});
}
if filtered.is_empty() {
return Ok(());
}
let damage = &filtered[..];
// Adjust src proportionally to the dst clamping.
let src_loc = src.loc.to_logical(1., Transform::Normal, &src.size);
let dst_to_src = src.size / dst.size.to_f64();
let crop = Rectangle::new(
src_loc + clamp_offset.to_f64().upscale(dst_to_src).to_logical(1.),
clamped_dst.size.to_f64().upscale(dst_to_src).to_logical(1.),
);
let program = Shaders::get_from_frame(frame).postprocess_and_clip.clone();
let uniforms = program
.is_some()
.then(|| self.compute_uniforms(crop, frame.transformation()));
let uniforms = uniforms.as_ref().map_or(&[][..], |x| &x[..]);
frame.render_texture_from_to(
texture,
Rectangle::from_size(texture.size().to_f64()),
clamped_dst,
damage,
&[],
// The intermediate texture has the same transform as the frame.
frame.transformation().invert(),
1.,
program.as_ref(),
uniforms,
)
}
}
impl<'render> RenderElement<TtyRenderer<'render>> for FramebufferEffectElement {
fn capture_framebuffer(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
cache: &UserDataMap,
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::capture_framebuffer(&self, gles_frame, src, dst, cache)?;
Ok(())
}
fn draw(
&self,
frame: &mut TtyFrame<'_, '_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(
&self,
gles_frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
Ok(())
}
}
impl Inner {
fn new(renderer: &mut GlesRenderer) -> Self {
Inner {
framebuffer: None,
blur: Blur::new(renderer),
intermediate: None,
subregion_damage: Vec::new(),
}
}
}
+21 -2
View File
@@ -3,6 +3,7 @@ use smithay::backend::renderer::gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::texture::TextureRenderElement;
@@ -96,10 +97,19 @@ impl RenderElement<GlesRenderer> for GradientFadeTextureRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
let uniforms = vec![Uniform::new("cutoff", self.cutoff)];
frame.override_default_tex_program(self.program.0.clone(), uniforms);
RenderElement::<GlesRenderer>::draw(&self.inner, frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(
&self.inner,
frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
frame.clear_tex_program_override();
Ok(())
}
@@ -119,9 +129,18 @@ impl<'render> RenderElement<TtyRenderer<'render>> for GradientFadeTextureRenderE
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self, gles_frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(
&self,
gles_frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
Ok(())
}
+126 -35
View File
@@ -1,27 +1,39 @@
use std::ptr;
use anyhow::{ensure, Context};
use anyhow::{ensure, Context as _};
use niri_config::BlockOutFrom;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::{Buffer, Fourcc};
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::element::{Element, Kind, RenderElement};
use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTarget, GlesTexture};
use smithay::backend::renderer::element::{Element, Kind, RenderElement, RenderElementStates};
use smithay::backend::renderer::gles::{
GlesError, GlesMapping, GlesRenderer, GlesTarget, GlesTexture,
};
use smithay::backend::renderer::sync::SyncPoint;
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
use smithay::backend::renderer::{
Bind, Color32F, ExportMem, Frame, Offscreen, Renderer, Texture as _,
};
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::shm;
use solid_color::{SolidColorBuffer, SolidColorRenderElement};
use self::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use self::texture::{TextureBuffer, TextureRenderElement};
use crate::render_helpers::renderer::AsGlesRenderer;
use crate::render_helpers::xray::Xray;
pub mod background_effect;
pub mod blur;
pub mod border;
pub mod clipped_surface;
pub mod damage;
pub mod debug;
pub mod effect_buffer;
pub mod framebuffer_effect;
pub mod gradient_fade_texture;
pub mod memory;
pub mod offscreen;
@@ -37,12 +49,44 @@ pub mod snapshot;
pub mod solid_color;
pub mod surface;
pub mod texture;
pub mod xray;
/// A rendering context.
///
/// Bundles together things needed by most rendering code.
pub struct RenderCtx<'a, R> {
pub renderer: &'a mut R,
pub target: RenderTarget,
pub xray: Option<&'a Xray>,
}
impl<'a, R> RenderCtx<'a, R> {
/// Reborrows this context with a smaller lifetime.
#[inline]
pub fn r<'b>(&'b mut self) -> RenderCtx<'b, R> {
RenderCtx {
renderer: self.renderer,
target: self.target,
xray: self.xray,
}
}
}
impl<'a, R: AsGlesRenderer> RenderCtx<'a, R> {
pub fn as_gles<'b>(&'b mut self) -> RenderCtx<'b, GlesRenderer> {
RenderCtx {
renderer: self.renderer.as_gles_renderer(),
target: self.target,
xray: self.xray,
}
}
}
/// What we're rendering for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderTarget {
/// Rendering to display on screen.
Output,
Output = 0,
/// Rendering for a screencast.
Screencast,
/// Rendering for any other screen capture.
@@ -71,6 +115,8 @@ pub trait ToRenderElement {
}
impl RenderTarget {
pub const COUNT: usize = 3;
pub fn should_block_out(self, block_out_from: Option<BlockOutFrom>) -> bool {
match block_out_from {
None => false,
@@ -126,6 +172,23 @@ pub fn encompassing_geo(
.unwrap_or_default()
}
pub fn create_texture(
renderer: &mut GlesRenderer,
size: Size<i32, Physical>,
fourcc: Fourcc,
) -> Result<GlesTexture, GlesError> {
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
renderer.create_buffer(fourcc, buffer_size)
}
pub fn copy_framebuffer(
renderer: &mut GlesRenderer,
target: &GlesTarget,
fourcc: Fourcc,
) -> Result<GlesMapping, GlesError> {
renderer.copy_framebuffer(target, Rectangle::from_size(target.size()), fourcc)
}
pub fn render_to_encompassing_texture(
renderer: &mut GlesRenderer,
scale: Scale<f64>,
@@ -154,11 +217,7 @@ pub fn render_to_texture(
) -> anyhow::Result<(GlesTexture, SyncPoint)> {
let _span = tracy_client::span!();
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
let mut texture: GlesTexture = renderer
.create_buffer(fourcc, buffer_size)
.context("error creating texture")?;
let mut texture = create_texture(renderer, size, fourcc).context("error creating texture")?;
let sync_point = {
let mut target = renderer
@@ -181,18 +240,15 @@ pub fn render_and_download(
) -> anyhow::Result<GlesMapping> {
let _span = tracy_client::span!();
let (mut texture, _) = render_to_texture(renderer, size, scale, transform, fourcc, elements)?;
let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal);
// FIXME: would be nice to avoid binding the second time here (after render_to_texture()), but
// borrowing makes this inconvenient.
let target = renderer
let mut texture = create_texture(renderer, size, fourcc).context("error creating texture")?;
let mut target = renderer
.bind(&mut texture)
.context("error binding texture")?;
let mapping = renderer
.copy_framebuffer(&target, Rectangle::from_size(buffer_size), fourcc)
.context("error copying framebuffer")?;
Ok(mapping)
let _sync = render_elements(renderer, &mut target, size, scale, transform, elements)
.context("error rendering")?;
copy_framebuffer(renderer, &target, fourcc).context("error copying framebuffer")
}
pub fn render_to_vec(
@@ -215,33 +271,44 @@ pub fn render_to_vec(
pub fn render_to_dmabuf(
renderer: &mut GlesRenderer,
damage_tracker: &mut OutputDamageTracker,
mut dmabuf: Dmabuf,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
elements: &[impl RenderElement<GlesRenderer>],
states: RenderElementStates,
) -> anyhow::Result<SyncPoint> {
let _span = tracy_client::span!();
let (size, _scale, _transform) = damage_tracker.mode().try_into().unwrap();
ensure!(
dmabuf.width() == size.w as u32 && dmabuf.height() == size.h as u32,
"invalid buffer size"
);
let mut target = renderer
.bind(&mut dmabuf)
.context("error binding texture")?;
render_elements(renderer, &mut target, size, scale, transform, elements)
let mut target = renderer.bind(&mut dmabuf).context("error binding dmabuf")?;
let res = damage_tracker
.render_output_with_states(
renderer,
&mut target,
0,
elements,
Color32F::TRANSPARENT,
states,
)
.context("error rendering to dmabuf")?;
Ok(res.sync)
}
pub fn render_to_shm(
renderer: &mut GlesRenderer,
damage_tracker: &mut OutputDamageTracker,
buffer: &WlBuffer,
size: Size<i32, Physical>,
scale: Scale<f64>,
transform: Transform,
elements: impl Iterator<Item = impl RenderElement<GlesRenderer>>,
elements: &[impl RenderElement<GlesRenderer>],
states: RenderElementStates,
) -> anyhow::Result<()> {
let _span = tracy_client::span!();
shm::with_buffer_contents_mut(buffer, |shm_buffer, shm_len, buffer_data| {
let (size, _scale, _transform) = damage_tracker.mode().try_into().unwrap();
let fourcc = Fourcc::Xrgb8888;
ensure!(
// The buffer prefers pixels in little endian ...
buffer_data.format == wl_shm::Format::Xrgb8888
@@ -251,9 +318,26 @@ pub fn render_to_shm(
&& shm_len == buffer_data.stride as usize * buffer_data.height as usize,
"invalid buffer format or size"
);
let mapping =
render_and_download(renderer, size, scale, transform, Fourcc::Xrgb8888, elements)?;
let mut texture =
create_texture(renderer, size, fourcc).context("error creating texture")?;
let mut target = renderer
.bind(&mut texture)
.context("error binding texture")?;
let _res = damage_tracker
.render_output_with_states(
renderer,
&mut target,
0,
elements,
Color32F::TRANSPARENT,
states,
)
.context("error rendering")?;
let mapping =
copy_framebuffer(renderer, &target, fourcc).context("error copying framebuffer")?;
let bytes = renderer
.map_texture(&mapping)
.context("error mapping texture")?;
@@ -306,8 +390,15 @@ fn render_elements(
if let Some(mut damage) = output_rect.intersection(dst) {
damage.loc -= dst.loc;
let cache = UserDataMap::new();
if element.is_framebuffer_effect() {
element
.capture_framebuffer(&mut frame, src, dst, &cache)
.context("error in capture_framebuffer()")?;
}
element
.draw(&mut frame, src, dst, &[damage], &[])
.draw(&mut frame, src, dst, &[damage], &[], Some(&cache))
.context("error drawing element")?;
}
}
+22 -9
View File
@@ -15,6 +15,7 @@ use smithay::backend::renderer::utils::{
use smithay::backend::renderer::{
Bind as _, Color32F, ContextId, Frame as _, Offscreen as _, Renderer, Texture as _,
};
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use super::encompassing_geo;
@@ -81,7 +82,12 @@ impl OffscreenBuffer {
RelocateRenderElement::from_element(ele, geo.loc.upscale(-1), Relocate::Relative)
}));
let src_size = geo.size;
// Guard against empty elements producing a zero size.
let mut src_size = geo.size;
if src_size.w == 0 || src_size.h == 0 {
src_size = Size::new(1, 1);
}
let src_size = src_size.to_logical(1).to_buffer(1, Transform::Normal);
let offset = geo.loc.to_f64().to_logical(scale);
@@ -157,13 +163,10 @@ impl OffscreenBuffer {
let res = {
let mut target = renderer.bind(&mut inner.texture)?;
inner.damage.render_output(
renderer,
&mut target,
1,
&elements,
Color32F::TRANSPARENT,
)?
inner
.damage
.render_output(renderer, &mut target, 1, &elements, Color32F::TRANSPARENT)
.context("error rendering")?
};
// Add the resulting damage to the outer tracker.
@@ -304,6 +307,7 @@ impl RenderElement<GlesRenderer> for OffscreenRenderElement {
dest: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
_cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
if frame.context_id() != self.renderer_context_id {
warn!("trying to render texture from different renderer");
@@ -338,9 +342,18 @@ impl<'render> RenderElement<TtyRenderer<'render>> for OffscreenRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self, gles_frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(
&self,
gles_frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
Ok(())
}
+21 -2
View File
@@ -1,6 +1,7 @@
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, UnderlyingStorage};
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexture};
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Physical, Rectangle, Scale, Transform};
use super::renderer::AsGlesFrame;
@@ -61,9 +62,18 @@ impl RenderElement<GlesRenderer> for PrimaryGpuTextureRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(
&self.0,
gles_frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
Ok(())
}
@@ -82,9 +92,18 @@ impl<'render> RenderElement<TtyRenderer<'render>> for PrimaryGpuTextureRenderEle
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let gles_frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(&self.0, gles_frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(
&self.0,
gles_frame,
src,
dst,
damage,
opaque_regions,
cache,
)?;
Ok(())
}
+38 -2
View File
@@ -93,11 +93,31 @@ macro_rules! niri_render_elements {
$($name::$variant(elem) => elem.kind()),+
}
}
fn is_framebuffer_effect(&self) -> bool {
match self {
$($name::$variant(elem) => elem.is_framebuffer_effect()),+
}
}
}
impl smithay::backend::renderer::element::RenderElement<smithay::backend::renderer::gles::GlesRenderer>
for $($name_R<smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_R)?
{
fn capture_framebuffer(
&self,
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
cache: &smithay::utils::user_data::UserDataMap,
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::capture_framebuffer(elem, frame, src, dst, cache)
})+
}
}
fn draw(
&self,
frame: &mut smithay::backend::renderer::gles::GlesFrame<'_, '_>,
@@ -105,10 +125,11 @@ macro_rules! niri_render_elements {
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
opaque_regions: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
cache: Option<&smithay::utils::user_data::UserDataMap>,
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage, opaque_regions)
smithay::backend::renderer::element::RenderElement::<smithay::backend::renderer::gles::GlesRenderer>::draw(elem, frame, src, dst, damage, opaque_regions, cache)
})+
}
}
@@ -123,6 +144,20 @@ macro_rules! niri_render_elements {
impl<'render> smithay::backend::renderer::element::RenderElement<$crate::backend::tty::TtyRenderer<'render>>
for $($name_R<$crate::backend::tty::TtyRenderer<'render>>)? $($name_no_R)?
{
fn capture_framebuffer(
&self,
frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>,
src: smithay::utils::Rectangle<f64, smithay::utils::Buffer>,
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
cache: &smithay::utils::user_data::UserDataMap,
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::capture_framebuffer(elem, frame, src, dst, cache)
})+
}
}
fn draw(
&self,
frame: &mut $crate::backend::tty::TtyFrame<'render, '_, '_>,
@@ -130,10 +165,11 @@ macro_rules! niri_render_elements {
dst: smithay::utils::Rectangle<i32, smithay::utils::Physical>,
damage: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
opaque_regions: &[smithay::utils::Rectangle<i32, smithay::utils::Physical>],
cache: Option<&smithay::utils::user_data::UserDataMap>,
) -> Result<(), $crate::backend::tty::TtyRendererError<'render>> {
match self {
$($name::$variant(elem) => {
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage, opaque_regions)
smithay::backend::renderer::element::RenderElement::<$crate::backend::tty::TtyRenderer<'render>>::draw(elem, frame, src, dst, damage, opaque_regions, cache)
})+
}
}
+13 -2
View File
@@ -8,6 +8,7 @@ use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesT
use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions};
use smithay::backend::renderer::Texture as _;
use smithay::gpu_span_location;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Size, Transform};
use super::renderer::{AsGlesFrame, NiriRenderer};
@@ -171,10 +172,19 @@ impl RenderElement<GlesRenderer> for ResizeRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
let _span = tracy_client::span!("ResizeRenderElement::draw");
frame.with_gpu_span(gpu_span_location!("ResizeRenderElement::draw"), |frame| {
RenderElement::<GlesRenderer>::draw(&self.0, frame, src, dst, damage, opaque_regions)
RenderElement::<GlesRenderer>::draw(
&self.0,
frame,
src,
dst,
damage,
opaque_regions,
cache,
)
})
}
@@ -191,9 +201,10 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ResizeRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions, cache)?;
Ok(())
}
+4 -1
View File
@@ -10,6 +10,7 @@ use smithay::backend::renderer::gles::{
};
use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions};
use smithay::backend::renderer::DebugFlags;
use smithay::utils::user_data::UserDataMap;
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size};
use super::renderer::AsGlesFrame;
@@ -293,6 +294,7 @@ impl RenderElement<GlesRenderer> for ShaderRenderElement {
dest: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
_opaque_regions: &[Rectangle<i32, Physical>],
_cache: Option<&UserDataMap>,
) -> Result<(), GlesError> {
let _span = tracy_client::span!("ShaderRenderElement::draw");
@@ -527,10 +529,11 @@ impl<'render> RenderElement<TtyRenderer<'render>> for ShaderRenderElement {
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque_regions: &[Rectangle<i32, Physical>],
cache: Option<&UserDataMap>,
) -> Result<(), TtyRendererError<'render>> {
let frame = frame.as_gles_frame();
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions)?;
RenderElement::<GlesRenderer>::draw(self, frame, src, dst, damage, opaque_regions, cache)?;
Ok(())
}
+11
View File
@@ -0,0 +1,11 @@
#version 100
attribute vec2 vert;
varying vec2 v_coords;
void main() {
v_coords = vert;
// vert goes from 0 to 1; position must be from -1 to 1.
vec2 position = vert * 2.0 - 1.0;
gl_Position = vec4(position, 1.0, 1.0);
}
+21
View File
@@ -0,0 +1,21 @@
#version 100
precision highp float;
varying vec2 v_coords;
uniform sampler2D tex;
uniform vec2 half_pixel;
uniform float offset;
void main() {
vec2 o = half_pixel * offset;
vec4 sum = texture2D(tex, v_coords) * 4.0;
sum += texture2D(tex, v_coords + vec2(-o.x, -o.y));
sum += texture2D(tex, v_coords + vec2( o.x, -o.y));
sum += texture2D(tex, v_coords + vec2(-o.x, o.y));
sum += texture2D(tex, v_coords + vec2( o.x, o.y));
gl_FragColor = sum / 8.0;
}
+29
View File
@@ -0,0 +1,29 @@
#version 100
precision highp float;
varying vec2 v_coords;
uniform sampler2D tex;
uniform vec2 half_pixel;
uniform float offset;
void main() {
vec2 o = half_pixel * offset;
vec4 sum = vec4(0.0);
// Four edge centers
sum += texture2D(tex, v_coords + vec2(-o.x * 2.0, 0.0));
sum += texture2D(tex, v_coords + vec2( o.x * 2.0, 0.0));
sum += texture2D(tex, v_coords + vec2(0.0, -o.y * 2.0));
sum += texture2D(tex, v_coords + vec2(0.0, o.y * 2.0));
// Four diagonal corners
sum += texture2D(tex, v_coords + vec2(-o.x, o.y)) * 2.0;
sum += texture2D(tex, v_coords + vec2( o.x, o.y)) * 2.0;
sum += texture2D(tex, v_coords + vec2(-o.x, -o.y)) * 2.0;
sum += texture2D(tex, v_coords + vec2( o.x, -o.y)) * 2.0;
gl_FragColor = sum / 12.0;
}
+3 -26
View File
@@ -208,35 +208,12 @@ vec4 gradient_color(vec2 coords) {
return color_mix(color_from, color_to, frac);
}
float rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius) {
vec2 center;
float radius;
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
radius = corner_radius.x;
center = vec2(radius, radius);
} else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) {
radius = corner_radius.y;
center = vec2(size.x - radius, radius);
} else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) {
radius = corner_radius.z;
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) {
radius = corner_radius.w;
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float dist = distance(coords, center);
float half_px = 0.5 / niri_scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius);
void main() {
vec3 coords_geo = input_to_geo * vec3(niri_v_coords, 1.0);
vec4 color = gradient_color(coords_geo.xy);
color = color * rounding_alpha(coords_geo.xy, geo_size, outer_radius);
color = color * niri_rounding_alpha(coords_geo.xy, geo_size, outer_radius);
if (border_width > 0.0) {
coords_geo -= vec3(border_width);
@@ -245,7 +222,7 @@ void main() {
&& 0.0 <= coords_geo.y && coords_geo.y <= inner_geo_size.y)
{
vec4 inner_radius = max(outer_radius - vec4(border_width), 0.0);
color = color * (1.0 - rounding_alpha(coords_geo.xy, inner_geo_size, inner_radius));
color = color * (1.0 - niri_rounding_alpha(coords_geo.xy, inner_geo_size, inner_radius));
}
}
@@ -26,30 +26,8 @@ uniform vec2 geo_size;
uniform vec4 corner_radius;
uniform mat3 input_to_geo;
float rounding_alpha(vec2 coords, vec2 size) {
vec2 center;
float radius;
if (coords.x < corner_radius.x && coords.y < corner_radius.x) {
radius = corner_radius.x;
center = vec2(radius, radius);
} else if (size.x - corner_radius.y < coords.x && coords.y < corner_radius.y) {
radius = corner_radius.y;
center = vec2(size.x - radius, radius);
} else if (size.x - corner_radius.z < coords.x && size.y - corner_radius.z < coords.y) {
radius = corner_radius.z;
center = vec2(size.x - radius, size.y - radius);
} else if (coords.x < corner_radius.w && size.y - corner_radius.w < coords.y) {
radius = corner_radius.w;
center = vec2(radius, size.y - radius);
} else {
return 1.0;
}
float dist = distance(coords, center);
float half_px = 0.5 / niri_scale;
return 1.0 - smoothstep(radius - half_px, radius + half_px, dist);
}
float niri_rounding_alpha(vec2 coords, vec2 size, vec4 corner_radius);
vec4 postprocess(vec4 color);
void main() {
vec3 coords_geo = input_to_geo * vec3(v_coords, 1.0);
@@ -60,12 +38,14 @@ void main() {
color = vec4(color.rgb, 1.0);
#endif
color = postprocess(color);
if (coords_geo.x < 0.0 || 1.0 < coords_geo.x || coords_geo.y < 0.0 || 1.0 < coords_geo.y) {
// Clip outside geometry.
color = vec4(0.0);
} else {
// Apply corner rounding inside geometry.
color = color * rounding_alpha(coords_geo.xy * geo_size, geo_size);
color = color * niri_rounding_alpha(coords_geo.xy * geo_size, geo_size, corner_radius);
}
// Apply final alpha and tint.

Some files were not shown because too many files have changed in this diff Show More