diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index 41c3d9277b..baa81ab6e4 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -12,6 +12,7 @@ packages: - uthash - libconfig - libglvnd + - libepoxy - dbus - pcre sources: @@ -19,7 +20,7 @@ sources: tasks: - setup: | cd picom - CPPFLAGS="-I/usr/local/include" meson -Dunittest=true build + CPPFLAGS="-I/usr/local/include" meson setup -Dunittest=true --werror build - build: | cd picom ninja -C build diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c348faf27..d20c03945e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,7 +29,7 @@ commands: - ".git" - run: name: config - command: CC=<< parameters.cc >> meson << parameters.build-config >> -Dunittest=true --werror . build + command: CC=<< parameters.cc >> meson setup << parameters.build-config >> -Dunittest=true --werror . build - run: name: build command: ninja -vC build @@ -54,7 +54,7 @@ jobs: command: ninja -vC build test - run: name: test config file parsing - command: xvfb-run -s "-screen 0 640x480x24" build/src/picom --config picom.sample.conf --no-vsync --diagnostics + command: xvfb-run -s "-screen 0 640x480x24" build/src/picom --config tests/configs/parsing_test.conf --no-vsync --diagnostics - run: name: run testsuite command: tests/run_tests.sh build/src/picom @@ -72,7 +72,7 @@ jobs: executor: e steps: - build: - build-config: -Dopengl=false -Ddbus=false -Dregex=false -Dconfig_file=false + build-config: -Dopengl=false -Ddbus=false -Dregex=false release: executor: e steps: @@ -104,7 +104,7 @@ jobs: steps: - build: cc: clang - build-config: -Dopengl=false -Ddbus=false -Dregex=false -Dconfig_file=false + build-config: -Dopengl=false -Ddbus=false -Dregex=false clang_nogl: executor: e steps: diff --git a/.clang-tidy b/.clang-tidy index 7b59c22f36..57d71dc561 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -11,7 +11,9 @@ Checks: > misc-static-assert, -clang-analyzer-*, -readability-isolate-declaration, - -readability-magic-numbers + -readability-magic-numbers, + -readability-identifier-length, + -bugprone-easily-swappable-parameters AnalyzeTemporaryDtors: false FormatStyle: file CheckOptions: @@ -19,3 +21,11 @@ CheckOptions: value: 4;8;16;24;32;1;2;3;4096;65536; - key: readability-magic-numbers.IgnoredFloatingPointValues value: 255.0;1.0; + - key: readability-function-cognitive-complexity.IgnoreMacros + value: true + - key: readability-function-cognitive-complexity.Threshold + value: 50 + - key: readability-function-cognitive-complexity.DescribeBasicIncrements + value: true + - key: bugprone-signed-char-misuse.CharTypdefsToIgnore + value: int8_t diff --git a/.editorconfig b/.editorconfig index b40d8e8bf5..fea97be835 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,3 +3,6 @@ root = true indent_style = tab indent_size = 8 max_line_length = 90 +[*.nix] +indent_style = space +indent_size = 2 diff --git a/.github/issue_template.md b/.github/issue_template.md index 4aee88a56a..195a06ecb0 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -53,5 +53,11 @@ +### OpenGL trace + + ### Other details diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..00e2f844be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: openbsd +on: push + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + - uses: cross-platform-actions/action@v0.24.0 + with: + operating_system: openbsd + version: '7.5' + shell: bash + run: | + sudo pkg_add libev xcb meson pkgconf cmake uthash libconfig dbus pcre2 + CPPFLAGS="-I/usr/local/include" LDFLAGS="-L/usr/local/lib" meson setup -Dunittest=true --werror build + ninja -C build + ninja -C build test + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0d2f58f391..29ccf790ac 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,30 +18,21 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # Install dependencies - - run: sudo apt install libxext-dev libxcb1-dev libxcb-damage0-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-render-util0-dev libxcb-render0-dev libxcb-randr0-dev libxcb-composite0-dev libxcb-image0-dev libxcb-present-dev libxcb-xinerama0-dev libxcb-glx0-dev libpixman-1-dev libdbus-1-dev libconfig-dev libgl1-mesa-dev libpcre2-dev libevdev-dev uthash-dev libev-dev libx11-xcb-dev meson ninja-build + - run: sudo apt update && sudo apt install libconfig-dev libdbus-1-dev libegl-dev libev-dev libgl-dev libpcre2-dev libpixman-1-dev libx11-xcb-dev libxcb1-dev libxcb-composite0-dev libxcb-damage0-dev libxcb-dpms0-dev libxcb-glx0-dev libxcb-image0-dev libxcb-present-dev libxcb-randr0-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-util-dev libxcb-xfixes0-dev libxext-dev meson ninja-build uthash-dev if: ${{ matrix.language == 'cpp' }} # Autobuild - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/coding-style-pr.yml b/.github/workflows/coding-style-pr.yml index e5b40c5485..5b9125f51b 100644 --- a/.github/workflows/coding-style-pr.yml +++ b/.github/workflows/coding-style-pr.yml @@ -6,8 +6,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }} - - uses: yshui/git-clang-format-lint@v1.11 + - uses: yshui/git-clang-format-lint@v1.15 with: base: ${{ github.event.pull_request.base.sha }} diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index e6f51ecdcc..96cc6965f3 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -6,9 +6,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 2 - - uses: yshui/git-clang-format-lint@v1.11 + - uses: yshui/git-clang-format-lint@v1.15 with: base: ${{ github.event.ref }}~1 diff --git a/.gitignore b/.gitignore index 20b39d0826..7639f8ab81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build files .deps +.direnv aclocal.m4 autom4te.cache config.log @@ -33,14 +34,29 @@ cmake_install.cmake CPackSourceConfig.cmake install_manifest.txt +# apitrace +*.trace # Vim files .sw[a-z] .*.sw[a-z] *~ +# Rust +target + +# Python +*.pyc +*.py +!/tests/testcases/*.py + # Misc files +*.conf +!/tests/configs/*.conf +perf.data +perf.data.old core.* +vgcore.* .gdb_history oprofile_data/ compton.plist diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..f42501ee44 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,117 @@ +# Unreleased + +## New features + +* Allow `corner-radius-rules` to override `corner-radius = 0`. Previously setting corner radius to 0 globally disables rounded corners. (#1170) +* New `picom-inspect` tool, which lets you test out your picom rules. Sample output: + + ``` + ... + Checking rounded-corners-exclude: + window_type = "dock" ... not matched + window_type = "desktop" ... not matched + window_type *= "menu" ... not matched + fullscreen = 1 ... not matched + Checking opacity-rule: + _NET_WM_STATE@[0] *= "_NET_WM_STATE_HIDDEN" ... not matched + Checking corner-radius-rule: + class_g = "Alacritty" ... matched/10 + + Here are some rule(s) that match this window: + name = '[0.2.1] ./picom-inspect: ~/p/picom(./picom-inspect: ~/p/picom)*' + class_i = 'Alacritty' + class_g = 'Alacritty' + window_type = 'normal' + ! fullscreen + border_width = 0 + ``` + +## Notable changes + +* `override_redirect` in rules now only matches top-level windows that doesn't have a client window. Some window managers (e.g. awesome) set override_redirect for all window manager frame windows, causing this rule to match against everything (#625). +* Marginally improve performance when resizing/opening/closing windows. (#1190) +* Type and format specifiers are no longer used in rules. These specifiers are what you put after the colon (':') in rules, e.g. the `:32c` in `"_GTK_FRAME_EXTENTS@:32c"`. Now this information is ignored and the property is matched regardless of format or type. + +## Deprecated features + +* Setting `--shadow-exclude-reg` is now a hard error. It was deprecated almost since the start of `picom`. `--clip-shadow-above` is the better alternative. (#1254) +* Remove command line options `-n`, `-a`, and `-s`. They were removed more than 10 years ago, it's time to finally get rid of them entirely. (#1254) +* Remove error message for `--glx-swap-method`, it was deprecated in v6. +* Remove error message for passing argument to `--vsync` arguments, it was deprecated in v5. +* Option `--opengl` is now deprecated, use `--backend=glx` instead. + +## Bug fixes + +* Fix ghosting artifacts that sometimes occur when window manager is restarted (#1081) +* Fix a bug where rounded corner is disabled after making a window fullscreen and back (#1216) + +## Dependency changes + +* picom now uses some OpenGL 4.3 features. +* picom now optionally depends on `rtkit` at runtime to give itself realtime scheduling priority. +* `libconfig` is now a mandatory dependency. + +# v11.2 (2024-Feb-13) + +## Build changes + +* `picom` now depends on `libepoxy` for OpenGL symbol management. + +## Bug fixes + +* Workaround a NVIDIA problem that causes high CPU usage after suspend/resume (#1172, #1168) +* Fix building on OpenBSD (#1189, #1188) +* Fix occasional freezes (#1040, #1145, #1166) +* Fix `corner-radius-rules` not applying sometimes (#1177) +* Fix window shader not having an effect when frame opacity is enabled (#1174) +* Fix binding root pixmap in case of depth mismatch (#984) + +# v11.1 (2024-Jan-28) + +## Bug fixes + +* Fix missing fading on window close for some window managers. (#704) + +# v11 (2024-Jan-20) + +## Build changes + +* Due to some caveats discovered related to setting the `CAP_SYS_NICE` capability, it is now recommended to **NOT** set this capability for picom. + +## Deprecations + +* Uses of `--sw-opti`, and `--respect-prop-shadow` are now hard errors. +* `-F` has been removed completely. It was deprecated before the picom fork. + +# v11-rc1 (2024-Jan-14) + +## Notable features + +* picom now uses dithering to prevent banding. Banding is most notable when using a strong background blur. (#952) +* Frame pacing. picom uses present feedback information to schedule new frames when it makes sense to do so. This improves latency, and replaces the `glFlush` and `GL_MaxFramesAllowed=1` hacks we used to do for NVIDIA. (#968 #1156) +* Some missing features have been implemented for the EGL backend (#1004 #1007) + +## Bug fixes + +* Many memory/resource leak fixes thanks to @absolutelynothelix . (#977 #978 #979 #980 #982 #985 #992 #1009 #1022) +* Fix tiling of wallpaper. (#1002) +* Fix some blur artifacts (#1095) +* Fix shadow color for transparent shadows (#1124) +* Don't spam logs when another compositor is running (#1104) +* Fix rounded corners showing as black with the xrender backend (#1003) +* Fix blur with rounded windows (#950) + +## Build changes + +* Dependency `pcre` has been replaced by `pcre2`. +* New dependency `xcb-util`. +* `xinerama` is no longer used. +* `picom` now tries to give itself a real-time scheduling priority. ~~Please consider giving `picom` the `CAP_SYS_NICE` capability when packaging it.~~ + +## Deprecations + +* The `kawase` blur method is removed. Note this is just an alias to the `dual_kawase` method, which is still available. (#1102) + +# Earlier versions + +Please see the GitHub releases page. diff --git a/CONTRIBUTORS b/CONTRIBUTORS index db8eb9090d..e203178966 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,60 +1,98 @@ -Sorted in alphabetical order -Open an issue or pull request if you don't want your name listed here. +Sorted in alphabetical order. Feel free to open an issue or create a +pull request if you want to change or remove your mention. -Adam Jackson -Alexander Kapshuna -Antonin Décimo -Antonio Vivace -Avi-D-coder -Ben Friesen -Bernd Busse -Brottweiler -Carl Worth -Christopher Jeffrey -Corax26 -Dan Elkouby -Dana Jansens -Dave Airlie +Adam Jackson +adelin-b +Alexander Kapshuna +Antonin Décimo +Antonio Vivace +Avi ד +Ben Friesen +Bernd Busse +Bert Gijsbers +bhagwan +Bodhi +Brottweiler +Carl Worth +Christopher Jeffrey +Corax26 +Dan Elkouby +Dana Jansens +Daniel Kwan +Dave Airlie David Schlachter dolio -Duncan -Dylan Araps -Einar Lielmanis -Eric Anholt +Duncan +Dylan Araps +Einar Lielmanis +Eric Anholt +Evgeniy Baskov Greg Flynn -hasufell -James Cloos -Jamey Sharp -Jan Beich -Jarrad -Javeed Shaikh -Jerónimo Navarro -Keith Packard -Kevin Kelley -mæp -Mark Tiefenbruck -Matthew Allum -Maxim Solovyov -Michael Reed -Michele Lambertucci -Namkhai Bourquin -Nate Hart -notfoss -@Paradigm0001 -Patrick Collins -Peter Mattern -Phil Blundell -Que Quotion -Richard Grenville -Rytis Karpuska -Scott Leggett -Tasos Sahanidis -Tilman Sauerbeck -Tim van Dalen -Tomas Janousek -Uli Schlachter -Walter Lapchynski -Will Dietz -XeCycle -Yuxuan Shui -ಠ_ಠ +h7x4 +Harish Rajagopal +hasufell +i-c-u-p +Ignacio Taranto +Istvan Petres +Ivan Malison +Jake +James Cloos +Jamey Sharp +Jan Beich +Jarrad +Javeed Shaikh +Jerónimo Navarro +jialeens +Johnny Pribyl +Jose Maldonado aka Yukiteru +Keith Packard +Kevin Kelley +ktprograms +Kurenshe Nurdaulet +Lukas Schmelzeisen +Mark Tiefenbruck +Matthew Allum +Maxim Solovyov +Michael Reed +Michele Lambertucci +mæp +Namkhai Bourquin +Nate Hart +nia +Nikolay Borodin +notfoss +Omar Polo +oofsauce +orbea +Paradigm0001 +Patrick Collins +Peter Mattern +Phil Blundell +Que Quotion +Rafael Kitover +Reith +Richard Grenville +Rytis Karpuska +Samuel Hand +Scott Leggett +scrouthtv +Sebastien Waegeneire +Stefan Radziuk +Subhaditya Nath +Tasos Sahanidis +Thiago Kenji Okada +Tilman Sauerbeck +Tim Siegel +Tim van Dalen +tokyoneon78 +Tom Dörr +Tomas Janousek +Toni Jarjour +Tuomas Kinnunen +Uli Schlachter +Walter Lapchynski +Will Dietz +XeCycle +Yuxuan Shui +zilrich +ಠ_ಠ diff --git a/README_orig.md b/History.md similarity index 72% rename from README_orig.md rename to History.md index 05c30a456f..8e5a9b4ab4 100644 --- a/README_orig.md +++ b/History.md @@ -1,5 +1,38 @@ +# Picom History + +Picom was forked in 2016 from the original Compton because it seemed to have become unmaintained. + +The battle plan of the fork was to refactor it to make the code _possible_ to maintain, so potential contributors won't be scared away when they take a look at the code. + +And also to try to fix bugs. + +## Rename + +In 2019 the project name was changed from Compton to picom (git revision 8ddbeb and following). + +### Rationale + +Since the inception of this fork, the existence of two compton repositories has caused some number of confusions. Mainly, people will report issues of this fork to the original compton, or report issues of the original compton here. Later, when distros started packaging this fork of compton, some wanted to differentiate the newer compton from the older version. They found themselves having no choice but to invent a name for this fork. This is less than ideal since this has the potential to cause more confusions among users. + +Therefore, we decided to move this fork to a new name. Personally, I consider this more than justified since this version of compton has gone through significant changes since it was forked. + +### The name + +The criteria for a good name were + +0. Being short, so it's easy to remember. +1. Pronounceability, again, helps memorability +2. Searchability, so when people search the name, it's easy for them to find this repository. + +Of course, choosing a name is never easy, and there is no apparent way to objectively evaluate the names. Yet, we have to solve the aforementioned problems as soon as possible. + +In the end, we picked `picom` (a portmanteau of `pico` and `composite`) as our new name. This name might not be perfect, but is what we will move forward with unless there's a compelling reason not to. + + # Compton +This is a copy of the README of the [original Compton project](https://github.com/chjj/compton/). + [![Join the chat at https://gitter.im/chjj/compton](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/chjj/compton?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) __Compton__ is a compositor for X, and a fork of __xcompmgr-dana__. diff --git a/README.md b/README.md index 8829b5c63f..f8de4998ad 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,15 @@ picom -======= +===== -**This is a development branch, bugs to be expected** - -This is forked from the original Compton because it seems to have become unmaintained. - -The current battle plan of this fork is to refactor it to make the code _possible_ to maintain, so potential contributors won't be scared away when they take a look at the code. - -We also try to fix bugs. - -You can leave your feedbacks or thoughts in the [discussion tab](https://github.com/yshui/picom/discussions). - -The original README can be found [here](README_orig.md) - -## Call for testers - -### `--experimental-backends` - -This flag enables the refactored/partially rewritten backends. - -Currently, new backends feature better vsync with the xrender backend and improved input lag with the glx backend (for non-NVIDIA users). The performance should be on par with the old backends. - -New backend features will only be implemented on the new backends from now on, and the old backends will eventually be phased out after the new backends stabilize. - -To test the new backends, add the `--experimental-backends` flag to the command you use to run picom. This flag is not available from the configuration file. - -To report issues with the new backends, please state explicitly you are using the new backends in your report. - -## Rename - -### Rationale - -Since the inception of this fork, the existence of two compton repositories has caused some number of confusions. Mainly, people will report issues of this fork to the original compton, or report issues of the original compton here. Later, when distros started packaging this fork of compton, some wanted to differentiate the newer compton from the older version. They found themselves having no choice but to invent a name for this fork. This is less than ideal since this has the potential to cause more confusions among users. +[![circleci](https://circleci.com/gh/yshui/picom.svg?style=shield)](https://circleci.com/gh/yshui/picom) +[![codecov](https://codecov.io/gh/yshui/picom/branch/next/graph/badge.svg?token=NRSegi0Gze)](https://codecov.io/gh/yshui/picom) +[![chat on discord](https://img.shields.io/discord/1106224720833159198?logo=discord)](https://discord.gg/SY5JJzPgME) -Therefore, we decided to move this fork to a new name. Personally, I consider this more than justified since this version of compton has gone through significant changes since it was forked. +__picom__ is a compositor for X, and a [fork of Compton](History.md). -### The name - -The criteria for a good name were - -0. Being short, so it's easy to remember. -1. Pronounceability, again, helps memorability -2. Searchability, so when people search the name, it's easy for them to find this repository. - -Of course, choosing a name is never easy, and there is no apparent way to objectively evaluate the names. Yet, we have to solve the aforementioned problems as soon as possible. - -In the end, we picked `picom` (a portmanteau of `pico` and `composite`) as our new name. This name might not be perfect, but is what we will move forward with unless there's a compelling reason not to. - -### Migration - -Following the [deprecation process](https://github.com/yshui/picom/issues/114), migration to the new name will be broken into 3 steps: - -1. All mentions of `compton` will be updated to `picom` in the code base. `compton` will still be installed, but only as a symlink to `picom`. When `picom` is launched via the symlink, a warning message is printed, alerting the user to migrate. Similarly, the old configuration file names and dbus interface names will still be accepted but warned. -2. 3 major releases after step 1, the warning messages will be prompted to error messages and `picom` will not start when launched via the symlink. -3. 3 major releases after step 2, the symlink will be removed. +**This is a development branch, bugs to be expected** -The dbus interface and service names are unchanged, so no migration needed for that. +You can leave your feedback or thoughts in the [discussion tab](https://github.com/yshui/picom/discussions), or chat with other users on [discord](https://discord.gg/SY5JJzPgME)! ## Change Log @@ -72,7 +26,9 @@ Assuming you already have all the usual building tools installed (e.g. gcc, pyth * libXext * xproto * xcb +* xcb-util * xcb-damage +* xcb-dpms * xcb-xfixes * xcb-shape * xcb-renderutil @@ -81,26 +37,25 @@ Assuming you already have all the usual building tools installed (e.g. gcc, pyth * xcb-composite * xcb-image * xcb-present -* xcb-xinerama * xcb-glx * pixman +* libconfig * libdbus (optional, disable with the `-Ddbus=false` meson configure flag) -* libconfig (optional, disable with the `-Dconfig_file=false` meson configure flag) -* libGL (optional, disable with the `-Dopengl=false` meson configure flag) -* libpcre (optional, disable with the `-Dregex=false` meson configure flag) +* libGL, libEGL, libepoxy (optional, disable with the `-Dopengl=false` meson configure flag) +* libpcre2 (optional, disable with the `-Dregex=false` meson configure flag) * libev * uthash On Debian based distributions (e.g. Ubuntu), the needed packages are ``` -libxext-dev libxcb1-dev libxcb-damage0-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-render-util0-dev libxcb-render0-dev libxcb-randr0-dev libxcb-composite0-dev libxcb-image0-dev libxcb-present-dev libxcb-xinerama0-dev libxcb-glx0-dev libpixman-1-dev libdbus-1-dev libconfig-dev libgl1-mesa-dev libpcre2-dev libpcre3-dev libevdev-dev uthash-dev libev-dev libx11-xcb-dev +libconfig-dev libdbus-1-dev libegl-dev libev-dev libgl-dev libepoxy-dev libpcre2-dev libpixman-1-dev libx11-xcb-dev libxcb1-dev libxcb-composite0-dev libxcb-damage0-dev libxcb-dpms0-dev libxcb-glx0-dev libxcb-image0-dev libxcb-present-dev libxcb-randr0-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-util-dev libxcb-xfixes0-dev libxext-dev meson ninja-build uthash-dev ``` On Fedora, the needed packages are ``` -dbus-devel gcc git libconfig-devel libdrm-devel libev-devel libX11-devel libX11-xcb libXext-devel libxcb-devel mesa-libGL-devel meson pcre-devel pixman-devel uthash-devel xcb-util-image-devel xcb-util-renderutil-devel xorg-x11-proto-devel +dbus-devel gcc git libconfig-devel libdrm-devel libev-devel libX11-devel libX11-xcb libXext-devel libxcb-devel libGL-devel libEGL-devel libepoxy-devel meson pcre2-devel pixman-devel uthash-devel xcb-util-image-devel xcb-util-renderutil-devel xorg-x11-proto-devel xcb-util-devel ``` To build the documents, you need `asciidoc` @@ -108,8 +63,7 @@ To build the documents, you need `asciidoc` ### To build ```bash -$ git submodule update --init --recursive -$ meson --buildtype=release . build +$ meson setup --buildtype=release build $ ninja -C build ``` @@ -120,13 +74,12 @@ If you have libraries and/or headers installed at non-default location (e.g. und You can do that by setting the `CPPFLAGS` and `LDFLAGS` environment variables when running `meson`. Like this: ```bash -$ LDFLAGS="-L/path/to/libraries" CPPFLAGS="-I/path/to/headers" meson --buildtype=release . build - +$ LDFLAGS="-L/path/to/libraries" CPPFLAGS="-I/path/to/headers" meson setup --buildtype=release build ``` As an example, on FreeBSD, you might have to run meson with: ```bash -$ LDFLAGS="-L/usr/local/lib" CPPFLAGS="-I/usr/local/include" meson --buildtype=release . build +$ LDFLAGS="-L/usr/local/lib" CPPFLAGS="-I/usr/local/include" meson setup --buildtype=release build $ ninja -C build ``` @@ -140,16 +93,29 @@ Default install prefix is `/usr/local`, you can change it with `meson configure ## How to Contribute -### Code +All contributions are welcome! + +New features you think should be included in picom, a fix for a bug you found - please open a PR! + +You can take a look at the [Issues](https://github.com/yshui/picom/issues). + +Contributions to the documents and wiki are also appreciated. -You can look at the [Projects](https://github.com/yshui/picom/projects) page, and see if there is anything that interests you. Or you can take a look at the [Issues](https://github.com/yshui/picom/issues). +Even if you don't want to add anything to picom, you are still helping by compiling and running this branch, and report any issue you can find. -### Non-code +### Become a Collaborator -Even if you don't want to contribute code, you can still contribute by compiling and running this branch, and report any issue you can find. +Becoming a collaborator of picom requires significant time commitment. You are expected to reply to issue reports, reviewing PRs, and sometimes fix bugs or implement new feature. You won't be able to push to the main branch directly, and all you code still has to go through code review. -Contributions to the documents and wiki will also be appreciated. +If this sounds good to you, feel free to contact me. ## Contributors See [CONTRIBUTORS](CONTRIBUTORS) + +The README for the [original Compton project](https://github.com/chjj/compton/) can be found [here](History.md#Compton). + +## Licensing + +picom is free software, made available under the [MIT](LICENSES/MIT) and [MPL-2.0](LICENSES/MPL-2.0) software +licenses. See the individual source files for details. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..a6a77dc697 --- /dev/null +++ b/flake.lock @@ -0,0 +1,81 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "git-ignore-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "ref": "master", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1712517181, + "narHash": "sha256-NfHaSxL89kX39s1V1/EnnIqBX8LXcc/7ow5fzfCwPrM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6507feebbd146188300681ca26bac5da3c12b9f1", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "git-ignore-nix": "git-ignore-nix", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..221a1a9114 --- /dev/null +++ b/flake.nix @@ -0,0 +1,80 @@ +{ + inputs = { + flake-utils.url = github:numtide/flake-utils; + git-ignore-nix = { + url = github:hercules-ci/gitignore.nix/master; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + outputs = { + self, + flake-utils, + nixpkgs, + git-ignore-nix, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + overlay = self: super: { + picom = super.picom.overrideAttrs (oldAttrs: rec { + version = "11"; + pname = "picom"; + buildInputs = + [ + self.pcre2 + self.xorg.xcbutil + self.libepoxy + ] + ++ self.lib.remove self.xorg.libXinerama ( + self.lib.remove self.pcre oldAttrs.buildInputs + ); + src = git-ignore-nix.lib.gitignoreSource ./.; + }); + }; + python = pkgs.python3.withPackages (ps: with ps; [ + xcffib pip dbus-next + ]); + + pkgs = import nixpkgs { + inherit system overlays; + config.allowBroken = true; + }; + profilePkgs = import nixpkgs { + inherit system; + overlays = overlays ++ [ + (final: prev: { + stdenv = prev.withCFlags "-fno-omit-frame-pointer" prev.stdenv; + }) + ]; + }; + + overlays = [overlay]; + mkDevShell = p: p.overrideAttrs (o: { + nativeBuildInputs = o.nativeBuildInputs ++ (with pkgs; [ + clang-tools_17 + llvmPackages_17.clang-unwrapped.python + python + ]); + hardeningDisable = ["fortify"]; + shellHook = '' + # Workaround a NixOS limitation on sanitizers: + # See: https://github.com/NixOS/nixpkgs/issues/287763 + export LD_LIBRARY_PATH+=":/run/opengl-driver/lib" + ''; + }); + in rec { + inherit + overlay + overlays + ; + defaultPackage = pkgs.picom; + devShells.default = mkDevShell defaultPackage; + devShells.useClang = devShells.default.override { + inherit (pkgs.llvmPackages_17) stdenv; + }; + # build picom and all dependencies with frame pointer, making profiling/debugging easier. + # WARNING! many many rebuilds + devShells.useClangProfile = (mkDevShell profilePkgs.picom).override { + stdenv = profilePkgs.withCFlags "-fno-omit-frame-pointer" profilePkgs.llvmPackages_17.stdenv; + }; + }); +} diff --git a/man/meson.build b/man/meson.build index 632483099e..c3b3eed9bd 100644 --- a/man/meson.build +++ b/man/meson.build @@ -1,4 +1,4 @@ -mans = ['picom.1', 'picom-trans.1'] +mans = ['picom.1', 'picom-inspect.1', 'picom-trans.1'] if get_option('with_docs') a2x = find_program('a2x') foreach m : mans diff --git a/man/picom-inspect.1.asciidoc b/man/picom-inspect.1.asciidoc new file mode 100644 index 0000000000..95d031fe45 --- /dev/null +++ b/man/picom-inspect.1.asciidoc @@ -0,0 +1,40 @@ +picom-inspect(1) +================ +:doctype: manpage +:man source: picom-inspect +:man version: {picom-version} +:man manual: User Commands + +NAME +---- +picom-inspect - easily test your picom rules + +SYNOPSIS +-------- +*picom-inspect* ['OPTIONS'] + +DESCRIPTION +----------- +*picom-inspect* matches your picom rules against a window of your choosing. It helps you test your rules, and shows you which ones of your rules (don't) work. + +OPTIONS +------- +*picom-inspect* accept the exact same set of options as *picom*. Naturally, most of those options will not be relevant. + +These are some of the options you might find useful (See *picom*(1) for descriptions of what they do): + +*--config*, *--log-level*, *--log-file*, all the options related to rules. + +NOTES +----- +*picom-inspect* is prototype right now. If you find any bug, for example, if rules are matched differently compared to *picom*, please submit bug reports to: + + + +RESOURCES +--------- +Homepage: + +SEE ALSO +-------- +*xcompmgr*(1), link:picom.html[*picom*(1)] diff --git a/man/picom.1.asciidoc b/man/picom.1.asciidoc index 629184d13d..cdd98a3cef 100644 --- a/man/picom.1.asciidoc +++ b/man/picom.1.asciidoc @@ -43,18 +43,12 @@ OPTIONS *-D*, *--fade-delta*='MILLISECONDS':: The time between steps in fade step, in milliseconds. (> 0, defaults to 10) -*-m*, *--menu-opacity*='OPACITY':: - Default opacity for dropdown menus and popup menus. (0.0 - 1.0, defaults to 1.0) - *-c*, *--shadow*:: Enabled client-side shadows on windows. Note desktop windows (windows with '_NET_WM_WINDOW_TYPE_DESKTOP') never get shadow, unless explicitly requested using the wintypes option. *-f*, *--fading*:: Fade windows in/out when opening/closing and when opacity changes, unless *--no-fading-openclose* is used. -*-F*:: - Equals to *-f*. Deprecated. - *-i*, *--inactive-opacity*='OPACITY':: Opacity of inactive windows. (0.1 - 1.0, defaults to 1.0) @@ -70,8 +64,8 @@ OPTIONS *--log-file*:: Set the log file. If *--log-file* is never specified, logs will be written to stderr. Otherwise, logs will to written to the given file, though some of the early logs might still be written to the stderr. When setting this option from the config file, it is recommended to use an absolute path. -*--experimental-backends*:: - Use the new, reimplemented version of the backends. The new backends are HIGHLY UNSTABLE at this point, you have been warned. This option is not available in the config file. +*--legacy-backends*:: + Use the old version of the backends. This option can not be set from the config file. *--show-all-xerrors*:: Show all X errors (for debugging). @@ -95,7 +89,7 @@ OPTIONS Blue color value of shadow (0.0 - 1.0, defaults to 0). *--inactive-opacity-override*:: - Let inactive opacity set by *-i* override the '_NET_WM_OPACITY' values of windows. + Let inactive opacity set by *-i* override the '_NET_WM_WINDOW_OPACITY' values of windows. *--active-opacity* 'OPACITY':: Default opacity for active windows. (0.0 - 1.0, defaults to 1.0) @@ -106,9 +100,15 @@ OPTIONS *--corner-radius* 'VALUE':: Sets the radius of rounded window corners. When > 0, the compositor will round the corners of windows. Does not interact well with *--transparent-clipping*. (defaults to 0). +*--corner-radius-rules* 'RADIUS':'CONDITION':: + Specify a list of corner radius rules. Overrides the corner radii of matching windows. This option takes precedence over the *--rounded-corners-exclude* option, and also overrides the default exclusion of fullscreen windows. The condition has the same format as *--opacity-rule*. + *--rounded-corners-exclude* 'CONDITION':: Exclude conditions for rounded corners. +*--no-frame-pacing*:: + Disable vsync-aware frame pacing. By default, the compositor tries to make sure it only renders once per vblank interval, and also the render happens as late as possible to minimize the latency from updates to the screen. However this can sometimes cause stuttering, or even lowered frame rate. This option can be used to disable frame pacing. + *--mark-wmwin-focused*:: Try to detect WM windows (a non-override-redirect window with no child that has 'WM_STATE') and mark them as active. @@ -128,10 +128,7 @@ OPTIONS Try to detect windows with rounded corners and don't consider them shaped windows. The accuracy is not very high, unfortunately. *--detect-client-opacity*:: - Detect '_NET_WM_OPACITY' on client windows, useful for window managers not passing '_NET_WM_OPACITY' of client windows to frame windows. - -*--refresh-rate* 'REFRESH_RATE':: - Specify refresh rate of the screen. If not specified or 0, picom will try detecting this with X RandR extension. + Detect '_NET_WM_WINDOW_OPACITY' on client windows, useful for window managers not passing '_NET_WM_WINDOW_OPACITY' of client windows to frame windows. *--vsync*, *--no-vsync*:: Enable/disable VSync. @@ -140,7 +137,13 @@ OPTIONS Use EWMH '_NET_ACTIVE_WINDOW' to determine currently focused window, rather than listening to 'FocusIn'/'FocusOut' event. Might have more accuracy, provided that the WM supports it. *--unredir-if-possible*:: - Unredirect all windows if a full-screen opaque window is detected, to maximize performance for full-screen windows. Known to cause flickering when redirecting/unredirecting windows. + Unredirect all windows in some cases. Known to cause flickering when redirecting/unredirecting windows. Currently, unredirecting is triggered by following conditions: + * If the top level window is taking up the entire screen. In multi-monitor setup, this means ALL monitors. + * If there is no window. + * If a window is fullscreen according to its WM hints. (can be disabled with *--no-ewmh-fullscreen*). + * If a window requests to bypass the compositor ('_NET_WM_BYPASS_COMPOSITOR'). + Windows are also unredirected unconditionally when monitors are powered off, regardless if *--unredir-if-possible* is set. + *--unredir-if-possible-delay* 'MILLISECONDS':: Delay before unredirecting the window, in milliseconds. Defaults to 0. @@ -167,7 +170,7 @@ OPTIONS Use 'WM_TRANSIENT_FOR' to group windows, and consider windows in the same group focused at the same time. *--detect-client-leader*:: - Use 'WM_CLIENT_LEADER' to group windows, and consider windows in the same group focused at the same time. 'WM_TRANSIENT_FOR' has higher priority if *--detect-transient* is enabled, too. + Use 'WM_CLIENT_LEADER' to group windows, and consider windows in the same group focused at the same time. This usually means windows from the same application will be considered focused or unfocused at the same time.'WM_TRANSIENT_FOR' has higher priority if *--detect-transient* is enabled, too. *--blur-method*, *--blur-size*, *--blur-deviation*, *--blur-strength*:: Parameters for background blurring, see the *BLUR* section for more information. @@ -200,7 +203,7 @@ A 7x7 Gaussian blur kernel (sigma = 0.84089642) looks like: --blur-kern '7,7,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0.000102,0.003494,0.029143,0.059106,0.029143,0.003494,0.000102,0.000849,0.029143,0.243117,0.493069,0.243117,0.029143,0.000849,0.001723,0.059106,0.493069,0.493069,0.059106,0.001723,0.000849,0.029143,0.243117,0.493069,0.243117,0.029143,0.000849,0.000102,0.003494,0.029143,0.059106,0.029143,0.003494,0.000102,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003' ---- + -May also be one of the predefined kernels: `3x3box` (default), `5x5box`, `7x7box`, `3x3gaussian`, `5x5gaussian`, `7x7gaussian`, `9x9gaussian`, `11x11gaussian`. All Gaussian kernels are generated with sigma = 0.84089642 . If you find yourself needing to generate custom blur kernels, you might want to try the new blur configuration supported by the experimental backends (See *BLUR* and *--experimental-backends*). +May also be one of the predefined kernels: `3x3box` (default), `5x5box`, `7x7box`, `3x3gaussian`, `5x5gaussian`, `7x7gaussian`, `9x9gaussian`, `11x11gaussian`. All Gaussian kernels are generated with sigma = 0.84089642 . If you find yourself needing to generate custom blur kernels, you might want to try the new blur configuration (See *BLUR*). *--blur-background-exclude* 'CONDITION':: Exclude conditions for background blur. @@ -214,11 +217,8 @@ May also be one of the predefined kernels: `3x3box` (default), `5x5box`, `7x7box *--opacity-rule* 'OPACITY':'CONDITION':: Specify a list of opacity rules, in the format `PERCENT:PATTERN`, like `50:name *= "Firefox"`. picom-trans is recommended over this. Note we don't make any guarantee about possible conflicts with other programs that set '_NET_WM_WINDOW_OPACITY' on frame or client windows. -*--shadow-exclude-reg* 'GEOMETRY':: - Specify a X geometry that describes the region in which shadow should not be painted in, such as a dock window region. Use `--shadow-exclude-reg x10+0-0`, for example, if the 10 pixels on the bottom of the screen should not have shadows painted on. - -*--xinerama-shadow-crop*:: - Crop shadow of a window fully on a particular Xinerama screen to the screen. +*--crop-shadow-to-monitor*:: + Crop shadow of a window fully on a particular monitor to that monitor. This is currently implemented using the X RandR extension. *--backend* 'BACKEND':: Specify the backend to use: `xrender`, `glx`, or `xr_glx_hybrid`. `xrender` is the default one. @@ -236,13 +236,13 @@ May also be one of the predefined kernels: `3x3box` (default), `5x5box`, `7x7box GLX backend: Avoid rebinding pixmap on window damage. Probably could improve performance on rapid window content changes, but is known to break things on some drivers (LLVMpipe, xf86-video-intel, etc.). Recommended if it works. *--no-use-damage*:: - Disable the use of damage information. This cause the whole screen to be redrawn everytime, instead of the part of the screen has actually changed. Potentially degrades the performance, but might fix some artifacts. + Disable the use of damage information. This cause the whole screen to be redrawn every time, instead of the part of the screen has actually changed. Potentially degrades the performance, but might fix some artifacts. *--xrender-sync-fence*:: Use X Sync fence to sync clients' draw calls, to make sure all draw calls are finished before picom starts drawing. Needed on nvidia-drivers with GLX backend for some users. *--glx-fshader-win* 'SHADER':: - GLX backend: Use specified GLSL fragment shader for rendering window contents. See `compton-default-fshader-win.glsl` and `compton-fake-transparency-fshader-win.glsl` in the source tree for examples. + GLX backend: Use specified GLSL fragment shader for rendering window contents. See `compton-default-fshader-win.glsl` and `compton-fake-transparency-fshader-win.glsl` in the source tree for examples. Only works with *--legacy-backends* enabled. *--force-win-blend*:: Force all windows to be painted with blending. Useful if you have a *--glx-fshader-win* that could turn opaque pixels transparent. @@ -265,21 +265,33 @@ May also be one of the predefined kernels: `3x3box` (default), `5x5box`, `7x7box *--transparent-clipping*:: Make transparent windows clip other windows like non-transparent windows do, instead of blending on top of them. +*--transparent-clipping-exclude* 'CONDITION':: + Specify a list of conditions of windows that should never have transparent clipping applied. Useful for screenshot tools, where you need to be able to see through transparent parts of the window. + +*--window-shader-fg* 'SHADER':: + Specify GLSL fragment shader path for rendering window contents. Does not work when *--legacy-backends* is enabled. Shader is searched first relative to the directory the configuration file is in, then in the usual places for a configuration file. See section *SHADER INTERFACE* below for more details on the interface. + +*--window-shader-fg-rule* 'SHADER':'CONDITION':: + Specify GLSL fragment shader path for rendering window contents using patterns. Similar to *--opacity-rule*, arguments should be in the format of 'SHADER:CONDITION', e.g. "shader.frag:name = \'window\'". Leading and trailing whitespaces in 'SHADER' will be trimmed. If 'SHADER' is "default", then the default shader will be used for the matching windows. (This also unfortunately means you can't use a shader file named "default"). Does not work when *--legacy-backends* is enabled. + +*--dithered-present* + Use higher precision during rendering, and apply dither when presenting the rendered screen. Reduces banding artifacts, but might cause performance degradation. Only works with OpenGL. + FORMAT OF CONDITIONS -------------------- Some options accept a condition string to match certain windows. A condition string is formed by one or more conditions, joined by logical operators. A condition with "exists" operator looks like this: - [] : + [] With equals operator it looks like: - [] : = + [] = With greater-than/less-than operators it looks like: - [] : + [] 'NEGATION' (optional) is one or more exclamation marks; @@ -289,17 +301,13 @@ With greater-than/less-than operators it looks like: 'INDEX' (optional) is the index number of the property to look up. For example, `[2]` means look at the third value in the property. If not specified, the first value (index `[0]`) is used implicitly. Use the special value `[*]` to perform matching against all available property values using logical OR. Do not specify it for predefined targets. -'FORMAT' (optional) specifies the format of the property, 8, 16, or 32. On absence we use format X reports. Do not specify it for predefined or string targets. - -'TYPE' is a single character representing the type of the property to match for: `c` for 'CARDINAL', `a` for 'ATOM', `w` for 'WINDOW', `d` for 'DRAWABLE', `s` for 'STRING' (and any other string types, such as 'UTF8_STRING'). Do not specify it for predefined targets. - 'OP QUALIFIER' (optional), applicable only for equals operator, could be `?` (ignore-case). 'MATCH TYPE' (optional), applicable only for equals operator, could be nothing (exact match), `*` (match anywhere), `^` (match from start), `%` (wildcard), or `~` (PCRE regular expression). 'OPERATOR' is one of `=` (equals), `<`, `>`, `<=`, `=>`, or nothing (exists). Exists operator checks whether a property exists on a window (but for predefined targets, exists means != 0 then). -'PATTERN' is either an integer or a string enclosed by single or double quotes. Python-3-style escape sequences and raw string are supported in the string format. +'PATTERN' is either an integer or a string enclosed by single or double quotes. Python-3-style escape sequences are supported in the string format. Supported logical operators are `&&` (and) and `||` (or). `&&` has higher precedence than `||`, left-to-right associativity. Use parentheses to change precedence. @@ -315,28 +323,26 @@ Examples: override_redirect != 1 # If the window is a menu window_type *= "menu" - _NET_WM_WINDOW_TYPE@:a *= "MENU" + _NET_WM_WINDOW_TYPE@ *= "MENU" # If the window is marked hidden: _NET_WM_STATE contains _NET_WM_STATE_HIDDEN - _NET_WM_STATE@[*]:a = "_NET_WM_STATE_HIDDEN" + _NET_WM_STATE@[*] = "_NET_WM_STATE_HIDDEN" # If the window is marked sticky: _NET_WM_STATE contains an atom that contains # "sticky", ignore case - _NET_WM_STATE@[*]:a *?= "sticky" + _NET_WM_STATE@[*] *?= "sticky" # If the window name contains "Firefox", ignore case name *?= "Firefox" - _NET_WM_NAME@:s *?= "Firefox" + _NET_WM_NAME@ *?= "Firefox" # If the window name ends with "Firefox" name %= "*Firefox" name ~= "Firefox$" # If the window has a property _COMPTON_SHADOW with value 0, type CARDINAL, # format 32, value 0, on its frame window - _COMPTON_SHADOW:32c = 0 + _COMPTON_SHADOW = 0 # If the third value of _NET_FRAME_EXTENTS is less than 20, or there's no # _NET_FRAME_EXTENTS property on client window - _NET_FRAME_EXTENTS@[2]:32c < 20 || !_NET_FRAME_EXTENTS@:32c + _NET_FRAME_EXTENTS@[2] < 20 || !_NET_FRAME_EXTENTS@ # The pattern here will be parsed as "dd4" name = "\x64\x64\o64" - # The pattern here will be parsed as "\x64\x64\x64" - name = r"\x64\x64\o64" LEGACY FORMAT OF CONDITIONS @@ -354,6 +360,63 @@ This is the old condition format we once used. Support of this format might be r 'PATTERN' is the actual pattern string. +SHADER INTERFACE +---------------- + +This secion describes the interface of a custom shader, how it is used by picom, and what parameters are passed by picom to the shader. This does not apply to the legacy backends. + +A custom shader is a GLSL fragment shader program, which can be used to override the default way of how a window is rendered. If a custom shader is used, the default picom effects (e.g. dimming, color inversion, etc.) will no longer be automatically applied. It would be the custom shader's responsibility to apply these effects. + +The interface between picom and a custom shader is dependent on which backend is being used. The xrender backend doesn't support shader at all. Here we descibe the interface provided by the glx backend. + +The shader must define a function, 'vec4 window_shader()', which would be the entry point of the shader. The returned 'vec4' will be used to set 'gl_FragColor'. A function, 'vec4 default_post_processing(vec4 c)', is provided for applying the default picom effects to input color 'c'. + +The following uniform/input variables are made available to the shader: + +[source,glsl] +---- +in vec2 texcoord; // texture coordinate of the fragment + +uniform float opacity; // opacity of the window (0.0 - 1.0) +uniform float dim; // dimming factor of the window (0.0 - 1.0, higher means more dim) +uniform float corner_radius; // corner radius of the window (pixels) +uniform float border_width; // estimated border width of the window (pixels) +uniform bool invert_color; // whether to invert the color of the window +uniform sampler2D tex; // texture of the window +uniform vec2 effective_size; // effective dimensions of the texture (repeats pixels if larger than tex) +uniform sampler2D brightness; // estimated brightness of the window, 1x1 texture +uniform float max_brightness; // configured maximum brightness of the window (0.0 - 1.0) +uniform float time; // time in milliseconds, counting from an unspecified starting point +---- + +The default behavior of picom window rendering can be replicated by the following shader: + +[source,glsl] +---- +#version 330 +in vec2 texcoord; // texture coordinate of the fragment + +uniform sampler2D tex; // texture of the window + +// Default window post-processing: +// 1) invert color +// 2) opacity / transparency +// 3) max-brightness clamping +// 4) rounded corners +vec4 default_post_processing(vec4 c); + +// Default window shader: +// 1) fetch the specified pixel +// 2) apply default post-processing +vec4 window_shader() { + vec2 texsize = textureSize(tex, 0); + vec4 c = texture2D(tex, texcoord / texsize, 0); + return default_post_processing(c); +} +---- + +The interface is expected to be mostly stable. + CONFIGURATION FILES ------------------- picom could read from a configuration file if libconfig support is compiled in. If *--config* is not used, picom will seek for a configuration file in `$XDG_CONFIG_HOME/picom.conf` (`~/.config/picom.conf`, usually), then `$XDG_CONFIG_HOME/picom/picom.conf`, then `$XDG_CONFIG_DIRS/picom.conf` (often `/etc/xdg/picom.conf`), then `$XDG_CONFIG_DIRS/picom/picom.conf`. @@ -383,13 +446,13 @@ Following per window-type options are available: :: Controls whether the window of this type is to be always considered focused. (By default, all window types except "normal" and "dialog" has this on.) blur-background::: - Controls wether the window of this type will have its transparent background blurred. + Controls whether the window of this type will have its transparent background blurred. full-shadow::: Controls whether shadow is drawn under the parts of the window that you normally won't be able to see. Useful when the window has parts of it transparent, and you want shadows in those areas. clip-shadow-above::: - Controls wether shadows that would have been drawn above the window should be clipped. Useful for dock windows that should have no shadow painted on top. + Controls whether shadows that would have been drawn above the window should be clipped. Useful for dock windows that should have no shadow painted on top. redir-ignore::: Controls whether this type of windows should cause screen to become redirected again after been unredirected. If you have *--unredir-if-possible* set, and doesn't want certain window to cause unnecessary screen redirection, you can set this to `true`. @@ -412,7 +475,7 @@ Available options of the 'blur' section are: :: *method*::: A string. Controls the blur method. Corresponds to the *--blur-method* command line option. Available choices are: 'none' to disable blurring; 'gaussian' for gaussian blur; 'box' for box blur; 'kernel' for convolution blur with a custom kernel; 'dual_kawase' for dual-filter kawase blur. - Note: 'gaussian', 'box' and 'dual_kawase' blur methods are only supported by the experimental backends. + Note: 'gaussian', 'box' and 'dual_kawase' blur methods are not supported by the legacy backends. (default: none) *size*::: @@ -425,7 +488,7 @@ Available options of the 'blur' section are: :: An integer in the range 0-20. The strength of the 'dual_kawase' blur method. Corresponds to the *--blur-strength* command line option. If set to zero, the value requested by *--blur-size* is approximated (default: 5). *kernel*::: - A string. The kernel to use for the 'kernel' blur method, specified in the same format as the *--blur-kerns* option. Corresponds to the *--blur-kerns* command line option. + A string. The kernel to use for the 'kernel' blur method, specified in the same format as the *--blur-kern* option. Corresponds to the *--blur-kern* command line option. SIGNALS ------- @@ -448,16 +511,16 @@ EXAMPLES $ picom --config /dev/null ------------ -* Run picom with client-side shadow and fading, disable shadow on dock windows and drag-and-drop windows: +* Run picom with client-side shadow and fading: + ------------ -$ picom -cCGf +$ picom -cf ------------ * Same thing as above, plus making inactive windows 80% transparent, making frame 80% transparent, don't fade on window open/close, and fork to background: + ------------ -$ picom -bcCGf -i 0.8 -e 0.8 --no-fading-openclose +$ picom -bcf -i 0.8 -e 0.8 --no-fading-openclose ------------ * Draw white shadows: @@ -490,4 +553,4 @@ Homepage: SEE ALSO -------- -*xcompmgr*(1), link:picom-trans.html[*picom-trans*(1)] +*xcompmgr*(1), link:picom-inspect.html[*picom-inspect*(1)], link:picom-trans.html[*picom-trans*(1)] diff --git a/meson.build b/meson.build index c8bdb24e19..65c0780286 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ -project('picom', 'c', version: '8', - default_options: ['c_std=c11']) +project('picom', 'c', version: '11', + default_options: ['c_std=c11', 'warning_level=1']) cc = meson.get_compiler('c') @@ -9,13 +9,13 @@ version = 'v'+meson.project_version() # use git describe if that's available git = find_program('git', required: false) if git.found() - gitv = run_command('git', 'rev-parse', '--short=5', 'HEAD') + gitv = run_command('git', 'rev-parse', '--short=5', 'HEAD', check: false) if gitv.returncode() == 0 version = 'vgit-'+gitv.stdout().strip() endif endif -add_global_arguments('-DCOMPTON_VERSION="'+version+'"', language: 'c') +add_global_arguments('-DPICOM_VERSION="'+version+'"', language: 'c') if get_option('buildtype') == 'release' add_global_arguments('-DNDEBUG', language: 'c') @@ -53,16 +53,27 @@ if cc.has_header('stdc-predef.h') add_global_arguments('-DHAS_STDC_PREDEF_H', language: 'c') endif -warns = [ 'all', 'cast-function-type', 'ignored-qualifiers', 'missing-parameter-type', - 'nonnull', 'shadow', 'no-type-limits', 'old-style-declaration', 'override-init', - 'sign-compare', 'type-limits', 'uninitialized', 'shift-negative-value', - 'unused-but-set-parameter', 'unused-parameter', 'implicit-fallthrough', - 'no-unknown-warning-option', 'no-missing-braces', 'conversion', 'empty-body' ] -foreach w : warns - if cc.has_argument('-W'+w) - add_global_arguments('-W'+w, language: 'c') - endif -endforeach +if get_option('warning_level') != '0' + warns = [ 'cast-function-type', 'ignored-qualifiers', 'missing-parameter-type', + 'nonnull', 'shadow', 'no-type-limits', 'old-style-declaration', 'override-init', + 'sign-compare', 'type-limits', 'uninitialized', 'shift-negative-value', + 'unused-but-set-parameter', 'unused-parameter', 'implicit-fallthrough=2', + 'no-unknown-warning-option', 'no-missing-braces', 'conversion', 'empty-body', + 'no-c2x-extensions' ] + bad_warns_ubsan = [ + 'no-format-overflow' # see gcc bug 87884, enabling UBSAN makes gcc spit out spurious warnings + # (if you saw this comment, went and checked gcc bugzilla, and found out + # bug has been fixed, please open a PR to remove this). + ] + if get_option('b_sanitize').contains('undefined') and cc.get_id() == 'gcc' + warns = warns + bad_warns_ubsan + endif + foreach w : warns + if cc.has_argument('-W'+w) + add_global_arguments('-W'+w, language: 'c') + endif + endforeach +endif test_h_dep = subproject('test.h').get_variable('test_h_dep') @@ -71,6 +82,7 @@ subdir('man') install_data('bin/picom-trans', install_dir: get_option('bindir')) install_data('picom.desktop', install_dir: 'share/applications') +install_data('picom.desktop', install_dir: get_option('sysconfdir') / 'xdg' / 'autostart') if get_option('compton') install_data('compton.desktop', install_dir: 'share/applications') diff --git a/meson/install.sh b/meson/install.sh old mode 100644 new mode 100755 diff --git a/meson_options.txt b/meson_options.txt index 9c3b0da1d5..ead0e8ec03 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,5 +1,4 @@ option('sanitize', type: 'boolean', value: false, description: 'Build with sanitizers enabled (deprecated)') -option('config_file', type: 'boolean', value: true, description: 'Enable config file support') option('regex', type: 'boolean', value: true, description: 'Enable regex support in window conditions') option('vsync_drm', type: 'boolean', value: false, description: 'Enable support for using drm for vsync') diff --git a/picom.desktop b/picom.desktop index 9d21f6df5f..051e8935bd 100644 --- a/picom.desktop +++ b/picom.desktop @@ -9,5 +9,7 @@ Categories=Utility; Keywords=compositor;composite manager;window effects;transparency;opacity; TryExec=picom Exec=picom +StartupNotify=false +Terminal=false # Thanks to quequotion for providing this file! Icon=picom diff --git a/picom.sample.conf b/picom.sample.conf index 797c867aba..c605af155c 100644 --- a/picom.sample.conf +++ b/picom.sample.conf @@ -38,31 +38,21 @@ shadow-offset-y = -7; # shadow-color = "#000000" # Specify a list of conditions of windows that should have no shadow. -# -# examples: -# shadow-exclude = "n:e:Notification"; -# # shadow-exclude = [] shadow-exclude = [ "name = 'Notification'", "class_g = 'Conky'", "class_g ?= 'Notify-osd'", "class_g = 'Cairo-clock'", - "_GTK_FRAME_EXTENTS@:c" + "_GTK_FRAME_EXTENTS@" ]; # Specify a list of conditions of windows that should have no shadow painted over, such as a dock window. # clip-shadow-above = [] -# Specify a X geometry that describes the region in which shadow should not -# be painted in, such as a dock window region. Use -# shadow-exclude-reg = "x10+0+0" -# for example, if the 10 pixels on the bottom of the screen should not have shadows painted on. -# -# shadow-exclude-reg = "" - -# Crop shadow of a window fully on a particular Xinerama screen to the screen. -# xinerama-shadow-crop = false +# Crop shadow of a window fully on a particular monitor to that monitor. This is +# currently implemented using the X RandR extension. +# crop-shadow-to-monitor = false ################################# @@ -109,7 +99,7 @@ inactive-opacity = 0.8; # frame-opacity = 1.0 frame-opacity = 0.7; -# Let inactive opacity set by -i override the '_NET_WM_OPACITY' values of windows. +# Let inactive opacity set by -i override the '_NET_WM_WINDOW_OPACITY' values of windows. # inactive-opacity-override = true inactive-opacity-override = false; @@ -196,29 +186,34 @@ blur-kern = "3x3box"; blur-background-exclude = [ "window_type = 'dock'", "window_type = 'desktop'", - "_GTK_FRAME_EXTENTS@:c" + "_GTK_FRAME_EXTENTS@" ]; ################################# # General Settings # ################################# +# Enable remote control via D-Bus. See the man page for more details. +# dbus = true + # Daemonize process. Fork to background after initialization. Causes issues with certain (badly-written) drivers. # daemon = false -# Specify the backend to use: `xrender`, `glx`, or `xr_glx_hybrid`. +# Specify the backend to use: `xrender`, `glx`, `egl` or `xr_glx_hybrid`. # `xrender` is the default one. # # backend = "glx" backend = "xrender"; +# Use higher precision during rendering, and apply dither when presenting the +# rendered screen. Reduces banding artifacts, but might cause performance +# degradation. Only works with OpenGL. +dithered-present = false; + # Enable/disable VSync. # vsync = false vsync = true; -# Enable remote control via D-Bus. See the *D-BUS API* section below for more details. -# dbus = false - # Try to detect WM windows (a non-override-redirect window with no # child that has 'WM_STATE') and mark them as active. # @@ -235,18 +230,12 @@ mark-ovredir-focused = true; # detect-rounded-corners = false detect-rounded-corners = true; -# Detect '_NET_WM_OPACITY' on client windows, useful for window managers -# not passing '_NET_WM_OPACITY' of client windows to frame windows. +# Detect '_NET_WM_WINDOW_OPACITY' on client windows, useful for window managers +# not passing '_NET_WM_WINDOW_OPACITY' of client windows to frame windows. # # detect-client-opacity = false detect-client-opacity = true; -# Specify refresh rate of the screen. If not specified or 0, picom will -# try detecting this with X RandR extension. -# -# refresh-rate = 60 -refresh-rate = 0; - # Use EWMH '_NET_ACTIVE_WINDOW' to determine currently focused window, # rather than listening to 'FocusIn'/'FocusOut' event. Might have more accuracy, # provided that the WM supports it. @@ -272,11 +261,11 @@ refresh-rate = 0; detect-transient = true; # Use 'WM_CLIENT_LEADER' to group windows, and consider windows in the same -# group focused at the same time. 'WM_TRANSIENT_FOR' has higher priority if -# detect-transient is enabled, too. +# group focused at the same time. This usually means windows from the same application +# will be considered focused or unfocused at the same time. +# 'WM_TRANSIENT_FOR' has higher priority if detect-transient is enabled, too. # # detect-client-leader = false -detect-client-leader = true; # Resize damaged region by a specific number of pixels. # A positive value enlarges it while a negative one shrinks it. @@ -311,7 +300,7 @@ detect-client-leader = true; # glx-no-rebind-pixmap = false # Disable the use of damage information. -# This cause the whole screen to be redrawn everytime, instead of the part of the screen +# This cause the whole screen to be redrawn every time, instead of the part of the screen # has actually changed. Potentially degrades the performance, but might fix some artifacts. # The opposing option is use-damage # @@ -324,11 +313,17 @@ use-damage = true; # # xrender-sync-fence = false -# GLX backend: Use specified GLSL fragment shader for rendering window contents. -# See `compton-default-fshader-win.glsl` and `compton-fake-transparency-fshader-win.glsl` -# in the source tree for examples. +# GLX backend: Use specified GLSL fragment shader for rendering window +# contents. Read the man page for a detailed explanation of the interface. +# +# window-shader-fg = "default" + +# Use rules to set per-window shaders. Syntax is SHADER_PATH:PATTERN, similar +# to opacity-rule. SHADER_PATH can be "default". This overrides window-shader-fg. # -# glx-fshader-win = "" +# window-shader-fg-rule = [ +# "my_shader.frag:window_type != 'dock'" +# ] # Force all windows to be painted with blending. Useful if you # have a glx-fshader-win that could turn opaque pixels transparent. @@ -352,6 +347,12 @@ use-damage = true; # # transparent-clipping = false +# Specify a list of conditions of windows that should never have transparent +# clipping applied. Useful for screenshot tools, where you need to be able to +# see through transparent parts of the window. +# +# transparent-clipping-exclude = [] + # Set the log level. Possible values are: # "trace", "debug", "info", "warn", "error" # in increasing level of importance. Case doesn't matter. @@ -400,7 +401,7 @@ log-level = "warn"; # transparent, and you want shadows in those areas. # # clip-shadow-above::: -# Controls wether shadows that would have been drawn above the window should +# Controls whether shadows that would have been drawn above the window should # be clipped. Useful for dock windows that should have no shadow painted on top. # # redir-ignore::: diff --git a/src/atom.c b/src/atom.c index 0272dc8364..f4b97ebc17 100644 --- a/src/atom.c +++ b/src/atom.c @@ -1,37 +1,166 @@ #include +#include #include #include "atom.h" +#include "cache.h" #include "common.h" -#include "utils.h" +#include "compiler.h" #include "log.h" +#include "utils.h" + +struct atom_entry { + struct cache_handle entry; + UT_hash_handle hh; + xcb_atom_t atom; +}; -static inline void *atom_getter(void *ud, const char *atom_name, int *err) { - xcb_connection_t *c = ud; +struct atom_impl { + struct atom base; + struct cache c; + struct atom_entry *atom_to_name; + cache_getter_t getter; +}; + +static inline int atom_getter(struct cache *cache, const char *atom_name, size_t keylen, + struct cache_handle **value, void *user_data) { + xcb_connection_t *c = user_data; + auto atoms = container_of(cache, struct atom_impl, c); xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply( - c, xcb_intern_atom(c, 0, to_u16_checked(strlen(atom_name)), atom_name), NULL); + c, xcb_intern_atom(c, 0, to_u16_checked(keylen), atom_name), NULL); xcb_atom_t atom = XCB_NONE; if (reply) { - log_debug("Atom %s is %d", atom_name, reply->atom); + log_debug("Atom %.*s is %d", (int)keylen, atom_name, reply->atom); atom = reply->atom; free(reply); } else { log_error("Failed to intern atoms"); - *err = 1; + return -1; + } + + struct atom_entry *entry = ccalloc(1, struct atom_entry); + entry->atom = atom; + HASH_ADD_INT(atoms->atom_to_name, atom, entry); + *value = &entry->entry; + return 0; +} + +static inline int +known_atom_getter(struct cache *cache attr_unused, const char *atom_name attr_unused, + size_t keylen attr_unused, struct cache_handle **value, void *user_data) { + auto atom = *(xcb_atom_t *)user_data; + struct atom_entry *entry = ccalloc(1, struct atom_entry); + entry->atom = atom; + *value = &entry->entry; + return 0; +} + +static inline void atom_entry_free(struct cache *cache, struct cache_handle *handle) { + auto entry = cache_entry(handle, struct atom_entry, entry); + auto atoms = container_of(cache, struct atom_impl, c); + HASH_DEL(atoms->atom_to_name, entry); + free(entry); +} + +xcb_atom_t get_atom(struct atom *a, const char *key, size_t keylen, xcb_connection_t *c) { + struct cache_handle *entry = NULL; + auto atoms = container_of(a, struct atom_impl, base); + if (cache_get_or_fetch(&atoms->c, key, keylen, &entry, c, atoms->getter) < 0) { + log_error("Failed to get atom %s", key); + return XCB_NONE; + } + return cache_entry(entry, struct atom_entry, entry)->atom; +} + +xcb_atom_t get_atom_cached(struct atom *a, const char *key, size_t keylen) { + auto atoms = container_of(a, struct atom_impl, base); + auto entry = cache_get(&atoms->c, key, keylen); + if (!entry) { + return XCB_NONE; + } + return cache_entry(entry, struct atom_entry, entry)->atom; +} + +const char *get_atom_name(struct atom *a, xcb_atom_t atom, xcb_connection_t *c) { + struct atom_entry *entry = NULL; + auto atoms = container_of(a, struct atom_impl, base); + HASH_FIND(hh, atoms->atom_to_name, &atom, sizeof(xcb_atom_t), entry); + if (!entry) { + BUG_ON(c == NULL); + auto r = xcb_get_atom_name_reply(c, xcb_get_atom_name(c, atom), NULL); + if (!r) { + log_error("Failed to get atom name"); + return NULL; + } + char *atom_name = xcb_get_atom_name_name(r); + auto len = (size_t)xcb_get_atom_name_name_length(r); + struct cache_handle *handle = NULL; + cache_get_or_fetch(&atoms->c, atom_name, len, &handle, &atom, known_atom_getter); + entry = cache_entry(handle, struct atom_entry, entry); + HASH_ADD_INT(atoms->atom_to_name, atom, entry); + free(r); + } + return entry->entry.key; +} + +const char *get_atom_name_cached(struct atom *a, xcb_atom_t atom) { + struct atom_entry *entry = NULL; + auto atoms = container_of(a, struct atom_impl, base); + HASH_FIND(hh, atoms->atom_to_name, &atom, sizeof(xcb_atom_t), entry); + if (!entry) { + return NULL; } - return (void *)(intptr_t)atom; + return entry->entry.key; +} + +static inline struct atom *init_atoms_impl(xcb_connection_t *c, cache_getter_t getter) { + auto atoms = ccalloc(1, struct atom_impl); + atoms->c = CACHE_INIT; + atoms->getter = getter; +#define ATOM_GET(x) atoms->base.a##x = get_atom(&atoms->base, #x, sizeof(#x) - 1, c) + LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST1); + LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST2); +#undef ATOM_GET + return &atoms->base; } /** * Create a new atom structure and fetch all predefined atoms */ struct atom *init_atoms(xcb_connection_t *c) { - auto atoms = ccalloc(1, struct atom); - atoms->c = new_cache((void *)c, atom_getter, NULL); -#define ATOM_GET(x) atoms->a##x = (xcb_atom_t)(intptr_t)cache_get(atoms->c, #x, NULL) - LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST1); - LIST_APPLY(ATOM_GET, SEP_COLON, ATOM_LIST2); -#undef ATOM_GET - return atoms; + return init_atoms_impl(c, atom_getter); +} + +void destroy_atoms(struct atom *a) { + auto atoms = container_of(a, struct atom_impl, base); + cache_invalidate_all(&atoms->c, atom_entry_free); + assert(atoms->atom_to_name == NULL); + free(a); +} + +#if defined(UNIT_TEST) || defined(CONFIG_FUZZER) + +static inline int mock_atom_getter(struct cache *cache, const char *atom_name attr_unused, + size_t atom_len attr_unused, struct cache_handle **value, + void *user_data attr_unused) { + auto atoms = container_of(cache, struct atom_impl, c); + xcb_atom_t atom = (xcb_atom_t)HASH_COUNT(atoms->atom_to_name) + 1; + struct atom_entry *entry = ccalloc(1, struct atom_entry); + entry->atom = atom; + HASH_ADD_INT(atoms->atom_to_name, atom, entry); + *value = &entry->entry; + return 0; +} + +struct atom *init_mock_atoms(void) { + return init_atoms_impl(NULL, mock_atom_getter); } + +#else + +struct atom *init_mock_atoms(void) { + abort(); +} + +#endif \ No newline at end of file diff --git a/src/atom.h b/src/atom.h index 6f4eae69f7..a24dcd4425 100644 --- a/src/atom.h +++ b/src/atom.h @@ -1,10 +1,8 @@ #pragma once -#include - #include -#include "meta.h" #include "cache.h" +#include "meta.h" // clang-format off // Splitted into 2 lists because of the limitation of our macros @@ -23,7 +21,11 @@ WM_CLIENT_MACHINE, \ _NET_ACTIVE_WINDOW, \ _COMPTON_SHADOW, \ - _NET_WM_WINDOW_TYPE + COMPTON_VERSION, \ + _NET_WM_WINDOW_TYPE, \ + _XROOTPMAP_ID, \ + ESETROOT_PMAP_ID, \ + _XSETROOT_ID #define ATOM_LIST2 \ _NET_WM_WINDOW_TYPE_DESKTOP, \ @@ -49,19 +51,32 @@ #define ATOM_DEF(x) xcb_atom_t a##x +struct atom_entry; struct atom { - struct cache *c; LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST1); LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST2); }; -struct atom *init_atoms(xcb_connection_t *); +/// Create a new atom object with a xcb connection. `struct atom` does not hold +/// a reference to the connection. +struct atom *init_atoms(xcb_connection_t *c); -static inline xcb_atom_t get_atom(struct atom *a, const char *key) { - return (xcb_atom_t)(intptr_t)cache_get(a->c, key, NULL); +xcb_atom_t get_atom(struct atom *a, const char *key, size_t keylen, xcb_connection_t *c); +static inline xcb_atom_t +get_atom_with_nul(struct atom *a, const char *key, xcb_connection_t *c) { + return get_atom(a, key, strlen(key), c); } - -static inline void destroy_atoms(struct atom *a) { - cache_free(a->c); - free(a); +xcb_atom_t get_atom_cached(struct atom *a, const char *key, size_t keylen); +static inline xcb_atom_t get_atom_cached_with_nul(struct atom *a, const char *key) { + return get_atom_cached(a, key, strlen(key)); } +const char *get_atom_name(struct atom *a, xcb_atom_t, xcb_connection_t *c); +const char *get_atom_name_cached(struct atom *a, xcb_atom_t atom); + +void destroy_atoms(struct atom *a); + +/// A mock atom object for unit testing. Successive calls to get_atom will return +/// secutive integers as atoms, starting from 1. Calling get_atom_name with atoms +/// previously seen will result in the string that was used to create the atom; if +/// the atom was never returned by get_atom, it will abort. +struct atom *init_mock_atoms(void); \ No newline at end of file diff --git a/src/backend/backend.c b/src/backend/backend.c index 42ce7b2e6a..3580faa17b 100644 --- a/src/backend/backend.c +++ b/src/backend/backend.c @@ -16,6 +16,7 @@ extern struct backend_operations xrender_ops, dummy_ops; #ifdef CONFIG_OPENGL extern struct backend_operations glx_ops; +extern struct backend_operations egl_ops; #endif struct backend_operations *backend_list[NUM_BKEND] = { @@ -23,426 +24,120 @@ struct backend_operations *backend_list[NUM_BKEND] = { [BKEND_DUMMY] = &dummy_ops, #ifdef CONFIG_OPENGL [BKEND_GLX] = &glx_ops, + [BKEND_EGL] = &egl_ops, #endif }; -/** - * @param all_damage if true ignore damage and repaint the whole screen - */ -region_t get_damage(session_t *ps, bool all_damage) { - region_t region; - auto buffer_age_fn = ps->backend_data->ops->buffer_age; - int buffer_age = buffer_age_fn ? buffer_age_fn(ps->backend_data) : -1; - - if (all_damage) { - buffer_age = -1; - } - - pixman_region32_init(®ion); - if (buffer_age == -1 || buffer_age > ps->ndamage) { - pixman_region32_copy(®ion, &ps->screen_reg); - } else { - for (int i = 0; i < buffer_age; i++) { - auto curr = ((ps->damage - ps->damage_ring) + i) % ps->ndamage; - log_trace("damage index: %d, damage ring offset: %ld", i, curr); - dump_region(&ps->damage_ring[curr]); - pixman_region32_union(®ion, ®ion, &ps->damage_ring[curr]); - } - pixman_region32_intersect(®ion, ®ion, &ps->screen_reg); - } - return region; -} - -/// paint all windows -void paint_all_new(session_t *ps, struct managed_win *t, bool ignore_damage) { - if (ps->o.xrender_sync_fence) { - if (ps->xsync_exists && !x_fence_sync(ps->c, ps->sync_fence)) { - log_error("x_fence_sync failed, xrender-sync-fence will be " - "disabled from now on."); - xcb_sync_destroy_fence(ps->c, ps->sync_fence); - ps->sync_fence = XCB_NONE; - ps->o.xrender_sync_fence = false; - ps->xsync_exists = false; - } - } - // All painting will be limited to the damage, if _some_ of - // the paints bleed out of the damage region, it will destroy - // part of the image we want to reuse - region_t reg_damage; - if (!ignore_damage) { - reg_damage = get_damage(ps, ps->o.monitor_repaint || !ps->o.use_damage); - } else { - pixman_region32_init(®_damage); - pixman_region32_copy(®_damage, &ps->screen_reg); - } - - if (!pixman_region32_not_empty(®_damage)) { - pixman_region32_fini(®_damage); - return; - } - -#ifdef DEBUG_REPAINT - static struct timespec last_paint = {0}; -#endif - - // - // If use_damage is enabled, we MUST make sure only the damaged regions of the - // screen are ever touched by the compositor. The reason is that at the beginning - // of each render, we clear the damaged regions with the wallpaper, and nothing - // else. If later during the render we changed anything outside the damaged - // region, that won't be cleared by the next render, and will thus accumulate. - // (e.g. if shadow is drawn outside the damaged region, it will become thicker and - // thicker over time.) - - /// The adjusted damaged regions - region_t reg_paint; - assert(ps->o.blur_method != BLUR_METHOD_INVALID); - if (ps->o.blur_method != BLUR_METHOD_NONE && ps->backend_data->ops->get_blur_size) { - int blur_width, blur_height; - ps->backend_data->ops->get_blur_size(ps->backend_blur_context, - &blur_width, &blur_height); - - // The region of screen a given window influences will be smeared - // out by blur. With more windows on top of the given window, the - // influences region will be smeared out more. - // - // Also, blurring requires data slightly outside the area that needs - // to be blurred. The more semi-transparent windows are stacked on top - // of each other, the larger the area will be. - // - // Instead of accurately calculate how much bigger the damage - // region will be because of blur, we assume the worst case here. - // That is, the damaged window is at the bottom of the stack, and - // all other windows have semi-transparent background - int resize_factor = 1; - if (t) { - resize_factor = t->stacking_rank; - } - resize_region_in_place(®_damage, blur_width * resize_factor, - blur_height * resize_factor); - reg_paint = resize_region(®_damage, blur_width * resize_factor, - blur_height * resize_factor); - pixman_region32_intersect(®_paint, ®_paint, &ps->screen_reg); - pixman_region32_intersect(®_damage, ®_damage, &ps->screen_reg); - } else { - pixman_region32_init(®_paint); - pixman_region32_copy(®_paint, ®_damage); - } - - // A hint to backend, the region that will be visible on screen - // backend can optimize based on this info - region_t reg_visible; - pixman_region32_init(®_visible); - pixman_region32_copy(®_visible, &ps->screen_reg); - if (t && !ps->o.transparent_clipping) { - // Calculate the region upon which the root window (wallpaper) is to be - // painted based on the ignore region of the lowest window, if available - // - // NOTE If transparent_clipping is enabled, transparent windows are - // included in the reg_ignore, but we still want to have the wallpaper - // beneath them, so we don't use reg_ignore for wallpaper in that case. - pixman_region32_subtract(®_visible, ®_visible, t->reg_ignore); - } - - // Region on screen we don't want any shadows on - region_t reg_shadow_clip; - pixman_region32_init(®_shadow_clip); - - if (ps->backend_data->ops->prepare) { - ps->backend_data->ops->prepare(ps->backend_data, ®_paint); - } - - if (ps->root_image) { - ps->backend_data->ops->compose(ps->backend_data, ps->root_image, 0, 0, - ®_paint, ®_visible); - } else { - ps->backend_data->ops->fill(ps->backend_data, (struct color){0, 0, 0, 1}, - ®_paint); - } - - // Windows are sorted from bottom to top - // Each window has a reg_ignore, which is the region obscured by all the windows - // on top of that window. This is used to reduce the number of pixels painted. +void handle_device_reset(session_t *ps) { + log_error("Device reset detected"); + // Wait for reset to complete + // Although ideally the backend should return DEVICE_STATUS_NORMAL after a reset + // is completed, it's not always possible. // - // Whether this is beneficial is to be determined XXX - for (auto w = t; w; w = w->prev_trans) { - pixman_region32_subtract(®_visible, &ps->screen_reg, w->reg_ignore); - assert(!(w->flags & WIN_FLAGS_IMAGE_ERROR)); - assert(!(w->flags & WIN_FLAGS_PIXMAP_STALE)); - assert(!(w->flags & WIN_FLAGS_PIXMAP_NONE)); - - // The bounding shape of the window, in global/target coordinates - // reminder: bounding shape contains the WM frame - auto reg_bound = win_get_bounding_shape_global_by_val(w); - - // The clip region for the current window, in global/target coordinates - // reg_paint_in_bound \in reg_paint - region_t reg_paint_in_bound; - pixman_region32_init(®_paint_in_bound); - pixman_region32_intersect(®_paint_in_bound, ®_bound, ®_paint); - if (ps->o.transparent_clipping) { - // - // If transparent_clipping is enabled, we need to be SURE that - // things are not drawn inside reg_ignore, because otherwise they - // will appear underneath transparent windows. - // So here we have make sure reg_paint_in_bound \in reg_visible - // There are a few other places below where this is needed as - // well. - pixman_region32_intersect(®_paint_in_bound, - ®_paint_in_bound, ®_visible); - } - - // Blur window background - /* TODO(yshui) since the backend might change the content of the window - * (e.g. with shaders), we should consult the backend whether the window - * is transparent or not. for now we will just rely on the force_win_blend - * option */ - auto real_win_mode = w->mode; - - if (w->blur_background && - (ps->o.force_win_blend || real_win_mode == WMODE_TRANS || - (ps->o.blur_background_frame && real_win_mode == WMODE_FRAME_TRANS))) { - // Minimize the region we try to blur, if the window - // itself is not opaque, only the frame is. - - double blur_opacity = 1; - if (w->opacity < (1.0 / MAX_ALPHA)) { - // Hide blur for fully transparent windows. - blur_opacity = 0; - } else if (w->state == WSTATE_MAPPING) { - // Gradually increase the blur intensity during - // fading in. - assert(w->opacity <= w->opacity_target); - blur_opacity = w->opacity / w->opacity_target; - } else if (w->state == WSTATE_UNMAPPING || - w->state == WSTATE_DESTROYING) { - // Gradually decrease the blur intensity during - // fading out. - assert(w->opacity <= w->opacity_target_old); - blur_opacity = w->opacity / w->opacity_target_old; - } else if (w->state == WSTATE_FADING) { - if (w->opacity < w->opacity_target && - w->opacity_target_old < (1.0 / MAX_ALPHA)) { - // Gradually increase the blur intensity during - // fading in. - assert(w->opacity <= w->opacity_target); - blur_opacity = w->opacity / w->opacity_target; - } else if (w->opacity > w->opacity_target && - w->opacity_target < (1.0 / MAX_ALPHA)) { - // Gradually decrease the blur intensity during - // fading out. - assert(w->opacity <= w->opacity_target_old); - blur_opacity = w->opacity / w->opacity_target_old; - } - } - assert(blur_opacity >= 0 && blur_opacity <= 1); - - if (real_win_mode == WMODE_TRANS || ps->o.force_win_blend) { - // We need to blur the bounding shape of the window - // (reg_paint_in_bound = reg_bound \cap reg_paint) - ps->backend_data->ops->blur( - ps->backend_data, blur_opacity, ps->backend_blur_context, - ®_paint_in_bound, ®_visible); - } else { - // Window itself is solid, we only need to blur the frame - // region - - // Readability assertions - assert(ps->o.blur_background_frame); - assert(real_win_mode == WMODE_FRAME_TRANS); - - auto reg_blur = win_get_region_frame_local_by_val(w); - pixman_region32_translate(®_blur, w->g.x, w->g.y); - // make sure reg_blur \in reg_paint - pixman_region32_intersect(®_blur, ®_blur, ®_paint); - if (ps->o.transparent_clipping) { - // ref: - pixman_region32_intersect(®_blur, ®_blur, - ®_visible); - } - ps->backend_data->ops->blur(ps->backend_data, blur_opacity, - ps->backend_blur_context, - ®_blur, ®_visible); - pixman_region32_fini(®_blur); - } - } - - // Draw shadow on target - if (w->shadow) { - assert(!(w->flags & WIN_FLAGS_SHADOW_NONE)); - // Clip region for the shadow - // reg_shadow \in reg_paint - auto reg_shadow = win_extents_by_val(w); - pixman_region32_intersect(®_shadow, ®_shadow, ®_paint); - if (!ps->o.wintype_option[w->window_type].full_shadow) { - pixman_region32_subtract(®_shadow, ®_shadow, ®_bound); - } - - // Mask out the region we don't want shadow on - if (pixman_region32_not_empty(&ps->shadow_exclude_reg)) { - pixman_region32_subtract(®_shadow, ®_shadow, - &ps->shadow_exclude_reg); - } - if (pixman_region32_not_empty(®_shadow_clip)) { - pixman_region32_subtract(®_shadow, ®_shadow, - ®_shadow_clip); - } + // According to ARB_robustness (emphasis mine): + // + // "If a reset status other than NO_ERROR is returned and subsequent + // calls return NO_ERROR, the context reset was encountered and + // completed. If a reset status is repeatedly returned, the context **may** + // be in the process of resetting." + // + // Which means it may also not be in the process of resetting. For example on + // AMDGPU devices, Mesa OpenGL always return CONTEXT_RESET after a reset has + // started, completed or not. + // + // So here we blindly wait 5 seconds and hope ourselves best of the luck. + sleep(5); - if (ps->o.xinerama_shadow_crop && w->xinerama_scr >= 0 && - w->xinerama_scr < ps->xinerama_nscrs) { - // There can be a window where number of screens is - // updated, but the screen number attached to the windows - // have not. - // - // Window screen number will be updated eventually, so - // here we just check to make sure we don't access out of - // bounds. - pixman_region32_intersect( - ®_shadow, ®_shadow, - &ps->xinerama_scr_regs[w->xinerama_scr]); - } + // Reset picom + log_info("Resetting picom after device reset"); + ev_break(ps->loop, EVBREAK_ALL); +} - if (ps->o.transparent_clipping) { - // ref: - pixman_region32_intersect(®_shadow, ®_shadow, - ®_visible); +/// Execute a list of backend commands on the backend +/// @param target the image to render into +/// @param root_image the image containing the desktop background +bool backend_execute(struct backend_base *backend, image_handle target, unsigned ncmds, + const struct backend_command cmds[ncmds]) { + bool succeeded = true; + for (auto cmd = &cmds[0]; succeeded && cmd != &cmds[ncmds]; cmd++) { + switch (cmd->op) { + case BACKEND_COMMAND_BLIT: + if (!pixman_region32_not_empty(cmd->blit.target_mask)) { + continue; } - - assert(w->shadow_image); - ps->backend_data->ops->set_image_property( - ps->backend_data, IMAGE_PROPERTY_OPACITY, w->shadow_image, - &w->opacity); - ps->backend_data->ops->compose( - ps->backend_data, w->shadow_image, w->g.x + w->shadow_dx, - w->g.y + w->shadow_dy, ®_shadow, ®_visible); - pixman_region32_fini(®_shadow); - } - - // Update image properties - { - double dim_opacity = 0.0; - if (w->dim) { - dim_opacity = ps->o.inactive_dim; - if (!ps->o.inactive_dim_fixed) { - dim_opacity *= w->opacity; - } + succeeded = + backend->ops->blit(backend, cmd->origin, target, &cmd->blit); + break; + case BACKEND_COMMAND_COPY_AREA: + if (!pixman_region32_not_empty(cmd->copy_area.region)) { + continue; } - - ps->backend_data->ops->set_image_property( - ps->backend_data, IMAGE_PROPERTY_MAX_BRIGHTNESS, w->win_image, - &ps->o.max_brightness); - ps->backend_data->ops->set_image_property( - ps->backend_data, IMAGE_PROPERTY_INVERTED, w->win_image, - &w->invert_color); - ps->backend_data->ops->set_image_property( - ps->backend_data, IMAGE_PROPERTY_DIM_LEVEL, w->win_image, - &dim_opacity); - ps->backend_data->ops->set_image_property( - ps->backend_data, IMAGE_PROPERTY_OPACITY, w->win_image, &w->opacity); - } - - if (w->opacity * MAX_ALPHA < 1) { - // We don't need to paint the window body itself if it's - // completely transparent. - goto skip; - } - - if (w->clip_shadow_above) { - // Add window bounds to shadow-clip region - pixman_region32_union(®_shadow_clip, ®_shadow_clip, ®_bound); - } else { - // Remove overlapping window bounds from shadow-clip region - pixman_region32_subtract(®_shadow_clip, ®_shadow_clip, ®_bound); - } - - // Draw window on target - if (w->frame_opacity == 1) { - ps->backend_data->ops->compose(ps->backend_data, w->win_image, - w->g.x, w->g.y, - ®_paint_in_bound, ®_visible); - } else { - // For window image processing, we don't have to limit the process - // region to damage for correctness. (see for - // details) - - // The visible region, in window local coordinates Although we - // don't limit process region to damage, we provide that info in - // reg_visible as a hint. Since window image data outside of the - // damage region won't be painted onto target - region_t reg_visible_local; - { - // The bounding shape, in window local coordinates - region_t reg_bound_local; - pixman_region32_init(®_bound_local); - pixman_region32_copy(®_bound_local, ®_bound); - pixman_region32_translate(®_bound_local, -w->g.x, -w->g.y); - - pixman_region32_init(®_visible_local); - pixman_region32_intersect(®_visible_local, - ®_visible, ®_paint); - pixman_region32_translate(®_visible_local, -w->g.x, - -w->g.y); - // Data outside of the bounding shape won't be visible, - // but it is not necessary to limit the image operations - // to the bounding shape yet. So pass that as the visible - // region, not the clip region. - pixman_region32_intersect( - ®_visible_local, ®_visible_local, ®_bound_local); - pixman_region32_fini(®_bound_local); + succeeded = backend->ops->copy_area(backend, cmd->origin, target, + cmd->copy_area.source_image, + cmd->copy_area.region); + break; + case BACKEND_COMMAND_BLUR: + if (!pixman_region32_not_empty(cmd->blur.target_mask)) { + continue; } - - auto new_img = ps->backend_data->ops->clone_image( - ps->backend_data, w->win_image, ®_visible_local); - auto reg_frame = win_get_region_frame_local_by_val(w); - ps->backend_data->ops->image_op( - ps->backend_data, IMAGE_OP_APPLY_ALPHA, new_img, ®_frame, - ®_visible_local, (double[]){w->frame_opacity}); - pixman_region32_fini(®_frame); - ps->backend_data->ops->compose(ps->backend_data, new_img, w->g.x, - w->g.y, ®_paint_in_bound, - ®_visible); - ps->backend_data->ops->release_image(ps->backend_data, new_img); - pixman_region32_fini(®_visible_local); + succeeded = + backend->ops->blur(backend, cmd->origin, target, &cmd->blur); + break; + case BACKEND_COMMAND_INVALID: + default: assert(false); } - skip: - pixman_region32_fini(®_bound); - pixman_region32_fini(®_paint_in_bound); } - pixman_region32_fini(®_paint); - pixman_region32_fini(®_shadow_clip); + return succeeded; +} - if (ps->o.monitor_repaint) { - const struct color DEBUG_COLOR = {0.5, 0, 0, 0.5}; - auto reg_damage_debug = get_damage(ps, false); - ps->backend_data->ops->fill(ps->backend_data, DEBUG_COLOR, ®_damage_debug); - pixman_region32_fini(®_damage_debug); +static inline const char *render_command_source_name(enum backend_command_source source) { + switch (source) { + case BACKEND_COMMAND_SOURCE_WINDOW: return "window"; + case BACKEND_COMMAND_SOURCE_SHADOW: return "shadow"; + case BACKEND_COMMAND_SOURCE_BACKGROUND: return "background"; } + unreachable(); +} - // Move the head of the damage ring - ps->damage = ps->damage - 1; - if (ps->damage < ps->damage_ring) { - ps->damage = ps->damage_ring + ps->ndamage - 1; +void log_backend_command_(enum log_level level, const char *func, + const struct backend_command *cmd) { + if (level < log_get_level_tls()) { + return; } - pixman_region32_clear(ps->damage); - if (ps->backend_data->ops->present) { - // Present the rendered scene - // Vsync is done here - ps->backend_data->ops->present(ps->backend_data, ®_damage); + log_printf(tls_logger, level, func, "Render command: %p", cmd); + switch (cmd->op) { + case BACKEND_COMMAND_BLIT: + log_printf(tls_logger, level, func, "blit %s%s", + render_command_source_name(cmd->source), + cmd->blit.source_mask != NULL ? ", with mask image" : ""); + log_printf(tls_logger, level, func, "origin: %d,%d", cmd->origin.x, + cmd->origin.y); + log_printf(tls_logger, level, func, "mask region:"); + log_region_(level, func, cmd->blit.target_mask); + log_printf(tls_logger, level, func, "opaque region:"); + log_region_(level, func, &cmd->opaque_region); + break; + case BACKEND_COMMAND_COPY_AREA: + log_printf(tls_logger, level, func, "copy area from %s", + render_command_source_name(cmd->source)); + log_printf(tls_logger, level, func, "origin: %d,%d", cmd->origin.x, + cmd->origin.y); + log_printf(tls_logger, level, func, "region:"); + log_region_(level, func, cmd->copy_area.region); + break; + case BACKEND_COMMAND_BLUR: + log_printf(tls_logger, level, func, "blur%s", + cmd->blur.source_mask != NULL ? ", with mask image" : ""); + log_printf(tls_logger, level, func, "origin: %d,%d", cmd->origin.x, + cmd->origin.y); + log_printf(tls_logger, level, func, "mask region:"); + log_region_(level, func, cmd->blur.target_mask); + break; + case BACKEND_COMMAND_INVALID: + log_printf(tls_logger, level, func, "invalid"); + break; } - - pixman_region32_fini(®_damage); - -#ifdef DEBUG_REPAINT - struct timespec now = get_time_timespec(); - struct timespec diff = {0}; - timespec_subtract(&diff, &now, &last_paint); - log_trace("[ %5ld:%09ld ] ", diff.tv_sec, diff.tv_nsec); - last_paint = now; - log_trace("paint:"); - for (win *w = t; w; w = w->prev_trans) - log_trace(" %#010lx", w->id); -#endif } // vim: set noet sw=8 ts=8 : diff --git a/src/backend/backend.h b/src/backend/backend.h index 775f570990..10bcd14332 100644 --- a/src/backend/backend.h +++ b/src/backend/backend.h @@ -21,8 +21,7 @@ struct backend_operations; typedef struct backend_base { struct backend_operations *ops; - xcb_connection_t *c; - xcb_window_t root; + struct x_connection *c; struct ev_loop *loop; /// Whether the backend can accept new render request at the moment @@ -32,33 +31,19 @@ typedef struct backend_base { typedef void (*backend_ready_callback_t)(void *); -// When image properties are actually applied to the image, they are applied in a -// particular order: -// -// Color inversion -> Dimming -> Opacity multiply -> Limit maximum brightness -enum image_properties { - // Whether the color of the image is inverted - // 1 boolean, default: false - IMAGE_PROPERTY_INVERTED, - // How much the image is dimmed - // 1 double, default: 0 - IMAGE_PROPERTY_DIM_LEVEL, - // Image opacity, i.e. an alpha value multiplied to the alpha channel - // 1 double, default: 1 - IMAGE_PROPERTY_OPACITY, - // The effective size of the image, the image will be tiled to fit. - // 2 int, default: the actual size of the image - IMAGE_PROPERTY_EFFECTIVE_SIZE, - // Limit how bright image can be. The image brightness is estimated by averaging - // the pixels in the image, and dimming will be applied to scale the average - // brightness down to the max brightness value. - // 1 double, default: 1 - IMAGE_PROPERTY_MAX_BRIGHTNESS, +// This mimics OpenGL's ARB_robustness extension, which enables detection of GPU context +// resets. +// See: https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_robustness.txt, section +// 2.6 "Graphics Reset Recovery". +enum device_status { + DEVICE_STATUS_NORMAL, + DEVICE_STATUS_RESETTING, }; -enum image_operations { - // Multiply the alpha channel by the argument - IMAGE_OP_APPLY_ALPHA, +enum shader_attributes { + // Whether the shader needs to be render regardless of whether the window is + // updated. + SHADER_ATTRIBUTE_ANIMATED = 1, }; struct gaussian_blur_args { @@ -80,15 +65,156 @@ struct dual_kawase_blur_args { int strength; }; +typedef struct image_handle { + // Intentionally left blank +} *image_handle; + +/// A mask for various backend operations. +/// +/// The mask is composed of both a mask region and a mask image. The resulting mask +/// is the intersection of the two. The mask image can be modified by the `corner_radius` +/// and `inverted` properties. Note these properties have no effect on the mask region. +struct backend_mask_image { + /// Mask image, can be NULL. + /// + /// Mask image must be an image that was created with the + /// `BACKEND_IMAGE_FORMAT_MASK` format. Using an image with a wrong format as mask + /// is undefined behavior. + image_handle image; + /// Corner radius of the mask image, the corners of the mask image will be + /// rounded. + double corner_radius; + /// Origin of the mask image, in the source image's coordinate. + ivec2 origin; + /// Whether the mask image should be inverted. + bool inverted; +}; + +struct backend_blur_args { + /// The blur context + void *blur_context; + /// The source mask for the blur operation, may be NULL. Only parts of the source + /// image covered by the mask should participate in the blur operation. + const struct backend_mask_image *source_mask; + /// Region of the target image that will be covered by the blur operation, in the + /// source image's coordinate. + const region_t *target_mask; + /// Source image + image_handle source_image; + /// Opacity of the blurred image + double opacity; +}; + +struct backend_blit_args { + /// Source image, can be NULL. + image_handle source_image; + /// Mask for the source image. may be NULL. Only contents covered by the mask + /// should participate in the blit operation. This applies to the source image + /// before it's scaled. + const struct backend_mask_image *source_mask; + /// Mask for the target image. Only regions of the target image covered by this + /// mask should be modified. This is the target's coordinate system. + const region_t *target_mask; + /// Custom shader for this blit operation. + void *shader; + /// Opacity of the source image. + double opacity; + /// Dim level of the source image. + double dim; + /// Brightness limit of the source image. Source image + /// will be normalized so that the maximum brightness is + /// this value. + double max_brightness; + /// Corner radius of the source image. The corners of + /// the source image will be rounded. + double corner_radius; + /// Effective size of the source image, set where the corners + /// of the image are. + ivec2 effective_size; + /// Border width of the source image. This is used with + /// `corner_radius` to create a border for the rounded corners. + /// Setting this has no effect if `corner_radius` is 0. + int border_width; + /// Whether the source image should be inverted. + bool color_inverted; +}; + +enum backend_image_format { + /// A format that can be used for normal rendering, and binding + /// X pixmaps. + /// Images created with `bind_pixmap` have this format automatically. + BACKEND_IMAGE_FORMAT_PIXMAP, + /// Like `BACKEND_IMAGE_FORMAT_PIXMAP`, but the image has a higher + /// precision. Support is optional. + BACKEND_IMAGE_FORMAT_PIXMAP_HIGH, + /// A format that can be used for masks. + BACKEND_IMAGE_FORMAT_MASK, +}; + +enum backend_image_capability { + /// Image can be sampled from. This is required for `blit` and `blur` source + /// images. All images except the back buffer should have this capability. + /// Note that `copy_area` should work without this capability, this is so that + /// blurring the back buffer could be done. + BACKEND_IMAGE_CAP_SRC = 1 << 0, + /// Image can be rendered to. This is required for target images of any operation. + /// All images except bound X pixmaps should have this capability. + BACKEND_IMAGE_CAP_DST = 1 << 1, +}; + +enum backend_command_op { + BACKEND_COMMAND_INVALID = -1, + BACKEND_COMMAND_BLIT, + BACKEND_COMMAND_BLUR, + BACKEND_COMMAND_COPY_AREA, +}; + +/// Symbolic references used as render command source images. The actual `image_handle` +/// will later be filled in by the renderer using this symbolic reference. +enum backend_command_source { + BACKEND_COMMAND_SOURCE_WINDOW, + BACKEND_COMMAND_SOURCE_SHADOW, + BACKEND_COMMAND_SOURCE_BACKGROUND, +}; + +// TODO(yshui) might need better names + +struct backend_command { + enum backend_command_op op; + ivec2 origin; + enum backend_command_source source; + union { + struct { + struct backend_blit_args blit; + /// Region of the screen that will be covered by this blit + /// operations, in screen coordinates. + region_t opaque_region; + }; + struct { + image_handle source_image; + const region_t *region; + } copy_area; + struct backend_blur_args blur; + }; + /// Source mask for the operation. + /// If the `source_mask` of the operation's argument points to this, a mask image + /// will be created for the operation for the renderer. + struct backend_mask_image source_mask; + /// Target mask for the operation. + region_t target_mask; +}; + +enum backend_quirk { + /// Backend cannot do blur quickly. The compositor will avoid using blur to create + /// shadows on this backend + BACKEND_QUIRK_SLOW_BLUR = 1 << 0, +}; + struct backend_operations { // =========== Initialization =========== /// Initialize the backend, prepare for rendering to the target window. - /// Here is how you should choose target window: - /// 1) if ps->overlay is not XCB_NONE, use that - /// 2) use ps->root otherwise - // TODO(yshui) make the target window a parameter - backend_t *(*init)(session_t *)attr_nonnull(1); + backend_t *(*init)(session_t *, xcb_window_t)attr_nonnull(1); void (*deinit)(backend_t *backend_data) attr_nonnull(1); /// Called when rendering will be stopped for an unknown amount of @@ -102,187 +228,231 @@ struct backend_operations { /// Optional, not yet used void (*resume)(backend_t *backend_data, session_t *ps); - /// Called when root property changed, returns the new - /// backend_data. Even if the backend_data changed, all - /// the existing image data returned by this backend should - /// remain valid. + /// Called when root window size changed. All existing image data ever + /// returned by this backend should remain valid after this call + /// returns. /// /// Optional - void *(*root_change)(backend_t *backend_data, session_t *ps); + void (*root_change)(backend_t *backend_data, session_t *ps); // =========== Rendering ============ - // NOTE: general idea about reg_paint/reg_op vs reg_visible is that reg_visible is - // merely a hint. Ignoring reg_visible entirely don't affect the correctness of - // the operation performed. OTOH reg_paint/reg_op is part of the parameters of the - // operation, and must be honored in order to complete the operation correctly. - - // NOTE: due to complications introduced by use-damage and blur, the rendering API - // is a bit weird. The idea is, `compose` and `blur` have to update a temporary - // buffer, because `blur` requires data from an area slightly larger than the area - // that will be visible. So the area outside the visible area has to be rendered, - // but we have to discard the result (because the result of blurring that area - // will be wrong). That's why we cannot render into the back buffer directly. - // After rendering is done, `present` is called to update a portion of the actual - // back buffer, then present it to the target (or update the target directly, - // if not back buffered). - /// Called before when a new frame starts. /// /// Optional void (*prepare)(backend_t *backend_data, const region_t *reg_damage); - /** - * Paint the content of an image onto the rendering buffer - * - * @param backend_data the backend data - * @param image_data the image to paint - * @param dst_x, dst_y the top left corner of the image in the target - * @param reg_paint the clip region, in target coordinates - * @param reg_visible the visible region, in target coordinates - */ - void (*compose)(backend_t *backend_data, void *image_data, int dst_x, int dst_y, - const region_t *reg_paint, const region_t *reg_visible); - - /// Fill rectangle of the rendering buffer, mostly for debug purposes, optional. - void (*fill)(backend_t *backend_data, struct color, const region_t *clip); - - /// Blur a given region of the rendering buffer. - bool (*blur)(backend_t *backend_data, double opacity, void *blur_ctx, - const region_t *reg_blur, const region_t *reg_visible) + /// Multiply the alpha channel of the target image by a given value. + /// + /// @param backend_data backend data + /// @param target an image handle, cannot be NULL. + /// @param alpha the alpha value to multiply + /// @param region the region to apply the alpha, in the target image's + /// coordinate. + bool (*apply_alpha)(struct backend_base *backend_data, image_handle target, + double alpha, const region_t *region) attr_nonnull(1, 2, 4); + + /// Copy pixels from a source image on to the target image. + /// + /// Some effects may be applied. If the region specified by the mask + /// contains parts that are outside the source image, the source image + /// will be repeated to fit. + /// + /// Source and target MUST NOT be the same image. + /// + /// @param backend_data backend data + /// @param origin the origin of the operation, in the target image's + /// coordinate. + /// @param target an image handle, cannot be NULL. + /// @param args arguments for blit + /// @return whether the operation is successful + bool (*blit)(struct backend_base *backend_data, ivec2 origin, image_handle target, + const struct backend_blit_args *args) attr_nonnull(1, 3, 4); + + /// Blur a given region of a source image and store the result in the + /// target image. + /// + /// The blur operation might access pixels outside the mask region, the + /// amount of pixels accessed can be queried with `get_blur_size`. If + /// pixels outside the source image are accessed, the result will be + /// clamped to the edge of the source image. + /// + /// Source and target may be the same image. + /// + /// @param backend_data backend data + /// @param origin the origin of the operation, in the target image's + /// coordinate. + /// @param target an image handle, cannot be NULL. + /// @param args argument for blur + /// @return whether the operation is successful + bool (*blur)(struct backend_base *backend_data, ivec2 origin, image_handle target, + const struct backend_blur_args *args) attr_nonnull(1, 3, 4); + + /// Direct copy of pixels from a source image on to the target image. + /// This is a simpler version of `blit`, without any effects. Note unlike `blit`, + /// if `region` tries to sample from outside the source image, instead of + /// repeating, the result will be clamped to the edge of the source image. + /// Blending should not be applied for the copy. + /// + /// Source and target MUST NOT be the same image. + /// + /// @param backend_data backend data + /// @param origin the origin of the operation, in the target image's + /// coordinate. + /// @param target an image handle, cannot be NULL. + /// @param source an image handle, cannot be NULL. + /// @param region the region to copy, in the target image's coordinate. + /// @return whether the operation is successful + bool (*copy_area)(struct backend_base *backend_data, ivec2 origin, + image_handle target, image_handle source, const region_t *region) attr_nonnull(1, 3, 4, 5); - /// Update part of the back buffer with the rendering buffer, then present the - /// back buffer onto the target window (if not back buffered, update part of the - /// target window directly). + /// Similar to `copy_area`, but is specialized for copying from a higher + /// precision format to a lower precision format. It has 2 major differences from + /// `copy_area`: /// - /// Optional, if NULL, indicates the backend doesn't have render output + /// 1. This function _may_ use dithering when copying from a higher precision + /// format to a lower precision format. But this is not required. + /// 2. This function only needs to support copying from an image with the SRC + /// capability. Unlike `copy_area`, which supports copying from any image. /// - /// @param region part of the target that should be updated - void (*present)(backend_t *backend_data, const region_t *region) attr_nonnull(1, 2); - - /** - * Bind a X pixmap to the backend's internal image data structure. - * - * @param backend_data backend data - * @param pixmap X pixmap to bind - * @param fmt information of the pixmap's visual - * @param owned whether the ownership of the pixmap is transfered to the backend - * @return backend internal data structure bound with this pixmap - */ - void *(*bind_pixmap)(backend_t *backend_data, xcb_pixmap_t pixmap, - struct xvisual_info fmt, bool owned); - - /// Create a shadow image based on the parameters - /// Default implementation: default_backend_render_shadow - void *(*render_shadow)(backend_t *backend_data, int width, int height, - const conv *kernel, double r, double g, double b, double a); + /// It's perfectly legal to have this pointing to the same function as + /// `copy_area`, if the backend doesn't support dithering. + /// + /// @param backend_data backend data + /// @param origin the origin of the operation, in the target image's + /// coordinate. + /// @param target an image handle, cannot be NULL. + /// @param source an image handle, cannot be NULL. + /// @param region the region to copy, in the target image's coordinate. + /// @return whether the operation is successful + bool (*copy_area_quantize)(struct backend_base *backend_data, ivec2 origin, + image_handle target, image_handle source, + const region_t *region) attr_nonnull(1, 3, 4, 5); + + /// Initialize an image with a given color value. If the image has a mask format, + /// only the alpha channel of the color is used. + /// + /// @param backend_data backend data + /// @param target an image handle, cannot be NULL. + /// @param color the color to fill the image with + /// @return whether the operation is successful + bool (*clear)(struct backend_base *backend_data, image_handle target, + struct color color) attr_nonnull(1, 2); + + /// Present the back buffer to the target window. Ideally the backend should keep + /// track of the region of the back buffer that has been updated, and use relevant + /// mechanism (when possible) to present only the updated region. + bool (*present)(struct backend_base *backend_data) attr_nonnull(1); // ============ Resource management =========== - /// Free resources associated with an image data structure - void (*release_image)(backend_t *backend_data, void *img_data) attr_nonnull(1, 2); + /// Create a shader object from a shader source. + /// + /// Optional + void *(*create_shader)(backend_t *backend_data, const char *source)attr_nonnull(1, 2); + + /// Free a shader object. + /// + /// Required if create_shader is present. + void (*destroy_shader)(backend_t *backend_data, void *shader) attr_nonnull(1, 2); + + /// Create a new, uninitialized image with the given format and size. + /// + /// @param backend_data backend data + /// @param format the format of the image + /// @param size the size of the image + image_handle (*new_image)(struct backend_base *backend_data, + enum backend_image_format format, ivec2 size) + attr_nonnull(1); + + /// Bind a X pixmap to the backend's internal image data structure. + /// + /// @param backend_data backend data + /// @param pixmap X pixmap to bind + /// @param fmt information of the pixmap's visual + /// @return backend specific image handle for the pixmap. May be + /// NULL. + image_handle (*bind_pixmap)(struct backend_base *backend_data, xcb_pixmap_t pixmap, + struct xvisual_info fmt) attr_nonnull(1); + + /// Acquire the image handle of the back buffer. + /// + /// @param backend_data backend data + image_handle (*back_buffer)(struct backend_base *backend_data); + + /// Free resources associated with an image data structure. Releasing the image + /// returned by `back_buffer` should be a no-op. + /// + /// @param image the image to be released, cannot be NULL. + /// @return if this image is created by `bind_pixmap`, the X pixmap; 0 + /// otherwise. + xcb_pixmap_t (*release_image)(struct backend_base *backend_data, image_handle image) + attr_nonnull(1, 2); // =========== Query =========== - /// Return if image is not completely opaque. + /// Get backend quirks + /// @return a bitmask of `enum backend_quirk`. + uint32_t (*quirks)(struct backend_base *backend_data) attr_nonnull(1); + + /// Check if an optional image format is supported by the backend. + bool (*is_format_supported)(struct backend_base *backend_data, + enum backend_image_format format) attr_nonnull(1); + + /// Return the capabilities of an image. + uint32_t (*image_capabilities)(struct backend_base *backend_data, image_handle image) + attr_nonnull(1, 2); + + /// Get the attributes of a shader. /// - /// This function is needed because some backend might change the content of the - /// window (e.g. when using a custom shader with the glx backend), so only the - /// backend knows if an image is transparent. - bool (*is_image_transparent)(backend_t *backend_data, void *image_data) + /// Optional, Returns a bitmask of attributes, see `shader_attributes`. + uint64_t (*get_shader_attributes)(backend_t *backend_data, void *shader) attr_nonnull(1, 2); - /// Get the age of the buffer content we are currently rendering ontop + /// Get the age of the buffer content we are currently rendering on top /// of. The buffer that has just been `present`ed has a buffer age of 1. - /// Everytime `present` is called, buffers get older. Return -1 if the + /// Every time `present` is called, buffers get older. Return -1 if the /// buffer is empty. /// /// Optional int (*buffer_age)(backend_t *backend_data); + /// Get the render time of the last frame. If the render is still in progress, + /// returns false. The time is returned in `ts`. Frames are delimited by the + /// present() calls. i.e. after a present() call, last_render_time() should start + /// reporting the time of the just presented frame. + /// + /// Optional, if not available, the most conservative estimation will be used. + bool (*last_render_time)(backend_t *backend_data, struct timespec *ts); + /// The maximum number buffer_age might return. int max_buffer_age; // =========== Post-processing ============ - - /* TODO(yshui) Consider preserving the order of image ops. - * Currently in both backends, the image ops are applied lazily when needed. - * However neither backends preserve the order of image ops, they just applied all - * pending lazy ops in a pre-determined fixed order, regardless in which order - * they were originally applied. This might lead to inconsistencies.*/ - - /** - * Change image properties - * - * @param backend_data backend data - * @param prop the property to change - * @param image_data an image data structure returned by the backend - * @param args property value - * @return whether the operation is successful - */ - bool (*set_image_property)(backend_t *backend_data, enum image_properties prop, - void *image_data, void *args); - - /** - * Manipulate an image. Image properties are untouched. - * - * @param backend_data backend data - * @param op the operation to perform - * @param image_data an image data structure returned by the backend - * @param reg_op the clip region, define the part of the image to be - * operated on. - * @param reg_visible define the part of the image that will eventually - * be visible on target. this is a hint to the backend - * for optimization purposes. - * @param args extra arguments, operation specific - * @return whether the operation is successful - */ - bool (*image_op)(backend_t *backend_data, enum image_operations op, void *image_data, - const region_t *reg_op, const region_t *reg_visible, void *args); - - /** - * Read the color of the pixel at given position of the given image. Image - * properties have no effect. - * - * @param backend_data backend_data - * @param image_data an image data structure previously returned by the - * backend. the image to read pixel from. - * @param x, y coordinate of the pixel to read - * @param[out] color the color of the pixel - * @return whether the operation is successful - */ - bool (*read_pixel)(backend_t *backend_data, void *image_data, int x, int y, - struct color *output); - - /// Create another instance of the `image_data`. All `image_op` and - /// `set_image_property` calls on the returned image should not affect the - /// original image - void *(*clone_image)(backend_t *base, const void *image_data, - const region_t *reg_visible); - - /// Create a blur context that can be used to call `blur` - void *(*create_blur_context)(backend_t *base, enum blur_method, void *args); + /// Create a blur context that can be used to call `blur` for images with a + /// specific format. + void *(*create_blur_context)(backend_t *base, enum blur_method, + enum backend_image_format format, void *args); /// Destroy a blur context void (*destroy_blur_context)(backend_t *base, void *ctx); /// Get how many pixels outside of the blur area is needed for blur void (*get_blur_size)(void *blur_context, int *width, int *height); - // =========== Hooks ============ - /// Let the backend hook into the event handling queue - /// Not implemented yet - void (*set_ready_callback)(backend_t *, backend_ready_callback_t cb); - /// Called right after the core has handled its events. - /// Not implemented yet - void (*handle_events)(backend_t *); // =========== Misc ============ /// Return the driver that is been used by the backend enum driver (*detect_driver)(backend_t *backend_data); void (*diagnostics)(backend_t *backend_data); + + enum device_status (*device_status)(backend_t *backend_data); }; extern struct backend_operations *backend_list[]; -void paint_all_new(session_t *ps, struct managed_win *const t, bool ignore_damage) - attr_nonnull(1); +bool backend_execute(struct backend_base *backend, image_handle target, unsigned ncmds, + const struct backend_command cmds[ncmds]); +void log_backend_command_(enum log_level level, const char *func, + const struct backend_command *cmd); +#define log_backend_command(level, cmd) \ + log_backend_command_(LOG_LEVEL_##level, __func__, &(cmd)); diff --git a/src/backend/backend_common.c b/src/backend/backend_common.c index c0377d3fc0..9ce7b05437 100644 --- a/src/backend/backend_common.c +++ b/src/backend/backend_common.c @@ -19,17 +19,18 @@ /** * Generate a 1x1 Picture of a particular color. */ -xcb_render_picture_t solid_picture(xcb_connection_t *c, xcb_drawable_t d, bool argb, - double a, double r, double g, double b) { +xcb_render_picture_t +solid_picture(struct x_connection *c, bool argb, double a, double r, double g, double b) { xcb_pixmap_t pixmap; xcb_render_picture_t picture; xcb_render_create_picture_value_list_t pa; xcb_render_color_t col; xcb_rectangle_t rect; - pixmap = x_create_pixmap(c, argb ? 32 : 8, d, 1, 1); - if (!pixmap) + pixmap = x_create_pixmap(c, argb ? 32 : 8, 1, 1); + if (!pixmap) { return XCB_NONE; + } pa.repeat = 1; picture = x_create_picture_with_standard_and_pixmap( @@ -37,7 +38,7 @@ xcb_render_picture_t solid_picture(xcb_connection_t *c, xcb_drawable_t d, bool a XCB_RENDER_CP_REPEAT, &pa); if (!picture) { - xcb_free_pixmap(c, pixmap); + xcb_free_pixmap(c->c, pixmap); return XCB_NONE; } @@ -51,14 +52,14 @@ xcb_render_picture_t solid_picture(xcb_connection_t *c, xcb_drawable_t d, bool a rect.width = 1; rect.height = 1; - xcb_render_fill_rectangles(c, XCB_RENDER_PICT_OP_SRC, picture, col, 1, &rect); - xcb_free_pixmap(c, pixmap); + xcb_render_fill_rectangles(c->c, XCB_RENDER_PICT_OP_SRC, picture, col, 1, &rect); + xcb_free_pixmap(c->c, pixmap); return picture; } -xcb_image_t * -make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, int height) { +xcb_image_t *make_shadow(struct x_connection *c, const conv *kernel, double opacity, + int width, int height) { /* * We classify shadows into 4 kinds of regions * r = shadow radius @@ -84,15 +85,16 @@ make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, assert(d % 2 == 1); assert(d > 0); - ximage = xcb_image_create_native(c, to_u16_checked(swidth), to_u16_checked(sheight), - XCB_IMAGE_FORMAT_Z_PIXMAP, 8, 0, 0, NULL); + ximage = + xcb_image_create_native(c->c, to_u16_checked(swidth), to_u16_checked(sheight), + XCB_IMAGE_FORMAT_Z_PIXMAP, 8, 0, 0, NULL); if (!ximage) { log_error("failed to create an X image"); return 0; } unsigned char *data = ximage->data; - long sstride = ximage->stride; + long long sstride = ximage->stride; // If the window body is smaller than the kernel, we do convolution directly if (width < r * 2 && height < r * 2) { @@ -100,7 +102,7 @@ make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, for (int x = 0; x < swidth; x++) { double sum = sum_kernel_normalized( kernel, d - x - 1, d - y - 1, width, height); - data[y * sstride + x] = (uint8_t)(sum * 255.0); + data[y * sstride + x] = (uint8_t)(sum * 255.0 * opacity); } } return ximage; @@ -118,14 +120,14 @@ make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, for (int x = 0; x < r * 2; x++) { double sum = sum_kernel_normalized(kernel, d - x - 1, d - y - 1, d, height) * - 255.0; + 255.0 * opacity; data[y * sstride + x] = (uint8_t)sum; data[y * sstride + swidth - x - 1] = (uint8_t)sum; } } for (int y = 0; y < sheight; y++) { - double sum = - sum_kernel_normalized(kernel, 0, d - y - 1, d, height) * 255.0; + double sum = sum_kernel_normalized(kernel, 0, d - y - 1, d, height) * + 255.0 * opacity; memset(&data[y * sstride + r * 2], (uint8_t)sum, (size_t)(width - 2 * r)); } @@ -137,14 +139,14 @@ make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, for (int x = 0; x < swidth; x++) { double sum = sum_kernel_normalized(kernel, d - x - 1, d - y - 1, width, d) * - 255.0; + 255.0 * opacity; data[y * sstride + x] = (uint8_t)sum; data[(sheight - y - 1) * sstride + x] = (uint8_t)sum; } } for (int x = 0; x < swidth; x++) { - double sum = - sum_kernel_normalized(kernel, d - x - 1, 0, width, d) * 255.0; + double sum = sum_kernel_normalized(kernel, d - x - 1, 0, width, d) * + 255.0 * opacity; for (int y = r * 2; y < height; y++) { data[y * sstride + x] = (uint8_t)sum; } @@ -193,7 +195,7 @@ make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, /** * Generate shadow Picture for a window. */ -bool build_shadow(xcb_connection_t *c, xcb_drawable_t d, double opacity, const int width, +bool build_shadow(struct x_connection *c, double opacity, const int width, const int height, const conv *kernel, xcb_render_picture_t shadow_pixel, xcb_pixmap_t *pixmap, xcb_render_picture_t *pict) { xcb_image_t *shadow_image = NULL; @@ -207,9 +209,9 @@ bool build_shadow(xcb_connection_t *c, xcb_drawable_t d, double opacity, const i return false; } - shadow_pixmap = x_create_pixmap(c, 8, d, shadow_image->width, shadow_image->height); + shadow_pixmap = x_create_pixmap(c, 8, shadow_image->width, shadow_image->height); shadow_pixmap_argb = - x_create_pixmap(c, 32, d, shadow_image->width, shadow_image->height); + x_create_pixmap(c, 32, shadow_image->width, shadow_image->height); if (!shadow_pixmap || !shadow_pixmap_argb) { log_error("Failed to create shadow pixmaps"); @@ -225,11 +227,11 @@ bool build_shadow(xcb_connection_t *c, xcb_drawable_t d, double opacity, const i } gc = x_new_id(c); - xcb_create_gc(c, gc, shadow_pixmap, 0, NULL); + xcb_create_gc(c->c, gc, shadow_pixmap, 0, NULL); // We need to make room for protocol metadata in the request. The metadata should // be 24 bytes plus padding, let's be generous and give it 1kb - auto maximum_image_size = xcb_get_maximum_request_length(c) * 4 - 1024; + auto maximum_image_size = xcb_get_maximum_request_length(c->c) * 4 - 1024; auto maximum_row = to_u16_checked(clamp(maximum_image_size / shadow_image->stride, 0, UINT16_MAX)); if (maximum_row <= 0) { @@ -248,23 +250,23 @@ bool build_shadow(xcb_connection_t *c, xcb_drawable_t d, double opacity, const i } uint32_t offset = row * shadow_image->stride / sizeof(*shadow_image->data); - xcb_put_image(c, (uint8_t)shadow_image->format, shadow_pixmap, gc, + xcb_put_image(c->c, (uint8_t)shadow_image->format, shadow_pixmap, gc, shadow_image->width, batch_height, 0, to_i16_checked(row), 0, shadow_image->depth, shadow_image->stride * batch_height, shadow_image->data + offset); } - xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, shadow_pixel, shadow_picture, + xcb_render_composite(c->c, XCB_RENDER_PICT_OP_SRC, shadow_pixel, shadow_picture, shadow_picture_argb, 0, 0, 0, 0, 0, 0, shadow_image->width, shadow_image->height); *pixmap = shadow_pixmap_argb; *pict = shadow_picture_argb; - xcb_free_gc(c, gc); + xcb_free_gc(c->c, gc); xcb_image_destroy(shadow_image); - xcb_free_pixmap(c, shadow_pixmap); - xcb_render_free_picture(c, shadow_picture); + xcb_free_pixmap(c->c, shadow_pixmap); + x_free_picture(c, shadow_picture); return true; @@ -273,44 +275,24 @@ bool build_shadow(xcb_connection_t *c, xcb_drawable_t d, double opacity, const i xcb_image_destroy(shadow_image); } if (shadow_pixmap) { - xcb_free_pixmap(c, shadow_pixmap); + xcb_free_pixmap(c->c, shadow_pixmap); } if (shadow_pixmap_argb) { - xcb_free_pixmap(c, shadow_pixmap_argb); + xcb_free_pixmap(c->c, shadow_pixmap_argb); } if (shadow_picture) { - xcb_render_free_picture(c, shadow_picture); + x_free_picture(c, shadow_picture); } if (shadow_picture_argb) { - xcb_render_free_picture(c, shadow_picture_argb); + x_free_picture(c, shadow_picture_argb); } if (gc) { - xcb_free_gc(c, gc); + xcb_free_gc(c->c, gc); } return false; } -void * -default_backend_render_shadow(backend_t *backend_data, int width, int height, - const conv *kernel, double r, double g, double b, double a) { - xcb_pixmap_t shadow_pixel = solid_picture(backend_data->c, backend_data->root, - true, 1, r, g, b), - shadow = XCB_NONE; - xcb_render_picture_t pict = XCB_NONE; - - if (!build_shadow(backend_data->c, backend_data->root, a, width, height, kernel, - shadow_pixel, &shadow, &pict)) { - return NULL; - } - - auto visual = x_get_visual_for_standard(backend_data->c, XCB_PICT_STANDARD_ARGB_32); - void *ret = backend_data->ops->bind_pixmap( - backend_data, shadow, x_get_visual_info(backend_data->c, visual), true); - xcb_render_free_picture(backend_data->c, pict); - return ret; -} - static struct conv **generate_box_blur_kernel(struct box_blur_args *args, int *kernel_count) { int r = args->size * 2 + 1; assert(r > 0); @@ -373,31 +355,31 @@ struct dual_kawase_params *generate_dual_kawase_params(void *args) { int min_radius; /// Approximate gauss-blur with at least this /// radius and std-deviation } strength_levels[20] = { - {.iterations = 1, .offset = 1.25f, .min_radius = 1}, // LVL 1 - {.iterations = 1, .offset = 2.25f, .min_radius = 6}, // LVL 2 - {.iterations = 2, .offset = 2.00f, .min_radius = 11}, // LVL 3 - {.iterations = 2, .offset = 3.00f, .min_radius = 17}, // LVL 4 - {.iterations = 2, .offset = 4.25f, .min_radius = 24}, // LVL 5 - {.iterations = 3, .offset = 2.50f, .min_radius = 32}, // LVL 6 - {.iterations = 3, .offset = 3.25f, .min_radius = 40}, // LVL 7 - {.iterations = 3, .offset = 4.25f, .min_radius = 51}, // LVL 8 - {.iterations = 3, .offset = 5.50f, .min_radius = 67}, // LVL 9 - {.iterations = 4, .offset = 3.25f, .min_radius = 83}, // LVL 10 - {.iterations = 4, .offset = 4.00f, .min_radius = 101}, // LVL 11 - {.iterations = 4, .offset = 5.00f, .min_radius = 123}, // LVL 12 - {.iterations = 4, .offset = 6.00f, .min_radius = 148}, // LVL 13 - {.iterations = 4, .offset = 7.25f, .min_radius = 178}, // LVL 14 - {.iterations = 4, .offset = 8.25f, .min_radius = 208}, // LVL 15 - {.iterations = 5, .offset = 4.50f, .min_radius = 236}, // LVL 16 - {.iterations = 5, .offset = 5.25f, .min_radius = 269}, // LVL 17 - {.iterations = 5, .offset = 6.25f, .min_radius = 309}, // LVL 18 - {.iterations = 5, .offset = 7.25f, .min_radius = 357}, // LVL 19 - {.iterations = 5, .offset = 8.50f, .min_radius = 417}, // LVL 20 + {.iterations = 1, .offset = 1.25F, .min_radius = 1}, // LVL 1 + {.iterations = 1, .offset = 2.25F, .min_radius = 6}, // LVL 2 + {.iterations = 2, .offset = 2.00F, .min_radius = 11}, // LVL 3 + {.iterations = 2, .offset = 3.00F, .min_radius = 17}, // LVL 4 + {.iterations = 2, .offset = 4.25F, .min_radius = 24}, // LVL 5 + {.iterations = 3, .offset = 2.50F, .min_radius = 32}, // LVL 6 + {.iterations = 3, .offset = 3.25F, .min_radius = 40}, // LVL 7 + {.iterations = 3, .offset = 4.25F, .min_radius = 51}, // LVL 8 + {.iterations = 3, .offset = 5.50F, .min_radius = 67}, // LVL 9 + {.iterations = 4, .offset = 3.25F, .min_radius = 83}, // LVL 10 + {.iterations = 4, .offset = 4.00F, .min_radius = 101}, // LVL 11 + {.iterations = 4, .offset = 5.00F, .min_radius = 123}, // LVL 12 + {.iterations = 4, .offset = 6.00F, .min_radius = 148}, // LVL 13 + {.iterations = 4, .offset = 7.25F, .min_radius = 178}, // LVL 14 + {.iterations = 4, .offset = 8.25F, .min_radius = 208}, // LVL 15 + {.iterations = 5, .offset = 4.50F, .min_radius = 236}, // LVL 16 + {.iterations = 5, .offset = 5.25F, .min_radius = 269}, // LVL 17 + {.iterations = 5, .offset = 6.25F, .min_radius = 309}, // LVL 18 + {.iterations = 5, .offset = 7.25F, .min_radius = 357}, // LVL 19 + {.iterations = 5, .offset = 8.50F, .min_radius = 417}, // LVL 20 }; auto params = ccalloc(1, struct dual_kawase_params); params->iterations = 0; - params->offset = 1.0f; + params->offset = 1.0F; if (blur_args->strength <= 0 && blur_args->size) { // find highest level that approximates blur-strength with the selected @@ -421,60 +403,18 @@ struct dual_kawase_params *generate_dual_kawase_params(void *args) { // - Smallest texture dimensions are halved `iterations`-times // - Upsample needs pixels two-times `offset` away from the border // - Plus one for interpolation differences - params->expand = (1 << params->iterations) * 2 * (int)ceil(params->offset) + 1; + params->expand = (1 << params->iterations) * 2 * (int)ceilf(params->offset) + 1; return params; } -void *default_clone_image(backend_t *base attr_unused, const void *image_data, - const region_t *reg_visible attr_unused) { - auto new_img = ccalloc(1, struct backend_image); - *new_img = *(struct backend_image *)image_data; - new_img->inner->refcount++; - return new_img; -} - -bool default_set_image_property(backend_t *base attr_unused, enum image_properties op, - void *image_data, void *arg) { - struct backend_image *tex = image_data; - int *iargs = arg; - bool *bargs = arg; - double *dargs = arg; - switch (op) { - case IMAGE_PROPERTY_INVERTED: tex->color_inverted = bargs[0]; break; - case IMAGE_PROPERTY_DIM_LEVEL: tex->dim = dargs[0]; break; - case IMAGE_PROPERTY_OPACITY: tex->opacity = dargs[0]; break; - case IMAGE_PROPERTY_EFFECTIVE_SIZE: - // texture is already set to repeat, so nothing else we need to do - tex->ewidth = iargs[0]; - tex->eheight = iargs[1]; - break; - case IMAGE_PROPERTY_MAX_BRIGHTNESS: tex->max_brightness = dargs[0]; break; - } - - return true; -} - -bool default_is_image_transparent(backend_t *base attr_unused, void *image_data) { - struct backend_image *img = image_data; - return img->opacity < 1 || img->inner->has_alpha; -} - -struct backend_image *default_new_backend_image(int w, int h) { - auto ret = ccalloc(1, struct backend_image); - ret->opacity = 1; - ret->dim = 0; - ret->max_brightness = 1; - ret->eheight = h; - ret->ewidth = w; - ret->color_inverted = false; - return ret; -} - void init_backend_base(struct backend_base *base, session_t *ps) { - base->c = ps->c; + base->c = &ps->c; base->loop = ps->loop; - base->root = ps->root; base->busy = false; base->ops = NULL; } + +uint32_t backend_no_quirks(struct backend_base *base attr_unused) { + return 0; +} diff --git a/src/backend/backend_common.h b/src/backend/backend_common.h index 702be5c3f3..e9091cbeb5 100644 --- a/src/backend/backend_common.h +++ b/src/backend/backend_common.h @@ -11,10 +11,10 @@ #include "config.h" #include "region.h" -typedef struct session session_t; -typedef struct win win; -typedef struct conv conv; -typedef struct backend_base backend_t; +struct session; +struct win; +struct conv; +struct backend_base; struct backend_operations; struct dual_kawase_params { @@ -26,51 +26,18 @@ struct dual_kawase_params { int expand; }; -struct backend_image_inner_base { - int refcount; - bool has_alpha; -}; - -struct backend_image { - // Backend dependent inner image data - struct backend_image_inner_base *inner; - double opacity; - double dim; - double max_brightness; - // Effective size of the image - int ewidth, eheight; - bool color_inverted; -}; - -bool build_shadow(xcb_connection_t *, xcb_drawable_t, double opacity, int width, - int height, const conv *kernel, xcb_render_picture_t shadow_pixel, +xcb_image_t *make_shadow(struct x_connection *c, const conv *kernel, double opacity, + int width, int height); +bool build_shadow(struct x_connection *, double opacity, int width, int height, + const conv *kernel, xcb_render_picture_t shadow_pixel, xcb_pixmap_t *pixmap, xcb_render_picture_t *pict); -xcb_render_picture_t solid_picture(xcb_connection_t *, xcb_drawable_t, bool argb, - double a, double r, double g, double b); - -xcb_image_t * -make_shadow(xcb_connection_t *c, const conv *kernel, double opacity, int width, int height); - -/// The default implementation of `is_win_transparent`, it simply looks at win::mode. So -/// this is not suitable for backends that alter the content of windows -bool default_is_win_transparent(void *, win *, void *); - -/// The default implementation of `is_frame_transparent`, it uses win::frame_opacity. Same -/// caveat as `default_is_win_transparent` applies. -bool default_is_frame_transparent(void *, win *, void *); - -void * -default_backend_render_shadow(backend_t *backend_data, int width, int height, - const conv *kernel, double r, double g, double b, double a); +xcb_render_picture_t +solid_picture(struct x_connection *, bool argb, double a, double r, double g, double b); void init_backend_base(struct backend_base *base, session_t *ps); struct conv **generate_blur_kernel(enum blur_method method, void *args, int *kernel_count); struct dual_kawase_params *generate_dual_kawase_params(void *args); -void *default_clone_image(backend_t *base, const void *image_data, const region_t *reg); -bool default_is_image_transparent(backend_t *base attr_unused, void *image_data); -bool default_set_image_property(backend_t *base attr_unused, enum image_properties op, - void *image_data, void *arg); -struct backend_image *default_new_backend_image(int w, int h); +uint32_t backend_no_quirks(struct backend_base *base attr_unused); diff --git a/src/backend/driver.c b/src/backend/driver.c index a41d2fdc1f..99ea4d7a3a 100644 --- a/src/backend/driver.c +++ b/src/backend/driver.c @@ -15,12 +15,20 @@ /// Apply driver specified global workarounds. It's safe to call this multiple times. void apply_driver_workarounds(struct session *ps, enum driver driver) { if (driver & DRIVER_NVIDIA) { - // setenv("__GL_YIELD", "usleep", true); - setenv("__GL_MaxFramesAllowed", "1", true); ps->o.xrender_sync_fence = true; } } +enum vblank_scheduler_type choose_vblank_scheduler(enum driver driver attr_unused) { + enum vblank_scheduler_type type = VBLANK_SCHEDULER_PRESENT; +#ifdef CONFIG_OPENGL + if (driver & DRIVER_NVIDIA) { + type = VBLANK_SCHEDULER_SGI_VIDEO_SYNC; + } +#endif + return type; +} + enum driver detect_driver(xcb_connection_t *c, backend_t *backend_data, xcb_window_t window) { enum driver ret = 0; // First we try doing backend agnostic detection using RANDR diff --git a/src/backend/driver.h b/src/backend/driver.h index a37cda3b67..1b0877c14b 100644 --- a/src/backend/driver.h +++ b/src/backend/driver.h @@ -7,6 +7,7 @@ #include #include +#include "config.h" #include "utils.h" struct session; @@ -41,13 +42,15 @@ enum driver detect_driver(xcb_connection_t *, struct backend_base *, xcb_window_ /// Apply driver specified global workarounds. It's safe to call this multiple times. void apply_driver_workarounds(struct session *ps, enum driver); +/// Choose a vblank scheduler based on the driver. +enum vblank_scheduler_type choose_vblank_scheduler(enum driver driver); // Print driver names to stdout, for diagnostics static inline void print_drivers(enum driver drivers) { const char *seen_drivers[ARR_SIZE(driver_names)]; int driver_count = 0; for (size_t i = 0; i < ARR_SIZE(driver_names); i++) { - if (drivers & (1ul << i)) { + if (drivers & (1UL << i)) { seen_drivers[driver_count++] = driver_names[i]; } } diff --git a/src/backend/dummy/dummy.c b/src/backend/dummy/dummy.c index 7fa53d3c6f..84d7edcd33 100644 --- a/src/backend/dummy/dummy.c +++ b/src/backend/dummy/dummy.c @@ -14,129 +14,171 @@ #include "x.h" struct dummy_image { + enum backend_image_format format; xcb_pixmap_t pixmap; - bool transparent; - int *refcount; + struct list_node siblings; UT_hash_handle hh; }; struct dummy_data { struct backend_base base; - struct dummy_image *images; + struct dummy_image *pixmap_images; + struct list_node non_pixmap_images; + + struct dummy_image back_buffer; }; -struct backend_base *dummy_init(struct session *ps attr_unused) { - auto ret = (struct backend_base *)ccalloc(1, struct dummy_data); - ret->c = ps->c; - ret->loop = ps->loop; - ret->root = ps->root; - ret->busy = false; - return ret; +struct backend_operations dummy_ops; + +struct backend_base *dummy_init(session_t *ps attr_unused, xcb_window_t target attr_unused) { + auto ret = ccalloc(1, struct dummy_data); + init_backend_base(&ret->base, ps); + ret->base.ops = &dummy_ops; + list_init_head(&ret->non_pixmap_images); + return &ret->base; } void dummy_deinit(struct backend_base *data) { auto dummy = (struct dummy_data *)data; - HASH_ITER2(dummy->images, img) { - log_warn("Backend image for pixmap %#010x is not freed", img->pixmap); - HASH_DEL(dummy->images, img); - free(img->refcount); + HASH_ITER2(dummy->pixmap_images, img) { + log_warn("Backend image %p for pixmap %#010x is not freed", img, img->pixmap); + HASH_DEL(dummy->pixmap_images, img); + xcb_free_pixmap(data->c->c, img->pixmap); + free(img); + } + list_foreach_safe(struct dummy_image, img, &dummy->non_pixmap_images, siblings) { + log_warn("Backend image %p for non-pixmap is not freed", img); + list_remove(&img->siblings); free(img); } free(dummy); } -static void dummy_check_image(struct backend_base *base, const struct dummy_image *img) { +static void dummy_check_image(struct backend_base *base, image_handle image) { auto dummy = (struct dummy_data *)base; + auto img = (struct dummy_image *)image; + if (img == (struct dummy_image *)&dummy->back_buffer) { + return; + } + if (!img->pixmap) { + return; + } struct dummy_image *tmp = NULL; - HASH_FIND_INT(dummy->images, &img->pixmap, tmp); + HASH_FIND_INT(dummy->pixmap_images, &img->pixmap, tmp); if (!tmp) { log_warn("Using an invalid (possibly freed) image"); assert(false); } - assert(*tmp->refcount > 0); -} - -void dummy_compose(struct backend_base *base, void *image, int dst_x attr_unused, - int dst_y attr_unused, const region_t *reg_paint attr_unused, - const region_t *reg_visible attr_unused) { - dummy_check_image(base, image); } -void dummy_fill(struct backend_base *backend_data attr_unused, struct color c attr_unused, - const region_t *clip attr_unused) { +bool dummy_blit(struct backend_base *base, ivec2 origin attr_unused, image_handle target, + const struct backend_blit_args *args) { + dummy_check_image(base, target); + dummy_check_image(base, args->source_image); + if (args->source_mask) { + auto mask = (struct dummy_image *)args->source_mask->image; + if (mask->format != BACKEND_IMAGE_FORMAT_MASK) { + log_error("Invalid mask image format"); + assert(false); + return false; + } + dummy_check_image(base, args->source_mask->image); + } + return true; } -bool dummy_blur(struct backend_base *backend_data attr_unused, double opacity attr_unused, - void *blur_ctx attr_unused, const region_t *reg_blur attr_unused, - const region_t *reg_visible attr_unused) { +bool dummy_blur(struct backend_base *base, ivec2 origin attr_unused, image_handle target, + const struct backend_blur_args *args) { + dummy_check_image(base, target); + dummy_check_image(base, args->source_image); + if (args->source_mask) { + auto mask = (struct dummy_image *)args->source_mask->image; + if (mask->format != BACKEND_IMAGE_FORMAT_MASK) { + log_error("Invalid mask image format"); + assert(false); + return false; + } + dummy_check_image(base, args->source_mask->image); + } return true; } -void *dummy_bind_pixmap(struct backend_base *base, xcb_pixmap_t pixmap, - struct xvisual_info fmt, bool owned attr_unused) { +image_handle dummy_bind_pixmap(struct backend_base *base, xcb_pixmap_t pixmap, + struct xvisual_info fmt attr_unused) { auto dummy = (struct dummy_data *)base; struct dummy_image *img = NULL; - HASH_FIND_INT(dummy->images, &pixmap, img); + HASH_FIND_INT(dummy->pixmap_images, &pixmap, img); if (img) { - (*img->refcount)++; - return img; + log_error("Pixmap %#010x is already bound to an image", pixmap); + return NULL; } img = ccalloc(1, struct dummy_image); + img->format = BACKEND_IMAGE_FORMAT_PIXMAP; img->pixmap = pixmap; - img->transparent = fmt.alpha_size != 0; - img->refcount = ccalloc(1, int); - *img->refcount = 1; - HASH_ADD_INT(dummy->images, pixmap, img); - return (void *)img; + HASH_ADD_INT(dummy->pixmap_images, pixmap, img); + return (image_handle)img; } -void dummy_release_image(backend_t *base, void *image) { +xcb_pixmap_t dummy_release_image(backend_t *base, image_handle image) { auto dummy = (struct dummy_data *)base; - auto img = (struct dummy_image *)image; - assert(*img->refcount > 0); - (*img->refcount)--; - if (*img->refcount == 0) { - HASH_DEL(dummy->images, img); - free(img->refcount); - free(img); + if ((struct dummy_image *)image == &dummy->back_buffer) { + return XCB_NONE; } -} - -bool dummy_is_image_transparent(struct backend_base *base, void *image) { auto img = (struct dummy_image *)image; - dummy_check_image(base, img); - return img->transparent; + xcb_pixmap_t pixmap = XCB_NONE; + if (img->pixmap) { + HASH_DEL(dummy->pixmap_images, img); + pixmap = img->pixmap; + } else { + list_remove(&img->siblings); + } + free(img); + return pixmap; } int dummy_buffer_age(struct backend_base *base attr_unused) { return 2; } -bool dummy_image_op(struct backend_base *base, enum image_operations op attr_unused, - void *image, const region_t *reg_op attr_unused, - const region_t *reg_visible attr_unused, void *args attr_unused) { - dummy_check_image(base, image); +bool dummy_apply_alpha(struct backend_base *base, image_handle target, + double alpha attr_unused, const region_t *reg attr_unused) { + dummy_check_image(base, target); return true; } -bool dummy_set_image_property(struct backend_base *base, enum image_properties prop attr_unused, - void *image, void *arg attr_unused) { - dummy_check_image(base, image); +bool dummy_copy_area(struct backend_base *base, ivec2 origin attr_unused, image_handle target, + image_handle source, const region_t *reg attr_unused) { + dummy_check_image(base, target); + dummy_check_image(base, source); return true; } -void *dummy_clone_image(struct backend_base *base, const void *image, - const region_t *reg_visible attr_unused) { - auto img = (const struct dummy_image *)image; - dummy_check_image(base, img); - (*img->refcount)++; - return (void *)img; +bool dummy_clear(struct backend_base *base, image_handle target, + struct color color attr_unused) { + dummy_check_image(base, target); + return true; +} + +image_handle dummy_new_image(struct backend_base *base, enum backend_image_format format, + ivec2 size attr_unused) { + auto new_img = ccalloc(1, struct dummy_image); + auto dummy = (struct dummy_data *)base; + list_insert_after(&dummy->non_pixmap_images, &new_img->siblings); + new_img->format = format; + return (image_handle)new_img; +} + +image_handle dummy_back_buffer(struct backend_base *base) { + auto dummy = (struct dummy_data *)base; + return (image_handle)&dummy->back_buffer; } void *dummy_create_blur_context(struct backend_base *base attr_unused, - enum blur_method method attr_unused, void *args attr_unused) { + enum blur_method method attr_unused, + enum backend_image_format format attr_unused, + void *args attr_unused) { static int dummy_context; return &dummy_context; } @@ -145,30 +187,43 @@ void dummy_destroy_blur_context(struct backend_base *base attr_unused, void *ctx } void dummy_get_blur_size(void *ctx attr_unused, int *width, int *height) { - // These numbers are arbitrary, to make sure the reisze_region code path is + // These numbers are arbitrary, to make sure the resize_region code path is // covered. *width = 5; *height = 5; } +uint32_t dummy_image_capabilities(struct backend_base *base attr_unused, + image_handle image attr_unused) { + return BACKEND_IMAGE_CAP_SRC | BACKEND_IMAGE_CAP_DST; +} + +bool dummy_is_format_supported(struct backend_base *base attr_unused, + enum backend_image_format format attr_unused) { + return true; +} + struct backend_operations dummy_ops = { - .init = dummy_init, - .deinit = dummy_deinit, - .compose = dummy_compose, - .fill = dummy_fill, + .apply_alpha = dummy_apply_alpha, + .back_buffer = dummy_back_buffer, + .blit = dummy_blit, .blur = dummy_blur, + .clear = dummy_clear, + .copy_area = dummy_copy_area, + .copy_area_quantize = dummy_copy_area, + .image_capabilities = dummy_image_capabilities, + .is_format_supported = dummy_is_format_supported, + .new_image = dummy_new_image, .bind_pixmap = dummy_bind_pixmap, - .render_shadow = default_backend_render_shadow, + .quirks = backend_no_quirks, .release_image = dummy_release_image, - .is_image_transparent = dummy_is_image_transparent, + + .init = dummy_init, + .deinit = dummy_deinit, .buffer_age = dummy_buffer_age, .max_buffer_age = 5, - .image_op = dummy_image_op, - .clone_image = dummy_clone_image, - .set_image_property = dummy_set_image_property, .create_blur_context = dummy_create_blur_context, .destroy_blur_context = dummy_destroy_blur_context, .get_blur_size = dummy_get_blur_size, - }; diff --git a/src/backend/gl/blur.c b/src/backend/gl/blur.c new file mode 100644 index 0000000000..b61eaa450a --- /dev/null +++ b/src/backend/gl/blur.c @@ -0,0 +1,879 @@ +#include +#include + +#include +#include + +#include "gl_common.h" + +struct gl_blur_context { + enum blur_method method; + struct gl_shader *blur_shader; + + /// Temporary textures used for blurring + GLuint *blur_textures; + int blur_texture_count; + /// Temporary fbos used for blurring + GLuint *blur_fbos; + int blur_fbo_count; + + /// Cached dimensions of each blur_texture. They are the same size as the target, + /// so they are always big enough without resizing. + /// Turns out calling glTexImage to resize is expensive, so we avoid that. + struct texture_size { + int width; + int height; + } *texture_sizes; + + /// Cached dimensions of the offscreen framebuffer. It's the same size as the + /// target but is expanded in either direction by resize_width / resize_height. + int fb_width, fb_height; + + /// How much do we need to resize the damaged region for blurring. + int resize_width, resize_height; + + int npasses; + + enum backend_image_format format; +}; + +// TODO(yshui) small optimization for kernel blur, if source and target are different, +// single pass blur can paint directly from source to target. Currently a temporary +// texture is always used. + +/** + * Blur contents in a particular region. + */ +static bool gl_kernel_blur(double opacity, struct gl_blur_context *bctx, + const struct backend_mask_image *mask, const GLuint vao[2], + const int vao_nelems[2], struct gl_texture *source, + GLuint blur_sampler, GLuint target_fbo, GLuint default_mask) { + int curr = 0; + for (int i = 0; i < bctx->npasses; ++i) { + auto p = &bctx->blur_shader[i]; + assert(p->prog); + + assert(bctx->blur_textures[curr]); + + // The origin to use when sampling from the source texture + GLint tex_width, tex_height; + GLuint src_texture; + + if (i == 0) { + src_texture = source->texture; + tex_width = source->width; + tex_height = source->height; + } else { + src_texture = bctx->blur_textures[curr]; + auto src_size = bctx->texture_sizes[curr]; + tex_width = src_size.width; + tex_height = src_size.height; + } + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, src_texture); + glBindSampler(0, blur_sampler); + glUseProgram(p->prog); + if (p->uniform_bitmask & (1 << UNIFORM_PIXEL_NORM_LOC)) { + // If the last pass is a trivial blend pass, it will not have + // pixel_norm. + glUniform2f(UNIFORM_PIXEL_NORM_LOC, 1.0F / (GLfloat)tex_width, + 1.0F / (GLfloat)tex_height); + } + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, default_mask); + + glUniform1i(UNIFORM_MASK_TEX_LOC, 1); + glUniform2f(UNIFORM_MASK_OFFSET_LOC, 0.0F, 0.0F); + glUniform1i(UNIFORM_MASK_INVERTED_LOC, 0); + glUniform1f(UNIFORM_MASK_CORNER_RADIUS_LOC, 0.0F); + + // The number of indices in the selected vertex array + GLsizei nelems; + + if (i < bctx->npasses - 1) { + assert(bctx->blur_fbos[0]); + assert(bctx->blur_textures[!curr]); + + // not last pass, draw into framebuffer, with resized regions + glBindVertexArray(vao[1]); + nelems = vao_nelems[1]; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[0]); + + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, bctx->blur_textures[!curr], 0); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { + return false; + } + + glUniform1f(UNIFORM_OPACITY_LOC, 1.0F); + } else { + // last pass, draw directly into the back buffer, with origin + // regions. And apply mask if requested + if (mask != NULL) { + auto inner = (struct gl_texture *)mask->image; + log_trace("Mask texture is %d", inner->texture); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, inner->texture); + glUniform1i(UNIFORM_MASK_INVERTED_LOC, mask->inverted); + glUniform1f(UNIFORM_MASK_CORNER_RADIUS_LOC, + (float)mask->corner_radius); + glUniform2f(UNIFORM_MASK_OFFSET_LOC, (float)(mask->origin.x), + (float)(mask->origin.y)); + } + glBindVertexArray(vao[0]); + nelems = vao_nelems[0]; + glBindFramebuffer(GL_FRAMEBUFFER, target_fbo); + + glUniform1f(UNIFORM_OPACITY_LOC, (float)opacity); + } + + glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + + // XXX use multiple draw calls is probably going to be slow than + // just simply blur the whole area. + + curr = !curr; + } + + return true; +} + +/// Do dual-kawase blur. +/// +/// @param vao two vertex array objects. +/// [0]: for sampling from blurred result into the target fbo. +/// [1]: for sampling from the source texture into blurred textures. +bool gl_dual_kawase_blur(double opacity, struct gl_blur_context *bctx, + const struct backend_mask_image *mask, const GLuint vao[2], + const int vao_nelems[2], struct gl_texture *source, + GLuint blur_sampler, GLuint target_fbo, GLuint default_mask) { + int iterations = bctx->blur_texture_count; + int scale_factor = 1; + + // Kawase downsample pass + auto down_pass = &bctx->blur_shader[0]; + assert(down_pass->prog); + glUseProgram(down_pass->prog); + + glBindVertexArray(vao[1]); + int nelems = vao_nelems[1]; + + for (int i = 0; i < iterations; ++i) { + // Scale output width / height by half in each iteration + scale_factor <<= 1; + + GLuint src_texture; + int tex_width, tex_height; + + if (i == 0) { + // first pass: copy from back buffer + src_texture = source->texture; + tex_width = source->width; + tex_height = source->height; + } else { + // copy from previous pass + src_texture = bctx->blur_textures[i - 1]; + auto src_size = bctx->texture_sizes[i - 1]; + tex_width = src_size.width; + tex_height = src_size.height; + } + + assert(src_texture); + assert(bctx->blur_fbos[i]); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, src_texture); + glBindSampler(0, blur_sampler); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i]); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + + glUniform1f(UNIFORM_SCALE_LOC, (GLfloat)scale_factor); + + glUniform2f(UNIFORM_PIXEL_NORM_LOC, 1.0F / (GLfloat)tex_width, + 1.0F / (GLfloat)tex_height); + + glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + } + + // Kawase upsample pass + auto up_pass = &bctx->blur_shader[1]; + assert(up_pass->prog); + glUseProgram(up_pass->prog); + + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, default_mask); + + glUniform1i(UNIFORM_MASK_TEX_LOC, 1); + glUniform2f(UNIFORM_MASK_OFFSET_LOC, 0.0F, 0.0F); + glUniform1i(UNIFORM_MASK_INVERTED_LOC, 0); + glUniform1f(UNIFORM_MASK_CORNER_RADIUS_LOC, 0.0F); + glUniform1f(UNIFORM_OPACITY_LOC, 1.0F); + + for (int i = iterations - 1; i >= 0; --i) { + // Scale output width / height back by two in each iteration + scale_factor >>= 1; + + const GLuint src_texture = bctx->blur_textures[i]; + assert(src_texture); + + // Calculate normalized half-width/-height of a src pixel + auto src_size = bctx->texture_sizes[i]; + int tex_width = src_size.width; + int tex_height = src_size.height; + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, src_texture); + glBindSampler(0, blur_sampler); + + if (i > 0) { + assert(bctx->blur_fbos[i - 1]); + + // not last pass, draw into next framebuffer + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i - 1]); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + } else { + // last pass, draw directly into the target fbo + if (mask != NULL) { + auto inner = (struct gl_texture *)mask->image; + log_trace("Mask texture is %d", inner->texture); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, inner->texture); + glUniform1i(UNIFORM_MASK_INVERTED_LOC, mask->inverted); + glUniform1f(UNIFORM_MASK_CORNER_RADIUS_LOC, + (float)mask->corner_radius); + glUniform2f(UNIFORM_MASK_OFFSET_LOC, (float)(mask->origin.x), + (float)(mask->origin.y)); + } + glBindVertexArray(vao[0]); + nelems = vao_nelems[0]; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target_fbo); + + glUniform1f(UNIFORM_OPACITY_LOC, (GLfloat)opacity); + } + + glUniform1f(UNIFORM_SCALE_LOC, (GLfloat)scale_factor); + glUniform2f(UNIFORM_PIXEL_NORM_LOC, 1.0F / (GLfloat)tex_width, + 1.0F / (GLfloat)tex_height); + + glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + } + + return true; +} + +static bool +gl_blur_context_preallocate_textures(struct gl_blur_context *bctx, ivec2 source_size) { + if (source_size.width != bctx->fb_width || source_size.height != bctx->fb_height) { + // Resize the temporary textures used for blur in case the root + // size changed + bctx->fb_width = source_size.width; + bctx->fb_height = source_size.height; + + for (int i = 0; i < bctx->blur_texture_count; ++i) { + auto tex_size = bctx->texture_sizes + i; + if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { + // Use smaller textures for each iteration (quarter of the + // previous texture) + tex_size->width = 1 + ((bctx->fb_width - 1) >> (i + 1)); + tex_size->height = 1 + ((bctx->fb_height - 1) >> (i + 1)); + } else { + tex_size->width = bctx->fb_width; + tex_size->height = bctx->fb_height; + } + + glBindTexture(GL_TEXTURE_2D, bctx->blur_textures[i]); + GLint format; + switch (bctx->format) { + case BACKEND_IMAGE_FORMAT_PIXMAP_HIGH: format = GL_RGBA16; break; + case BACKEND_IMAGE_FORMAT_PIXMAP: format = GL_RGBA8; break; + case BACKEND_IMAGE_FORMAT_MASK: format = GL_R8; break; + default: unreachable(); + } + glTexImage2D(GL_TEXTURE_2D, 0, format, tex_size->width, + tex_size->height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); + + if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { + // Attach texture to FBO target + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i]); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + bctx->blur_textures[i], 0); + if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return false; + } + } + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + return true; +} + +bool gl_blur(struct backend_base *base, ivec2 origin, image_handle target_, + const struct backend_blur_args *args) { + auto gd = (struct gl_data *)base; + auto target = (struct gl_texture *)target_; + auto source = (struct gl_texture *)args->source_image; + auto bctx = (struct gl_blur_context *)args->blur_context; + log_trace("Blur size: %dx%d, method: %d", source->width, source->height, bctx->method); + bool ret = false; + + // Remainder: regions are in Xorg coordinates + auto reg_blur_resized = + resize_region(args->target_mask, bctx->resize_width, bctx->resize_height); + const rect_t *extent = pixman_region32_extents(args->target_mask); + int width = extent->x2 - extent->x1, height = extent->y2 - extent->y1; + if (width == 0 || height == 0) { + return true; + } + + int nrects, nrects_resized; + const rect_t *rects = pixman_region32_rectangles(args->target_mask, &nrects), + *rects_resized = + pixman_region32_rectangles(®_blur_resized, &nrects_resized); + if (!nrects || !nrects_resized) { + return true; + } + + if (!gl_blur_context_preallocate_textures( + bctx, (ivec2){source->width, source->height})) { + return false; + } + + // Original region for the final compositing step from blur result to target. + auto coord = ccalloc(nrects * 16, GLint); + auto indices = ccalloc(nrects * 6, GLuint); + gl_mask_rects_to_coords(origin, nrects, rects, coord, indices); + if (!target->y_inverted) { + gl_y_flip_target(nrects, coord, target->height); + } + + // Resize region for sampling from source texture, and for blur passes + auto coord_resized = ccalloc(nrects_resized * 16, GLint); + auto indices_resized = ccalloc(nrects_resized * 6, GLuint); + gl_mask_rects_to_coords(origin, nrects_resized, rects_resized, coord_resized, + indices_resized); + pixman_region32_fini(®_blur_resized); + // FIXME(yshui) In theory we should handle blurring a non-y-inverted source, but + // we never actually use that capability anywhere. + assert(source->y_inverted); + + GLuint vao[2]; + glGenVertexArrays(2, vao); + GLuint bo[4]; + glGenBuffers(4, bo); + + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glBindVertexArray(vao[0]); + glBindBuffer(GL_ARRAY_BUFFER, bo[0]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * nrects * 16, coord, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * nrects * 6, + indices, GL_STREAM_DRAW); + glEnableVertexAttribArray(vert_coord_loc); + glEnableVertexAttribArray(vert_in_texcoord_loc); + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, + sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + + glBindVertexArray(vao[1]); + glBindBuffer(GL_ARRAY_BUFFER, bo[2]); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[3]); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord_resized) * nrects_resized * 16, + coord_resized, GL_STREAM_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, + (long)sizeof(*indices_resized) * nrects_resized * 6, indices_resized, + GL_STREAM_DRAW); + glEnableVertexAttribArray(vert_coord_loc); + glEnableVertexAttribArray(vert_in_texcoord_loc); + glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); + glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, + sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); + + int vao_nelems[2] = {nrects * 6, nrects_resized * 6}; + + auto target_fbo = gl_bind_image_to_fbo(gd, (image_handle)target); + if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { + ret = gl_dual_kawase_blur(args->opacity, bctx, args->source_mask, vao, + vao_nelems, source, gd->samplers[GL_SAMPLER_BLUR], + target_fbo, gd->default_mask_texture); + } else { + ret = gl_kernel_blur(args->opacity, bctx, args->source_mask, vao, + vao_nelems, source, gd->samplers[GL_SAMPLER_BLUR], + target_fbo, gd->default_mask_texture); + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDeleteBuffers(4, bo); + glBindVertexArray(0); + glDeleteVertexArrays(2, vao); + glUseProgram(0); + + free(indices); + free(coord); + free(indices_resized); + free(coord_resized); + + gl_check_err(); + return ret; +} + +static inline void gl_free_blur_shader(struct gl_shader *shader) { + if (shader->prog) { + glDeleteProgram(shader->prog); + } + + shader->prog = 0; +} + +void gl_destroy_blur_context(backend_t *base attr_unused, void *ctx) { + auto bctx = (struct gl_blur_context *)ctx; + // Free GLSL shaders/programs + for (int i = 0; i < bctx->npasses; ++i) { + gl_free_blur_shader(&bctx->blur_shader[i]); + } + free(bctx->blur_shader); + + if (bctx->blur_texture_count && bctx->blur_textures) { + glDeleteTextures(bctx->blur_texture_count, bctx->blur_textures); + free(bctx->blur_textures); + } + if (bctx->blur_texture_count && bctx->texture_sizes) { + free(bctx->texture_sizes); + } + if (bctx->blur_fbo_count && bctx->blur_fbos) { + glDeleteFramebuffers(bctx->blur_fbo_count, bctx->blur_fbos); + free(bctx->blur_fbos); + } + + bctx->blur_texture_count = 0; + bctx->blur_fbo_count = 0; + + free(bctx); + + gl_check_err(); +} + +/** + * Initialize GL blur filters. + */ +bool gl_create_kernel_blur_context(void *blur_context, GLfloat *projection, + enum blur_method method, void *args) { + bool success = false; + auto ctx = (struct gl_blur_context *)blur_context; + + struct conv **kernels; + + int nkernels; + ctx->method = BLUR_METHOD_KERNEL; + if (method == BLUR_METHOD_KERNEL) { + nkernels = ((struct kernel_blur_args *)args)->kernel_count; + kernels = ((struct kernel_blur_args *)args)->kernels; + } else { + kernels = generate_blur_kernel(method, args, &nkernels); + } + + if (!nkernels) { + ctx->method = BLUR_METHOD_NONE; + return true; + } + + // Specify required textures and FBOs + ctx->blur_texture_count = 2; + ctx->blur_fbo_count = 1; + + ctx->blur_shader = ccalloc(max2(2, nkernels), struct gl_shader); + + char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); + // Enforce LC_NUMERIC locale "C" here to make sure decimal point is sane + // Thanks to hiciu for reporting. + setlocale(LC_NUMERIC, "C"); + + // clang-format off + static const char *FRAG_SHADER_BLUR = GLSL(330, + %s\n // other extension pragmas + layout(location = UNIFORM_TEX_SRC_LOC) + uniform sampler2D tex_src; + layout(location = UNIFORM_PIXEL_NORM_LOC) + uniform vec2 pixel_norm; + layout(location = UNIFORM_OPACITY_LOC) + uniform float opacity; + in vec2 texcoord; + out vec4 out_color; + float mask_factor(); + void main() { + vec2 uv = texcoord * pixel_norm; + vec4 sum = vec4(0.0, 0.0, 0.0, 0.0); + %s //body of the convolution + out_color = sum / float(%.7g) * opacity * mask_factor(); + } + ); + static const char *FRAG_SHADER_BLUR_ADD = QUOTE( + sum += float(%.7g) * texture2D(tex_src, uv + pixel_norm * vec2(%.7g, %.7g)); + ); + // clang-format on + + const char *shader_add = FRAG_SHADER_BLUR_ADD; + char *extension = strdup(""); + + for (int i = 0; i < nkernels; i++) { + auto kern = kernels[i]; + // Build shader + int width = kern->w, height = kern->h; + int nele = width * height; + // '%.7g' is at most 14 characters, inserted 3 times + size_t body_len = (strlen(shader_add) + 42) * (uint)nele; + char *shader_body = ccalloc(body_len, char); + char *pc = shader_body; + + // Make use of the linear interpolation hardware by sampling 2 pixels with + // one texture access by sampling between both pixels based on their + // relative weight. Easiest done in a single dimension as 2D bilinear + // filtering would raise additional constraints on the kernels. Therefore + // only use interpolation along the larger dimension. + double sum = 0.0; + if (width > height) { + // use interpolation in x dimension (width) + for (int j = 0; j < height; ++j) { + for (int k = 0; k < width; k += 2) { + double val1, val2; + val1 = kern->data[j * width + k]; + val2 = (k + 1 < width) + ? kern->data[j * width + k + 1] + : 0; + + double combined_weight = val1 + val2; + if (combined_weight == 0) { + continue; + } + sum += combined_weight; + + double offset_x = + k + (val2 / combined_weight) - (width / 2); + double offset_y = j - (height / 2); + pc += snprintf( + pc, body_len - (ulong)(pc - shader_body), + shader_add, combined_weight, offset_x, offset_y); + assert(pc < shader_body + body_len); + } + } + } else { + // use interpolation in y dimension (height) + for (int j = 0; j < height; j += 2) { + for (int k = 0; k < width; ++k) { + double val1, val2; + val1 = kern->data[j * width + k]; + val2 = (j + 1 < height) + ? kern->data[(j + 1) * width + k] + : 0; + + double combined_weight = val1 + val2; + if (combined_weight == 0) { + continue; + } + sum += combined_weight; + + double offset_x = k - (width / 2); + double offset_y = + j + (val2 / combined_weight) - (height / 2); + pc += snprintf( + pc, body_len - (ulong)(pc - shader_body), + shader_add, combined_weight, offset_x, offset_y); + assert(pc < shader_body + body_len); + } + } + } + + auto pass = ctx->blur_shader + i; + size_t shader_len = strlen(FRAG_SHADER_BLUR) + strlen(extension) + + strlen(shader_body) + 10 /* sum */ + + 1 /* null terminator */; + char *shader_str = ccalloc(shader_len, char); + auto real_shader_len = snprintf(shader_str, shader_len, FRAG_SHADER_BLUR, + extension, shader_body, sum); + CHECK(real_shader_len >= 0); + CHECK((size_t)real_shader_len < shader_len); + free(shader_body); + + // Build program + pass->prog = gl_create_program_from_strv( + (const char *[]){vertex_shader, NULL}, + (const char *[]){shader_str, masking_glsl, NULL}); + free(shader_str); + if (!pass->prog) { + log_error("Failed to create GLSL program."); + success = false; + goto out; + } + pass->uniform_bitmask = 1 << UNIFORM_PIXEL_NORM_LOC; + glBindFragDataLocation(pass->prog, 0, "out_color"); + + // Setup projection matrix + glUseProgram(pass->prog); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection); + glUseProgram(0); + + ctx->resize_width += kern->w / 2; + ctx->resize_height += kern->h / 2; + } + + if (nkernels == 1) { + // Generate an extra null pass so we don't need special code path for + // the single pass case + auto pass = &ctx->blur_shader[1]; + pass->prog = gl_create_program_from_strv( + (const char *[]){vertex_shader, NULL}, + (const char *[]){blend_with_mask_frag, masking_glsl, NULL}); + + // Setup projection matrix + glUseProgram(pass->prog); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection); + glUseProgram(0); + + ctx->npasses = 2; + } else { + ctx->npasses = nkernels; + } + + success = true; +out: + if (method != BLUR_METHOD_KERNEL) { + // We generated the blur kernels, so we need to free them + for (int i = 0; i < nkernels; i++) { + free(kernels[i]); + } + free(kernels); + } + + free(extension); + // Restore LC_NUMERIC + setlocale(LC_NUMERIC, lc_numeric_old); + free(lc_numeric_old); + + return success; +} + +bool gl_create_dual_kawase_blur_context(void *blur_context, GLfloat *projection, + enum blur_method method, void *args) { + bool success = false; + auto ctx = (struct gl_blur_context *)blur_context; + + ctx->method = method; + + auto blur_params = generate_dual_kawase_params(args); + + // Specify required textures and FBOs + ctx->blur_texture_count = blur_params->iterations; + ctx->blur_fbo_count = blur_params->iterations; + + ctx->resize_width += blur_params->expand; + ctx->resize_height += blur_params->expand; + + ctx->npasses = 2; + ctx->blur_shader = ccalloc(ctx->npasses, struct gl_shader); + + char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); + // Enforce LC_NUMERIC locale "C" here to make sure decimal point is sane + // Thanks to hiciu for reporting. + setlocale(LC_NUMERIC, "C"); + + // Dual-kawase downsample shader / program + auto down_pass = ctx->blur_shader; + { + // clang-format off + static const char *FRAG_SHADER_DOWN = GLSL(330, + layout(location = UNIFORM_TEX_SRC_LOC) + uniform sampler2D tex_src; + layout(location = UNIFORM_SCALE_LOC) + uniform float scale = 1.0; + layout(location = UNIFORM_PIXEL_NORM_LOC) + uniform vec2 pixel_norm; + in vec2 texcoord; + out vec4 out_color; + void main() { + vec2 offset = %.7g * pixel_norm; + vec2 uv = texcoord * pixel_norm * (2.0 / scale); + vec4 sum = texture2D(tex_src, uv) * 4.0; + sum += texture2D(tex_src, uv - vec2(0.5, 0.5) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, 0.5) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, -0.5) * offset); + sum += texture2D(tex_src, uv - vec2(0.5, -0.5) * offset); + out_color = sum / 8.0; + } + ); + // clang-format on + + // Build shader + size_t shader_len = + strlen(FRAG_SHADER_DOWN) + 10 /* offset */ + 1 /* null terminator */; + char *shader_str = ccalloc(shader_len, char); + auto real_shader_len = + snprintf(shader_str, shader_len, FRAG_SHADER_DOWN, blur_params->offset); + CHECK(real_shader_len >= 0); + CHECK((size_t)real_shader_len < shader_len); + + // Build program + down_pass->prog = gl_create_program_from_str(vertex_shader, shader_str); + free(shader_str); + if (!down_pass->prog) { + log_error("Failed to create GLSL program."); + success = false; + goto out; + } + glBindFragDataLocation(down_pass->prog, 0, "out_color"); + + // Setup projection matrix + glUseProgram(down_pass->prog); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection); + glUseProgram(0); + } + + // Dual-kawase upsample shader / program + auto up_pass = ctx->blur_shader + 1; + { + // clang-format off + static const char *FRAG_SHADER_UP = GLSL(330, + layout(location = UNIFORM_TEX_SRC_LOC) + uniform sampler2D tex_src; + layout(location = UNIFORM_SCALE_LOC) + uniform float scale = 1.0; + layout(location = UNIFORM_PIXEL_NORM_LOC) + uniform vec2 pixel_norm; + layout(location = UNIFORM_OPACITY_LOC) + uniform float opacity; + in vec2 texcoord; + out vec4 out_color; + float mask_factor(); + void main() { + vec2 offset = %.7g * pixel_norm; + vec2 uv = texcoord * pixel_norm / (2 * scale); + vec4 sum = texture2D(tex_src, uv + vec2(-1.0, 0.0) * offset); + sum += texture2D(tex_src, uv + vec2(-0.5, 0.5) * offset) * 2.0; + sum += texture2D(tex_src, uv + vec2(0.0, 1.0) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, 0.5) * offset) * 2.0; + sum += texture2D(tex_src, uv + vec2(1.0, 0.0) * offset); + sum += texture2D(tex_src, uv + vec2(0.5, -0.5) * offset) * 2.0; + sum += texture2D(tex_src, uv + vec2(0.0, -1.0) * offset); + sum += texture2D(tex_src, uv + vec2(-0.5, -0.5) * offset) * 2.0; + out_color = sum / 12.0 * opacity * mask_factor(); + } + ); + // clang-format on + + // Build shader + size_t shader_len = + strlen(FRAG_SHADER_UP) + 10 /* offset */ + 1 /* null terminator */; + char *shader_str = ccalloc(shader_len, char); + auto real_shader_len = + snprintf(shader_str, shader_len, FRAG_SHADER_UP, blur_params->offset); + CHECK(real_shader_len >= 0); + CHECK((size_t)real_shader_len < shader_len); + + // Build program + up_pass->prog = gl_create_program_from_strv( + (const char *[]){vertex_shader, NULL}, + (const char *[]){shader_str, masking_glsl, NULL}); + free(shader_str); + if (!up_pass->prog) { + log_error("Failed to create GLSL program."); + success = false; + goto out; + } + glBindFragDataLocation(up_pass->prog, 0, "out_color"); + + // Setup projection matrix + glUseProgram(up_pass->prog); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection); + glUseProgram(0); + } + + success = true; +out: + free(blur_params); + + if (!success) { + ctx = NULL; + } + + // Restore LC_NUMERIC + setlocale(LC_NUMERIC, lc_numeric_old); + free(lc_numeric_old); + + return success; +} + +void *gl_create_blur_context(backend_t *base, enum blur_method method, + enum backend_image_format format, void *args) { + bool success; + auto gd = (struct gl_data *)base; + + auto ctx = ccalloc(1, struct gl_blur_context); + + if (!method || method >= BLUR_METHOD_INVALID) { + ctx->method = BLUR_METHOD_NONE; + return ctx; + } + + // Set projection matrix to gl viewport dimensions so we can use screen + // coordinates for all vertices + // Note: OpenGL matrices are column major + GLint viewport_dimensions[2]; + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); + GLfloat projection_matrix[4][4] = {{2.0F / (GLfloat)viewport_dimensions[0], 0, 0, 0}, + {0, 2.0F / (GLfloat)viewport_dimensions[1], 0, 0}, + {0, 0, 0, 0}, + {-1, -1, 0, 1}}; + + if (method == BLUR_METHOD_DUAL_KAWASE) { + success = gl_create_dual_kawase_blur_context(ctx, projection_matrix[0], + method, args); + } else { + success = + gl_create_kernel_blur_context(ctx, projection_matrix[0], method, args); + } + if (!success || ctx->method == BLUR_METHOD_NONE) { + goto out; + } + + // Texture size will be defined by gl_blur + ctx->blur_textures = ccalloc(ctx->blur_texture_count, GLuint); + ctx->texture_sizes = ccalloc(ctx->blur_texture_count, struct texture_size); + ctx->format = format; + glGenTextures(ctx->blur_texture_count, ctx->blur_textures); + + // Generate FBO and textures when needed + ctx->blur_fbos = ccalloc(ctx->blur_fbo_count, GLuint); + glGenFramebuffers(ctx->blur_fbo_count, ctx->blur_fbos); + + for (int i = 0; i < ctx->blur_fbo_count; ++i) { + if (!ctx->blur_fbos[i]) { + log_error("Failed to generate framebuffer objects for blur"); + success = false; + goto out; + } + } + +out: + if (!success) { + gl_destroy_blur_context(&gd->base, ctx); + ctx = NULL; + } + + gl_check_err(); + return ctx; +} + +void gl_get_blur_size(void *blur_context, int *width, int *height) { + auto ctx = (struct gl_blur_context *)blur_context; + *width = ctx->resize_width; + *height = ctx->resize_height; +} diff --git a/src/backend/gl/egl.c b/src/backend/gl/egl.c new file mode 100644 index 0000000000..8ff0754648 --- /dev/null +++ b/src/backend/gl/egl.c @@ -0,0 +1,392 @@ +// SPDX-License-Identifier: MPL-2.0 +/* + * Copyright (c) 2022 Yuxuan Shui + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "backend/gl/egl.h" +#include "backend/gl/gl_common.h" +#include "common.h" +#include "compiler.h" +#include "config.h" +#include "log.h" +#include "picom.h" +#include "utils.h" +#include "x.h" + +struct egl_data { + struct gl_data gl; + EGLDisplay display; + EGLSurface target_win; + EGLContext ctx; +}; + +const char *eglGetErrorString(EGLint error) { +#define CASE_STR(value) \ + case value: return #value; + switch (error) { + CASE_STR(EGL_SUCCESS) + CASE_STR(EGL_NOT_INITIALIZED) + CASE_STR(EGL_BAD_ACCESS) + CASE_STR(EGL_BAD_ALLOC) + CASE_STR(EGL_BAD_ATTRIBUTE) + CASE_STR(EGL_BAD_CONTEXT) + CASE_STR(EGL_BAD_CONFIG) + CASE_STR(EGL_BAD_CURRENT_SURFACE) + CASE_STR(EGL_BAD_DISPLAY) + CASE_STR(EGL_BAD_SURFACE) + CASE_STR(EGL_BAD_MATCH) + CASE_STR(EGL_BAD_PARAMETER) + CASE_STR(EGL_BAD_NATIVE_PIXMAP) + CASE_STR(EGL_BAD_NATIVE_WINDOW) + CASE_STR(EGL_CONTEXT_LOST) + default: return "Unknown"; + } +#undef CASE_STR +} + +/** + * Free a gl_texture_t. + */ +static void egl_release_image(backend_t *base, struct gl_texture *tex) { + struct egl_data *gd = (void *)base; + EGLImage *p = tex->user_data; + // Release binding + if (p && *p != EGL_NO_IMAGE) { + eglDestroyImage(gd->display, *p); + *p = EGL_NO_IMAGE; + } + + free(p); + tex->user_data = NULL; +} + +/** + * Destroy EGL related resources. + */ +void egl_deinit(backend_t *base) { + struct egl_data *gd = (void *)base; + + gl_deinit(&gd->gl); + + // Destroy EGL context + if (gd->ctx != EGL_NO_CONTEXT) { + eglMakeCurrent(gd->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext(gd->display, gd->ctx); + gd->ctx = EGL_NO_CONTEXT; + } + + if (gd->target_win != EGL_NO_SURFACE) { + eglDestroySurface(gd->display, gd->target_win); + gd->target_win = EGL_NO_SURFACE; + } + + if (gd->display != EGL_NO_DISPLAY) { + eglTerminate(gd->display); + gd->display = EGL_NO_DISPLAY; + } + + free(gd); +} + +static void *egl_decouple_user_data(backend_t *base attr_unused, void *ud attr_unused) { + return NULL; +} + +static bool egl_set_swap_interval(int interval, EGLDisplay dpy) { + return eglSwapInterval(dpy, interval); +} + +struct backend_operations egl_ops; +/** + * Initialize OpenGL. + */ +static backend_t *egl_init(session_t *ps, xcb_window_t target) { + bool success = false; + struct egl_data *gd = NULL; + + // Check if we have the X11 platform + const char *exts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + if (strstr(exts, "EGL_EXT_platform_x11") == NULL) { + log_error("X11 platform not available."); + return NULL; + } + + gd = ccalloc(1, struct egl_data); + gd->display = eglGetPlatformDisplayEXT(EGL_PLATFORM_X11_EXT, ps->c.dpy, + (EGLint[]){ + EGL_PLATFORM_X11_SCREEN_EXT, + ps->c.screen, + EGL_NONE, + }); + if (gd->display == EGL_NO_DISPLAY) { + log_error("Failed to get EGL display."); + goto end; + } + + EGLint major, minor; + if (!eglInitialize(gd->display, &major, &minor)) { + log_error("Failed to initialize EGL."); + goto end; + } + + if (major < 1 || (major == 1 && minor < 5)) { + log_error("EGL version too old, need at least 1.5."); + goto end; + } + + // Check if EGL supports OpenGL + const char *apis = eglQueryString(gd->display, EGL_CLIENT_APIS); + if (strstr(apis, "OpenGL") == NULL) { + log_error("EGL does not support OpenGL."); + goto end; + } + + eglext_init(gd->display); + init_backend_base(&gd->gl.base, ps); + gd->gl.base.ops = &egl_ops; + if (!eglext.has_EGL_KHR_image_pixmap) { + log_error("EGL_KHR_image_pixmap not available."); + goto end; + } + + auto visual_info = x_get_visual_info(&ps->c, ps->c.screen_info->root_visual); + EGLConfig config = NULL; + int nconfigs = 1; + // clang-format off + if (eglChooseConfig(gd->display, + (EGLint[]){ + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_RED_SIZE, visual_info.red_size, + EGL_GREEN_SIZE, visual_info.green_size, + EGL_BLUE_SIZE, visual_info.blue_size, + EGL_ALPHA_SIZE, visual_info.alpha_size, + EGL_STENCIL_SIZE, 1, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE, + }, &config, nconfigs, &nconfigs) != EGL_TRUE) { + log_error("Failed to choose EGL config for the root window."); + goto end; + } + // clang-format on + + gd->target_win = + eglCreatePlatformWindowSurfaceEXT(gd->display, config, &target, NULL); + if (gd->target_win == EGL_NO_SURFACE) { + log_error("Failed to create EGL surface."); + goto end; + } + + if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) { + log_error("Failed to bind OpenGL API."); + goto end; + } + + gd->ctx = eglCreateContext(gd->display, config, NULL, NULL); + if (gd->ctx == EGL_NO_CONTEXT) { + log_error("Failed to get EGL context."); + goto end; + } + + if (!eglMakeCurrent(gd->display, gd->target_win, gd->target_win, gd->ctx)) { + log_error("Failed to attach EGL context."); + goto end; + } + + if (!gl_init(&gd->gl, ps)) { + log_error("Failed to setup OpenGL"); + goto end; + } + if (!gd->gl.has_egl_image_storage) { + log_error("GL_EXT_EGL_image_storage extension not available."); + goto end; + } + + gd->gl.decouple_texture_user_data = egl_decouple_user_data; + gd->gl.release_user_data = egl_release_image; + + if (ps->o.vsync) { + if (!egl_set_swap_interval(1, gd->display)) { + log_error("Failed to enable vsync. %#x", eglGetError()); + } + } else { + egl_set_swap_interval(0, gd->display); + } + + success = true; + +end: + if (!success) { + if (gd != NULL) { + egl_deinit(&gd->gl.base); + } + return NULL; + } + + return &gd->gl.base; +} + +static image_handle +egl_bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt) { + struct egl_data *gd = (void *)base; + EGLImage *eglpixmap = NULL; + + auto r = + xcb_get_geometry_reply(base->c->c, xcb_get_geometry(base->c->c, pixmap), NULL); + if (!r) { + log_error("Invalid pixmap %#010x", pixmap); + return NULL; + } + + log_trace("Binding pixmap %#010x", pixmap); + auto inner = ccalloc(1, struct gl_texture); + inner->format = BACKEND_IMAGE_FORMAT_PIXMAP; + inner->width = r->width; + inner->height = r->height; + free(r); + + log_debug("depth %d", fmt.visual_depth); + + inner->y_inverted = true; + inner->pixmap = pixmap; + + eglpixmap = cmalloc(EGLImage); + *eglpixmap = eglCreateImage(gd->display, EGL_NO_CONTEXT, EGL_NATIVE_PIXMAP_KHR, + (EGLClientBuffer)(uintptr_t)pixmap, NULL); + + if (*eglpixmap == EGL_NO_IMAGE) { + log_error("Failed to create eglpixmap for pixmap %#010x: %s", pixmap, + eglGetErrorString(eglGetError())); + goto err; + } + + log_trace("EGLImage %p", *eglpixmap); + + // Create texture + inner->user_data = eglpixmap; + inner->texture = gl_new_texture(); + glBindTexture(GL_TEXTURE_2D, inner->texture); + glEGLImageTargetTexStorageEXT(GL_TEXTURE_2D, *eglpixmap, NULL); + glBindTexture(GL_TEXTURE_2D, 0); + + gl_check_err(); + return (image_handle)inner; +err: + if (eglpixmap && *eglpixmap) { + eglDestroyImage(gd->display, *eglpixmap); + } + free(eglpixmap); + return NULL; +} + +static bool egl_present(backend_t *base) { + struct egl_data *gd = (void *)base; + gl_finish_render(&gd->gl); + eglSwapBuffers(gd->display, gd->target_win); + return true; +} + +static int egl_buffer_age(backend_t *base) { + if (!eglext.has_EGL_EXT_buffer_age) { + return -1; + } + + struct egl_data *gd = (void *)base; + EGLint val; + eglQuerySurface(gd->display, (EGLSurface)gd->target_win, EGL_BUFFER_AGE_EXT, &val); + return (int)val ?: -1; +} + +static void egl_diagnostics(backend_t *base) { + struct egl_data *gd = (void *)base; + bool warn_software_rendering = false; + const char *software_renderer_names[] = {"llvmpipe", "SWR", "softpipe"}; + auto egl_vendor = eglQueryString(gd->display, EGL_VENDOR); + printf("* Driver vendors:\n"); + printf(" * EGL: %s\n", egl_vendor); + if (eglext.has_EGL_MESA_query_driver) { + printf(" * EGL driver: %s\n", eglGetDisplayDriverName(gd->display)); + } + printf(" * GL: %s\n", glGetString(GL_VENDOR)); + + auto gl_renderer = (const char *)glGetString(GL_RENDERER); + printf("* GL renderer: %s\n", gl_renderer); + if (strstr(egl_vendor, "Mesa")) { + for (size_t i = 0; i < ARR_SIZE(software_renderer_names); i++) { + if (strstr(gl_renderer, software_renderer_names[i]) != NULL) { + warn_software_rendering = true; + break; + } + } + } + + if (warn_software_rendering) { + printf("\n(You are using a software renderer. Unless you are doing this\n" + "intentionally, this means you don't have a graphics driver\n" + "properly installed. Performance will suffer. Please fix this\n" + "before reporting your issue.)\n"); + } +} + +struct backend_operations egl_ops = { + .apply_alpha = gl_apply_alpha, + .back_buffer = gl_back_buffer, + .blit = gl_blit, + .blur = gl_blur, + .bind_pixmap = egl_bind_pixmap, + .clear = gl_clear, + .copy_area = gl_copy_area, + .copy_area_quantize = gl_copy_area_quantize, + .is_format_supported = gl_is_format_supported, + .image_capabilities = gl_image_capabilities, + .new_image = gl_new_image, + .present = egl_present, + .quirks = backend_no_quirks, + .release_image = gl_release_image, + + .init = egl_init, + .deinit = egl_deinit, + .root_change = gl_root_change, + .prepare = gl_prepare, + .buffer_age = egl_buffer_age, + .last_render_time = gl_last_render_time, + .create_blur_context = gl_create_blur_context, + .destroy_blur_context = gl_destroy_blur_context, + .get_blur_size = gl_get_blur_size, + .diagnostics = egl_diagnostics, + .device_status = gl_device_status, + .create_shader = gl_create_window_shader, + .destroy_shader = gl_destroy_window_shader, + .get_shader_attributes = gl_get_shader_attributes, + .max_buffer_age = 5, // Why? +}; + +struct eglext_info eglext = {0}; + +void eglext_init(EGLDisplay dpy) { + if (eglext.initialized) { + return; + } + eglext.initialized = true; +#define check_ext(name) \ + eglext.has_##name = epoxy_has_egl_extension(dpy, #name); \ + log_info("Extension " #name " - %s", eglext.has_##name ? "present" : "absent") + + check_ext(EGL_EXT_buffer_age); + check_ext(EGL_EXT_create_context_robustness); + check_ext(EGL_KHR_image_pixmap); +#ifdef EGL_MESA_query_driver + check_ext(EGL_MESA_query_driver); +#endif +#undef check_ext +} diff --git a/src/backend/gl/egl.h b/src/backend/gl/egl.h new file mode 100644 index 0000000000..033e84da37 --- /dev/null +++ b/src/backend/gl/egl.h @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#pragma once +#include +#include +#include +#include +#include + +#include "compiler.h" +#include "log.h" +#include "utils.h" +#include "x.h" + +struct eglext_info { + bool initialized; + bool has_EGL_MESA_query_driver; + bool has_EGL_EXT_buffer_age; + bool has_EGL_EXT_create_context_robustness; + bool has_EGL_KHR_image_pixmap; +}; + +extern struct eglext_info eglext; + +void eglext_init(EGLDisplay); diff --git a/src/backend/gl/gl_common.c b/src/backend/gl/gl_common.c index 7cb7e6ba2a..7d543561f7 100644 --- a/src/backend/gl/gl_common.c +++ b/src/backend/gl/gl_common.c @@ -1,70 +1,29 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui -#include -#include -#include +#include #include +#include +#include #include #include +#include #include // for xcb_render_fixed_t, XXX #include "backend/backend.h" #include "common.h" #include "compiler.h" #include "config.h" -#include "kernel.h" #include "log.h" #include "region.h" -#include "string_utils.h" #include "types.h" #include "utils.h" #include "backend/backend_common.h" #include "backend/gl/gl_common.h" -#define GLSL(version, ...) "#version " #version "\n" #__VA_ARGS__ -#define QUOTE(...) #__VA_ARGS__ - -static const GLuint vert_coord_loc = 0; -static const GLuint vert_in_texcoord_loc = 1; - -struct gl_blur_context { - enum blur_method method; - gl_blur_shader_t *blur_shader; - - /// Temporary textures used for blurring - GLuint *blur_textures; - int blur_texture_count; - /// Temporary fbos used for blurring - GLuint *blur_fbos; - int blur_fbo_count; - - /// Cached dimensions of each blur_texture. They are the same size as the target, - /// so they are always big enough without resizing. - /// Turns out calling glTexImage to resize is expensive, so we avoid that. - struct texture_size { - int width; - int height; - } * texture_sizes; - - /// Cached dimensions of the offscreen framebuffer. It's the same size as the - /// target but is expanded in either direction by resize_width / resize_height. - int fb_width, fb_height; - - /// How much do we need to resize the damaged region for blurring. - int resize_width, resize_height; - - int npasses; -}; - -static GLint glGetUniformLocationChecked(GLuint p, const char *name) { - auto ret = glGetUniformLocation(p, name); - if (ret < 0) { - log_info("Failed to get location of uniform '%s'. This is normal when " - "using custom shaders.", - name); - } - return ret; +void gl_prepare(backend_t *base, const region_t *reg attr_unused) { + auto gd = (struct gl_data *)base; + glBeginQuery(GL_TIME_ELAPSED, gd->frame_timing[gd->current_frame_timing]); } GLuint gl_create_shader(GLenum shader_type, const char *shader_str) { @@ -83,7 +42,7 @@ GLuint gl_create_shader(GLenum shader_type, const char *shader_str) { { GLint status = GL_FALSE; glGetShaderiv(shader, GL_COMPILE_STATUS, &status); - if (GL_FALSE == status) { + if (status == GL_FALSE) { GLint log_len = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_len); if (log_len) { @@ -103,6 +62,7 @@ GLuint gl_create_shader(GLenum shader_type, const char *shader_str) { glDeleteShader(shader); shader = 0; } + gl_check_err(); return shader; } @@ -115,15 +75,16 @@ GLuint gl_create_program(const GLuint *const shaders, int nshaders) { goto end; } - for (int i = 0; i < nshaders; ++i) + for (int i = 0; i < nshaders; ++i) { glAttachShader(program, shaders[i]); + } glLinkProgram(program); // Get program status { GLint status = GL_FALSE; glGetProgramiv(program, GL_LINK_STATUS, &status); - if (GL_FALSE == status) { + if (status == GL_FALSE) { GLint log_len = 0; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_len); if (log_len) { @@ -138,59 +99,85 @@ GLuint gl_create_program(const GLuint *const shaders, int nshaders) { end: if (program) { - for (int i = 0; i < nshaders; ++i) + for (int i = 0; i < nshaders; ++i) { glDetachShader(program, shaders[i]); + } } if (program && !success) { glDeleteProgram(program); program = 0; } + gl_check_err(); return program; } /** - * @brief Create a program from vertex and fragment shader strings. + * @brief Create a program from NULL-terminated arrays of vertex and fragment shader + * strings. */ -GLuint gl_create_program_from_str(const char *vert_shader_str, const char *frag_shader_str) { - GLuint vert_shader = 0; - GLuint frag_shader = 0; - GLuint prog = 0; - - if (vert_shader_str) - vert_shader = gl_create_shader(GL_VERTEX_SHADER, vert_shader_str); - if (frag_shader_str) - frag_shader = gl_create_shader(GL_FRAGMENT_SHADER, frag_shader_str); +GLuint gl_create_program_from_strv(const char **vert_shaders, const char **frag_shaders) { + int vert_count, frag_count; + for (vert_count = 0; vert_shaders && vert_shaders[vert_count]; ++vert_count) { + } + for (frag_count = 0; frag_shaders && frag_shaders[frag_count]; ++frag_count) { + } - { - GLuint shaders[2]; - int count = 0; - if (vert_shader) { - shaders[count++] = vert_shader; - } - if (frag_shader) { - shaders[count++] = frag_shader; + GLuint prog = 0; + auto shaders = (GLuint *)ccalloc(vert_count + frag_count, GLuint); + for (int i = 0; i < vert_count; ++i) { + shaders[i] = gl_create_shader(GL_VERTEX_SHADER, vert_shaders[i]); + if (shaders[i] == 0) { + goto out; } - if (count) { - prog = gl_create_program(shaders, count); + } + for (int i = 0; i < frag_count; ++i) { + shaders[vert_count + i] = + gl_create_shader(GL_FRAGMENT_SHADER, frag_shaders[i]); + if (shaders[vert_count + i] == 0) { + goto out; } } - if (vert_shader) - glDeleteShader(vert_shader); - if (frag_shader) - glDeleteShader(frag_shader); + prog = gl_create_program(shaders, vert_count + frag_count); + +out: + for (int i = 0; i < vert_count + frag_count; ++i) { + if (shaders[i] != 0) { + glDeleteShader(shaders[i]); + } + } + free(shaders); + gl_check_err(); return prog; } -static void gl_free_prog_main(gl_win_shader_t *pprogram) { - if (!pprogram) +/** + * @brief Create a program from vertex and fragment shader strings. + */ +GLuint gl_create_program_from_str(const char *vert_shader_str, const char *frag_shader_str) { + const char *vert_shaders[2] = {vert_shader_str, NULL}; + const char *frag_shaders[2] = {frag_shader_str, NULL}; + + return gl_create_program_from_strv(vert_shaders, frag_shaders); +} + +static void gl_destroy_window_shader_inner(struct gl_shader *shader) { + if (!shader) { return; - if (pprogram->prog) { - glDeleteProgram(pprogram->prog); - pprogram->prog = 0; } + + if (shader->prog) { + glDeleteProgram(shader->prog); + shader->prog = 0; + } + gl_check_err(); +} + +void gl_destroy_window_shader(backend_t *backend_data attr_unused, void *shader) { + gl_destroy_window_shader_inner(shader); + free(shader); } /* @@ -199,7 +186,7 @@ static void gl_free_prog_main(gl_win_shader_t *pprogram) { * @note In order to reduce number of textures which needs to be * allocated and deleted during this recursive render * we reuse the same two textures for render source and - * destination simply by alterating between them. + * destination simply by alternating between them. * Unfortunately on first iteration source_texture might * be read-only. In this case we will select auxiliary_texture as * destination_texture in order not to touch that read-only source @@ -208,7 +195,7 @@ static void gl_free_prog_main(gl_win_shader_t *pprogram) { * between each other on each render iteration. */ static GLuint -_gl_average_texture_color(backend_t *base, GLuint source_texture, GLuint destination_texture, +_gl_average_texture_color(GLuint source_texture, GLuint destination_texture, GLuint auxiliary_texture, GLuint fbo, int width, int height) { const int max_width = 1; const int max_height = 1; @@ -238,13 +225,14 @@ _gl_average_texture_color(backend_t *base, GLuint source_texture, GLuint destina glBufferSubData(GL_ARRAY_BUFFER, 0, (long)sizeof(*coord) * 16, coord); // Prepare framebuffer for new render iteration - glBindTexture(GL_TEXTURE_2D, destination_texture); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination_texture, 0); gl_check_fb_complete(GL_FRAMEBUFFER); // Bind source texture as downscaling shader uniform input + glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, source_texture); + glBindSampler(0, 0); // Render into framebuffer glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); @@ -255,9 +243,8 @@ _gl_average_texture_color(backend_t *base, GLuint source_texture, GLuint destina GLuint new_source_texture = destination_texture; GLuint new_destination_texture = auxiliary_texture != 0 ? auxiliary_texture : source_texture; - result = _gl_average_texture_color(base, new_source_texture, - new_destination_texture, 0, fbo, - to_width, to_height); + result = _gl_average_texture_color( + new_source_texture, new_destination_texture, 0, fbo, to_width, to_height); } else { result = destination_texture; } @@ -267,44 +254,38 @@ _gl_average_texture_color(backend_t *base, GLuint source_texture, GLuint destina /* * @brief Builds a 1x1 texture which has color corresponding to the average of all - * pixels of img by recursively rendering into texture of quorter the size (half + * pixels of img by recursively rendering into texture of quarter the size (half * width and half height). * Returned texture must not be deleted, since it's owned by the gl_image. It will be * deleted when the gl_image is released. */ -static GLuint gl_average_texture_color(backend_t *base, struct backend_image *img) { - auto gd = (struct gl_data *)base; - auto inner = (struct gl_texture *)img->inner; - +static GLuint gl_average_texture_color(struct gl_data *gd, struct gl_texture *img) { // Prepare textures which will be used for destination and source of rendering // during downscaling. - const int texture_count = ARR_SIZE(inner->auxiliary_texture); - if (!inner->auxiliary_texture[0]) { - assert(!inner->auxiliary_texture[1]); - glGenTextures(texture_count, inner->auxiliary_texture); + const int texture_count = ARR_SIZE(img->auxiliary_texture); + if (!img->auxiliary_texture[0]) { + assert(!img->auxiliary_texture[1]); + glGenTextures(texture_count, img->auxiliary_texture); glActiveTexture(GL_TEXTURE0); for (int i = 0; i < texture_count; i++) { - glBindTexture(GL_TEXTURE_2D, inner->auxiliary_texture[i]); + glBindTexture(GL_TEXTURE_2D, img->auxiliary_texture[i]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, (GLint[]){0, 0, 0, 0}); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, inner->width, - inner->height, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, img->width, img->height, + 0, GL_BGR, GL_UNSIGNED_BYTE, NULL); } } // Prepare framebuffer used for rendering and bind it - GLuint fbo; - glGenFramebuffers(1, &fbo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gd->temp_fbo); glDrawBuffer(GL_COLOR_ATTACHMENT0); // Enable shaders glUseProgram(gd->brightness_shader.prog); - glUniform2f(glGetUniformLocationChecked(gd->brightness_shader.prog, "texsize"), - (GLfloat)inner->width, (GLfloat)inner->height); + glUniform2f(UNIFORM_TEXSIZE_LOC, (GLfloat)img->width, (GLfloat)img->height); // Prepare vertex attributes GLuint vao; @@ -323,14 +304,14 @@ static GLuint gl_average_texture_color(backend_t *base, struct backend_image *im // Allocate buffers for render input GLint coord[16] = {0}; GLuint indices[] = {0, 1, 2, 2, 3, 0}; - glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * 16, coord, GL_DYNAMIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * 16, coord, GL_STREAM_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * 6, indices, - GL_STATIC_DRAW); + GL_STREAM_DRAW); // Do actual recursive render to 1x1 texture GLuint result_texture = _gl_average_texture_color( - base, inner->texture, inner->auxiliary_texture[0], - inner->auxiliary_texture[1], fbo, inner->width, inner->height); + img->texture, img->auxiliary_texture[0], img->auxiliary_texture[1], + gd->temp_fbo, img->width, img->height); // Cleanup vertex attributes glDisableVertexAttribArray(vert_coord_loc); @@ -345,7 +326,6 @@ static GLuint gl_average_texture_color(backend_t *base, struct backend_image *im glUseProgram(0); // Cleanup framebuffers - glDeleteFramebuffers(1, &fbo); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glDrawBuffer(GL_BACK); @@ -356,61 +336,101 @@ static GLuint gl_average_texture_color(backend_t *base, struct backend_image *im return result_texture; } +struct gl_texture_unit { + GLuint texture; + GLuint sampler; +}; + +struct gl_uniform_value { + GLenum type; + union { + struct gl_texture_unit tu; + GLint i; + GLfloat f; + GLint i2[2]; + GLfloat f2[2]; + GLfloat f4[4]; + }; +}; + +struct gl_vertex_attrib { + GLenum type; + GLuint loc; + void *offset; +}; + +struct gl_vertex_attribs_definition { + GLsizeiptr stride; + unsigned count; + struct gl_vertex_attrib attribs[]; +}; + +static const struct gl_vertex_attribs_definition gl_blit_vertex_attribs = { + .stride = sizeof(GLint) * 4, + .count = 2, + .attribs = {{GL_INT, vert_coord_loc, NULL}, + {GL_INT, vert_in_texcoord_loc, ((GLint *)NULL) + 2}}, +}; /** * Render a region with texture data. * - * @param ptex the texture - * @param target the framebuffer to render into - * @param dst_x,dst_y the top left corner of region where this texture - * should go. In OpenGL coordinate system (important!). - * @param reg_tgt the clip region, in Xorg coordinate system - * @param reg_visible ignored + * @param target_fbo the FBO to render into + * @param nrects number of rectangles to render + * @param coord GL vertices + * @param indices GL indices + * @param vert_attribs vertex attributes layout in `coord` + * @param shader shader to use + * @param nuniforms number of uniforms for `shader` + * @param uniforms uniforms for `shader` */ -static void _gl_compose(backend_t *base, struct backend_image *img, GLuint target, - GLint *coord, GLuint *indices, int nrects) { - auto gd = (struct gl_data *)base; - auto inner = (struct gl_texture *)img->inner; - if (!img || !inner->texture) { - log_error("Missing texture."); - return; - } - - GLuint brightness = 0; - if (img->max_brightness < 1.0) { - brightness = gl_average_texture_color(base, img); - } +static void gl_blit_inner(GLuint target_fbo, int nrects, GLint *coord, GLuint *indices, + const struct gl_vertex_attribs_definition *vert_attribs, + const struct gl_shader *shader, int nuniforms, + struct gl_uniform_value *uniforms) { + // FIXME(yshui) breaks when `mask` and `img` doesn't have the same y_inverted + // value. but we don't ever hit this problem because all of our + // images and masks are y_inverted. + log_trace("Blitting %d rectangles", nrects); + assert(shader); + assert(shader->prog); + glUseProgram(shader->prog); + // TEXTURE0 reserved for the default texture + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, 0); + GLuint texture_unit = GL_TEXTURE1; + for (int i = 0; i < nuniforms; i++) { + if (!(shader->uniform_bitmask & (1 << i))) { + continue; + } - assert(gd->win_shader.prog); - glUseProgram(gd->win_shader.prog); - if (gd->win_shader.unifm_opacity >= 0) { - glUniform1f(gd->win_shader.unifm_opacity, (float)img->opacity); - } - if (gd->win_shader.unifm_invert_color >= 0) { - glUniform1i(gd->win_shader.unifm_invert_color, img->color_inverted); - } - if (gd->win_shader.unifm_tex >= 0) { - glUniform1i(gd->win_shader.unifm_tex, 0); - } - if (gd->win_shader.unifm_dim >= 0) { - glUniform1f(gd->win_shader.unifm_dim, (float)img->dim); - } - if (gd->win_shader.unifm_brightness >= 0) { - glUniform1i(gd->win_shader.unifm_brightness, 1); - } - if (gd->win_shader.unifm_max_brightness >= 0) { - glUniform1f(gd->win_shader.unifm_max_brightness, (float)img->max_brightness); + auto uniform = &uniforms[i]; + switch (uniform->type) { + case 0: break; + case GL_TEXTURE_2D: + if (uniform->tu.texture == 0) { + glUniform1i(i, 0); + } else { + glActiveTexture(texture_unit); + glBindTexture(GL_TEXTURE_2D, uniform->tu.texture); + glBindSampler(texture_unit - GL_TEXTURE0, uniform->tu.sampler); + glUniform1i(i, (GLint)(texture_unit - GL_TEXTURE0)); + texture_unit += 1; + } + break; + case GL_INT: glUniform1i(i, uniform->i); break; + case GL_FLOAT: glUniform1f(i, uniform->f); break; + case GL_INT_VEC2: glUniform2iv(i, 1, uniform->i2); break; + case GL_FLOAT_VEC2: glUniform2fv(i, 1, uniform->f2); break; + case GL_FLOAT_VEC4: glUniform4fv(i, 1, uniform->f4); break; + default: assert(false); + } } + gl_check_err(); // log_trace("Draw: %d, %d, %d, %d -> %d, %d (%d, %d) z %d\n", // x, y, width, height, dx, dy, ptex->width, ptex->height, z); - // Bind texture - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, brightness); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, inner->texture); - GLuint vao; glGenVertexArrays(1, &vao); glBindVertexArray(vao); @@ -419,16 +439,17 @@ static void _gl_compose(backend_t *base, struct backend_image *img, GLuint targe glGenBuffers(2, bo); glBindBuffer(GL_ARRAY_BUFFER, bo[0]); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); - glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * nrects * 16, coord, GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, vert_attribs->stride * nrects * 4, coord, GL_STREAM_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * nrects * 6, - indices, GL_STATIC_DRAW); + indices, GL_STREAM_DRAW); + for (ptrdiff_t i = 0; i < vert_attribs->count; i++) { + auto attrib = &vert_attribs->attribs[i]; + glEnableVertexAttribArray(attrib->loc); + glVertexAttribPointer(attrib->loc, 2, attrib->type, GL_FALSE, + (GLsizei)vert_attribs->stride, attrib->offset); + } - glEnableVertexAttribArray(vert_coord_loc); - glEnableVertexAttribArray(vert_in_texcoord_loc); - glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); - glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, - sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target_fbo); glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL); glDisableVertexAttribArray(vert_coord_loc); @@ -437,7 +458,11 @@ static void _gl_compose(backend_t *base, struct backend_image *img, GLuint targe glDeleteVertexArrays(1, &vao); // Cleanup - glBindTexture(GL_TEXTURE_2D, 0); + for (GLuint i = GL_TEXTURE1; i < texture_unit; i++) { + glActiveTexture(i); + glBindTexture(GL_TEXTURE_2D, 0); + } + glActiveTexture(GL_TEXTURE0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glDrawBuffer(GL_BACK); @@ -448,63 +473,26 @@ static void _gl_compose(backend_t *base, struct backend_image *img, GLuint targe glUseProgram(0); gl_check_err(); - - return; } -/// Convert rectangles in X coordinates to OpenGL vertex and texture coordinates -/// @param[in] nrects, rects rectangles -/// @param[in] dst_x, dst_y origin of the OpenGL texture, affect the calculated texture -/// coordinates -/// @param[in] texture_height height of the OpenGL texture -/// @param[in] root_height height of the back buffer -/// @param[in] y_inverted whether the texture is y inverted -/// @param[out] coord, indices output -static void -x_rect_to_coords(int nrects, const rect_t *rects, int dst_x, int dst_y, int texture_height, - int root_height, bool y_inverted, GLint *coord, GLuint *indices) { - dst_y = root_height - dst_y; - if (y_inverted) { - dst_y -= texture_height; - } - - for (int i = 0; i < nrects; i++) { - // Y-flip. Note after this, crect.y1 > crect.y2 - rect_t crect = rects[i]; - crect.y1 = root_height - crect.y1; - crect.y2 = root_height - crect.y2; - - // Calculate texture coordinates - // (texture_x1, texture_y1), texture coord for the _bottom left_ corner - GLint texture_x1 = crect.x1 - dst_x, texture_y1 = crect.y2 - dst_y, - texture_x2 = texture_x1 + (crect.x2 - crect.x1), - texture_y2 = texture_y1 + (crect.y1 - crect.y2); - - // X pixmaps might be Y inverted, invert the texture coordinates - if (y_inverted) { - texture_y1 = texture_height - texture_y1; - texture_y2 = texture_height - texture_y2; - } - - // Vertex coordinates - auto vx1 = crect.x1; - auto vy1 = crect.y2; - auto vx2 = crect.x2; - auto vy2 = crect.y1; - - // log_trace("Rect %d: %f, %f, %f, %f -> %d, %d, %d, %d", - // ri, rx, ry, rxe, rye, rdx, rdy, rdxe, rdye); +void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, GLint *coord, + GLuint *indices) { + for (ptrdiff_t i = 0; i < nrects; i++) { + // Rectangle in source image coordinates + rect_t rect_src = region_translate_rect(rects[i], ivec2_neg(origin)); + // Rectangle in target image coordinates + rect_t rect_dst = rects[i]; memcpy(&coord[i * 16], ((GLint[][2]){ - {vx1, vy1}, - {texture_x1, texture_y1}, - {vx2, vy1}, - {texture_x2, texture_y1}, - {vx2, vy2}, - {texture_x2, texture_y2}, - {vx1, vy2}, - {texture_x1, texture_y2}, + {rect_dst.x1, rect_dst.y1}, // Vertex, bottom-left + {rect_src.x1, rect_src.y1}, // Texture + {rect_dst.x2, rect_dst.y1}, // Vertex, bottom-right + {rect_src.x2, rect_src.y1}, // Texture + {rect_dst.x2, rect_dst.y2}, // Vertex, top-right + {rect_src.x2, rect_src.y2}, // Texture + {rect_dst.x1, rect_dst.y2}, // Vertex, top-left + {rect_src.x1, rect_src.y2}, // Texture }), sizeof(GLint[2]) * 8); @@ -515,397 +503,264 @@ x_rect_to_coords(int nrects, const rect_t *rects, int dst_x, int dst_y, int text } } -// TODO(yshui) make use of reg_visible -void gl_compose(backend_t *base, void *image_data, int dst_x, int dst_y, - const region_t *reg_tgt, const region_t *reg_visible attr_unused) { - auto gd = (struct gl_data *)base; - struct backend_image *img = image_data; - auto inner = (struct gl_texture *)img->inner; +/// Flip the texture coordinates returned by `gl_mask_rects_to_coords` vertically relative +/// to the texture. Target coordinates are unchanged. +/// +/// @param[in] nrects number of rectangles +/// @param[in] coord OpenGL vertex coordinates +/// @param[in] texture_height height of the source image +static inline void gl_y_flip_texture(int nrects, GLint *coord, GLint texture_height) { + for (ptrdiff_t i = 0; i < nrects; i++) { + auto current_rect = &coord[i * 16]; // 16 numbers per rectangle + for (ptrdiff_t j = 0; j < 4; j++) { + // 4 numbers per vertex, texture coordinates are the last two + auto current_vertex = ¤t_rect[j * 4 + 2]; + current_vertex[1] = texture_height - current_vertex[1]; + } + } +} - // Painting +/// Lower `struct backend_blit_args` into a list of GL coordinates, vertex indices, a +/// shader, and uniforms. +static int +gl_lower_blit_args(struct gl_data *gd, ivec2 origin, const struct backend_blit_args *args, + GLint **coord, GLuint **indices, struct gl_shader **shader, + struct gl_uniform_value *uniforms) { + auto img = (struct gl_texture *)args->source_image; int nrects; const rect_t *rects; - rects = pixman_region32_rectangles((region_t *)reg_tgt, &nrects); + rects = pixman_region32_rectangles(args->target_mask, &nrects); if (!nrects) { // Nothing to paint - return; + return 0; + } + *coord = ccalloc(nrects * 16, GLint); + *indices = ccalloc(nrects * 6, GLuint); + gl_mask_rects_to_coords(origin, nrects, rects, *coord, *indices); + if (!img->y_inverted) { + gl_y_flip_texture(nrects, *coord, img->height); } - // Until we start to use glClipControl, reg_tgt, dst_x and dst_y and - // in a different coordinate system than the one OpenGL uses. - // OpenGL window coordinate (or NDC) has the origin at the lower left of the - // screen, with y axis pointing up; Xorg has the origin at the upper left of the - // screen, with y axis pointing down. We have to do some coordinate conversion in - // this function - - auto coord = ccalloc(nrects * 16, GLint); - auto indices = ccalloc(nrects * 6, GLuint); - x_rect_to_coords(nrects, rects, dst_x, dst_y, inner->height, gd->height, - inner->y_inverted, coord, indices); - _gl_compose(base, img, gd->back_fbo, coord, indices, nrects); + auto mask_texture = gd->default_mask_texture; + auto mask_sampler = gd->samplers[GL_SAMPLER_REPEAT]; + if (args->source_mask != NULL) { + mask_texture = ((struct gl_texture *)args->source_mask->image)->texture; + mask_sampler = gd->samplers[GL_SAMPLER_BORDER]; + } + GLuint brightness = 0; // 0 means the default texture, which will be + // incomplete, and sampling from it will return (0, + // 0, 0, 1), which should be fine. + if (args->max_brightness < 1.0) { + brightness = gl_average_texture_color(gd, img); + } + auto border_width = args->border_width; + if (border_width > args->corner_radius) { + border_width = 0; + } + // clang-format off + struct gl_uniform_value from_uniforms[] = { + [UNIFORM_OPACITY_LOC] = {.type = GL_FLOAT, .f = (float)args->opacity}, + [UNIFORM_INVERT_COLOR_LOC] = {.type = GL_INT, .i = args->color_inverted}, + [UNIFORM_TEX_LOC] = {.type = GL_TEXTURE_2D, + .tu = {img->texture, gd->samplers[GL_SAMPLER_REPEAT]}}, + [UNIFORM_EFFECTIVE_SIZE_LOC] = {.type = GL_FLOAT_VEC2, + .f2 = {(float)args->effective_size.width, + (float)args->effective_size.height}}, + [UNIFORM_DIM_LOC] = {.type = GL_FLOAT, .f = (float)args->dim}, + [UNIFORM_BRIGHTNESS_LOC] = {.type = GL_TEXTURE_2D, + .tu = {brightness, gd->samplers[GL_SAMPLER_EDGE]}}, + [UNIFORM_MAX_BRIGHTNESS_LOC] = {.type = GL_FLOAT, .f = (float)args->max_brightness}, + [UNIFORM_CORNER_RADIUS_LOC] = {.type = GL_FLOAT, .f = (float)args->corner_radius}, + [UNIFORM_BORDER_WIDTH_LOC] = {.type = GL_FLOAT, .f = (float)border_width}, + [UNIFORM_MASK_TEX_LOC] = {.type = GL_TEXTURE_2D, + .tu = {mask_texture, mask_sampler}}, + [UNIFORM_MASK_OFFSET_LOC] = {.type = GL_FLOAT_VEC2, .f2 = {0.0F, 0.0F}}, + [UNIFORM_MASK_INVERTED_LOC] = {.type = GL_INT, .i = 0}, + [UNIFORM_MASK_CORNER_RADIUS_LOC] = {.type = GL_FLOAT, .f = 0.0F}, + }; + // clang-format on - free(indices); - free(coord); + if (args->source_mask != NULL) { + from_uniforms[UNIFORM_MASK_OFFSET_LOC].f2[0] = + (float)args->source_mask->origin.x; + from_uniforms[UNIFORM_MASK_OFFSET_LOC].f2[1] = + (float)args->source_mask->origin.y; + from_uniforms[UNIFORM_MASK_CORNER_RADIUS_LOC].f = + (float)args->source_mask->corner_radius; + from_uniforms[UNIFORM_MASK_INVERTED_LOC].i = args->source_mask->inverted; + } + *shader = args->shader ?: &gd->default_shader; + if ((*shader)->uniform_bitmask & (1 << UNIFORM_TIME_LOC)) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + from_uniforms[UNIFORM_TIME_LOC] = (struct gl_uniform_value){ + .type = GL_FLOAT, + .f = (float)ts.tv_sec * 1000.0F + (float)ts.tv_nsec / 1.0e6F, + }; + } + memcpy(uniforms, from_uniforms, sizeof(from_uniforms)); + return nrects; } -/** - * Blur contents in a particular region. - */ -bool gl_kernel_blur(backend_t *base, double opacity, void *ctx, const rect_t *extent, - const GLuint vao[2], const int vao_nelems[2]) { - auto bctx = (struct gl_blur_context *)ctx; +bool gl_blit(backend_t *base, ivec2 origin, image_handle target_, + const struct backend_blit_args *args) { auto gd = (struct gl_data *)base; + auto source = (struct gl_texture *)args->source_image; + auto target = (struct gl_texture *)target_; - int dst_y_fb_coord = bctx->fb_height - extent->y2; - - int curr = 0; - for (int i = 0; i < bctx->npasses; ++i) { - const gl_blur_shader_t *p = &bctx->blur_shader[i]; - assert(p->prog); - - assert(bctx->blur_textures[curr]); - - // The origin to use when sampling from the source texture - GLint texorig_x = extent->x1, texorig_y = dst_y_fb_coord; - GLint tex_width, tex_height; - GLuint src_texture; - - if (i == 0) { - src_texture = gd->back_texture; - tex_width = gd->width; - tex_height = gd->height; - } else { - src_texture = bctx->blur_textures[curr]; - auto src_size = bctx->texture_sizes[curr]; - tex_width = src_size.width; - tex_height = src_size.height; - } - - glBindTexture(GL_TEXTURE_2D, src_texture); - glUseProgram(p->prog); - glUniform2f(p->unifm_pixel_norm, 1.0f / (GLfloat)tex_width, - 1.0f / (GLfloat)tex_height); - - // The number of indices in the selected vertex array - GLsizei nelems; - - if (i < bctx->npasses - 1) { - assert(bctx->blur_fbos[0]); - assert(bctx->blur_textures[!curr]); - - // not last pass, draw into framebuffer, with resized regions - glBindVertexArray(vao[1]); - nelems = vao_nelems[1]; - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[0]); - - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - GL_TEXTURE_2D, bctx->blur_textures[!curr], 0); - glDrawBuffer(GL_COLOR_ATTACHMENT0); - if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { - return false; - } + if (source == &gd->back_image) { + log_error("Trying to blit from the back texture, this is not allowed"); + return false; + } - glUniform1f(p->unifm_opacity, 1.0); - } else { - // last pass, draw directly into the back buffer, with origin - // regions - glBindVertexArray(vao[0]); - nelems = vao_nelems[0]; - glBindFramebuffer(GL_FRAMEBUFFER, gd->back_fbo); + GLint *coord; + GLuint *indices; + struct gl_shader *shader; + struct gl_uniform_value uniforms[NUMBER_OF_UNIFORMS] = {}; + int nrects = + gl_lower_blit_args(gd, origin, args, &coord, &indices, &shader, uniforms); + if (nrects == 0) { + return true; + } + if (!target->y_inverted) { + log_trace("Flipping target texture"); + gl_y_flip_target(nrects, coord, target->height); + } - glUniform1f(p->unifm_opacity, (float)opacity); - } + auto fbo = gl_bind_image_to_fbo(gd, target_); + // X pixmap is in premultiplied alpha, so we might just as well use it too. + // Thanks to derhass for help. + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + gl_blit_inner(fbo, nrects, coord, indices, &gl_blit_vertex_attribs, shader, + NUMBER_OF_UNIFORMS, uniforms); - glUniform2f(p->texorig_loc, (GLfloat)texorig_x, (GLfloat)texorig_y); - glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + free(indices); + free(coord); + return true; +} - // XXX use multiple draw calls is probably going to be slow than - // just simply blur the whole area. +/// Copy areas by glBlitFramebuffer. This is only used to copy data from the back +/// buffer. +static bool gl_copy_area_blit_fbo(struct gl_data *gd, ivec2 origin, image_handle target, + const region_t *region) { + gl_bind_image_to_fbo(gd, target); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); - curr = !curr; + int nrects; + const rect_t *rects = pixman_region32_rectangles(region, &nrects); + for (ptrdiff_t i = 0; i < nrects; i++) { + // Remember GL back buffer is Y-up, but the destination image is only + // allowed to be Y-down + // clang-format off + glBlitFramebuffer( + rects[i].x1 , gd->back_image.height - rects[i].y1, + rects[i].x2 , gd->back_image.height - rects[i].y2, + rects[i].x1 + origin.x, rects[i].y1 + origin.y , + rects[i].x2 + origin.x, rects[i].y2 + origin.y , + GL_COLOR_BUFFER_BIT, GL_NEAREST); + // clang-format on } - + gl_check_err(); return true; } -bool gl_dual_kawase_blur(backend_t *base, double opacity, void *ctx, const rect_t *extent, - const GLuint vao[2], const int vao_nelems[2]) { - auto bctx = (struct gl_blur_context *)ctx; - auto gd = (struct gl_data *)base; - - int dst_y_fb_coord = bctx->fb_height - extent->y2; +/// Copy areas by drawing. This is the common path to copy from one texture to another. +static bool gl_copy_area_draw(struct gl_data *gd, ivec2 origin, + image_handle target_handle, image_handle source_handle, + struct gl_shader *shader, const region_t *region) { + auto source = (struct gl_texture *)source_handle; + auto target = (struct gl_texture *)target_handle; + assert(source->y_inverted); - int iterations = bctx->blur_texture_count; - int scale_factor = 1; - - // Kawase downsample pass - const gl_blur_shader_t *down_pass = &bctx->blur_shader[0]; - assert(down_pass->prog); - glUseProgram(down_pass->prog); - - glUniform2f(down_pass->texorig_loc, (GLfloat)extent->x1, (GLfloat)dst_y_fb_coord); - - for (int i = 0; i < iterations; ++i) { - // Scale output width / height by half in each iteration - scale_factor <<= 1; - - GLuint src_texture; - int tex_width, tex_height; - - if (i == 0) { - // first pass: copy from back buffer - src_texture = gd->back_texture; - tex_width = gd->width; - tex_height = gd->height; - } else { - // copy from previous pass - src_texture = bctx->blur_textures[i - 1]; - auto src_size = bctx->texture_sizes[i - 1]; - tex_width = src_size.width; - tex_height = src_size.height; - } - - assert(src_texture); - assert(bctx->blur_fbos[i]); - - glBindTexture(GL_TEXTURE_2D, src_texture); - glBindVertexArray(vao[1]); - auto nelems = vao_nelems[1]; - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i]); - glDrawBuffer(GL_COLOR_ATTACHMENT0); - - glUniform1f(down_pass->scale_loc, (GLfloat)scale_factor); - - glUniform2f(down_pass->unifm_pixel_norm, 1.0f / (GLfloat)tex_width, - 1.0f / (GLfloat)tex_height); - - glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + int nrects; + const rect_t *rects = pixman_region32_rectangles(region, &nrects); + if (nrects == 0) { + return true; } - // Kawase upsample pass - const gl_blur_shader_t *up_pass = &bctx->blur_shader[1]; - assert(up_pass->prog); - glUseProgram(up_pass->prog); - - glUniform2f(up_pass->texorig_loc, (GLfloat)extent->x1, (GLfloat)dst_y_fb_coord); - - for (int i = iterations - 1; i >= 0; --i) { - // Scale output width / height back by two in each iteration - scale_factor >>= 1; - - const GLuint src_texture = bctx->blur_textures[i]; - assert(src_texture); - - // Calculate normalized half-width/-height of a src pixel - auto src_size = bctx->texture_sizes[i]; - int tex_width = src_size.width; - int tex_height = src_size.height; - - // The number of indices in the selected vertex array - GLsizei nelems; - - glBindTexture(GL_TEXTURE_2D, src_texture); - if (i > 0) { - assert(bctx->blur_fbos[i - 1]); - - // not last pass, draw into next framebuffer - glBindVertexArray(vao[1]); - nelems = vao_nelems[1]; - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i - 1]); - glDrawBuffer(GL_COLOR_ATTACHMENT0); - - glUniform1f(up_pass->unifm_opacity, (GLfloat)1); - } else { - // last pass, draw directly into the back buffer - glBindVertexArray(vao[0]); - nelems = vao_nelems[0]; - glBindFramebuffer(GL_FRAMEBUFFER, gd->back_fbo); - - glUniform1f(up_pass->unifm_opacity, (GLfloat)opacity); - } - - glUniform1f(up_pass->scale_loc, (GLfloat)scale_factor); - glUniform2f(up_pass->unifm_pixel_norm, 1.0f / (GLfloat)tex_width, - 1.0f / (GLfloat)tex_height); - - glDrawElements(GL_TRIANGLES, nelems, GL_UNSIGNED_INT, NULL); + auto coord = ccalloc(16 * nrects, GLint); + auto indices = ccalloc(6 * nrects, GLuint); + gl_mask_rects_to_coords(origin, nrects, rects, coord, indices); + if (!target->y_inverted) { + gl_y_flip_target(nrects, coord, target->height); } + struct gl_uniform_value uniforms[] = { + [UNIFORM_TEX_LOC] = {.type = GL_TEXTURE_2D, + .tu = {source->texture, gd->samplers[GL_SAMPLER_EDGE]}}, + }; + auto fbo = gl_bind_image_to_fbo(gd, target_handle); + glBlendFunc(GL_ONE, GL_ZERO); + gl_blit_inner(fbo, nrects, coord, indices, &gl_blit_vertex_attribs, shader, + ARR_SIZE(uniforms), uniforms); + free(indices); + free(coord); return true; } -bool gl_blur(backend_t *base, double opacity, void *ctx, const region_t *reg_blur, - const region_t *reg_visible attr_unused) { - auto bctx = (struct gl_blur_context *)ctx; - auto gd = (struct gl_data *)base; - - bool ret = false; - - if (gd->width != bctx->fb_width || gd->height != bctx->fb_height) { - // Resize the temporary textures used for blur in case the root - // size changed - bctx->fb_width = gd->width; - bctx->fb_height = gd->height; - - for (int i = 0; i < bctx->blur_texture_count; ++i) { - auto tex_size = bctx->texture_sizes + i; - if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { - // Use smaller textures for each iteration (quarter of the - // previous texture) - tex_size->width = 1 + ((bctx->fb_width - 1) >> (i + 1)); - tex_size->height = 1 + ((bctx->fb_height - 1) >> (i + 1)); - } else { - tex_size->width = bctx->fb_width; - tex_size->height = bctx->fb_height; - } - - glBindTexture(GL_TEXTURE_2D, bctx->blur_textures[i]); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, tex_size->width, - tex_size->height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); - - if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { - // Attach texture to FBO target - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, bctx->blur_fbos[i]); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, - GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - bctx->blur_textures[i], 0); - if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { - glBindFramebuffer(GL_FRAMEBUFFER, 0); - return false; - } - } - } - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); +bool gl_copy_area(backend_t *backend_data, ivec2 origin, image_handle target, + image_handle source, const region_t *region) { + auto gd = (struct gl_data *)backend_data; + if ((struct gl_texture *)source == &gd->back_image) { + return gl_copy_area_blit_fbo(gd, origin, target, region); } + return gl_copy_area_draw(gd, origin, target, source, &gd->copy_area_prog, region); +} - // Remainder: regions are in Xorg coordinates - auto reg_blur_resized = - resize_region(reg_blur, bctx->resize_width, bctx->resize_height); - const rect_t *extent = pixman_region32_extents((region_t *)reg_blur), - *extent_resized = pixman_region32_extents(®_blur_resized); - int width = extent->x2 - extent->x1, height = extent->y2 - extent->y1; - if (width == 0 || height == 0) { - return true; - } +bool gl_copy_area_quantize(backend_t *backend_data, ivec2 origin, image_handle target_handle, + image_handle source_handle, const region_t *region) { + auto gd = (struct gl_data *)backend_data; + auto target = (struct gl_texture *)target_handle; + auto source = (struct gl_texture *)source_handle; + if (source->format != BACKEND_IMAGE_FORMAT_PIXMAP_HIGH || + source->format == target->format) { + return gl_copy_area(backend_data, origin, target_handle, source_handle, region); + } + return gl_copy_area_draw(gd, origin, target_handle, source_handle, + &gd->copy_area_with_dither_prog, region); +} - int nrects, nrects_resized; - const rect_t *rects = pixman_region32_rectangles((region_t *)reg_blur, &nrects), - *rects_resized = - pixman_region32_rectangles(®_blur_resized, &nrects_resized); - if (!nrects || !nrects_resized) { - return true; +uint32_t gl_image_capabilities(backend_t *base, image_handle img) { + auto gd = (struct gl_data *)base; + auto inner = (struct gl_texture *)img; + if (&gd->back_image == inner) { + return BACKEND_IMAGE_CAP_DST; } - - auto coord = ccalloc(nrects * 16, GLint); - auto indices = ccalloc(nrects * 6, GLuint); - x_rect_to_coords(nrects, rects, extent_resized->x1, extent_resized->y2, - bctx->fb_height, gd->height, false, coord, indices); - - auto coord_resized = ccalloc(nrects_resized * 16, GLint); - auto indices_resized = ccalloc(nrects_resized * 6, GLuint); - x_rect_to_coords(nrects_resized, rects_resized, extent_resized->x1, - extent_resized->y2, bctx->fb_height, bctx->fb_height, false, - coord_resized, indices_resized); - pixman_region32_fini(®_blur_resized); - - GLuint vao[2]; - glGenVertexArrays(2, vao); - GLuint bo[4]; - glGenBuffers(4, bo); - - glBindVertexArray(vao[0]); - glBindBuffer(GL_ARRAY_BUFFER, bo[0]); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); - glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * nrects * 16, coord, GL_STATIC_DRAW); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * nrects * 6, - indices, GL_STATIC_DRAW); - glEnableVertexAttribArray(vert_coord_loc); - glEnableVertexAttribArray(vert_in_texcoord_loc); - glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); - glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, - sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); - - glBindVertexArray(vao[1]); - glBindBuffer(GL_ARRAY_BUFFER, bo[2]); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[3]); - glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord_resized) * nrects_resized * 16, - coord_resized, GL_STATIC_DRAW); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, - (long)sizeof(*indices_resized) * nrects_resized * 6, indices_resized, - GL_STATIC_DRAW); - glEnableVertexAttribArray(vert_coord_loc); - glEnableVertexAttribArray(vert_in_texcoord_loc); - glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); - glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, - sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); - - int vao_nelems[2] = {nrects * 6, nrects_resized * 6}; - - if (bctx->method == BLUR_METHOD_DUAL_KAWASE) { - ret = gl_dual_kawase_blur(base, opacity, ctx, extent_resized, vao, vao_nelems); - } else { - ret = gl_kernel_blur(base, opacity, ctx, extent_resized, vao, vao_nelems); + if (inner->user_data) { + // user_data indicates that the image is a bound X pixmap + return BACKEND_IMAGE_CAP_SRC; } - - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glBindTexture(GL_TEXTURE_2D, 0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glDeleteBuffers(4, bo); - glBindVertexArray(0); - glDeleteVertexArrays(2, vao); - glUseProgram(0); - - free(indices); - free(coord); - free(indices_resized); - free(coord_resized); - - gl_check_err(); - return ret; + return BACKEND_IMAGE_CAP_SRC | BACKEND_IMAGE_CAP_DST; } -// clang-format off -const char *vertex_shader = GLSL(330, - uniform mat4 projection; - uniform float scale = 1.0; - uniform vec2 texorig; - layout(location = 0) in vec2 coord; - layout(location = 1) in vec2 in_texcoord; - out vec2 texcoord; - void main() { - gl_Position = projection * vec4(coord, 0, scale); - texcoord = in_texcoord + texorig; - } -); -// clang-format on +bool gl_is_format_supported(backend_t *base attr_unused, + enum backend_image_format format attr_unused) { + return true; +} /** * Load a GLSL main program from shader strings. */ -static int gl_win_shader_from_string(const char *vshader_str, const char *fshader_str, - gl_win_shader_t *ret) { +static bool gl_shader_from_stringv(const char **vshader_strv, const char **fshader_strv, + struct gl_shader *ret) { // Build program - ret->prog = gl_create_program_from_str(vshader_str, fshader_str); + ret->prog = gl_create_program_from_strv(vshader_strv, fshader_strv); if (!ret->prog) { log_error("Failed to create GLSL program."); - return -1; + gl_check_err(); + return false; } - // Get uniform addresses - ret->unifm_opacity = glGetUniformLocationChecked(ret->prog, "opacity"); - ret->unifm_invert_color = glGetUniformLocationChecked(ret->prog, "invert_color"); - ret->unifm_tex = glGetUniformLocationChecked(ret->prog, "tex"); - ret->unifm_dim = glGetUniformLocationChecked(ret->prog, "dim"); - ret->unifm_brightness = glGetUniformLocationChecked(ret->prog, "brightness"); - ret->unifm_max_brightness = - glGetUniformLocationChecked(ret->prog, "max_brightness"); - gl_check_err(); return true; } +void gl_root_change(backend_t *base, session_t *ps) { + auto gd = (struct gl_data *)base; + gl_resize(gd, ps->root_width, ps->root_height); +} + /** * Callback to run on root window size change. */ @@ -913,672 +768,108 @@ void gl_resize(struct gl_data *gd, int width, int height) { GLint viewport_dimensions[2]; glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); - gd->height = height; - gd->width = width; - - assert(viewport_dimensions[0] >= gd->width); - assert(viewport_dimensions[1] >= gd->height); + gd->back_image.height = height; + gd->back_image.width = width; - glBindTexture(GL_TEXTURE_2D, gd->back_texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_BGR, - GL_UNSIGNED_BYTE, NULL); + assert(viewport_dimensions[0] >= gd->back_image.width); + assert(viewport_dimensions[1] >= gd->back_image.height); gl_check_err(); } -// clang-format off -static const char dummy_frag[] = GLSL(330, - uniform sampler2D tex; - in vec2 texcoord; - void main() { - gl_FragColor = texelFetch(tex, ivec2(texcoord.xy), 0); - } -); - -static const char fill_frag[] = GLSL(330, - uniform vec4 color; - void main() { - gl_FragColor = color; - } -); - -static const char fill_vert[] = GLSL(330, - layout(location = 0) in vec2 in_coord; - uniform mat4 projection; - void main() { - gl_Position = projection * vec4(in_coord, 0, 1); - } -); - -static const char interpolating_frag[] = GLSL(330, - uniform sampler2D tex; - in vec2 texcoord; - void main() { - gl_FragColor = vec4(texture2D(tex, vec2(texcoord.xy), 0).rgb, 1); - } -); - -static const char interpolating_vert[] = GLSL(330, - uniform mat4 projection; - uniform vec2 texsize; - layout(location = 0) in vec2 in_coord; - layout(location = 1) in vec2 in_texcoord; - out vec2 texcoord; - void main() { - gl_Position = projection * vec4(in_coord, 0, 1); - texcoord = in_texcoord / texsize; - } -); -// clang-format on - -/// Fill a given region in bound framebuffer. -/// @param[in] y_inverted whether the y coordinates in `clip` should be inverted -static void _gl_fill(backend_t *base, struct color c, const region_t *clip, GLuint target, - int height, bool y_inverted) { - static const GLuint fill_vert_in_coord_loc = 0; - int nrects; - const rect_t *rect = pixman_region32_rectangles((region_t *)clip, &nrects); +xcb_pixmap_t gl_release_image(backend_t *base, image_handle image) { + auto inner = (struct gl_texture *)image; auto gd = (struct gl_data *)base; + if (inner == &gd->back_image) { + return XCB_NONE; + } - GLuint vao; - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); + xcb_pixmap_t pixmap = inner->user_data ? inner->pixmap : XCB_NONE; + if (inner->user_data) { + gd->release_user_data(base, inner); + } + assert(inner->user_data == NULL); - GLuint bo[2]; - glGenBuffers(2, bo); - glUseProgram(gd->fill_shader.prog); - glUniform4f(gd->fill_shader.color_loc, (GLfloat)c.red, (GLfloat)c.green, - (GLfloat)c.blue, (GLfloat)c.alpha); - glEnableVertexAttribArray(fill_vert_in_coord_loc); - glBindBuffer(GL_ARRAY_BUFFER, bo[0]); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); + glDeleteTextures(1, &inner->texture); + glDeleteTextures(2, inner->auxiliary_texture); + free(inner); + gl_check_err(); + return pixmap; +} - auto coord = ccalloc(nrects * 8, GLint); - auto indices = ccalloc(nrects * 6, GLuint); - for (int i = 0; i < nrects; i++) { - GLint y1 = y_inverted ? height - rect[i].y2 : rect[i].y1, - y2 = y_inverted ? height - rect[i].y1 : rect[i].y2; - // clang-format off - memcpy(&coord[i * 8], - ((GLint[][2]){ - {rect[i].x1, y1}, {rect[i].x2, y1}, - {rect[i].x2, y2}, {rect[i].x1, y2}}), - sizeof(GLint[2]) * 4); - // clang-format on - indices[i * 6 + 0] = (GLuint)i * 4 + 0; - indices[i * 6 + 1] = (GLuint)i * 4 + 1; - indices[i * 6 + 2] = (GLuint)i * 4 + 2; - indices[i * 6 + 3] = (GLuint)i * 4 + 2; - indices[i * 6 + 4] = (GLuint)i * 4 + 3; - indices[i * 6 + 5] = (GLuint)i * 4 + 0; - } - glBufferData(GL_ARRAY_BUFFER, nrects * 8 * (long)sizeof(*coord), coord, GL_STREAM_DRAW); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, nrects * 6 * (long)sizeof(*indices), - indices, GL_STREAM_DRAW); +static inline void gl_init_uniform_bitmask(struct gl_shader *shader) { + GLint number_of_uniforms = 0; + glGetProgramiv(shader->prog, GL_ACTIVE_UNIFORMS, &number_of_uniforms); + for (int i = 0; i < number_of_uniforms; i++) { + char name[32]; + glGetActiveUniformName(shader->prog, (GLuint)i, sizeof(name), NULL, name); + GLint loc = glGetUniformLocation(shader->prog, name); + assert(loc >= 0 && loc <= UNIFORM_TEXSIZE_LOC); + shader->uniform_bitmask |= 1U << loc; + } +} - glVertexAttribPointer(fill_vert_in_coord_loc, 2, GL_INT, GL_FALSE, - sizeof(*coord) * 2, (void *)0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target); - glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL); +static bool gl_create_window_shader_inner(struct gl_shader *out_shader, const char *source) { + const char *vert[2] = {vertex_shader, NULL}; + const char *frag[] = {blit_shader_glsl, masking_glsl, source, NULL}; - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glDisableVertexAttribArray(fill_vert_in_coord_loc); - glBindVertexArray(0); - glDeleteVertexArrays(1, &vao); - - glDeleteBuffers(2, bo); - free(indices); - free(coord); - - gl_check_err(); -} - -void gl_fill(backend_t *base, struct color c, const region_t *clip) { - auto gd = (struct gl_data *)base; - return _gl_fill(base, c, clip, gd->back_fbo, gd->height, true); -} - -static void gl_release_image_inner(backend_t *base, struct gl_texture *inner) { - auto gd = (struct gl_data *)base; - gd->release_user_data(base, inner); - assert(inner->user_data == NULL); - - glDeleteTextures(1, &inner->texture); - glDeleteTextures(2, inner->auxiliary_texture); - free(inner); - gl_check_err(); -} - -void gl_release_image(backend_t *base, void *image_data) { - struct backend_image *wd = image_data; - auto inner = (struct gl_texture *)wd->inner; - inner->refcount--; - assert(inner->refcount >= 0); - if (inner->refcount == 0) { - gl_release_image_inner(base, inner); - } - free(wd); -} - -static inline void gl_free_blur_shader(gl_blur_shader_t *shader) { - if (shader->prog) { - glDeleteProgram(shader->prog); - } - - shader->prog = 0; -} - -void gl_destroy_blur_context(backend_t *base attr_unused, void *ctx) { - auto bctx = (struct gl_blur_context *)ctx; - // Free GLSL shaders/programs - for (int i = 0; i < bctx->npasses; ++i) { - gl_free_blur_shader(&bctx->blur_shader[i]); - } - free(bctx->blur_shader); - - if (bctx->blur_texture_count && bctx->blur_textures) { - glDeleteTextures(bctx->blur_texture_count, bctx->blur_textures); - free(bctx->blur_textures); - } - if (bctx->blur_texture_count && bctx->texture_sizes) { - free(bctx->texture_sizes); - } - if (bctx->blur_fbo_count && bctx->blur_fbos) { - glDeleteFramebuffers(bctx->blur_fbo_count, bctx->blur_fbos); - free(bctx->blur_fbos); - } - - bctx->blur_texture_count = 0; - bctx->blur_fbo_count = 0; - - free(bctx); - - gl_check_err(); -} - -/** - * Initialize GL blur filters. - */ -bool gl_create_kernel_blur_context(void *blur_context, GLfloat *projection, - enum blur_method method, void *args) { - bool success = false; - auto ctx = (struct gl_blur_context *)blur_context; - - struct conv **kernels; - - int nkernels; - ctx->method = BLUR_METHOD_KERNEL; - if (method == BLUR_METHOD_KERNEL) { - nkernels = ((struct kernel_blur_args *)args)->kernel_count; - kernels = ((struct kernel_blur_args *)args)->kernels; - } else { - kernels = generate_blur_kernel(method, args, &nkernels); - } - - if (!nkernels) { - ctx->method = BLUR_METHOD_NONE; - return true; - } - - // Specify required textures and FBOs - ctx->blur_texture_count = 2; - ctx->blur_fbo_count = 1; - - ctx->blur_shader = ccalloc(max2(2, nkernels), gl_blur_shader_t); - - char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); - // Enforce LC_NUMERIC locale "C" here to make sure decimal point is sane - // Thanks to hiciu for reporting. - setlocale(LC_NUMERIC, "C"); - - // clang-format off - static const char *FRAG_SHADER_BLUR = GLSL(330, - %s\n // other extension pragmas - uniform sampler2D tex_src; - uniform vec2 pixel_norm; - uniform float opacity; - in vec2 texcoord; - out vec4 out_color; - void main() { - vec2 uv = texcoord * pixel_norm; - vec4 sum = vec4(0.0, 0.0, 0.0, 0.0); - %s //body of the convolution - out_color = sum / float(%.7g) * opacity; - } - ); - static const char *FRAG_SHADER_BLUR_ADD = QUOTE( - sum += float(%.7g) * texture2D(tex_src, uv + pixel_norm * vec2(%.7g, %.7g)); - ); - // clang-format on - - const char *shader_add = FRAG_SHADER_BLUR_ADD; - char *extension = strdup(""); - - for (int i = 0; i < nkernels; i++) { - auto kern = kernels[i]; - // Build shader - int width = kern->w, height = kern->h; - int nele = width * height; - // '%.7g' is at most 14 characters, inserted 3 times - size_t body_len = (strlen(shader_add) + 42) * (uint)nele; - char *shader_body = ccalloc(body_len, char); - char *pc = shader_body; - - // Make use of the linear interpolation hardware by sampling 2 pixels with - // one texture access by sampling between both pixels based on their - // relative weight. Easiest done in a single dimension as 2D bilinear - // filtering would raise additional constraints on the kernels. Therefore - // only use interpolation along the larger dimension. - double sum = 0.0; - if (width > height) { - // use interpolation in x dimension (width) - for (int j = 0; j < height; ++j) { - for (int k = 0; k < width; k += 2) { - double val1, val2; - val1 = kern->data[j * width + k]; - val2 = (k + 1 < width) - ? kern->data[j * width + k + 1] - : 0; - - double combined_weight = val1 + val2; - if (combined_weight == 0) { - continue; - } - sum += combined_weight; - - double offset_x = - k + (val2 / combined_weight) - (width / 2); - double offset_y = j - (height / 2); - pc += snprintf( - pc, body_len - (ulong)(pc - shader_body), - shader_add, combined_weight, offset_x, offset_y); - assert(pc < shader_body + body_len); - } - } - } else { - // use interpolation in y dimension (height) - for (int j = 0; j < height; j += 2) { - for (int k = 0; k < width; ++k) { - double val1, val2; - val1 = kern->data[j * width + k]; - val2 = (j + 1 < height) - ? kern->data[(j + 1) * width + k] - : 0; - - double combined_weight = val1 + val2; - if (combined_weight == 0) { - continue; - } - sum += combined_weight; - - double offset_x = k - (width / 2); - double offset_y = - j + (val2 / combined_weight) - (height / 2); - pc += snprintf( - pc, body_len - (ulong)(pc - shader_body), - shader_add, combined_weight, offset_x, offset_y); - assert(pc < shader_body + body_len); - } - } - } - - auto pass = ctx->blur_shader + i; - size_t shader_len = strlen(FRAG_SHADER_BLUR) + strlen(extension) + - strlen(shader_body) + 10 /* sum */ + - 1 /* null terminator */; - char *shader_str = ccalloc(shader_len, char); - auto real_shader_len = snprintf(shader_str, shader_len, FRAG_SHADER_BLUR, - extension, shader_body, sum); - CHECK(real_shader_len >= 0); - CHECK((size_t)real_shader_len < shader_len); - free(shader_body); - - // Build program - pass->prog = gl_create_program_from_str(vertex_shader, shader_str); - free(shader_str); - if (!pass->prog) { - log_error("Failed to create GLSL program."); - success = false; - goto out; - } - glBindFragDataLocation(pass->prog, 0, "out_color"); - - // Get uniform addresses - pass->unifm_pixel_norm = - glGetUniformLocationChecked(pass->prog, "pixel_norm"); - pass->unifm_opacity = glGetUniformLocationChecked(pass->prog, "opacity"); - pass->texorig_loc = glGetUniformLocationChecked(pass->prog, "texorig"); - - // Setup projection matrix - glUseProgram(pass->prog); - int pml = glGetUniformLocationChecked(pass->prog, "projection"); - glUniformMatrix4fv(pml, 1, false, projection); - glUseProgram(0); - - ctx->resize_width += kern->w / 2; - ctx->resize_height += kern->h / 2; - } - - if (nkernels == 1) { - // Generate an extra null pass so we don't need special code path for - // the single pass case - auto pass = &ctx->blur_shader[1]; - pass->prog = gl_create_program_from_str(vertex_shader, dummy_frag); - pass->unifm_pixel_norm = -1; - pass->unifm_opacity = -1; - pass->texorig_loc = glGetUniformLocationChecked(pass->prog, "texorig"); - - // Setup projection matrix - glUseProgram(pass->prog); - int pml = glGetUniformLocationChecked(pass->prog, "projection"); - glUniformMatrix4fv(pml, 1, false, projection); - glUseProgram(0); - - ctx->npasses = 2; - } else { - ctx->npasses = nkernels; - } - - success = true; -out: - if (method != BLUR_METHOD_KERNEL) { - // We generated the blur kernels, so we need to free them - for (int i = 0; i < nkernels; i++) { - free(kernels[i]); - } - free(kernels); - } - - free(extension); - // Restore LC_NUMERIC - setlocale(LC_NUMERIC, lc_numeric_old); - free(lc_numeric_old); - - return success; -} - -bool gl_create_dual_kawase_blur_context(void *blur_context, GLfloat *projection, - enum blur_method method, void *args) { - bool success = false; - auto ctx = (struct gl_blur_context *)blur_context; - - ctx->method = method; - - auto blur_params = generate_dual_kawase_params(args); - - // Specify required textures and FBOs - ctx->blur_texture_count = blur_params->iterations; - ctx->blur_fbo_count = blur_params->iterations; - - ctx->resize_width += blur_params->expand; - ctx->resize_height += blur_params->expand; - - ctx->npasses = 2; - ctx->blur_shader = ccalloc(ctx->npasses, gl_blur_shader_t); - - char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); - // Enforce LC_NUMERIC locale "C" here to make sure decimal point is sane - // Thanks to hiciu for reporting. - setlocale(LC_NUMERIC, "C"); - - // Dual-kawase downsample shader / program - auto down_pass = ctx->blur_shader; - { - // clang-format off - static const char *FRAG_SHADER_DOWN = GLSL(330, - uniform sampler2D tex_src; - uniform float scale = 1.0; - uniform vec2 pixel_norm; - in vec2 texcoord; - out vec4 out_color; - void main() { - vec2 offset = %.7g * pixel_norm; - vec2 uv = texcoord * pixel_norm * (2.0 / scale); - vec4 sum = texture2D(tex_src, uv) * 4.0; - sum += texture2D(tex_src, uv - vec2(0.5, 0.5) * offset); - sum += texture2D(tex_src, uv + vec2(0.5, 0.5) * offset); - sum += texture2D(tex_src, uv + vec2(0.5, -0.5) * offset); - sum += texture2D(tex_src, uv - vec2(0.5, -0.5) * offset); - out_color = sum / 8.0; - } - ); - // clang-format on - - // Build shader - size_t shader_len = - strlen(FRAG_SHADER_DOWN) + 10 /* offset */ + 1 /* null terminator */; - char *shader_str = ccalloc(shader_len, char); - auto real_shader_len = - snprintf(shader_str, shader_len, FRAG_SHADER_DOWN, blur_params->offset); - CHECK(real_shader_len >= 0); - CHECK((size_t)real_shader_len < shader_len); - - // Build program - down_pass->prog = gl_create_program_from_str(vertex_shader, shader_str); - free(shader_str); - if (!down_pass->prog) { - log_error("Failed to create GLSL program."); - success = false; - goto out; - } - glBindFragDataLocation(down_pass->prog, 0, "out_color"); - - // Get uniform addresses - down_pass->unifm_pixel_norm = - glGetUniformLocationChecked(down_pass->prog, "pixel_norm"); - down_pass->texorig_loc = - glGetUniformLocationChecked(down_pass->prog, "texorig"); - down_pass->scale_loc = - glGetUniformLocationChecked(down_pass->prog, "scale"); - - // Setup projection matrix - glUseProgram(down_pass->prog); - int pml = glGetUniformLocationChecked(down_pass->prog, "projection"); - glUniformMatrix4fv(pml, 1, false, projection); - glUseProgram(0); - } - - // Dual-kawase upsample shader / program - auto up_pass = ctx->blur_shader + 1; - { - // clang-format off - static const char *FRAG_SHADER_UP = GLSL(330, - uniform sampler2D tex_src; - uniform float scale = 1.0; - uniform vec2 pixel_norm; - uniform float opacity; - in vec2 texcoord; - out vec4 out_color; - void main() { - vec2 offset = %.7g * pixel_norm; - vec2 uv = texcoord * pixel_norm / (2 * scale); - vec4 sum = texture2D(tex_src, uv + vec2(-1.0, 0.0) * offset); - sum += texture2D(tex_src, uv + vec2(-0.5, 0.5) * offset) * 2.0; - sum += texture2D(tex_src, uv + vec2(0.0, 1.0) * offset); - sum += texture2D(tex_src, uv + vec2(0.5, 0.5) * offset) * 2.0; - sum += texture2D(tex_src, uv + vec2(1.0, 0.0) * offset); - sum += texture2D(tex_src, uv + vec2(0.5, -0.5) * offset) * 2.0; - sum += texture2D(tex_src, uv + vec2(0.0, -1.0) * offset); - sum += texture2D(tex_src, uv + vec2(-0.5, -0.5) * offset) * 2.0; - out_color = sum / 12.0 * opacity; - } - ); - // clang-format on - - // Build shader - size_t shader_len = - strlen(FRAG_SHADER_UP) + 10 /* offset */ + 1 /* null terminator */; - char *shader_str = ccalloc(shader_len, char); - auto real_shader_len = - snprintf(shader_str, shader_len, FRAG_SHADER_UP, blur_params->offset); - CHECK(real_shader_len >= 0); - CHECK((size_t)real_shader_len < shader_len); - - // Build program - up_pass->prog = gl_create_program_from_str(vertex_shader, shader_str); - free(shader_str); - if (!up_pass->prog) { - log_error("Failed to create GLSL program."); - success = false; - goto out; - } - glBindFragDataLocation(up_pass->prog, 0, "out_color"); - - // Get uniform addresses - up_pass->unifm_pixel_norm = - glGetUniformLocationChecked(up_pass->prog, "pixel_norm"); - up_pass->unifm_opacity = - glGetUniformLocationChecked(up_pass->prog, "opacity"); - up_pass->texorig_loc = - glGetUniformLocationChecked(up_pass->prog, "texorig"); - up_pass->scale_loc = glGetUniformLocationChecked(up_pass->prog, "scale"); - - // Setup projection matrix - glUseProgram(up_pass->prog); - int pml = glGetUniformLocationChecked(up_pass->prog, "projection"); - glUniformMatrix4fv(pml, 1, false, projection); - glUseProgram(0); - } - - success = true; -out: - free(blur_params); - - if (!success) { - ctx = NULL; + if (!gl_shader_from_stringv(vert, frag, out_shader)) { + return false; } - // Restore LC_NUMERIC - setlocale(LC_NUMERIC, lc_numeric_old); - free(lc_numeric_old); - - return success; -} - -void *gl_create_blur_context(backend_t *base, enum blur_method method, void *args) { - bool success; - auto gd = (struct gl_data *)base; - - auto ctx = ccalloc(1, struct gl_blur_context); - - if (!method || method >= BLUR_METHOD_INVALID) { - ctx->method = BLUR_METHOD_NONE; - return ctx; - } + GLint viewport_dimensions[2]; + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); // Set projection matrix to gl viewport dimensions so we can use screen // coordinates for all vertices // Note: OpenGL matrices are column major - GLint viewport_dimensions[2]; - glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); - GLfloat projection_matrix[4][4] = {{2.0f / (GLfloat)viewport_dimensions[0], 0, 0, 0}, - {0, 2.0f / (GLfloat)viewport_dimensions[1], 0, 0}, + GLfloat projection_matrix[4][4] = {{2.0F / (GLfloat)viewport_dimensions[0], 0, 0, 0}, + {0, 2.0F / (GLfloat)viewport_dimensions[1], 0, 0}, {0, 0, 0, 0}, {-1, -1, 0, 1}}; - if (method == BLUR_METHOD_DUAL_KAWASE) { - success = gl_create_dual_kawase_blur_context(ctx, projection_matrix[0], - method, args); - } else { - success = - gl_create_kernel_blur_context(ctx, projection_matrix[0], method, args); - } - if (!success || ctx->method == BLUR_METHOD_NONE) { - goto out; - } - - // Texture size will be defined by gl_blur - ctx->blur_textures = ccalloc(ctx->blur_texture_count, GLuint); - ctx->texture_sizes = ccalloc(ctx->blur_texture_count, struct texture_size); - glGenTextures(ctx->blur_texture_count, ctx->blur_textures); - - for (int i = 0; i < ctx->blur_texture_count; ++i) { - glBindTexture(GL_TEXTURE_2D, ctx->blur_textures[i]); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - - // Generate FBO and textures when needed - ctx->blur_fbos = ccalloc(ctx->blur_fbo_count, GLuint); - glGenFramebuffers(ctx->blur_fbo_count, ctx->blur_fbos); - - for (int i = 0; i < ctx->blur_fbo_count; ++i) { - if (!ctx->blur_fbos[i]) { - log_error("Failed to generate framebuffer objects for blur"); - success = false; - goto out; - } - } - -out: - if (!success) { - gl_destroy_blur_context(&gd->base, ctx); - ctx = NULL; - } + glUseProgram(out_shader->prog); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection_matrix[0]); + glUseProgram(0); + gl_init_uniform_bitmask(out_shader); gl_check_err(); - return ctx; -} -void gl_get_blur_size(void *blur_context, int *width, int *height) { - auto ctx = (struct gl_blur_context *)blur_context; - *width = ctx->resize_width; - *height = ctx->resize_height; + return true; } -// clang-format off -const char *win_shader_glsl = GLSL(330, - uniform float opacity; - uniform float dim; - uniform bool invert_color; - in vec2 texcoord; - uniform sampler2D tex; - uniform sampler2D brightness; - uniform float max_brightness; - - void main() { - vec4 c = texelFetch(tex, ivec2(texcoord), 0); - if (invert_color) { - c = vec4(c.aaa - c.rgb, c.a); - } - c = vec4(c.rgb * (1.0 - dim), c.a) * opacity; - - vec3 rgb_brightness = texelFetch(brightness, ivec2(0, 0), 0).rgb; - // Ref: https://en.wikipedia.org/wiki/Relative_luminance - float brightness = rgb_brightness.r * 0.21 + - rgb_brightness.g * 0.72 + - rgb_brightness.b * 0.07; - if (brightness > max_brightness) - c.rgb = c.rgb * (max_brightness / brightness); - - gl_FragColor = c; +void *gl_create_window_shader(backend_t *backend_data attr_unused, const char *source) { + auto ret = ccalloc(1, struct gl_shader); + if (!gl_create_window_shader_inner(ret, source)) { + free(ret); + return NULL; } -); + return ret; +} -const char *present_vertex_shader = GLSL(330, - uniform mat4 projection; - layout(location = 0) in vec2 coord; - out vec2 texcoord; - void main() { - gl_Position = projection * vec4(coord, 0, 1); - texcoord = coord; +uint64_t gl_get_shader_attributes(backend_t *backend_data attr_unused, void *shader) { + auto win_shader = (struct gl_shader *)shader; + uint64_t ret = 0; + if (glGetUniformLocation(win_shader->prog, "time") >= 0) { + ret |= SHADER_ATTRIBUTE_ANIMATED; } -); -// clang-format on + return ret; +} bool gl_init(struct gl_data *gd, session_t *ps) { - // Initialize GLX data structure + if (!epoxy_has_gl_extension("GL_ARB_explicit_uniform_location")) { + log_error("GL_ARB_explicit_uniform_location support is required but " + "missing."); + return false; + } + glGenQueries(2, gd->frame_timing); + gd->current_frame_timing = 0; + + // Initialize GL data structure glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); glEnable(GL_BLEND); - // X pixmap is in premultiplied alpha, so we might just as well use it too. - // Thanks to derhass for help. - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); // Initialize stencil buffer glDisable(GL_STENCIL_TEST); @@ -1594,55 +885,70 @@ bool gl_init(struct gl_data *gd, session_t *ps) { glViewport(0, 0, viewport_dimensions[0], viewport_dimensions[1]); // Clear screen - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClearColor(0.0F, 0.0F, 0.0F, 1.0F); glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); - glGenFramebuffers(1, &gd->back_fbo); - glGenTextures(1, &gd->back_texture); - if (!gd->back_fbo || !gd->back_texture) { - log_error("Failed to generate a framebuffer object"); + glGenFramebuffers(1, &gd->temp_fbo); + + gd->default_mask_texture = gl_new_texture(); + if (!gd->default_mask_texture) { + log_error("Failed to generate a default mask texture"); return false; } - glBindTexture(GL_TEXTURE_2D, gd->back_texture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, gd->default_mask_texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, + (GLbyte[]){'\xff'}); glBindTexture(GL_TEXTURE_2D, 0); + // Initialize shaders + if (!gl_create_window_shader_inner(&gd->default_shader, blit_shader_default)) { + log_error("Failed to create window shaders"); + return false; + } + // Set projection matrix to gl viewport dimensions so we can use screen // coordinates for all vertices // Note: OpenGL matrices are column major - GLfloat projection_matrix[4][4] = {{2.0f / (GLfloat)viewport_dimensions[0], 0, 0, 0}, - {0, 2.0f / (GLfloat)viewport_dimensions[1], 0, 0}, + GLfloat projection_matrix[4][4] = {{2.0F / (GLfloat)viewport_dimensions[0], 0, 0, 0}, + {0, 2.0F / (GLfloat)viewport_dimensions[1], 0, 0}, {0, 0, 0, 0}, {-1, -1, 0, 1}}; - // Initialize shaders - gl_win_shader_from_string(vertex_shader, win_shader_glsl, &gd->win_shader); - int pml = glGetUniformLocationChecked(gd->win_shader.prog, "projection"); - glUseProgram(gd->win_shader.prog); - glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); - glUseProgram(0); - gd->fill_shader.prog = gl_create_program_from_str(fill_vert, fill_frag); - gd->fill_shader.color_loc = glGetUniformLocation(gd->fill_shader.prog, "color"); - pml = glGetUniformLocationChecked(gd->fill_shader.prog, "projection"); + gd->fill_shader.uniform_bitmask = (uint32_t)-1; // make sure our uniforms + // are not ignored. glUseProgram(gd->fill_shader.prog); - glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection_matrix[0]); glUseProgram(0); - gd->present_prog = gl_create_program_from_str(present_vertex_shader, dummy_frag); - if (!gd->present_prog) { - log_error("Failed to create the present shader"); + gd->dithered_present = ps->o.dithered_present; + gd->copy_area_prog.prog = gl_create_program_from_strv( + (const char *[]){vertex_shader, NULL}, (const char *[]){copy_area_frag, NULL}); + if (!gd->copy_area_prog.prog) { + log_error("Failed to create the copy_area shader"); return false; } - pml = glGetUniformLocationChecked(gd->present_prog, "projection"); - glUseProgram(gd->present_prog); - glUniform1i(glGetUniformLocationChecked(gd->present_prog, "tex"), 0); - glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); - glUseProgram(0); + gd->copy_area_prog.uniform_bitmask = (uint32_t)-1; // make sure our + // uniforms are not + // ignored. + + glUseProgram(gd->copy_area_prog.prog); + glUniform1i(UNIFORM_TEX_LOC, 0); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection_matrix[0]); + + gd->copy_area_with_dither_prog.prog = gl_create_program_from_strv( + (const char *[]){vertex_shader, NULL}, + (const char *[]){copy_area_with_dither_frag, dither_glsl, NULL}); + if (!gd->copy_area_with_dither_prog.prog) { + log_error("Failed to create the copy_area with dither shader"); + return false; + } + gd->copy_area_with_dither_prog.uniform_bitmask = (uint32_t)-1; + + glUseProgram(gd->copy_area_with_dither_prog.prog); + glUniform1i(UNIFORM_TEX_LOC, 0); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection_matrix[0]); gd->brightness_shader.prog = gl_create_program_from_str(interpolating_vert, interpolating_frag); @@ -1650,23 +956,29 @@ bool gl_init(struct gl_data *gd, session_t *ps) { log_error("Failed to create the brightness shader"); return false; } - pml = glGetUniformLocationChecked(gd->brightness_shader.prog, "projection"); glUseProgram(gd->brightness_shader.prog); - glUniform1i(glGetUniformLocationChecked(gd->brightness_shader.prog, "tex"), 0); - glUniformMatrix4fv(pml, 1, false, projection_matrix[0]); + glUniform1i(UNIFORM_TEX_LOC, 0); + glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection_matrix[0]); glUseProgram(0); - // Set up the size of the back texture - gl_resize(gd, ps->root_width, ps->root_height); - - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gd->back_fbo); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - gd->back_texture, 0); - glDrawBuffer(GL_COLOR_ATTACHMENT0); - if (!gl_check_fb_complete(GL_FRAMEBUFFER)) { - return false; - } - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + gd->back_image.width = ps->root_width; + gd->back_image.height = ps->root_height; + + glGenSamplers(GL_MAX_SAMPLERS, gd->samplers); + for (int i = 0; i < GL_MAX_SAMPLERS; i++) { + glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + glSamplerParameterf(gd->samplers[GL_SAMPLER_EDGE], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glSamplerParameterf(gd->samplers[GL_SAMPLER_EDGE], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glSamplerParameterf(gd->samplers[GL_SAMPLER_BLUR], GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glSamplerParameterf(gd->samplers[GL_SAMPLER_BORDER], GL_TEXTURE_WRAP_S, + GL_CLAMP_TO_BORDER); + glSamplerParameterf(gd->samplers[GL_SAMPLER_BORDER], GL_TEXTURE_WRAP_T, + GL_CLAMP_TO_BORDER); gd->logger = gl_string_marker_logger_new(); if (gd->logger) { @@ -1676,239 +988,176 @@ bool gl_init(struct gl_data *gd, session_t *ps) { const char *vendor = (const char *)glGetString(GL_VENDOR); log_debug("GL_VENDOR = %s", vendor); if (strcmp(vendor, "NVIDIA Corporation") == 0) { - log_info("GL vendor is NVIDIA, don't use glFinish"); + log_info("GL vendor is NVIDIA, enable xrender sync fence."); gd->is_nvidia = true; } else { gd->is_nvidia = false; } + gd->has_robustness = epoxy_has_gl_extension("GL_ARB_robustness"); + gd->has_egl_image_storage = epoxy_has_gl_extension("GL_EXT_EGL_image_storage"); + gd->back_image.y_inverted = false; + + gl_check_err(); return true; } void gl_deinit(struct gl_data *gd) { - gl_free_prog_main(&gd->win_shader); - if (gd->logger) { log_remove_target_tls(gd->logger); gd->logger = NULL; } + gl_destroy_window_shader_inner(&gd->default_shader); + glDeleteProgram(gd->copy_area_prog.prog); + glDeleteProgram(gd->copy_area_with_dither_prog.prog); + gd->copy_area_prog.prog = 0; + gd->copy_area_with_dither_prog.prog = 0; + + glDeleteProgram(gd->fill_shader.prog); + glDeleteProgram(gd->brightness_shader.prog); + gd->fill_shader.prog = 0; + gd->brightness_shader.prog = 0; + + glDeleteTextures(1, &gd->default_mask_texture); + + for (int i = 0; i < GL_MAX_SAMPLERS; i++) { + glDeleteSamplers(1, &gd->samplers[i]); + } + + glDeleteFramebuffers(1, &gd->temp_fbo); + + glDeleteQueries(2, gd->frame_timing); + gl_check_err(); } -GLuint gl_new_texture(GLenum target) { +GLuint gl_new_texture(void) { GLuint texture; glGenTextures(1, &texture); if (!texture) { log_error("Failed to generate texture"); return 0; } - - glBindTexture(target, texture); - glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT); - glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT); - glBindTexture(target, 0); - return texture; } -/// Actually duplicate a texture into a new one, if this texture is shared -static inline void gl_image_decouple(backend_t *base, struct backend_image *img) { - if (img->inner->refcount == 1) { - return; - } - auto gd = (struct gl_data *)base; - auto inner = (struct gl_texture *)img->inner; - auto new_tex = ccalloc(1, struct gl_texture); - - new_tex->texture = gl_new_texture(GL_TEXTURE_2D); - new_tex->y_inverted = true; - new_tex->height = inner->height; - new_tex->width = inner->width; - new_tex->refcount = 1; - new_tex->user_data = gd->decouple_texture_user_data(base, inner->user_data); - - glBindTexture(GL_TEXTURE_2D, new_tex->texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, new_tex->width, new_tex->height, 0, - GL_BGRA, GL_UNSIGNED_BYTE, NULL); - glBindTexture(GL_TEXTURE_2D, 0); - - assert(gd->present_prog); - glUseProgram(gd->present_prog); - glBindTexture(GL_TEXTURE_2D, inner->texture); - - GLuint fbo; - glGenFramebuffers(1, &fbo); +bool gl_clear(backend_t *backend_data, image_handle target, struct color color) { + auto gd = (struct gl_data *)backend_data; + auto fbo = gl_bind_image_to_fbo(gd, target); + auto target_image = (struct gl_texture *)target; glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - new_tex->texture, 0); - glDrawBuffer(GL_COLOR_ATTACHMENT0); - gl_check_fb_complete(GL_DRAW_FRAMEBUFFER); - - glClearColor(0, 0, 0, 0); + if (target_image->format == BACKEND_IMAGE_FORMAT_MASK) { + glClearColor((GLfloat)color.alpha, 0, 0, 1); + } else { + glClearColor((GLfloat)color.red, (GLfloat)color.green, + (GLfloat)color.blue, (GLfloat)color.alpha); + } glClear(GL_COLOR_BUFFER_BIT); - - // clang-format off - GLint coord[] = { - // top left - 0, 0, // vertex coord - 0, 0, // texture coord - - // top right - new_tex->width, 0, // vertex coord - new_tex->width, 0, // texture coord - - // bottom right - new_tex->width, new_tex->height, - new_tex->width, new_tex->height, - - // bottom left - 0, new_tex->height, - 0, new_tex->height, - }; - // clang-format on - GLuint indices[] = {0, 1, 2, 2, 3, 0}; - - GLuint vao; - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - - GLuint bo[2]; - glGenBuffers(2, bo); - glBindBuffer(GL_ARRAY_BUFFER, bo[0]); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); - glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * 16, coord, GL_STATIC_DRAW); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * 6, indices, - GL_STATIC_DRAW); - - glEnableVertexAttribArray(vert_coord_loc); - glEnableVertexAttribArray(vert_in_texcoord_loc); - glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 4, NULL); - glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_INT, GL_FALSE, - sizeof(GLint) * 4, (void *)(sizeof(GLint) * 2)); - - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); - - glDisableVertexAttribArray(vert_coord_loc); - glDisableVertexAttribArray(vert_in_texcoord_loc); - glBindVertexArray(0); - glDeleteVertexArrays(1, &vao); - - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glDeleteBuffers(2, bo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glDeleteFramebuffers(1, &fbo); - - glBindTexture(GL_TEXTURE_2D, 0); - glUseProgram(0); + return true; +} - gl_check_err(); +image_handle gl_back_buffer(struct backend_base *base) { + auto gd = (struct gl_data *)base; + return (image_handle)&gd->back_image; +} - img->inner = (struct backend_image_inner_base *)new_tex; - inner->refcount--; +image_handle gl_new_image(backend_t *backend_data attr_unused, + enum backend_image_format format, ivec2 size) { + auto tex = ccalloc(1, struct gl_texture); + log_trace("Creating texture %dx%d", size.width, size.height); + tex->format = format; + tex->width = size.width; + tex->height = size.height; + tex->texture = gl_new_texture(); + tex->y_inverted = true; + tex->user_data = NULL; + tex->pixmap = XCB_NONE; + + GLint gl_format; + switch (format) { + case BACKEND_IMAGE_FORMAT_PIXMAP: gl_format = GL_RGBA8; break; + case BACKEND_IMAGE_FORMAT_PIXMAP_HIGH: gl_format = GL_RGBA16; break; + case BACKEND_IMAGE_FORMAT_MASK: gl_format = GL_R8; break; + } + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, tex->texture); + glTexImage2D(GL_TEXTURE_2D, 0, gl_format, size.width, size.height, 0, GL_RGBA, + GL_UNSIGNED_BYTE, NULL); + if (format == BACKEND_IMAGE_FORMAT_MASK) { + // Mask images needs a border, so sampling from outside of the texture + // will correctly return 0 + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, + (GLfloat[]){0, 0, 0, 0}); + } + glBindTexture(GL_TEXTURE_2D, 0); + return (image_handle)tex; } -static void gl_image_apply_alpha(backend_t *base, struct backend_image *img, - const region_t *reg_op, double alpha) { +bool gl_apply_alpha(backend_t *base, image_handle target, double alpha, const region_t *reg_op) { + auto gd = (struct gl_data *)base; + static const struct gl_vertex_attribs_definition vertex_attribs = { + .stride = sizeof(GLint) * 4, + .count = 1, + .attribs = {{GL_INT, vert_coord_loc, NULL}}, + }; + if (alpha == 1.0 || !pixman_region32_not_empty(reg_op)) { + return true; + } + gl_bind_image_to_fbo(gd, target); // Result color = 0 (GL_ZERO) + alpha (GL_CONSTANT_ALPHA) * original color - auto inner = (struct gl_texture *)img->inner; glBlendFunc(GL_ZERO, GL_CONSTANT_ALPHA); glBlendColor(0, 0, 0, (GLclampf)alpha); - GLuint fbo; - glGenFramebuffers(1, &fbo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, - inner->texture, 0); - glDrawBuffer(GL_COLOR_ATTACHMENT0); - _gl_fill(base, (struct color){0, 0, 0, 0}, reg_op, fbo, 0, false); - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - glDeleteFramebuffers(1, &fbo); -} - -void gl_present(backend_t *base, const region_t *region) { - auto gd = (struct gl_data *)base; int nrects; - const rect_t *rect = pixman_region32_rectangles((region_t *)region, &nrects); - auto coord = ccalloc(nrects * 8, GLint); - auto indices = ccalloc(nrects * 6, GLuint); - for (int i = 0; i < nrects; i++) { - // clang-format off - memcpy(&coord[i * 8], - ((GLint[]){rect[i].x1, gd->height - rect[i].y2, - rect[i].x2, gd->height - rect[i].y2, - rect[i].x2, gd->height - rect[i].y1, - rect[i].x1, gd->height - rect[i].y1}), - sizeof(GLint) * 8); - // clang-format on + const rect_t *rect = pixman_region32_rectangles(reg_op, &nrects); - GLuint u = (GLuint)(i * 4); - memcpy(&indices[i * 6], - ((GLuint[]){u + 0, u + 1, u + 2, u + 2, u + 3, u + 0}), - sizeof(GLuint) * 6); - } - - glUseProgram(gd->present_prog); - glBindTexture(GL_TEXTURE_2D, gd->back_texture); - - GLuint vao; - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - - GLuint bo[2]; - glGenBuffers(2, bo); - glEnableVertexAttribArray(vert_coord_loc); - glBindBuffer(GL_ARRAY_BUFFER, bo[0]); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bo[1]); - glBufferData(GL_ARRAY_BUFFER, (long)sizeof(GLint) * nrects * 8, coord, GL_STREAM_DRAW); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(GLuint) * nrects * 6, indices, - GL_STREAM_DRAW); - - glVertexAttribPointer(vert_coord_loc, 2, GL_INT, GL_FALSE, sizeof(GLint) * 2, NULL); - glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL); - - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glBindVertexArray(0); - glDeleteBuffers(2, bo); - glDeleteVertexArrays(1, &vao); + auto coord = ccalloc(nrects * 16, GLint); + auto indices = ccalloc(nrects * 6, GLuint); - free(coord); + struct gl_uniform_value uniforms[] = { + [UNIFORM_COLOR_LOC] = {.type = GL_FLOAT_VEC4, .f4 = {0, 0, 0, 0}}, + }; + gl_mask_rects_to_coords_simple(nrects, rect, coord, indices); + gl_blit_inner(gd->temp_fbo, nrects, coord, indices, &vertex_attribs, + &gd->fill_shader, ARR_SIZE(uniforms), uniforms); free(indices); + free(coord); + + gl_check_err(); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + return true; } -bool gl_image_op(backend_t *base, enum image_operations op, void *image_data, - const region_t *reg_op, const region_t *reg_visible attr_unused, void *arg) { - struct backend_image *tex = image_data; - switch (op) { - case IMAGE_OP_APPLY_ALPHA: - gl_image_decouple(base, tex); - assert(tex->inner->refcount == 1); - gl_image_apply_alpha(base, tex, reg_op, *(double *)arg); - break; +bool gl_last_render_time(backend_t *base, struct timespec *ts) { + auto gd = (struct gl_data *)base; + GLint available = 0; + glGetQueryObjectiv(gd->frame_timing[gd->current_frame_timing ^ 1], + GL_QUERY_RESULT_AVAILABLE, &available); + if (!available) { + return false; } + GLuint64 time; + glGetQueryObjectui64v(gd->frame_timing[gd->current_frame_timing ^ 1], + GL_QUERY_RESULT, &time); + ts->tv_sec = (long)(time / 1000000000); + ts->tv_nsec = (long)(time % 1000000000); + gl_check_err(); return true; } -bool gl_read_pixel(backend_t *base attr_unused, void *image_data, int x, int y, - struct color *output) { - struct backend_image *tex = image_data; - auto inner = (struct gl_texture *)tex->inner; - GLfloat color[4]; - glReadPixels(x, inner->y_inverted ? inner->height - y : y, 1, 1, GL_RGBA, - GL_FLOAT, color); - output->alpha = color[3]; - output->red = color[0]; - output->green = color[1]; - output->blue = color[2]; - - bool ret = glGetError() == GL_NO_ERROR; - gl_clear_err(); - return ret; +enum device_status gl_device_status(backend_t *base) { + auto gd = (struct gl_data *)base; + if (!gd->has_robustness) { + return DEVICE_STATUS_NORMAL; + } + if (glGetGraphicsResetStatusARB() == GL_NO_ERROR) { + return DEVICE_STATUS_NORMAL; + } + return DEVICE_STATUS_RESETTING; } diff --git a/src/backend/gl/gl_common.h b/src/backend/gl/gl_common.h index 4fb50c8986..3a679c757e 100644 --- a/src/backend/gl/gl_common.h +++ b/src/backend/gl/gl_common.h @@ -1,72 +1,105 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once -#include -#include +#include #include #include +#include #include "backend/backend.h" +#include "backend/backend_common.h" #include "log.h" #include "region.h" #define CASESTRRET(s) \ case s: return #s - -// Program and uniforms for window shader -typedef struct { - GLuint prog; - GLint unifm_opacity; - GLint unifm_invert_color; - GLint unifm_tex; - GLint unifm_dim; - GLint unifm_brightness; - GLint unifm_max_brightness; -} gl_win_shader_t; - -// Program and uniforms for brightness shader -typedef struct { +struct gl_blur_context; + +// Fragment shader uniforms +#define UNIFORM_OPACITY_LOC 1 +#define UNIFORM_INVERT_COLOR_LOC 2 +#define UNIFORM_TEX_LOC 3 +#define UNIFORM_EFFECTIVE_SIZE_LOC 4 +#define UNIFORM_DIM_LOC 5 +#define UNIFORM_BRIGHTNESS_LOC 6 +#define UNIFORM_MAX_BRIGHTNESS_LOC 7 +#define UNIFORM_CORNER_RADIUS_LOC 8 +#define UNIFORM_BORDER_WIDTH_LOC 9 +#define UNIFORM_TIME_LOC 10 +#define UNIFORM_COLOR_LOC 11 +#define UNIFORM_PIXEL_NORM_LOC 12 +#define UNIFORM_TEX_SRC_LOC 13 +#define UNIFORM_MASK_TEX_LOC 14 +#define UNIFORM_MASK_OFFSET_LOC 15 +#define UNIFORM_MASK_CORNER_RADIUS_LOC 16 +#define UNIFORM_MASK_INVERTED_LOC 17 + +// Vertex shader uniforms +#define UNIFORM_SCALE_LOC 18 +#define UNIFORM_PROJECTION_LOC 19 +#define UNIFORM_TEXSIZE_LOC 21 +#define NUMBER_OF_UNIFORMS (UNIFORM_TEXSIZE_LOC + 1) + +struct gl_shader { GLuint prog; -} gl_brightness_shader_t; - -// Program and uniforms for blur shader -typedef struct { - GLuint prog; - GLint unifm_pixel_norm; - GLint unifm_opacity; - GLint texorig_loc; - GLint scale_loc; -} gl_blur_shader_t; - -typedef struct { - GLuint prog; - GLint color_loc; -} gl_fill_shader_t; + // If the shader is user controlled, we don't know which uniform will be + // active, so we need to track which one is. + // This is not used if the shader code is fully controlled by us. + uint32_t uniform_bitmask; +}; -/// @brief Wrapper of a binded GLX texture. +/// @brief Wrapper of a bound GL texture. struct gl_texture { - int refcount; - bool has_alpha; + enum backend_image_format format; GLuint texture; int width, height; + /// Whether the texture is Y-inverted + /// This is always true for all our internal textures. Textures created from + /// binding a X pixmap might not be. And our OpenGL back buffer is never + /// Y-inverted (until we can start using glClipControl). bool y_inverted; + xcb_pixmap_t pixmap; // Textures for auxiliary uses. GLuint auxiliary_texture[2]; void *user_data; }; +enum gl_sampler { + GL_SAMPLER_REPEAT = 0, + /// Clamp to edge + GL_SAMPLER_EDGE, + /// Clamp to border, border color will be (0, 0, 0, 0) + GL_SAMPLER_BORDER, + /// Special sampler for blurring, same as `GL_SAMPLER_CLAMP_TO_EDGE`, + /// but uses linear filtering. + GL_SAMPLER_BLUR, + GL_MAX_SAMPLERS = GL_SAMPLER_BLUR + 1, +}; + struct gl_data { - backend_t base; + struct backend_base base; // If we are using proprietary NVIDIA driver bool is_nvidia; - // Height and width of the root window - int height, width; - gl_win_shader_t win_shader; - gl_brightness_shader_t brightness_shader; - gl_fill_shader_t fill_shader; - GLuint back_texture, back_fbo; - GLuint present_prog; + // If ARB_robustness extension is present + bool has_robustness; + // If EXT_EGL_image_storage extension is present + bool has_egl_image_storage; + /// A symbolic image representing the back buffer. + struct gl_texture back_image; + struct gl_shader default_shader; + struct gl_shader brightness_shader; + struct gl_shader fill_shader; + GLuint temp_fbo; + GLuint frame_timing[2]; + int current_frame_timing; + struct gl_shader copy_area_prog; + struct gl_shader copy_area_with_dither_prog; + GLuint samplers[GL_MAX_SAMPLERS]; + + bool dithered_present; + + GLuint default_mask_texture; /// Called when an gl_texture is decoupled from the texture it refers. Returns /// the decoupled user_data @@ -83,43 +116,109 @@ typedef struct session session_t; #define GL_PROG_MAIN_INIT \ { .prog = 0, .unifm_opacity = -1, .unifm_invert_color = -1, .unifm_tex = -1, } +void gl_prepare(backend_t *base, const region_t *reg); +/// Convert a mask formed by a collection of rectangles to OpenGL vertex and texture +/// coordinates. +/// +/// @param[in] origin origin of the source image in target coordinates +/// @param[in] mask_origin origin of the mask in source coordinates +/// @param[in] nrects number of rectangles +/// @param[in] rects mask rectangles, in mask coordinates +/// @param[out] coord OpenGL vertex coordinates, suitable for creating VAO/VBO +/// @param[out] indices OpenGL vertex indices, suitable for creating VAO/VBO +void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, GLint *coord, + GLuint *indices); +/// Like `gl_mask_rects_to_coords`, but with `origin` and `mask_origin` set to 0. i.e. all +/// coordinates are in the same space. +static inline void gl_mask_rects_to_coords_simple(int nrects, const rect_t *rects, + GLint *coord, GLuint *indices) { + return gl_mask_rects_to_coords((ivec2){0, 0}, nrects, rects, coord, indices); +} + GLuint gl_create_shader(GLenum shader_type, const char *shader_str); -GLuint gl_create_program(const GLuint *const shaders, int nshaders); +GLuint gl_create_program(const GLuint *shaders, int nshaders); GLuint gl_create_program_from_str(const char *vert_shader_str, const char *frag_shader_str); +GLuint gl_create_program_from_strv(const char **vert_shaders, const char **frag_shaders); +void *gl_create_window_shader(backend_t *backend_data, const char *source); +void gl_destroy_window_shader(backend_t *backend_data, void *shader); +uint64_t gl_get_shader_attributes(backend_t *backend_data, void *shader); +bool gl_last_render_time(backend_t *backend_data, struct timespec *time); -/** - * @brief Render a region with texture data. - */ -void gl_compose(backend_t *, void *ptex, int dst_x, int dst_y, const region_t *reg_tgt, - const region_t *reg_visible); +bool gl_blit(backend_t *base, ivec2 origin, image_handle target, + const struct backend_blit_args *args); +image_handle gl_new_image(backend_t *backend_data attr_unused, + enum backend_image_format format, ivec2 size); +bool gl_clear(backend_t *backend_data, image_handle target, struct color color); + +void gl_root_change(backend_t *base, session_t *); void gl_resize(struct gl_data *, int width, int height); bool gl_init(struct gl_data *gd, session_t *); void gl_deinit(struct gl_data *gd); -GLuint gl_new_texture(GLenum target); - -bool gl_image_op(backend_t *base, enum image_operations op, void *image_data, - const region_t *reg_op, const region_t *reg_visible, void *arg); +GLuint gl_new_texture(void); -void gl_release_image(backend_t *base, void *image_data); +xcb_pixmap_t gl_release_image(backend_t *base, image_handle image); -void *gl_clone(backend_t *base, const void *image_data, const region_t *reg_visible); +image_handle gl_clone(backend_t *base, image_handle image, const region_t *reg_visible); -bool gl_blur(backend_t *base, double opacity, void *, const region_t *reg_blur, - const region_t *reg_visible); -void *gl_create_blur_context(backend_t *base, enum blur_method, void *args); +bool gl_blur(struct backend_base *gd, ivec2 origin, image_handle target, + const struct backend_blur_args *args); +bool gl_copy_area(backend_t *backend_data, ivec2 origin, image_handle target, + image_handle source, const region_t *region); +bool gl_copy_area_quantize(backend_t *backend_data, ivec2 origin, image_handle target_handle, + image_handle source_handle, const region_t *region); +bool gl_apply_alpha(backend_t *base, image_handle target, double alpha, const region_t *reg_op); +image_handle gl_back_buffer(struct backend_base *base); +uint32_t gl_image_capabilities(backend_t *base, image_handle img); +bool gl_is_format_supported(backend_t *base, enum backend_image_format format); +void *gl_create_blur_context(backend_t *base, enum blur_method, + enum backend_image_format format, void *args); void gl_destroy_blur_context(backend_t *base, void *ctx); void gl_get_blur_size(void *blur_context, int *width, int *height); -void gl_fill(backend_t *base, struct color, const region_t *clip); +enum device_status gl_device_status(backend_t *base); -void gl_present(backend_t *base, const region_t *); -bool gl_read_pixel(backend_t *base, void *image_data, int x, int y, struct color *output); +#define gl_check_fb_complete(fb) gl_check_fb_complete_(__func__, __LINE__, (fb)) +static inline bool gl_check_fb_complete_(const char *func, int line, GLenum fb); -static inline void gl_delete_texture(GLuint texture) { - glDeleteTextures(1, &texture); +static inline void gl_finish_render(struct gl_data *gd) { + glEndQuery(GL_TIME_ELAPSED); + gd->current_frame_timing ^= 1; +} + +/// Return a FBO with `image` bound to the first color attachment. `GL_DRAW_FRAMEBUFFER` +/// will be bound to the returned FBO. +static inline GLuint gl_bind_image_to_fbo(struct gl_data *gd, image_handle image_) { + auto image = (struct gl_texture *)image_; + if (image == &gd->back_image) { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + return 0; + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gd->temp_fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + image->texture, 0); + CHECK(gl_check_fb_complete(GL_DRAW_FRAMEBUFFER)); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + return gd->temp_fbo; +} + +/// Flip the target coordinates returned by `gl_mask_rects_to_coords` vertically relative +/// to the target. Texture coordinates are unchanged. +/// +/// @param[in] nrects number of rectangles +/// @param[in] coord OpenGL vertex coordinates +/// @param[in] target_height height of the target image +static inline void gl_y_flip_target(int nrects, GLint *coord, GLint target_height) { + for (ptrdiff_t i = 0; i < nrects; i++) { + auto current_rect = &coord[i * 16]; // 16 numbers per rectangle + for (ptrdiff_t j = 0; j < 4; j++) { + // 4 numbers per vertex, target coordinates are the first two + auto current_vertex = ¤t_rect[j * 4]; + current_vertex[1] = target_height - current_vertex[1]; + } + } } /** @@ -148,7 +247,7 @@ static inline const char *gl_get_err_str(GLenum err) { } /** - * Check for GLX error. + * Check for GL error. * * http://blog.nobel-joergensen.com/2013/01/29/debugging-opengl-using-glgeterror/ */ @@ -159,17 +258,17 @@ static inline void gl_check_err_(const char *func, int line) { const char *errtext = gl_get_err_str(err); if (errtext) { log_printf(tls_logger, LOG_LEVEL_ERROR, func, - "GLX error at line %d: %s", line, errtext); + "GL error at line %d: %s", line, errtext); } else { log_printf(tls_logger, LOG_LEVEL_ERROR, func, - "GLX error at line %d: %d", line, err); + "GL error at line %d: %d", line, err); } } } static inline void gl_clear_err(void) { - while (glGetError() != GL_NO_ERROR) - ; + while (glGetError() != GL_NO_ERROR) { + } } #define gl_check_err() gl_check_err_(__func__, __LINE__) @@ -196,24 +295,17 @@ static inline bool gl_check_fb_complete_(const char *func, int line, GLenum fb) return false; } -#define gl_check_fb_complete(fb) gl_check_fb_complete_(__func__, __LINE__, (fb)) +static const GLuint vert_coord_loc = 0; +static const GLuint vert_in_texcoord_loc = 1; -/** - * Check if a GLX extension exists. - */ -static inline bool gl_has_extension(const char *ext) { - int nexts = 0; - glGetIntegerv(GL_NUM_EXTENSIONS, &nexts); - for (int i = 0; i < nexts || !nexts; i++) { - const char *exti = (const char *)glGetStringi(GL_EXTENSIONS, (GLuint)i); - if (exti == NULL) { - break; - } - if (strcmp(ext, exti) == 0) { - return true; - } - } - gl_clear_err(); - log_info("Missing GL extension %s.", ext); - return false; -} +// Add one level of indirection so macros in VA_ARGS can be expanded. +#define GLSL_(version, ...) \ + "#version " #version "\n" \ + "#extension GL_ARB_explicit_uniform_location : enable\n" #__VA_ARGS__ +#define GLSL(version, ...) GLSL_(version, __VA_ARGS__) +#define QUOTE(...) #__VA_ARGS__ + +extern const char vertex_shader[], blend_with_mask_frag[], masking_glsl[], + copy_area_frag[], copy_area_with_dither_frag[], fill_frag[], fill_vert[], + interpolating_frag[], interpolating_vert[], blit_shader_glsl[], blit_shader_default[], + present_vertex_shader[], dither_glsl[]; diff --git a/src/backend/gl/glx.c b/src/backend/gl/glx.c index 1397d1936e..feaccfcf6d 100644 --- a/src/backend/gl/glx.c +++ b/src/backend/gl/glx.c @@ -17,8 +17,10 @@ #include #include #include +#include #include #include +#include #include "backend/backend.h" #include "backend/backend_common.h" @@ -34,36 +36,38 @@ #include "win.h" #include "x.h" -struct _glx_pixmap { - GLXPixmap glpixmap; - xcb_pixmap_t pixmap; - bool owned; -}; - struct _glx_data { struct gl_data gl; - Display *display; - int screen; xcb_window_t target_win; GLXContext ctx; + struct glx_fbconfig_cache *cached_fbconfigs; +}; + +struct glx_fbconfig_cache { + UT_hash_handle hh; + struct xvisual_info visual_info; + struct glx_fbconfig_info info; }; #define glXGetFBConfigAttribChecked(a, b, attr, c) \ do { \ if (glXGetFBConfigAttrib(a, b, attr, c)) { \ log_info("Cannot get FBConfig attribute " #attr); \ - continue; \ + break; \ } \ } while (0) -struct glx_fbconfig_info *glx_find_fbconfig(Display *dpy, int screen, struct xvisual_info m) { - log_debug("Looking for FBConfig for RGBA%d%d%d%d, depth %d", m.red_size, - m.blue_size, m.green_size, m.alpha_size, m.visual_depth); +bool glx_find_fbconfig(struct x_connection *c, struct xvisual_info m, + struct glx_fbconfig_info *info) { + log_debug("Looking for FBConfig for RGBA%d%d%d%d, depth: %d, visual id: %#x", m.red_size, + m.blue_size, m.green_size, m.alpha_size, m.visual_depth, m.visual); + + info->cfg = NULL; int ncfg; // clang-format off GLXFBConfig *cfg = - glXChooseFBConfig(dpy, screen, (int[]){ + glXChooseFBConfig(c->dpy, c->screen, (int[]){ GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT, GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, @@ -87,25 +91,26 @@ struct glx_fbconfig_info *glx_find_fbconfig(Display *dpy, int screen, struct xvi GLXFBConfig ret; for (int i = 0; i < ncfg; i++) { int depthbuf, stencil, doublebuf, bufsize; - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BUFFER_SIZE, &bufsize); - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_DEPTH_SIZE, &depthbuf); - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_STENCIL_SIZE, &stencil); - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_DOUBLEBUFFER, &doublebuf); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_BUFFER_SIZE, &bufsize); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_DEPTH_SIZE, &depthbuf); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_STENCIL_SIZE, &stencil); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_DOUBLEBUFFER, &doublebuf); if (depthbuf + stencil + bufsize * (doublebuf + 1) >= min_cost) { continue; } int red, green, blue; - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_RED_SIZE, &red); - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BLUE_SIZE, &blue); - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_GREEN_SIZE, &green); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_RED_SIZE, &red); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_BLUE_SIZE, &blue); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_GREEN_SIZE, &green); if (red != m.red_size || green != m.green_size || blue != m.blue_size) { // Color size doesn't match, this cannot work continue; } int rgb, rgba; - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGB_EXT, &rgb); - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGBA_EXT, &rgba); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGB_EXT, &rgb); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGBA_EXT, + &rgba); if (!rgb && !rgba) { log_info("FBConfig is neither RGBA nor RGB, we cannot " "handle this setup."); @@ -113,9 +118,9 @@ struct glx_fbconfig_info *glx_find_fbconfig(Display *dpy, int screen, struct xvi } int visual; - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_VISUAL_ID, &visual); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_VISUAL_ID, &visual); if (m.visual_depth != -1 && - x_get_visual_depth(XGetXCBConnection(dpy), (xcb_visualid_t)visual) != + xcb_aux_get_depth_of_visual(c->screen_info, (xcb_visualid_t)visual) != m.visual_depth) { // FBConfig and the correspondent X Visual might not have the same // depth. (e.g. 32 bit FBConfig with a 24 bit Visual). This is @@ -129,9 +134,9 @@ struct glx_fbconfig_info *glx_find_fbconfig(Display *dpy, int screen, struct xvi // All check passed, we are using this one. found = true; ret = cfg[i]; - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_TARGETS_EXT, - &texture_tgts); - glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_Y_INVERTED_EXT, &y_inverted); + glXGetFBConfigAttribChecked( + c->dpy, cfg[i], GLX_BIND_TO_TEXTURE_TARGETS_EXT, &texture_tgts); + glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_Y_INVERTED_EXT, &y_inverted); // Prefer the texture format with matching alpha, with the other one as // fallback @@ -145,41 +150,31 @@ struct glx_fbconfig_info *glx_find_fbconfig(Display *dpy, int screen, struct xvi min_cost = depthbuf + stencil + bufsize * (doublebuf + 1); } free(cfg); - if (!found) { - return NULL; + if (found) { + info->cfg = ret; + info->texture_tgts = texture_tgts; + info->texture_fmt = texture_fmt; + info->y_inverted = y_inverted; } - - auto info = cmalloc(struct glx_fbconfig_info); - info->cfg = ret; - info->texture_tgts = texture_tgts; - info->texture_fmt = texture_fmt; - info->y_inverted = y_inverted; - return info; + return found; } /** * Free a glx_texture_t. */ static void glx_release_image(backend_t *base, struct gl_texture *tex) { - struct _glx_data *gd = (void *)base; - - struct _glx_pixmap *p = tex->user_data; + GLXPixmap *p = tex->user_data; // Release binding - if (p->glpixmap && tex->texture) { + if (p && tex->texture) { glBindTexture(GL_TEXTURE_2D, tex->texture); - glXReleaseTexImageEXT(gd->display, p->glpixmap, GLX_FRONT_LEFT_EXT); + glXReleaseTexImageEXT(base->c->dpy, *p, GLX_FRONT_LEFT_EXT); glBindTexture(GL_TEXTURE_2D, 0); } // Free GLX Pixmap - if (p->glpixmap) { - glXDestroyPixmap(gd->display, p->glpixmap); - p->glpixmap = 0; - } - - if (p->owned) { - xcb_free_pixmap(base->c, p->pixmap); - p->pixmap = XCB_NONE; + if (p) { + glXDestroyPixmap(base->c->dpy, *p); + *p = 0; } free(p); @@ -196,20 +191,22 @@ void glx_deinit(backend_t *base) { // Destroy GLX context if (gd->ctx) { - glXMakeCurrent(gd->display, None, NULL); - glXDestroyContext(gd->display, gd->ctx); + glXMakeCurrent(base->c->dpy, None, NULL); + glXDestroyContext(base->c->dpy, gd->ctx); gd->ctx = 0; } + struct glx_fbconfig_cache *cached_fbconfig = NULL, *tmp = NULL; + HASH_ITER(hh, gd->cached_fbconfigs, cached_fbconfig, tmp) { + HASH_DEL(gd->cached_fbconfigs, cached_fbconfig); + free(cached_fbconfig); + } + free(gd); } static void *glx_decouple_user_data(backend_t *base attr_unused, void *ud attr_unused) { - auto ret = cmalloc(struct _glx_pixmap); - ret->owned = false; - ret->glpixmap = 0; - ret->pixmap = 0; - return ret; + return NULL; } static bool glx_set_swap_interval(int interval, Display *dpy, GLXDrawable drawable) { @@ -228,18 +225,18 @@ static bool glx_set_swap_interval(int interval, Display *dpy, GLXDrawable drawab return vsync_enabled; } +struct backend_operations glx_ops; /** * Initialize OpenGL. */ -static backend_t *glx_init(session_t *ps) { +static backend_t *glx_init(session_t *ps, xcb_window_t target) { bool success = false; - glxext_init(ps->dpy, ps->scr); + glxext_init(ps->c.dpy, ps->c.screen); auto gd = ccalloc(1, struct _glx_data); init_backend_base(&gd->gl.base, ps); + gd->gl.base.ops = &glx_ops; - gd->display = ps->dpy; - gd->screen = ps->scr; - gd->target_win = session_get_target_window(ps); + gd->target_win = target; XVisualInfo *pvis = NULL; @@ -251,8 +248,8 @@ static backend_t *glx_init(session_t *ps) { // Get XVisualInfo int nitems = 0; - XVisualInfo vreq = {.visualid = ps->vis}; - pvis = XGetVisualInfo(ps->dpy, VisualIDMask, &vreq, &nitems); + XVisualInfo vreq = {.visualid = ps->c.screen_info->root_visual}; + pvis = XGetVisualInfo(ps->c.dpy, VisualIDMask, &vreq, &nitems); if (!pvis) { log_error("Failed to acquire XVisualInfo for current visual."); goto end; @@ -260,22 +257,22 @@ static backend_t *glx_init(session_t *ps) { // Ensure the visual is double-buffered int value = 0; - if (glXGetConfig(ps->dpy, pvis, GLX_USE_GL, &value) || !value) { + if (glXGetConfig(ps->c.dpy, pvis, GLX_USE_GL, &value) || !value) { log_error("Root visual is not a GL visual."); goto end; } - if (glXGetConfig(ps->dpy, pvis, GLX_STENCIL_SIZE, &value) || !value) { + if (glXGetConfig(ps->c.dpy, pvis, GLX_STENCIL_SIZE, &value) || !value) { log_error("Root visual lacks stencil buffer."); goto end; } - if (glXGetConfig(ps->dpy, pvis, GLX_DOUBLEBUFFER, &value) || !value) { + if (glXGetConfig(ps->c.dpy, pvis, GLX_DOUBLEBUFFER, &value) || !value) { log_error("Root visual is not a double buffered GL visual."); goto end; } - if (glXGetConfig(ps->dpy, pvis, GLX_RGBA, &value) || !value) { + if (glXGetConfig(ps->c.dpy, pvis, GLX_RGBA, &value) || !value) { log_error("Root visual is a color index visual, not supported"); goto end; } @@ -293,25 +290,30 @@ static backend_t *glx_init(session_t *ps) { // Find a fbconfig with visualid matching the one from the target win, so we can // be sure that the fbconfig is compatible with our target window. int ncfgs; - GLXFBConfig *cfg = glXGetFBConfigs(gd->display, gd->screen, &ncfgs); + GLXFBConfig *cfg = glXGetFBConfigs(ps->c.dpy, ps->c.screen, &ncfgs); bool found = false; for (int i = 0; i < ncfgs; i++) { int visualid; - glXGetFBConfigAttribChecked(gd->display, cfg[i], GLX_VISUAL_ID, &visualid); + glXGetFBConfigAttribChecked(ps->c.dpy, cfg[i], GLX_VISUAL_ID, &visualid); if ((VisualID)visualid != pvis->visualid) { continue; } - gd->ctx = glXCreateContextAttribsARB(ps->dpy, cfg[i], 0, true, - (int[]){ - GLX_CONTEXT_MAJOR_VERSION_ARB, - 3, - GLX_CONTEXT_MINOR_VERSION_ARB, - 3, - GLX_CONTEXT_PROFILE_MASK_ARB, - GLX_CONTEXT_CORE_PROFILE_BIT_ARB, - 0, - }); + int *attributes = (int[]){GLX_CONTEXT_MAJOR_VERSION_ARB, + 3, + GLX_CONTEXT_MINOR_VERSION_ARB, + 3, + GLX_CONTEXT_PROFILE_MASK_ARB, + GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + 0, + 0, + 0}; + if (glxext.has_GLX_ARB_create_context_robustness) { + attributes[6] = GLX_CONTEXT_RESET_NOTIFICATION_STRATEGY_ARB; + attributes[7] = GLX_LOSE_CONTEXT_ON_RESET_ARB; + } + + gd->ctx = glXCreateContextAttribsARB(ps->c.dpy, cfg[i], 0, true, attributes); free(cfg); if (!gd->ctx) { @@ -329,7 +331,7 @@ static backend_t *glx_init(session_t *ps) { // Attach GLX context GLXDrawable tgt = gd->target_win; - if (!glXMakeCurrent(ps->dpy, tgt, gd->ctx)) { + if (!glXMakeCurrent(ps->c.dpy, tgt, gd->ctx)) { log_error("Failed to attach GLX context."); goto end; } @@ -343,11 +345,11 @@ static backend_t *glx_init(session_t *ps) { gd->gl.release_user_data = glx_release_image; if (ps->o.vsync) { - if (!glx_set_swap_interval(1, ps->dpy, tgt)) { + if (!glx_set_swap_interval(1, ps->c.dpy, tgt)) { log_error("Failed to enable vsync."); } } else { - glx_set_swap_interval(0, ps->dpy, tgt); + glx_set_swap_interval(0, ps->c.dpy, tgt); } success = true; @@ -365,111 +367,105 @@ static backend_t *glx_init(session_t *ps) { return &gd->gl.base; } -static void * -glx_bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt, bool owned) { - struct _glx_data *gd = (void *)base; - struct _glx_pixmap *glxpixmap = NULL; - // Retrieve pixmap parameters, if they aren't provided - if (fmt.visual_depth > OPENGL_MAX_DEPTH) { - log_error("Requested depth %d higher than max possible depth %d.", - fmt.visual_depth, OPENGL_MAX_DEPTH); - return false; - } +static image_handle +glx_bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt) { + GLXPixmap *glxpixmap = NULL; + auto gd = (struct _glx_data *)base; if (fmt.visual_depth < 0) { log_error("Pixmap %#010x with invalid depth %d", pixmap, fmt.visual_depth); - return false; + return NULL; } - auto r = xcb_get_geometry_reply(base->c, xcb_get_geometry(base->c, pixmap), NULL); + auto r = + xcb_get_geometry_reply(base->c->c, xcb_get_geometry(base->c->c, pixmap), NULL); if (!r) { log_error("Invalid pixmap %#010x", pixmap); return NULL; } log_trace("Binding pixmap %#010x", pixmap); - auto wd = ccalloc(1, struct backend_image); - wd->max_brightness = 1; auto inner = ccalloc(1, struct gl_texture); - inner->width = wd->ewidth = r->width; - inner->height = wd->eheight = r->height; - wd->inner = (struct backend_image_inner_base *)inner; + inner->width = r->width; + inner->height = r->height; + inner->format = BACKEND_IMAGE_FORMAT_PIXMAP; free(r); - auto fbcfg = glx_find_fbconfig(gd->display, gd->screen, fmt); - if (!fbcfg) { - log_error("Couldn't find FBConfig with requested visual %x", fmt.visual); - goto err; + struct glx_fbconfig_cache *cached_fbconfig = NULL; + HASH_FIND(hh, gd->cached_fbconfigs, &fmt, sizeof(fmt), cached_fbconfig); + if (!cached_fbconfig) { + struct glx_fbconfig_info fbconfig; + if (!glx_find_fbconfig(base->c, fmt, &fbconfig)) { + log_error("Couldn't find FBConfig with requested visual %#x", + fmt.visual); + goto err; + } + cached_fbconfig = cmalloc(struct glx_fbconfig_cache); + cached_fbconfig->visual_info = fmt; + cached_fbconfig->info = fbconfig; + HASH_ADD(hh, gd->cached_fbconfigs, visual_info, sizeof(fmt), cached_fbconfig); + } else { + log_debug("Found cached FBConfig for RGBA%d%d%d%d, depth: %d, visual id: " + "%#x", + fmt.red_size, fmt.blue_size, fmt.green_size, fmt.alpha_size, + fmt.visual_depth, fmt.visual); } + struct glx_fbconfig_info *fbconfig = &cached_fbconfig->info; // Choose a suitable texture target for our pixmap. // Refer to GLX_EXT_texture_om_pixmap spec to see what are the mean // of the bits in texture_tgts - if (!(fbcfg->texture_tgts & GLX_TEXTURE_2D_BIT_EXT)) { + if (!(fbconfig->texture_tgts & GLX_TEXTURE_2D_BIT_EXT)) { log_error("Cannot bind pixmap to GL_TEXTURE_2D, giving up"); goto err; } log_debug("depth %d, rgba %d", fmt.visual_depth, - (fbcfg->texture_fmt == GLX_TEXTURE_FORMAT_RGBA_EXT)); + (fbconfig->texture_fmt == GLX_TEXTURE_FORMAT_RGBA_EXT)); GLint attrs[] = { GLX_TEXTURE_FORMAT_EXT, - fbcfg->texture_fmt, + fbconfig->texture_fmt, GLX_TEXTURE_TARGET_EXT, GLX_TEXTURE_2D_EXT, 0, }; - inner->y_inverted = fbcfg->y_inverted; + inner->y_inverted = fbconfig->y_inverted; - glxpixmap = cmalloc(struct _glx_pixmap); - glxpixmap->pixmap = pixmap; - glxpixmap->glpixmap = glXCreatePixmap(gd->display, fbcfg->cfg, pixmap, attrs); - glxpixmap->owned = owned; - free(fbcfg); + glxpixmap = cmalloc(GLXPixmap); + inner->pixmap = pixmap; + *glxpixmap = glXCreatePixmap(base->c->dpy, fbconfig->cfg, pixmap, attrs); - if (!glxpixmap->glpixmap) { + if (!*glxpixmap) { log_error("Failed to create glpixmap for pixmap %#010x", pixmap); goto err; } - log_trace("GLXPixmap %#010lx", glxpixmap->glpixmap); + log_trace("GLXPixmap %#010lx", *glxpixmap); // Create texture inner->user_data = glxpixmap; - inner->texture = gl_new_texture(GL_TEXTURE_2D); - inner->has_alpha = fmt.alpha_size != 0; - wd->opacity = 1; - wd->color_inverted = false; - wd->dim = 0; - wd->inner->refcount = 1; + inner->texture = gl_new_texture(); glBindTexture(GL_TEXTURE_2D, inner->texture); - glXBindTexImageEXT(gd->display, glxpixmap->glpixmap, GLX_FRONT_LEFT_EXT, NULL); + glXBindTexImageEXT(base->c->dpy, *glxpixmap, GLX_FRONT_LEFT_EXT, NULL); glBindTexture(GL_TEXTURE_2D, 0); gl_check_err(); - return wd; + return (image_handle)inner; err: - if (glxpixmap && glxpixmap->glpixmap) { - glXDestroyPixmap(gd->display, glxpixmap->glpixmap); + if (glxpixmap && *glxpixmap) { + glXDestroyPixmap(base->c->dpy, *glxpixmap); } free(glxpixmap); - - if (owned) { - xcb_free_pixmap(base->c, pixmap); - } - free(wd); return NULL; } -static void glx_present(backend_t *base, const region_t *region attr_unused) { +static bool glx_present(backend_t *base) { struct _glx_data *gd = (void *)base; - gl_present(base, region); - glXSwapBuffers(gd->display, gd->target_win); - if (!gd->gl.is_nvidia) { - glFinish(); - } + gl_finish_render(&gd->gl); + glXSwapBuffers(base->c->dpy, gd->target_win); + return true; } static int glx_buffer_age(backend_t *base) { @@ -479,22 +475,21 @@ static int glx_buffer_age(backend_t *base) { struct _glx_data *gd = (void *)base; unsigned int val; - glXQueryDrawable(gd->display, gd->target_win, GLX_BACK_BUFFER_AGE_EXT, &val); + glXQueryDrawable(base->c->dpy, gd->target_win, GLX_BACK_BUFFER_AGE_EXT, &val); return (int)val ?: -1; } static void glx_diagnostics(backend_t *base) { - struct _glx_data *gd = (void *)base; bool warn_software_rendering = false; const char *software_renderer_names[] = {"llvmpipe", "SWR", "softpipe"}; - auto glx_vendor = glXGetClientString(gd->display, GLX_VENDOR); + auto glx_vendor = glXGetClientString(base->c->dpy, GLX_VENDOR); printf("* Driver vendors:\n"); printf(" * GLX: %s\n", glx_vendor); printf(" * GL: %s\n", glGetString(GL_VENDOR)); auto gl_renderer = (const char *)glGetString(GL_RENDERER); printf("* GL renderer: %s\n", gl_renderer); - if (strcmp(glx_vendor, "Mesa Project and SGI")) { + if (strcmp(glx_vendor, "Mesa Project and SGI") == 0) { for (size_t i = 0; i < ARR_SIZE(software_renderer_names); i++) { if (strstr(gl_renderer, software_renderer_names[i]) != NULL) { warn_software_rendering = true; @@ -523,84 +518,49 @@ static void glx_diagnostics(backend_t *base) { } struct backend_operations glx_ops = { - .init = glx_init, - .deinit = glx_deinit, + .apply_alpha = gl_apply_alpha, + .back_buffer = gl_back_buffer, .bind_pixmap = glx_bind_pixmap, - .release_image = gl_release_image, - .compose = gl_compose, - .image_op = gl_image_op, - .set_image_property = default_set_image_property, - .read_pixel = gl_read_pixel, - .clone_image = default_clone_image, + .blit = gl_blit, .blur = gl_blur, - .is_image_transparent = default_is_image_transparent, + .clear = gl_clear, + .copy_area = gl_copy_area, + .copy_area_quantize = gl_copy_area_quantize, + .image_capabilities = gl_image_capabilities, + .is_format_supported = gl_is_format_supported, + .new_image = gl_new_image, .present = glx_present, + .quirks = backend_no_quirks, + .release_image = gl_release_image, + + .init = glx_init, + .deinit = glx_deinit, + .root_change = gl_root_change, + .prepare = gl_prepare, .buffer_age = glx_buffer_age, - .render_shadow = default_backend_render_shadow, - .fill = gl_fill, + .last_render_time = gl_last_render_time, .create_blur_context = gl_create_blur_context, .destroy_blur_context = gl_destroy_blur_context, .get_blur_size = gl_get_blur_size, .diagnostics = glx_diagnostics, + .device_status = gl_device_status, + .create_shader = gl_create_window_shader, + .destroy_shader = gl_destroy_window_shader, + .get_shader_attributes = gl_get_shader_attributes, .max_buffer_age = 5, // Why? }; -/** - * Check if a GLX extension exists. - */ -static inline bool glx_has_extension(Display *dpy, int screen, const char *ext) { - const char *glx_exts = glXQueryExtensionsString(dpy, screen); - if (!glx_exts) { - log_error("Failed get GLX extension list."); - return false; - } - - auto inlen = strlen(ext); - const char *curr = glx_exts; - bool match = false; - while (curr && !match) { - const char *end = strchr(curr, ' '); - if (!end) { - // Last extension string - match = strcmp(ext, curr) == 0; - } else if (curr + inlen == end) { - // Length match, do match string - match = strncmp(ext, curr, (unsigned long)(end - curr)) == 0; - } - curr = end ? end + 1 : NULL; - } - - if (!match) { - log_info("Missing GLX extension %s.", ext); - } else { - log_info("Found GLX extension %s.", ext); - } - - return match; -} - struct glxext_info glxext = {0}; -PFNGLXGETVIDEOSYNCSGIPROC glXGetVideoSyncSGI; -PFNGLXWAITVIDEOSYNCSGIPROC glXWaitVideoSyncSGI; -PFNGLXGETSYNCVALUESOMLPROC glXGetSyncValuesOML; -PFNGLXWAITFORMSCOMLPROC glXWaitForMscOML; -PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT; -PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI; -PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA; -PFNGLXBINDTEXIMAGEEXTPROC glXBindTexImageEXT; -PFNGLXRELEASETEXIMAGEEXTPROC glXReleaseTexImageEXT; -PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB; - -#ifdef GLX_MESA_query_renderer -PFNGLXQUERYCURRENTRENDERERINTEGERMESAPROC glXQueryCurrentRendererIntegerMESA; -#endif void glxext_init(Display *dpy, int screen) { if (glxext.initialized) { return; } glxext.initialized = true; -#define check_ext(name) glxext.has_##name = glx_has_extension(dpy, screen, #name) +#define check_ext(name) \ + glxext.has_##name = epoxy_has_glx_extension(dpy, screen, #name); \ + log_info("Extension " #name " - %s", glxext.has_##name ? "present" : "absent") + check_ext(GLX_SGI_video_sync); check_ext(GLX_SGI_swap_control); check_ext(GLX_OML_sync_control); @@ -609,40 +569,9 @@ void glxext_init(Display *dpy, int screen) { check_ext(GLX_EXT_texture_from_pixmap); check_ext(GLX_ARB_create_context); check_ext(GLX_EXT_buffer_age); + check_ext(GLX_ARB_create_context_robustness); #ifdef GLX_MESA_query_renderer check_ext(GLX_MESA_query_renderer); #endif #undef check_ext - -#define lookup(name) (name = (__typeof__(name))glXGetProcAddress((GLubyte *)#name)) - // Checking if the returned function pointer is NULL is not really necessary, - // or maybe not even useful, since glXGetProcAddress might always return - // something. We are doing it just for completeness' sake. - if (!lookup(glXGetVideoSyncSGI) || !lookup(glXWaitVideoSyncSGI)) { - glxext.has_GLX_SGI_video_sync = false; - } - if (!lookup(glXSwapIntervalEXT)) { - glxext.has_GLX_EXT_swap_control = false; - } - if (!lookup(glXSwapIntervalMESA)) { - glxext.has_GLX_MESA_swap_control = false; - } - if (!lookup(glXSwapIntervalSGI)) { - glxext.has_GLX_SGI_swap_control = false; - } - if (!lookup(glXWaitForMscOML) || !lookup(glXGetSyncValuesOML)) { - glxext.has_GLX_OML_sync_control = false; - } - if (!lookup(glXBindTexImageEXT) || !lookup(glXReleaseTexImageEXT)) { - glxext.has_GLX_EXT_texture_from_pixmap = false; - } - if (!lookup(glXCreateContextAttribsARB)) { - glxext.has_GLX_ARB_create_context = false; - } -#ifdef GLX_MESA_query_renderer - if (!lookup(glXQueryCurrentRendererIntegerMESA)) { - glxext.has_GLX_MESA_query_renderer = false; - } -#endif -#undef lookup } diff --git a/src/backend/gl/glx.h b/src/backend/gl/glx.h index 1061f0bbc4..7167936542 100644 --- a/src/backend/gl/glx.h +++ b/src/backend/gl/glx.h @@ -1,22 +1,14 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once -#include -// Older version of glx.h defines function prototypes for these extensions... -// Rename them to avoid conflicts -#define glXSwapIntervalMESA glXSwapIntervalMESA_ -#define glXBindTexImageEXT glXBindTexImageEXT_ -#define glXReleaseTexImageEXT glXReleaseTexImageEXT -#include -#undef glXSwapIntervalMESA -#undef glXBindTexImageEXT -#undef glXReleaseTexImageEXT #include -#include +#include +#include #include +#include -#include "log.h" #include "compiler.h" +#include "log.h" #include "utils.h" #include "x.h" @@ -27,22 +19,8 @@ struct glx_fbconfig_info { int y_inverted; }; -/// The search criteria for glx_find_fbconfig -struct glx_fbconfig_criteria { - /// Bit width of the red component - int red_size; - /// Bit width of the green component - int green_size; - /// Bit width of the blue component - int blue_size; - /// Bit width of the alpha component - int alpha_size; - /// The depth of X visual - int visual_depth; -}; - -struct glx_fbconfig_info *glx_find_fbconfig(Display *, int screen, struct xvisual_info); - +bool glx_find_fbconfig(struct x_connection *c, struct xvisual_info m, + struct glx_fbconfig_info *info); struct glxext_info { bool initialized; @@ -55,23 +33,9 @@ struct glxext_info { bool has_GLX_ARB_create_context; bool has_GLX_EXT_buffer_age; bool has_GLX_MESA_query_renderer; + bool has_GLX_ARB_create_context_robustness; }; extern struct glxext_info glxext; -extern PFNGLXGETVIDEOSYNCSGIPROC glXGetVideoSyncSGI; -extern PFNGLXWAITVIDEOSYNCSGIPROC glXWaitVideoSyncSGI; -extern PFNGLXGETSYNCVALUESOMLPROC glXGetSyncValuesOML; -extern PFNGLXWAITFORMSCOMLPROC glXWaitForMscOML; -extern PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT; -extern PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI; -extern PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA; -extern PFNGLXBINDTEXIMAGEEXTPROC glXBindTexImageEXT; -extern PFNGLXRELEASETEXIMAGEEXTPROC glXReleaseTexImageEXT; -extern PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB; - -#ifdef GLX_MESA_query_renderer -extern PFNGLXQUERYCURRENTRENDERERINTEGERMESAPROC glXQueryCurrentRendererIntegerMESA; -#endif - void glxext_init(Display *, int screen); diff --git a/src/backend/gl/shaders.c b/src/backend/gl/shaders.c new file mode 100644 index 0000000000..3a87324fb1 --- /dev/null +++ b/src/backend/gl/shaders.c @@ -0,0 +1,233 @@ +#include "gl_common.h" +// Don't macro expand bool! +#undef bool +// clang-format off +const char copy_area_frag[] = GLSL(330, + layout(location = UNIFORM_TEX_LOC) + uniform sampler2D tex; + in vec2 texcoord; + void main() { + vec2 texsize = textureSize(tex, 0); + gl_FragColor = texture2D(tex, texcoord / texsize, 0); + } +); + +const char copy_area_with_dither_frag[] = GLSL(330, + layout(location = UNIFORM_TEX_LOC) + uniform sampler2D tex; + in vec2 texcoord; + vec4 dither(vec4, vec2); + void main() { + vec2 texsize = textureSize(tex, 0); + gl_FragColor = dither(texture2D(tex, texcoord / texsize, 0), gl_FragCoord.xy); + } +); + +const char blend_with_mask_frag[] = GLSL(330, + layout(location = UNIFORM_TEX_LOC) + uniform sampler2D tex; + layout(location = UNIFORM_OPACITY_LOC) + uniform float opacity; + in vec2 texcoord; + float mask_factor(); + void main() { + gl_FragColor = texelFetch(tex, ivec2(texcoord.xy), 0) * opacity * mask_factor(); + } +); + +const char fill_frag[] = GLSL(330, + layout(location = UNIFORM_COLOR_LOC) + uniform vec4 color; + void main() { + gl_FragColor = color; + } +); + +const char fill_vert[] = GLSL(330, + layout(location = 0) in vec2 in_coord; + layout(location = UNIFORM_PROJECTION_LOC) + uniform mat4 projection; + void main() { + gl_Position = projection * vec4(in_coord, 0, 1); + } +); + +const char interpolating_frag[] = GLSL(330, + layout(location = UNIFORM_TEX_LOC) + uniform sampler2D tex; + in vec2 texcoord; + void main() { + gl_FragColor = vec4(texture2D(tex, vec2(texcoord.xy), 0).rgb, 1); + } +); + +const char interpolating_vert[] = GLSL(330, + layout(location = UNIFORM_PROJECTION_LOC) + uniform mat4 projection; + layout(location = UNIFORM_TEXSIZE_LOC) + uniform vec2 texsize; + layout(location = 0) in vec2 in_coord; + layout(location = 1) in vec2 in_texcoord; + out vec2 texcoord; + void main() { + gl_Position = projection * vec4(in_coord, 0, 1); + texcoord = in_texcoord / texsize; + } +); +const char masking_glsl[] = GLSL(330, + layout(location = UNIFORM_MASK_TEX_LOC) + uniform sampler2D mask_tex; + layout(location = UNIFORM_MASK_OFFSET_LOC) + uniform vec2 mask_offset; + layout(location = UNIFORM_MASK_CORNER_RADIUS_LOC) + uniform float mask_corner_radius; + layout(location = UNIFORM_MASK_INVERTED_LOC) + uniform bool mask_inverted; + in vec2 texcoord; + float mask_rectangle_sdf(vec2 point, vec2 half_size) { + vec2 d = abs(point) - half_size; + return length(max(d, 0.0)); + } + float mask_factor() { + vec2 mask_size = textureSize(mask_tex, 0); + vec2 maskcoord = texcoord - mask_offset; + vec4 mask = texture2D(mask_tex, maskcoord / mask_size); + if (mask_corner_radius != 0) { + vec2 inner_size = mask_size - vec2(mask_corner_radius) * 2.0f; + float dist = mask_rectangle_sdf(maskcoord - mask_size / 2.0f, + inner_size / 2.0f) - mask_corner_radius; + if (dist > 0.0f) { + mask.r *= (1.0f - clamp(dist, 0.0f, 1.0f)); + } + } + if (mask_inverted) { + mask.rgb = 1.0 - mask.rgb; + } + return mask.r; + } +); +const char blit_shader_glsl[] = GLSL(330, + layout(location = UNIFORM_OPACITY_LOC) + uniform float opacity; + layout(location = UNIFORM_DIM_LOC) + uniform float dim; + layout(location = UNIFORM_CORNER_RADIUS_LOC) + uniform float corner_radius; + layout(location = UNIFORM_BORDER_WIDTH_LOC) + uniform float border_width; + layout(location = UNIFORM_INVERT_COLOR_LOC) + uniform bool invert_color; + in vec2 texcoord; + layout(location = UNIFORM_TEX_LOC) + uniform sampler2D tex; + layout(location = UNIFORM_EFFECTIVE_SIZE_LOC) + uniform vec2 effective_size; + layout(location = UNIFORM_BRIGHTNESS_LOC) + uniform sampler2D brightness; + layout(location = UNIFORM_MAX_BRIGHTNESS_LOC) + uniform float max_brightness; + layout(location = UNIFORM_TIME_LOC) + uniform float time; + // Signed distance field for rectangle center at (0, 0), with size of + // half_size * 2 + float rectangle_sdf(vec2 point, vec2 half_size) { + vec2 d = abs(point) - half_size; + return length(max(d, 0.0)); + } + + vec4 default_post_processing(vec4 c) { + vec4 border_color = texture(tex, vec2(0.0, 0.5)); + if (invert_color) { + c = vec4(c.aaa - c.rgb, c.a); + border_color = vec4(border_color.aaa - border_color.rgb, border_color.a); + } + c = vec4(c.rgb * (1.0 - dim), c.a) * opacity; + border_color = vec4(border_color.rgb * (1.0 - dim), border_color.a) * opacity; + + vec3 rgb_brightness = texelFetch(brightness, ivec2(0, 0), 0).rgb; + // Ref: https://en.wikipedia.org/wiki/Relative_luminance + float brightness = rgb_brightness.r * 0.21 + + rgb_brightness.g * 0.72 + + rgb_brightness.b * 0.07; + if (brightness > max_brightness) { + c.rgb = c.rgb * (max_brightness / brightness); + border_color.rgb = border_color.rgb * (max_brightness / brightness); + } + + // Rim color is the color of the outer rim of the window, if there is no + // border, it's the color of the window itself, otherwise it's the border. + // Using mix() to avoid a branch here. + vec4 rim_color = mix(c, border_color, clamp(border_width, 0.0f, 1.0f)); + + vec2 outer_size = effective_size; + vec2 inner_size = outer_size - vec2(corner_radius) * 2.0f; + float rect_distance = rectangle_sdf(texcoord - outer_size / 2.0f, + inner_size / 2.0f) - corner_radius; + if (rect_distance > 0.0f) { + c = (1.0f - clamp(rect_distance, 0.0f, 1.0f)) * rim_color; + } else { + float factor = clamp(rect_distance + border_width, 0.0f, 1.0f); + c = (1.0f - factor) * c + factor * border_color; + } + + return c; + } + + vec4 window_shader(); + float mask_factor(); + + void main() { + gl_FragColor = window_shader() * mask_factor(); + } +); + +const char blit_shader_default[] = GLSL(330, + in vec2 texcoord; + uniform sampler2D tex; + vec4 default_post_processing(vec4 c); + vec4 window_shader() { + vec2 texsize = textureSize(tex, 0); + vec4 c = texture2D(tex, texcoord / texsize, 0); + return default_post_processing(c); + } +); + +const char vertex_shader[] = GLSL(330, + layout(location = UNIFORM_PROJECTION_LOC) + uniform mat4 projection; + layout(location = UNIFORM_SCALE_LOC) + uniform float scale = 1.0f; + layout(location = 0) in vec2 coord; + layout(location = 1) in vec2 in_texcoord; + out vec2 texcoord; + void main() { + gl_Position = projection * vec4(coord, 0, scale); + texcoord = in_texcoord; + } +); +/// Add dithering for downsampling from 16-bit color to 8-bit color. +const char dither_glsl[] = GLSL(330, + // Stolen from: https://www.shadertoy.com/view/7sfXDn + float bayer2(vec2 a) { + a = floor(a); + return fract(a.x / 2. + a.y * a.y * .75); + } + // 16 * 16 is 2^8, so in total we have equivalent of 16-bit + // color depth, should be enough? + float bayer(vec2 a16) { + vec2 a8 = a16 * .5; + vec2 a4 = a8 * .5; + vec2 a2 = a4 * .5; + float bayer32 = ((bayer2(a2) * .25 + bayer2( a4)) + * .25 + bayer2( a8)) + * .25 + bayer2(a16); + return bayer32; + } + vec4 dither(vec4 c, vec2 coord) { + vec4 residual = mod(c, 1.0 / 255.0); + residual = min(residual, vec4(1.0 / 255.0) - residual); + vec4 dithered = vec4(greaterThan(residual, vec4(1.0 / 65535.0))); + return vec4(c + dithered * bayer(coord) / 255.0); + } +); +// clang-format on diff --git a/src/backend/meson.build b/src/backend/meson.build index b8f0ad9a1f..afc2d51dd1 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -1,7 +1,17 @@ # enable xrender -srcs += [ files('backend_common.c', 'xrender/xrender.c', 'dummy/dummy.c', 'backend.c', 'driver.c') ] +srcs += [ + files( + 'dummy/dummy.c', + 'xrender/xrender.c', + 'backend.c', + 'backend_common.c', + 'driver.c', + ), +] # enable opengl if get_option('opengl') - srcs += [ files('gl/gl_common.c', 'gl/glx.c') ] + srcs += [ + files('gl/blur.c', 'gl/egl.c', 'gl/gl_common.c', 'gl/glx.c', 'gl/shaders.c'), + ] endif diff --git a/src/backend/xrender/xrender.c b/src/backend/xrender/xrender.c index 32ddabf8ed..11decd2e9f 100644 --- a/src/backend/xrender/xrender.c +++ b/src/backend/xrender/xrender.c @@ -2,6 +2,7 @@ // Copyright (c) Yuxuan Shui #include #include +#include #include #include @@ -13,7 +14,9 @@ #include "backend/backend.h" #include "backend/backend_common.h" +#include "backend/driver.h" #include "common.h" +#include "compiler.h" #include "config.h" #include "kernel.h" #include "log.h" @@ -24,24 +27,44 @@ #include "win.h" #include "x.h" -typedef struct _xrender_data { - backend_t base; - /// If vsync is enabled and supported by the current system - bool vsync; - xcb_visualid_t default_visual; +struct xrender_image_data_inner { + ivec2 size; + enum backend_image_format format; + struct xrender_rounded_rectangle_cache *rounded_rectangle; + // Pixmap that the client window draws to, + // it will contain the content of client window. + xcb_pixmap_t pixmap; + // A Picture links to the Pixmap + xcb_render_picture_t pict; + xcb_render_pictformat_t pictfmt; + uint8_t depth; + // Whether we allocated it this pixmap. + // or not, i.e. this pixmap is passed in via xrender_bind_pixmap + bool is_pixmap_internal; + bool has_alpha; +}; + +typedef struct xrender_data { + struct backend_base base; + /// Quirks + uint32_t quirks; /// Target window xcb_window_t target_win; /// Painting target, it is either the root or the overlay xcb_render_picture_t target; /// Back buffers. Double buffer, with 1 for temporary render use - xcb_render_picture_t back[3]; + xcb_render_picture_t back[2]; + /// Fake image to represent the back buffer + struct xrender_image_data_inner back_image; + /// Damaged region of the back image since the last present + region_t back_damaged; /// The back buffer that is for temporary use /// Age of each back buffer. - int buffer_age[3]; + int buffer_age[2]; /// The back buffer we should be painting into int curr_back; /// The corresponding pixmap to the back buffer - xcb_pixmap_t back_pixmap[3]; + xcb_pixmap_t back_pixmap[2]; /// Pictures of pixel of different alpha value, used as a mask to /// paint transparent images xcb_render_picture_t alpha_pict[256]; @@ -53,13 +76,16 @@ typedef struct _xrender_data { /// 1x1 black picture xcb_render_picture_t black_pixel; - /// Width and height of the target pixmap - int target_width, target_height; - xcb_special_event_t *present_event; + + /// Cache an X region to avoid creating and destroying it every frame. A + /// workaround for yshui/picom#1166. + xcb_xfixes_region_t present_region; + /// If vsync is enabled and supported by the current system + bool vsync; } xrender_data; -struct _xrender_blur_context { +struct xrender_blur_context { enum blur_method method; /// Blur kernels converted to X format struct x_convolution_kernel **x_blur_kernel; @@ -70,83 +96,275 @@ struct _xrender_blur_context { int x_blur_kernel_count; }; -struct _xrender_image_data_inner { - // struct backend_image_inner_base - int refcount; - bool has_alpha; - - // Pixmap that the client window draws to, - // it will contain the content of client window. - xcb_pixmap_t pixmap; - // A Picture links to the Pixmap - xcb_render_picture_t pict; - int width, height; - xcb_visualid_t visual; - uint8_t depth; - // Whether we own this image, e.g. we allocated it; - // or not, e.g. this is a named pixmap of a X window. - bool owned; +struct xrender_rounded_rectangle_cache { + // A cached picture of a rounded rectangle. Xorg rasterizes shapes on CPU so it's + // exceedingly slow. + xcb_render_picture_t p; + int radius; }; -static void compose_impl(struct _xrender_data *xd, const struct backend_image *img, - int dst_x, int dst_y, const region_t *reg_paint, - const region_t *reg_visible, xcb_render_picture_t result) { - auto alpha_pict = xd->alpha_pict[(int)(img->opacity * MAX_ALPHA)]; - auto inner = (struct _xrender_image_data_inner *)img->inner; - region_t reg; - - bool has_alpha = inner->has_alpha || img->opacity != 1; - const auto tmpw = to_u16_checked(inner->width); - const auto tmph = to_u16_checked(inner->height); - const auto tmpew = to_u16_checked(img->ewidth); - const auto tmpeh = to_u16_checked(img->eheight); +/// Make a picture of size width x height, which has a rounded rectangle of corner_radius +/// rendered in it. +struct xrender_rounded_rectangle_cache * +xrender_make_rounded_corner_cache(struct x_connection *c, xcb_render_picture_t src, + int width, int height, int corner_radius) { + auto picture = x_create_picture_with_standard(c, width, height, + XCB_PICT_STANDARD_ARGB_32, 0, NULL); + if (picture == XCB_NONE) { + return NULL; + } + + int inner_height = height - 2 * corner_radius; + int cap_height = corner_radius; + if (inner_height < 0) { + cap_height = height / 2; + inner_height = 0; + } + auto points = ccalloc(cap_height * 4 + 4, xcb_render_pointfix_t); + int point_count = 0; + +#define ADD_POINT(px, py) \ + assert(point_count < cap_height * 4 + 4); \ + points[point_count].x = DOUBLE_TO_XFIXED(px); \ + points[point_count].y = DOUBLE_TO_XFIXED(py); \ + point_count += 1; + + // The top cap + for (int i = 0; i <= cap_height; i++) { + double y = corner_radius - i; + double delta = sqrt(corner_radius * corner_radius - y * y); + double left = corner_radius - delta; + double right = width - corner_radius + delta; + if (left >= right) { + continue; + } + ADD_POINT(left, i); + ADD_POINT(right, i); + } + + // The middle rectangle + if (inner_height > 0) { + ADD_POINT(0, cap_height + inner_height); + ADD_POINT(width, cap_height + inner_height); + } + + // The bottom cap + for (int i = cap_height + inner_height + 1; i <= height; i++) { + double y = corner_radius - (height - i); + double delta = sqrt(corner_radius * corner_radius - y * y); + double left = corner_radius - delta; + double right = width - corner_radius + delta; + if (left >= right) { + break; + } + ADD_POINT(left, i); + ADD_POINT(right, i); + } +#undef ADD_POINT + + XCB_AWAIT_VOID(xcb_render_tri_strip, c->c, XCB_RENDER_PICT_OP_SRC, src, picture, + x_get_pictfmt_for_standard(c, XCB_PICT_STANDARD_A_8), 0, 0, + (uint32_t)point_count, points); + free(points); + auto ret = ccalloc(1, struct xrender_rounded_rectangle_cache); + ret->p = picture; + ret->radius = corner_radius; + return ret; +} + +static void +xrender_release_rounded_corner_cache(backend_t *base, + struct xrender_rounded_rectangle_cache *cache) { + if (!cache) { + return; + } + + x_free_picture(base->c, cache->p); + free(cache); +} + +static inline void xrender_set_picture_repeat(struct xrender_data *xd, + xcb_render_picture_t pict, uint32_t repeat) { + xcb_render_change_picture_value_list_t values = { + .repeat = repeat, + }; + set_cant_fail_cookie(xd->base.c, xcb_render_change_picture(xd->base.c->c, pict, + XCB_RENDER_CP_REPEAT, + (uint32_t *)&values)); +} + +static inline void xrender_record_back_damage(struct xrender_data *xd, + struct xrender_image_data_inner *target, + const region_t *region) { + if (target == &xd->back_image && xd->vsync) { + pixman_region32_union(&xd->back_damaged, &xd->back_damaged, region); + } +} + +/// Normalize a mask, applying inversion and corner radius. +/// +/// @param extent the extent covered by mask region, in mask coordinate +/// @param alpha_pict the picture to use for alpha mask +/// @param new_origin the new origin of the normalized mask picture +/// @param allocated whether the returned picture is newly allocated +static xcb_render_picture_t +xrender_process_mask(struct xrender_data *xd, const struct backend_mask_image *mask, + rect_t extent, xcb_render_picture_t alpha_pict, ivec2 *new_origin, + bool *allocated) { + auto inner = (struct xrender_image_data_inner *)mask->image; + if (!inner) { + *allocated = false; + return alpha_pict; + } + if (!mask->inverted && mask->corner_radius == 0 && alpha_pict == XCB_NONE) { + *allocated = false; + return inner->pict; + } + auto const w_u16 = to_u16_checked(extent.x2 - extent.x1); + auto const h_u16 = to_u16_checked(extent.y2 - extent.y1); + *allocated = true; + *new_origin = + (ivec2){.x = extent.x1 + mask->origin.x, .y = extent.y1 + mask->origin.y}; + x_clear_picture_clip_region(xd->base.c, inner->pict); + auto ret = x_create_picture_with_pictfmt( + xd->base.c, extent.x2 - extent.x1, extent.y2 - extent.y1, inner->pictfmt, + inner->depth, XCB_RENDER_CP_REPEAT, + (xcb_render_create_picture_value_list_t[]){XCB_RENDER_REPEAT_NONE}); + xrender_set_picture_repeat(xd, inner->pict, XCB_RENDER_REPEAT_NONE); + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_SRC, inner->pict, XCB_NONE, + ret, to_i16_checked(extent.x1 - mask->origin.x), + to_i16_checked(extent.y1 - mask->origin.y), 0, 0, 0, 0, + w_u16, h_u16); + if (mask->corner_radius != 0) { + if (inner->rounded_rectangle != NULL && + inner->rounded_rectangle->radius != (int)mask->corner_radius) { + xrender_release_rounded_corner_cache(&xd->base, + inner->rounded_rectangle); + inner->rounded_rectangle = NULL; + } + if (inner->rounded_rectangle == NULL) { + inner->rounded_rectangle = xrender_make_rounded_corner_cache( + xd->base.c, xd->white_pixel, inner->size.width, + inner->size.height, (int)mask->corner_radius); + } + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_IN_REVERSE, + inner->rounded_rectangle->p, XCB_NONE, ret, + to_i16_checked(extent.x1), to_i16_checked(extent.y1), + 0, 0, 0, 0, w_u16, h_u16); + } + + if (mask->inverted) { + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_XOR, xd->white_pixel, + XCB_NONE, ret, 0, 0, 0, 0, 0, 0, w_u16, h_u16); + } + + if (alpha_pict != XCB_NONE) { + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_IN_REVERSE, alpha_pict, + XCB_NONE, ret, 0, 0, 0, 0, 0, 0, w_u16, h_u16); + } + + return ret; +} + +static bool xrender_blit(struct backend_base *base, ivec2 origin, + image_handle target_handle, const struct backend_blit_args *args) { + auto xd = (struct xrender_data *)base; + auto inner = (struct xrender_image_data_inner *)args->source_image; + auto target = (struct xrender_image_data_inner *)target_handle; + bool mask_allocated = false; + auto mask_pict = xd->alpha_pict[(int)(args->opacity * MAX_ALPHA)]; + auto extent = *pixman_region32_extents(args->target_mask); + if (!pixman_region32_not_empty(args->target_mask)) { + return true; + } + int16_t mask_pict_dst_x = 0, mask_pict_dst_y = 0; + if (args->source_mask != NULL) { + ivec2 mask_origin = args->source_mask->origin; + mask_pict = xrender_process_mask(xd, args->source_mask, extent, + args->opacity < 1.0 ? mask_pict : XCB_NONE, + &mask_origin, &mask_allocated); + mask_pict_dst_x = to_i16_checked(-mask_origin.x); + mask_pict_dst_y = to_i16_checked(-mask_origin.y); + } + + // After this point, mask_pict and mask->region have different origins. + + bool has_alpha = inner->has_alpha || args->opacity != 1; + auto const tmpw = to_u16_checked(inner->size.width); + auto const tmph = to_u16_checked(inner->size.height); + auto const tmpew = to_u16_checked(args->effective_size.width); + auto const tmpeh = to_u16_checked(args->effective_size.height); const xcb_render_color_t dim_color = { - .red = 0, .green = 0, .blue = 0, .alpha = (uint16_t)(0xffff * img->dim)}; + .red = 0, .green = 0, .blue = 0, .alpha = (uint16_t)(0xffff * args->dim)}; // Clip region of rendered_pict might be set during rendering, clear it to // make sure we get everything into the buffer x_clear_picture_clip_region(xd->base.c, inner->pict); - - pixman_region32_init(®); - pixman_region32_intersect(®, (region_t *)reg_paint, (region_t *)reg_visible); - x_set_picture_clip_region(xd->base.c, result, 0, 0, ®); - if ((img->color_inverted || img->dim != 0) && has_alpha) { + xrender_set_picture_repeat(xd, inner->pict, XCB_RENDER_REPEAT_NORMAL); + + x_set_picture_clip_region(xd->base.c, target->pict, 0, 0, args->target_mask); + if (args->corner_radius != 0) { + if (inner->rounded_rectangle != NULL && + inner->rounded_rectangle->radius != (int)args->corner_radius) { + xrender_release_rounded_corner_cache(&xd->base, + inner->rounded_rectangle); + inner->rounded_rectangle = NULL; + } + if (inner->rounded_rectangle == NULL) { + inner->rounded_rectangle = xrender_make_rounded_corner_cache( + xd->base.c, xd->white_pixel, inner->size.width, + inner->size.height, (int)args->corner_radius); + } + } + if (((args->color_inverted || args->dim != 0) && has_alpha) || + args->corner_radius != 0) { // Apply image properties using a temporary image, because the source - // image is transparent. Otherwise the properties can be applied directly - // on the target image. - auto tmp_pict = - x_create_picture_with_visual(xd->base.c, xd->base.root, inner->width, - inner->height, inner->visual, 0, NULL); - - // Set clip region translated to source coordinate - x_set_picture_clip_region(xd->base.c, tmp_pict, to_i16_checked(-dst_x), - to_i16_checked(-dst_y), ®); + // image is transparent or will get transparent corners. Otherwise the + // properties can be applied directly on the target image. + // Also force a 32-bit ARGB format for transparent corners, otherwise the + // corners become black. + auto pictfmt = inner->pictfmt; + uint8_t depth = inner->depth; + if (args->corner_radius != 0 && inner->depth != 32) { + pictfmt = x_get_pictfmt_for_standard(xd->base.c, + XCB_PICT_STANDARD_ARGB_32); + depth = 32; + } + auto tmp_pict = x_create_picture_with_pictfmt( + xd->base.c, inner->size.width, inner->size.height, pictfmt, depth, 0, NULL); + + x_set_picture_clip_region(xd->base.c, tmp_pict, to_i16_checked(-origin.x), + to_i16_checked(-origin.y), args->target_mask); // Copy source -> tmp - xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_SRC, inner->pict, + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_SRC, inner->pict, XCB_NONE, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); - if (img->color_inverted) { + + if (args->color_inverted) { if (inner->has_alpha) { - auto tmp_pict2 = x_create_picture_with_visual( - xd->base.c, xd->base.root, tmpw, tmph, inner->visual, + auto tmp_pict2 = x_create_picture_with_pictfmt( + xd->base.c, tmpw, tmph, inner->pictfmt, inner->depth, 0, NULL); - xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_SRC, + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_SRC, tmp_pict, XCB_NONE, tmp_pict2, 0, 0, 0, 0, 0, 0, tmpw, tmph); - xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_DIFFERENCE, + xcb_render_composite(xd->base.c->c, + XCB_RENDER_PICT_OP_DIFFERENCE, xd->white_pixel, XCB_NONE, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); xcb_render_composite( - xd->base.c, XCB_RENDER_PICT_OP_IN_REVERSE, tmp_pict2, + xd->base.c->c, XCB_RENDER_PICT_OP_IN_REVERSE, tmp_pict2, XCB_NONE, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); - xcb_render_free_picture(xd->base.c, tmp_pict2); + x_free_picture(xd->base.c, tmp_pict2); } else { - xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_DIFFERENCE, + xcb_render_composite(xd->base.c->c, + XCB_RENDER_PICT_OP_DIFFERENCE, xd->white_pixel, XCB_NONE, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); } } - if (img->dim != 0) { + + if (args->dim != 0) { // Dim the actually content of window xcb_rectangle_t rect = { .x = 0, @@ -155,94 +373,131 @@ static void compose_impl(struct _xrender_data *xd, const struct backend_image *i .height = tmph, }; - xcb_render_fill_rectangles(xd->base.c, XCB_RENDER_PICT_OP_OVER, + xcb_render_fill_rectangles(xd->base.c->c, XCB_RENDER_PICT_OP_OVER, tmp_pict, dim_color, 1, &rect); } - xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_OVER, tmp_pict, - alpha_pict, result, 0, 0, 0, 0, to_i16_checked(dst_x), - to_i16_checked(dst_y), tmpew, tmpeh); - xcb_render_free_picture(xd->base.c, tmp_pict); + if (args->corner_radius != 0 && inner->rounded_rectangle != NULL) { + // Clip tmp_pict with a rounded rectangle + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_IN_REVERSE, + inner->rounded_rectangle->p, XCB_NONE, + tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); + } + + xcb_render_composite(xd->base.c->c, XCB_RENDER_PICT_OP_OVER, tmp_pict, + mask_pict, target->pict, 0, 0, mask_pict_dst_x, + mask_pict_dst_y, to_i16_checked(origin.x), + to_i16_checked(origin.y), tmpew, tmpeh); + xcb_render_free_picture(xd->base.c->c, tmp_pict); } else { uint8_t op = (has_alpha ? XCB_RENDER_PICT_OP_OVER : XCB_RENDER_PICT_OP_SRC); - xcb_render_composite(xd->base.c, op, inner->pict, alpha_pict, result, 0, - 0, 0, 0, to_i16_checked(dst_x), - to_i16_checked(dst_y), tmpew, tmpeh); - if (img->dim != 0 || img->color_inverted) { + xcb_render_composite(xd->base.c->c, op, inner->pict, mask_pict, + target->pict, 0, 0, mask_pict_dst_x, mask_pict_dst_y, + to_i16_checked(origin.x), to_i16_checked(origin.y), + tmpew, tmpeh); + if (args->dim != 0 || args->color_inverted) { // Apply properties, if we reach here, then has_alpha == false assert(!has_alpha); - if (img->color_inverted) { - xcb_render_composite(xd->base.c, XCB_RENDER_PICT_OP_DIFFERENCE, - xd->white_pixel, XCB_NONE, result, 0, - 0, 0, 0, to_i16_checked(dst_x), - to_i16_checked(dst_y), tmpew, tmpeh); + if (args->color_inverted) { + xcb_render_composite( + xd->base.c->c, XCB_RENDER_PICT_OP_DIFFERENCE, + xd->white_pixel, XCB_NONE, target->pict, 0, 0, 0, 0, + to_i16_checked(origin.x), to_i16_checked(origin.y), + tmpew, tmpeh); } - if (img->dim != 0) { + if (args->dim != 0) { // Dim the actually content of window xcb_rectangle_t rect = { - .x = to_i16_checked(dst_x), - .y = to_i16_checked(dst_y), + .x = to_i16_checked(origin.x), + .y = to_i16_checked(origin.y), .width = tmpew, .height = tmpeh, }; - xcb_render_fill_rectangles(xd->base.c, XCB_RENDER_PICT_OP_OVER, - result, dim_color, 1, &rect); + xcb_render_fill_rectangles( + xd->base.c->c, XCB_RENDER_PICT_OP_OVER, target->pict, + dim_color, 1, &rect); } } } - pixman_region32_fini(®); + if (mask_allocated) { + x_free_picture(xd->base.c, mask_pict); + } + xrender_record_back_damage(xd, target, args->target_mask); + return true; } -static void compose(backend_t *base, void *img_data, int dst_x, int dst_y, - const region_t *reg_paint, const region_t *reg_visible) { - struct _xrender_data *xd = (void *)base; - return compose_impl(xd, img_data, dst_x, dst_y, reg_paint, reg_visible, xd->back[2]); +static bool +xrender_clear(struct backend_base *base, image_handle target_handle, struct color color) { + auto xd = (struct xrender_data *)base; + auto target = (struct xrender_image_data_inner *)target_handle; + xcb_render_color_t col = { + .red = (uint16_t)(color.red * 0xffff), + .green = (uint16_t)(color.green * 0xffff), + .blue = (uint16_t)(color.blue * 0xffff), + .alpha = (uint16_t)(color.alpha * 0xffff), + }; + x_clear_picture_clip_region(base->c, target->pict); + xcb_render_fill_rectangles( + xd->base.c->c, XCB_RENDER_PICT_OP_SRC, target->pict, col, 1, + (xcb_rectangle_t[]){{.x = 0, + .y = 0, + .width = to_u16_checked(target->size.width), + .height = to_u16_checked(target->size.height)}}); + if (target == &xd->back_image) { + pixman_region32_clear(&xd->back_damaged); + pixman_region32_union_rect(&xd->back_damaged, &xd->back_damaged, 0, 0, + (unsigned)target->size.width, + (unsigned)target->size.height); + } + return true; } -static void fill(backend_t *base, struct color c, const region_t *clip) { - struct _xrender_data *xd = (void *)base; - const rect_t *extent = pixman_region32_extents((region_t *)clip); - x_set_picture_clip_region(base->c, xd->back[2], 0, 0, clip); - // color is in X fixed point representation - xcb_render_fill_rectangles( - base->c, XCB_RENDER_PICT_OP_OVER, xd->back[2], - (xcb_render_color_t){.red = (uint16_t)(c.red * 0xffff), - .green = (uint16_t)(c.green * 0xffff), - .blue = (uint16_t)(c.blue * 0xffff), - .alpha = (uint16_t)(c.alpha * 0xffff)}, - 1, - (xcb_rectangle_t[]){{.x = to_i16_checked(extent->x1), - .y = to_i16_checked(extent->y1), - .width = to_u16_checked(extent->x2 - extent->x1), - .height = to_u16_checked(extent->y2 - extent->y1)}}); +static bool +xrender_copy_area(struct backend_base *base, ivec2 origin, image_handle target_handle, + image_handle source_handle, const region_t *region) { + auto xd = (struct xrender_data *)base; + auto source = (struct xrender_image_data_inner *)source_handle; + auto target = (struct xrender_image_data_inner *)target_handle; + auto extent = pixman_region32_extents(region); + x_set_picture_clip_region(base->c, source->pict, 0, 0, region); + x_clear_picture_clip_region(base->c, target->pict); + xrender_set_picture_repeat(xd, source->pict, XCB_RENDER_REPEAT_PAD); + xcb_render_composite( + base->c->c, XCB_RENDER_PICT_OP_SRC, source->pict, XCB_NONE, target->pict, + to_i16_checked(extent->x1), to_i16_checked(extent->y1), 0, 0, + to_i16_checked(origin.x + extent->x1), to_i16_checked(origin.y + extent->y1), + to_u16_checked(extent->x2 - extent->x1), to_u16_checked(extent->y2 - extent->y1)); + xrender_record_back_damage(xd, target, region); + return true; } -static bool blur(backend_t *backend_data, double opacity, void *ctx_, - const region_t *reg_blur, const region_t *reg_visible) { - struct _xrender_blur_context *bctx = ctx_; +static bool xrender_blur(struct backend_base *base, ivec2 origin, + image_handle target_handle, const struct backend_blur_args *args) { + auto bctx = (struct xrender_blur_context *)args->blur_context; + auto source_mask = args->source_image + ? (struct xrender_image_data_inner *)args->source_mask->image + : NULL; + auto source = (struct xrender_image_data_inner *)args->source_image; + auto target = (struct xrender_image_data_inner *)target_handle; if (bctx->method == BLUR_METHOD_NONE) { return true; } - struct _xrender_data *xd = (void *)backend_data; - xcb_connection_t *c = xd->base.c; - region_t reg_op; - pixman_region32_init(®_op); - pixman_region32_intersect(®_op, (region_t *)reg_blur, (region_t *)reg_visible); - if (!pixman_region32_not_empty(®_op)) { - pixman_region32_fini(®_op); + auto xd = (struct xrender_data *)base; + auto c = xd->base.c; + if (!pixman_region32_not_empty(args->target_mask)) { return true; } region_t reg_op_resized = - resize_region(®_op, bctx->resize_width, bctx->resize_height); + resize_region(args->target_mask, bctx->resize_width, bctx->resize_height); const pixman_box32_t *extent_resized = pixman_region32_extents(®_op_resized); - const auto height_resized = to_u16_checked(extent_resized->y2 - extent_resized->y1); - const auto width_resized = to_u16_checked(extent_resized->x2 - extent_resized->x1); + auto const height_resized = to_u16_checked(extent_resized->y2 - extent_resized->y1); + auto const width_resized = to_u16_checked(extent_resized->x2 - extent_resized->x1); static const char *filter0 = "Nearest"; // The "null" filter static const char *filter = "convolution"; @@ -251,14 +506,15 @@ static bool blur(backend_t *backend_data, double opacity, void *ctx_, const uint32_t pic_attrs_mask = XCB_RENDER_CP_REPEAT; const xcb_render_create_picture_value_list_t pic_attrs = {.repeat = XCB_RENDER_REPEAT_PAD}; xcb_render_picture_t tmp_picture[2] = { - x_create_picture_with_visual(xd->base.c, xd->base.root, width_resized, height_resized, - xd->default_visual, pic_attrs_mask, &pic_attrs), - x_create_picture_with_visual(xd->base.c, xd->base.root, width_resized, height_resized, - xd->default_visual, pic_attrs_mask, &pic_attrs)}; + x_create_picture_with_pictfmt(xd->base.c, width_resized, height_resized, + source->pictfmt, source->depth, pic_attrs_mask, + &pic_attrs), + x_create_picture_with_pictfmt(xd->base.c, width_resized, height_resized, + source->pictfmt, source->depth, pic_attrs_mask, + &pic_attrs)}; if (!tmp_picture[0] || !tmp_picture[1]) { log_error("Failed to build intermediate Picture."); - pixman_region32_fini(®_op); pixman_region32_fini(®_op_resized); return false; } @@ -271,186 +527,205 @@ static bool blur(backend_t *backend_data, double opacity, void *ctx_, x_set_picture_clip_region(c, tmp_picture[1], 0, 0, &clip); pixman_region32_fini(&clip); - xcb_render_picture_t src_pict = xd->back[2], dst_pict = tmp_picture[0]; - auto alpha_pict = xd->alpha_pict[(int)(opacity * MAX_ALPHA)]; - int current = 0; + xcb_render_picture_t src_pict = source->pict; + auto mask_pict = xd->alpha_pict[(int)(args->opacity * MAX_ALPHA)]; + bool mask_allocated = false; + auto mask_pict_origin = args->source_mask->origin; + if (source_mask != NULL) { + // Translate the target mask region to the mask's coordinate + auto mask_extent = *pixman_region32_extents(args->target_mask); + region_translate_rect( + mask_extent, ivec2_neg(ivec2_add(origin, args->source_mask->origin))); + mask_pict = xrender_process_mask(xd, args->source_mask, mask_extent, + args->opacity != 1.0 ? mask_pict : XCB_NONE, + &mask_pict_origin, &mask_allocated); + mask_pict_origin.x -= extent_resized->x1; + mask_pict_origin.y -= extent_resized->y1; + } else { + // Sampling the 1x1 alpha pict out-of-bound while the X server is under + // heavy load, which it will be if blur is enabled, produces unpredictable + // results... This is a workaround for that. + mask_pict_origin = (ivec2){0, 0}; + } x_set_picture_clip_region(c, src_pict, 0, 0, ®_op_resized); + x_set_picture_clip_region(c, target->pict, 0, 0, args->target_mask); // For more than 1 pass, we do: - // back -(pass 1)-> tmp0 -(pass 2)-> tmp1 ... - // -(pass n-1)-> tmp0 or tmp1 -(pass n)-> back - // For 1 pass, we do - // back -(pass 1)-> tmp0 -(copy)-> target_buffer - int i; - for (i = 0; i < bctx->x_blur_kernel_count; i++) { + // source -(pass 1)-> tmp0 -(pass 2)-> tmp1 ... + // -(pass n-1)-> tmp0 or tmp1 -(pass n)-> target + // For 1 pass, we do: + // (if source == target) + // source -(pass 1)-> tmp0 -(copy)-> target + // (if source != target) + // source -(pass 1)-> target + xcb_render_picture_t dst_pict = target == source ? tmp_picture[0] : target->pict; + ivec2 src_origin = {.x = extent_resized->x1, .y = extent_resized->y1}; + ivec2 dst_origin = {}; + int npasses = bctx->x_blur_kernel_count; + if (source == target && npasses == 1) { + npasses = 2; + } + for (int i = 0; i < npasses; i++) { // Copy from source picture to destination. The filter must // be applied on source picture, to get the nearby pixels outside the // window. - xcb_render_set_picture_filter(c, src_pict, to_u16_checked(strlen(filter)), - filter, - to_u32_checked(bctx->x_blur_kernel[i]->size), - bctx->x_blur_kernel[i]->kernel); - - if (i == 0) { - // First pass, back buffer -> tmp picture - // (we do this even if this is also the last pass, because we - // cannot do back buffer -> back buffer) - xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, src_pict, XCB_NONE, - dst_pict, to_i16_checked(extent_resized->x1), - to_i16_checked(extent_resized->y1), 0, 0, 0, - 0, width_resized, height_resized); - } else if (i < bctx->x_blur_kernel_count - 1) { - // This is not the last pass or the first pass, - // tmp picture 1 -> tmp picture 2 - xcb_render_composite(c, XCB_RENDER_PICT_OP_SRC, src_pict, - XCB_NONE, dst_pict, 0, 0, 0, 0, 0, 0, - width_resized, height_resized); - } else { - x_set_picture_clip_region(c, xd->back[2], 0, 0, ®_op); - // This is the last pass, and we are doing more than 1 pass - xcb_render_composite(c, XCB_RENDER_PICT_OP_OVER, src_pict, - alpha_pict, xd->back[2], 0, 0, 0, 0, - to_i16_checked(extent_resized->x1), - to_i16_checked(extent_resized->y1), - width_resized, height_resized); + xcb_render_picture_t pass_mask_pict = + dst_pict == target->pict ? mask_pict : XCB_NONE; + const uint8_t op = dst_pict == target->pict ? XCB_RENDER_PICT_OP_OVER + : XCB_RENDER_PICT_OP_SRC; + if (i < bctx->x_blur_kernel_count) { + xcb_render_set_picture_filter( + c->c, src_pict, to_u16_checked(strlen(filter)), filter, + to_u32_checked(bctx->x_blur_kernel[i]->size), + bctx->x_blur_kernel[i]->kernel); } + // clang-format off + xcb_render_composite(c->c, op, src_pict, pass_mask_pict, dst_pict, + to_i16_checked(src_origin.x) , to_i16_checked(src_origin.y), + to_i16_checked(-mask_pict_origin.x) , to_i16_checked(-mask_pict_origin.y), + to_i16_checked(dst_origin.x) , to_i16_checked(dst_origin.y), + width_resized , height_resized); + // clang-format on + // reset filter xcb_render_set_picture_filter( - c, src_pict, to_u16_checked(strlen(filter0)), filter0, 0, NULL); - - src_pict = tmp_picture[current]; - dst_pict = tmp_picture[!current]; - current = !current; + c->c, src_pict, to_u16_checked(strlen(filter0)), filter0, 0, NULL); + + auto next_tmp = src_pict == source->pict ? tmp_picture[1] : src_pict; + src_pict = dst_pict; + if (i + 1 == npasses - 1) { + // Intermediary to target + dst_pict = target->pict; + dst_origin = (ivec2){.x = origin.x + extent_resized->x1, + .y = origin.y + extent_resized->y1}; + } else { + // Intermediary to intermediary + dst_pict = next_tmp; + dst_origin = (ivec2){.x = 0, .y = 0}; + } + src_origin = (ivec2){.x = 0, .y = 0}; } - // There is only 1 pass - if (i == 1) { - x_set_picture_clip_region(c, xd->back[2], 0, 0, ®_op); - xcb_render_composite( - c, XCB_RENDER_PICT_OP_OVER, src_pict, alpha_pict, xd->back[2], 0, 0, - 0, 0, to_i16_checked(extent_resized->x1), - to_i16_checked(extent_resized->y1), width_resized, height_resized); + if (mask_allocated) { + x_free_picture(c, mask_pict); } - - xcb_render_free_picture(c, tmp_picture[0]); - xcb_render_free_picture(c, tmp_picture[1]); - pixman_region32_fini(®_op); + x_free_picture(c, tmp_picture[0]); + x_free_picture(c, tmp_picture[1]); pixman_region32_fini(®_op_resized); + + xrender_record_back_damage(xd, target, args->target_mask); return true; } -static void * -bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt, bool owned) { +static image_handle +xrender_bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt) { xcb_generic_error_t *e; - auto r = xcb_get_geometry_reply(base->c, xcb_get_geometry(base->c, pixmap), &e); + auto r = xcb_get_geometry_reply(base->c->c, xcb_get_geometry(base->c->c, pixmap), &e); if (!r) { log_error("Invalid pixmap: %#010x", pixmap); x_print_error(e->full_sequence, e->major_code, e->minor_code, e->error_code); + free(e); return NULL; } - auto img = ccalloc(1, struct backend_image); - auto inner = ccalloc(1, struct _xrender_image_data_inner); - inner->depth = (uint8_t)fmt.visual_depth; - inner->width = img->ewidth = r->width; - inner->height = img->eheight = r->height; - inner->pixmap = pixmap; - inner->has_alpha = fmt.alpha_size != 0; - inner->pict = - x_create_picture_with_visual_and_pixmap(base->c, fmt.visual, pixmap, 0, NULL); - inner->owned = owned; - inner->visual = fmt.visual; - inner->refcount = 1; - - img->inner = (struct backend_image_inner_base *)inner; - img->opacity = 1; + auto img = ccalloc(1, struct xrender_image_data_inner); + img->depth = (uint8_t)fmt.visual_depth; + img->has_alpha = fmt.alpha_size > 0; + img->size = (ivec2){ + .width = r->width, + .height = r->height, + }; + img->format = BACKEND_IMAGE_FORMAT_PIXMAP; + img->pixmap = pixmap; + xcb_render_create_picture_value_list_t pic_attrs = {.repeat = XCB_RENDER_REPEAT_NORMAL}; + img->pict = x_create_picture_with_visual_and_pixmap( + base->c, fmt.visual, pixmap, XCB_RENDER_CP_REPEAT, &pic_attrs); + auto pictfmt_info = x_get_pictform_for_visual(base->c, fmt.visual); + img->pictfmt = pictfmt_info->id; + assert(pictfmt_info->depth == img->depth); + img->is_pixmap_internal = false; free(r); - if (inner->pict == XCB_NONE) { - free(inner); + if (img->pict == XCB_NONE) { free(img); return NULL; } - return img; + return (image_handle)img; } -static void release_image_inner(backend_t *base, struct _xrender_image_data_inner *inner) { - xcb_render_free_picture(base->c, inner->pict); - if (inner->owned) { - xcb_free_pixmap(base->c, inner->pixmap); + +static xcb_pixmap_t xrender_release_image(backend_t *base, image_handle image) { + auto img = (struct xrender_image_data_inner *)image; + auto xd = (struct xrender_data *)base; + if (img == &xd->back_image) { + return XCB_NONE; } - free(inner); -} -static void release_image(backend_t *base, void *image) { - struct backend_image *img = image; - img->inner->refcount--; - if (img->inner->refcount == 0) { - release_image_inner(base, (void *)img->inner); + + xrender_release_rounded_corner_cache(base, img->rounded_rectangle); + x_free_picture(base->c, img->pict); + if (img->is_pixmap_internal && img->pixmap != XCB_NONE) { + xcb_free_pixmap(base->c->c, img->pixmap); + img->pixmap = XCB_NONE; } + + auto pixmap = img->pixmap; free(img); + return pixmap; } -static void deinit(backend_t *backend_data) { - struct _xrender_data *xd = (void *)backend_data; +static void xrender_deinit(backend_t *backend_data) { + auto xd = (struct xrender_data *)backend_data; for (int i = 0; i < 256; i++) { - xcb_render_free_picture(xd->base.c, xd->alpha_pict[i]); + x_free_picture(xd->base.c, xd->alpha_pict[i]); } - xcb_render_free_picture(xd->base.c, xd->target); + x_free_picture(xd->base.c, xd->target); for (int i = 0; i < 2; i++) { - xcb_render_free_picture(xd->base.c, xd->back[i]); - xcb_free_pixmap(xd->base.c, xd->back_pixmap[i]); + if (xd->back[i] != XCB_NONE) { + x_free_picture(xd->base.c, xd->back[i]); + } + if (xd->back_pixmap[i] != XCB_NONE) { + xcb_free_pixmap(xd->base.c->c, xd->back_pixmap[i]); + } } + x_destroy_region(xd->base.c, xd->present_region); if (xd->present_event) { - xcb_unregister_for_special_event(xd->base.c, xd->present_event); + xcb_unregister_for_special_event(xd->base.c->c, xd->present_event); } - xcb_render_free_picture(xd->base.c, xd->white_pixel); - xcb_render_free_picture(xd->base.c, xd->black_pixel); + x_free_picture(xd->base.c, xd->white_pixel); + x_free_picture(xd->base.c, xd->black_pixel); free(xd); } -static void present(backend_t *base, const region_t *region) { - struct _xrender_data *xd = (void *)base; - const rect_t *extent = pixman_region32_extents((region_t *)region); - int16_t orig_x = to_i16_checked(extent->x1), orig_y = to_i16_checked(extent->y1); - uint16_t region_width = to_u16_checked(extent->x2 - extent->x1), - region_height = to_u16_checked(extent->y2 - extent->y1); - - // compose() sets clip region on the back buffer, so clear it first - x_clear_picture_clip_region(base->c, xd->back[xd->curr_back]); - - // limit the region of update - x_set_picture_clip_region(base->c, xd->back[2], 0, 0, region); - +static bool xrender_present(struct backend_base *base) { + auto xd = (struct xrender_data *)base; if (xd->vsync) { - // Update the back buffer first, then present - xcb_render_composite(base->c, XCB_RENDER_PICT_OP_SRC, xd->back[2], - XCB_NONE, xd->back[xd->curr_back], orig_x, orig_y, 0, - 0, orig_x, orig_y, region_width, region_height); - // Make sure we got reply from PresentPixmap before waiting for events, // to avoid deadlock auto e = xcb_request_check( - base->c, xcb_present_pixmap_checked( - xd->base.c, xd->target_win, - xd->back_pixmap[xd->curr_back], 0, XCB_NONE, XCB_NONE, 0, - 0, XCB_NONE, XCB_NONE, XCB_NONE, 0, 0, 0, 0, 0, NULL)); + base->c->c, + xcb_present_pixmap_checked( + base->c->c, xd->target_win, xd->back_pixmap[xd->curr_back], 0, XCB_NONE, + x_set_region(base->c, xd->present_region, &xd->back_damaged) + ? xd->present_region + : XCB_NONE, + 0, 0, XCB_NONE, XCB_NONE, XCB_NONE, 0, 0, 0, 0, 0, NULL)); if (e) { log_error("Failed to present pixmap"); free(e); - return; + return false; } // TODO(yshui) don't block wait for present completion - xcb_present_generic_event_t *pev = - (void *)xcb_wait_for_special_event(base->c, xd->present_event); + auto pev = (xcb_present_generic_event_t *)xcb_wait_for_special_event( + base->c->c, xd->present_event); if (!pev) { // We don't know what happened, maybe X died // But reset buffer age, so in case we do recover, we will // render correctly. xd->buffer_age[0] = xd->buffer_age[1] = -1; - return; + return false; } assert(pev->evtype == XCB_PRESENT_COMPLETE_NOTIFY); - xcb_present_complete_notify_event_t *pcev = (void *)pev; + auto pcev = (xcb_present_complete_notify_event_t *)pev; // log_trace("Present complete: %d %ld", pcev->mode, pcev->msc); xd->buffer_age[xd->curr_back] = 1; @@ -461,18 +736,17 @@ static void present(backend_t *base, const region_t *region) { if (pcev->mode == XCB_PRESENT_COMPLETE_MODE_FLIP) { // We cannot use the pixmap we used anymore xd->curr_back = 1 - xd->curr_back; + xd->back_image.pict = xd->back[xd->curr_back]; } free(pev); - } else { - // No vsync needed, draw into the target picture directly - xcb_render_composite(base->c, XCB_RENDER_PICT_OP_SRC, xd->back[2], - XCB_NONE, xd->target, orig_x, orig_y, 0, 0, orig_x, - orig_y, region_width, region_height); } + // Without vsync, we are rendering into the front buffer directly + pixman_region32_clear(&xd->back_damaged); + return true; } -static int buffer_age(backend_t *backend_data) { - struct _xrender_data *xd = (void *)backend_data; +static int xrender_buffer_age(backend_t *backend_data) { + auto xd = (struct xrender_data *)backend_data; if (!xd->vsync) { // Only the target picture really holds the screen content, and its // content is always up to date. So buffer age is always 1. @@ -481,101 +755,29 @@ static int buffer_age(backend_t *backend_data) { return xd->buffer_age[xd->curr_back]; } -static struct _xrender_image_data_inner * -new_inner(backend_t *base, int w, int h, xcb_visualid_t visual, uint8_t depth) { - auto new_inner = ccalloc(1, struct _xrender_image_data_inner); - new_inner->pixmap = x_create_pixmap(base->c, depth, base->root, w, h); - if (new_inner->pixmap == XCB_NONE) { - log_error("Failed to create pixmap for copy"); - free(new_inner); - return NULL; - } - new_inner->pict = x_create_picture_with_visual_and_pixmap( - base->c, visual, new_inner->pixmap, 0, NULL); - if (new_inner->pict == XCB_NONE) { - log_error("Failed to create picture for copy"); - xcb_free_pixmap(base->c, new_inner->pixmap); - free(new_inner); - return NULL; - } - new_inner->width = w; - new_inner->height = h; - new_inner->visual = visual; - new_inner->depth = depth; - new_inner->refcount = 1; - new_inner->owned = true; - return new_inner; -} +static bool xrender_apply_alpha(struct backend_base *base, image_handle image, + double alpha, const region_t *reg_op) { + auto xd = (struct xrender_data *)base; + auto img = (struct xrender_image_data_inner *)image; + assert(reg_op); -static bool decouple_image(backend_t *base, struct backend_image *img, const region_t *reg) { - if (img->inner->refcount == 1) { + if (!pixman_region32_not_empty(reg_op) || alpha == 1) { return true; } - auto inner = (struct _xrender_image_data_inner *)img->inner; - // Force new pixmap to a 32-bit ARGB visual to allow for transparent frames around - // non-transparent windows - auto visual = (inner->depth == 32) - ? inner->visual - : x_get_visual_for_standard(base->c, XCB_PICT_STANDARD_ARGB_32); - auto inner2 = new_inner(base, inner->width, inner->height, visual, 32); - if (!inner2) { - return false; - } - - x_set_picture_clip_region(base->c, inner->pict, 0, 0, reg); - xcb_render_composite(base->c, XCB_RENDER_PICT_OP_SRC, inner->pict, XCB_NONE, - inner2->pict, 0, 0, 0, 0, 0, 0, to_u16_checked(inner->width), - to_u16_checked(inner->height)); - img->inner = (struct backend_image_inner_base *)inner2; - inner->refcount--; - return true; -} - -static bool image_op(backend_t *base, enum image_operations op, void *image, - const region_t *reg_op, const region_t *reg_visible, void *arg) { - struct _xrender_data *xd = (void *)base; - struct backend_image *img = image; - region_t reg; - double *dargs = arg; - - pixman_region32_init(®); - pixman_region32_intersect(®, (region_t *)reg_op, (region_t *)reg_visible); - - switch (op) { - case IMAGE_OP_APPLY_ALPHA: - assert(reg_op); - - if (!pixman_region32_not_empty(®)) { - break; - } - - if (dargs[0] == 1) { - break; - } - - if (!decouple_image(base, img, reg_visible)) { - pixman_region32_fini(®); - return false; - } - - auto inner = (struct _xrender_image_data_inner *)img->inner; - auto alpha_pict = xd->alpha_pict[(int)((1 - dargs[0]) * MAX_ALPHA)]; - x_set_picture_clip_region(base->c, inner->pict, 0, 0, ®); - xcb_render_composite(base->c, XCB_RENDER_PICT_OP_OUT_REVERSE, alpha_pict, - XCB_NONE, inner->pict, 0, 0, 0, 0, 0, 0, - to_u16_checked(inner->width), - to_u16_checked(inner->height)); - inner->has_alpha = true; - break; - } - pixman_region32_fini(®); + auto alpha_pict = xd->alpha_pict[(int)((1 - alpha) * MAX_ALPHA)]; + x_set_picture_clip_region(base->c, img->pict, 0, 0, reg_op); + xcb_render_composite(base->c->c, XCB_RENDER_PICT_OP_OUT_REVERSE, alpha_pict, XCB_NONE, + img->pict, 0, 0, 0, 0, 0, 0, to_u16_checked(img->size.width), + to_u16_checked(img->size.height)); + xrender_record_back_damage(xd, img, reg_op); return true; } static void * -create_blur_context(backend_t *base attr_unused, enum blur_method method, void *args) { - auto ret = ccalloc(1, struct _xrender_blur_context); +xrender_create_blur_context(backend_t *base attr_unused, enum blur_method method, + enum backend_image_format format attr_unused, void *args) { + auto ret = ccalloc(1, struct xrender_blur_context); if (!method || method >= BLUR_METHOD_INVALID) { ret->method = BLUR_METHOD_NONE; return ret; @@ -617,8 +819,8 @@ create_blur_context(backend_t *base attr_unused, enum blur_method method, void * return ret; } -static void destroy_blur_context(backend_t *base attr_unused, void *ctx_) { - struct _xrender_blur_context *ctx = ctx_; +static void xrender_destroy_blur_context(backend_t *base attr_unused, void *ctx_) { + struct xrender_blur_context *ctx = ctx_; for (int i = 0; i < ctx->x_blur_kernel_count; i++) { free(ctx->x_blur_kernel[i]); } @@ -626,72 +828,54 @@ static void destroy_blur_context(backend_t *base attr_unused, void *ctx_) { free(ctx); } -static void get_blur_size(void *blur_context, int *width, int *height) { - struct _xrender_blur_context *ctx = blur_context; +static void xrender_get_blur_size(void *blur_context, int *width, int *height) { + struct xrender_blur_context *ctx = blur_context; *width = ctx->resize_width; *height = ctx->resize_height; } - -static bool -read_pixel(backend_t *backend_data, void *image_data, int x, int y, struct color *output) { - auto xd = (struct _xrender_data *)backend_data; - auto img = (struct backend_image *)image_data; - auto inner = (struct _xrender_image_data_inner *)img->inner; - - auto r = XCB_AWAIT(xcb_get_image, xd->base.c, XCB_IMAGE_FORMAT_XY_PIXMAP, inner->pixmap, - to_i16_checked(x), to_i16_checked(y), 1, 1, (uint32_t)-1L); - - if (!r) { - return false; +struct backend_operations xrender_ops; +static backend_t *xrender_init(session_t *ps, xcb_window_t target) { + if (ps->o.dithered_present) { + log_warn("\"dithered-present\" is not supported by the xrender backend."); } - // Color format seems to be BGRA8888, see glamor_format_for_pixmap from the - // Xserver codebase. - uint8_t *pixels = xcb_get_image_data(r); - output->blue = pixels[0] / 255.0; - output->green = pixels[1] / 255.0; - output->red = pixels[2] / 255.0; - output->alpha = pixels[3] / 255.0; - - return true; -} - -static backend_t *backend_xrender_init(session_t *ps) { - auto xd = ccalloc(1, struct _xrender_data); + auto xd = ccalloc(1, struct xrender_data); init_backend_base(&xd->base, ps); + xd->base.ops = &xrender_ops; for (int i = 0; i <= MAX_ALPHA; ++i) { double o = (double)i / (double)MAX_ALPHA; - xd->alpha_pict[i] = solid_picture(ps->c, ps->root, false, o, 0, 0, 0); + xd->alpha_pict[i] = solid_picture(&ps->c, false, o, 0, 0, 0); assert(xd->alpha_pict[i] != XCB_NONE); } - xd->target_width = ps->root_width; - xd->target_height = ps->root_height; - xd->default_visual = ps->vis; - xd->black_pixel = solid_picture(ps->c, ps->root, true, 1, 0, 0, 0); - xd->white_pixel = solid_picture(ps->c, ps->root, true, 1, 1, 1, 1); + auto root_pictfmt = x_get_pictform_for_visual(&ps->c, ps->c.screen_info->root_visual); + assert(root_pictfmt->depth == ps->c.screen_info->root_depth); + xd->back_image = (struct xrender_image_data_inner){ + .pictfmt = root_pictfmt->id, + .depth = ps->c.screen_info->root_depth, + .has_alpha = false, + .format = BACKEND_IMAGE_FORMAT_PIXMAP, + .size = {.width = ps->root_width, .height = ps->root_height}, + }; + xd->black_pixel = solid_picture(&ps->c, true, 1, 0, 0, 0); + xd->white_pixel = solid_picture(&ps->c, true, 1, 1, 1, 1); - xd->target_win = session_get_target_window(ps); + xd->target_win = target; xcb_render_create_picture_value_list_t pa = { .subwindowmode = XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS, }; xd->target = x_create_picture_with_visual_and_pixmap( - ps->c, ps->vis, xd->target_win, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); - - auto pictfmt = x_get_pictform_for_visual(ps->c, ps->vis); - if (!pictfmt) { - log_fatal("Default visual is invalid"); - abort(); - } + &ps->c, ps->c.screen_info->root_visual, xd->target_win, + XCB_RENDER_CP_SUBWINDOW_MODE, &pa); xd->vsync = ps->o.vsync; if (ps->present_exists) { - auto eid = x_new_id(ps->c); + auto eid = x_new_id(&ps->c); auto e = - xcb_request_check(ps->c, xcb_present_select_input_checked( - ps->c, eid, xd->target_win, - XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY)); + xcb_request_check(ps->c.c, xcb_present_select_input_checked( + ps->c.c, eid, xd->target_win, + XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY)); if (e) { log_error("Cannot select present input, vsync will be disabled"); xd->vsync = false; @@ -699,7 +883,7 @@ static backend_t *backend_xrender_init(session_t *ps) { } xd->present_event = - xcb_register_for_special_xge(ps->c, &xcb_present_id, eid, NULL); + xcb_register_for_special_xge(ps->c.c, &xcb_present_id, eid, NULL); if (!xd->present_event) { log_error("Cannot register for special XGE, vsync will be " "disabled"); @@ -709,18 +893,23 @@ static backend_t *backend_xrender_init(session_t *ps) { xd->vsync = false; } + if (xd->vsync) { + xd->present_region = x_create_region(&ps->c, &ps->screen_reg); + } + // We might need to do double buffering for vsync, and buffer 0 and 1 are for // double buffering. - int first_buffer_index = xd->vsync ? 0 : 2; - for (int i = first_buffer_index; i < 3; i++) { - xd->back_pixmap[i] = x_create_pixmap(ps->c, pictfmt->depth, ps->root, + int buffer_count = xd->vsync ? 2 : 0; + for (int i = 0; i < buffer_count; i++) { + xd->back_pixmap[i] = x_create_pixmap(&ps->c, ps->c.screen_info->root_depth, to_u16_checked(ps->root_width), to_u16_checked(ps->root_height)); const uint32_t pic_attrs_mask = XCB_RENDER_CP_REPEAT; const xcb_render_create_picture_value_list_t pic_attrs = { .repeat = XCB_RENDER_REPEAT_PAD}; - xd->back[i] = x_create_picture_with_pictfmt_and_pixmap( - ps->c, pictfmt, xd->back_pixmap[i], pic_attrs_mask, &pic_attrs); + xd->back[i] = x_create_picture_with_visual_and_pixmap( + &ps->c, ps->c.screen_info->root_visual, xd->back_pixmap[i], + pic_attrs_mask, &pic_attrs); xd->buffer_age[i] = -1; if (xd->back_pixmap[i] == XCB_NONE || xd->back[i] == XCB_NONE) { log_error("Cannot create pixmap for rendering"); @@ -728,36 +917,99 @@ static backend_t *backend_xrender_init(session_t *ps) { } } xd->curr_back = 0; + xd->back_image.pict = xd->vsync ? xd->back[xd->curr_back] : xd->target; + + auto drivers = detect_driver(xd->base.c->c, &xd->base, xd->target_win); + if (drivers & DRIVER_MODESETTING) { + // I believe other xf86-video drivers have accelerated blur? + xd->quirks |= BACKEND_QUIRK_SLOW_BLUR; + } return &xd->base; err: - deinit(&xd->base); + xrender_deinit(&xd->base); return NULL; } +static image_handle +xrender_new_image(struct backend_base *base, enum backend_image_format format, ivec2 size) { + auto xd = (struct xrender_data *)base; + auto img = ccalloc(1, struct xrender_image_data_inner); + img->format = format; + img->size = size; + img->has_alpha = true; + if (format == BACKEND_IMAGE_FORMAT_MASK) { + img->depth = 8; + img->pictfmt = x_get_pictfmt_for_standard(base->c, XCB_PICT_STANDARD_A_8); + } else { + img->depth = 32; + img->pictfmt = + x_get_pictfmt_for_standard(xd->base.c, XCB_PICT_STANDARD_ARGB_32); + } + img->pixmap = x_create_pixmap(xd->base.c, img->depth, to_u16_checked(size.width), + to_u16_checked(size.height)); + if (img->pixmap == XCB_NONE) { + free(img); + return NULL; + } + img->pict = x_create_picture_with_pictfmt_and_pixmap(xd->base.c, img->pictfmt, + img->pixmap, 0, NULL); + if (img->pict == XCB_NONE) { + xcb_free_pixmap(xd->base.c->c, img->pixmap); + free(img); + return NULL; + } + img->is_pixmap_internal = true; + return (image_handle)img; +} + +static uint32_t xrender_image_capabilities(struct backend_base *base attr_unused, + image_handle image attr_unused) { + // All of xrender's picture can be used as both a source and a destination. + return BACKEND_IMAGE_CAP_DST | BACKEND_IMAGE_CAP_SRC; +} + +static bool xrender_is_format_supported(struct backend_base *base attr_unused, + enum backend_image_format format) { + return format == BACKEND_IMAGE_FORMAT_MASK || format == BACKEND_IMAGE_FORMAT_PIXMAP; +} + +static image_handle xrender_back_buffer(struct backend_base *base) { + auto xd = (struct xrender_data *)base; + return (image_handle)&xd->back_image; +} + +uint32_t xrender_quirks(struct backend_base *base) { + return ((struct xrender_data *)base)->quirks; +} + struct backend_operations xrender_ops = { - .init = backend_xrender_init, - .deinit = deinit, - .blur = blur, - .present = present, - .compose = compose, - .fill = fill, - .bind_pixmap = bind_pixmap, - .release_image = release_image, - .render_shadow = default_backend_render_shadow, - //.prepare_win = prepare_win, - //.release_win = release_win, - .is_image_transparent = default_is_image_transparent, - .buffer_age = buffer_age, + .apply_alpha = xrender_apply_alpha, + .back_buffer = xrender_back_buffer, + .bind_pixmap = xrender_bind_pixmap, + .blit = xrender_blit, + .blur = xrender_blur, + .clear = xrender_clear, + .copy_area = xrender_copy_area, + .copy_area_quantize = xrender_copy_area, + .image_capabilities = xrender_image_capabilities, + .is_format_supported = xrender_is_format_supported, + .new_image = xrender_new_image, + .present = xrender_present, + .quirks = xrender_quirks, + .release_image = xrender_release_image, + + .init = xrender_init, + .deinit = xrender_deinit, + // TODO(yshui) make blur faster so we can use `backend_render_shadow_from_mask` for + // `render_shadow`, and `backend_compat_shadow_from_mask` for + // `shadow_from_mask` + .buffer_age = xrender_buffer_age, .max_buffer_age = 2, - - .image_op = image_op, - .read_pixel = read_pixel, - .clone_image = default_clone_image, - .set_image_property = default_set_image_property, - .create_blur_context = create_blur_context, - .destroy_blur_context = destroy_blur_context, - .get_blur_size = get_blur_size, + .create_blur_context = xrender_create_blur_context, + .destroy_blur_context = xrender_destroy_blur_context, + .get_blur_size = xrender_get_blur_size + // end }; // vim: set noet sw=8 ts=8: diff --git a/src/c2.c b/src/c2.c index 3500f7b25f..2e8da44444 100644 --- a/src/c2.c +++ b/src/c2.c @@ -10,22 +10,19 @@ * */ +#include #include #include +#include +#include #include #include +#include // libpcre #ifdef CONFIG_REGEX_PCRE -#include - -// For compatibility with #endif @@ -38,6 +35,7 @@ #include "config.h" #include "log.h" #include "string_utils.h" +#include "test.h" #include "utils.h" #include "win.h" #include "x.h" @@ -60,6 +58,56 @@ typedef struct { }; } c2_ptr_t; +struct c2_tracked_property_key { + xcb_atom_t property; + bool is_on_client; + char padding[3]; +}; +static_assert(sizeof(struct c2_tracked_property_key) == 8, "Padding bytes in " + "c2_tracked_property_key"); + +struct c2_tracked_property { + UT_hash_handle hh; + struct c2_tracked_property_key key; + unsigned int id; + /// Highest indices of this property that + /// are tracked. -1 mean all indices are tracked. + int max_indices; +}; + +struct c2_state { + struct c2_tracked_property *tracked_properties; + struct atom *atoms; + xcb_get_property_cookie_t *cookies; + uint32_t *propert_lengths; +}; + +// TODO(yshui) this has some overlap with winprop_t, consider merging them. +struct c2_property_value { + union { + struct { + char *string; + }; + struct { + int64_t numbers[4]; + }; + struct { + int64_t *array; + unsigned int capacity; + }; + }; + /// Number of items if the property is a number type, + /// or number of bytes in the string if the property is a string type. + uint32_t length; + enum { + C2_PROPERTY_TYPE_STRING, + C2_PROPERTY_TYPE_NUMBER, + C2_PROPERTY_TYPE_ATOM, + } type; + bool valid; + bool needs_update; +}; + /// Initializer for c2_ptr_t. #define C2_PTR_INIT \ { .isbranch = false, .l = NULL, } @@ -89,78 +137,78 @@ struct _c2_b { /// Structure for leaf element in a window condition struct _c2_l { bool neg : 1; - enum { C2_L_OEXISTS, - C2_L_OEQ, - C2_L_OGT, - C2_L_OGTEQ, - C2_L_OLT, - C2_L_OLTEQ, + enum { + C2_L_OEXISTS = 0, + C2_L_OEQ, + C2_L_OGT, + C2_L_OGTEQ, + C2_L_OLT, + C2_L_OLTEQ, } op : 3; - enum { C2_L_MEXACT, - C2_L_MSTART, - C2_L_MCONTAINS, - C2_L_MWILDCARD, - C2_L_MPCRE, + enum { + C2_L_MEXACT, + C2_L_MSTART, + C2_L_MCONTAINS, + C2_L_MWILDCARD, + C2_L_MPCRE, } match : 3; bool match_ignorecase : 1; char *tgt; + unsigned int target_id; xcb_atom_t tgtatom; - bool tgt_onframe; + bool target_on_client; int index; - enum { C2_L_PUNDEFINED = -1, - C2_L_PID = 0, - C2_L_PX, - C2_L_PY, - C2_L_PX2, - C2_L_PY2, - C2_L_PWIDTH, - C2_L_PHEIGHT, - C2_L_PWIDTHB, - C2_L_PHEIGHTB, - C2_L_PBDW, - C2_L_PFULLSCREEN, - C2_L_POVREDIR, - C2_L_PARGB, - C2_L_PFOCUSED, - C2_L_PWMWIN, - C2_L_PBSHAPED, - C2_L_PROUNDED, - C2_L_PCLIENT, - C2_L_PWINDOWTYPE, - C2_L_PLEADER, - C2_L_PNAME, - C2_L_PCLASSG, - C2_L_PCLASSI, - C2_L_PROLE, + // TODO(yshui) translate some of the pre-defined targets to + // generic window properties. e.g. `name = "xterm"` + // should be translated to: + // "WM_NAME = 'xterm' || _NET_WM_NAME = 'xterm'" + enum { + C2_L_PUNDEFINED = -1, + C2_L_PID = 0, + C2_L_PX, + C2_L_PY, + C2_L_PX2, + C2_L_PY2, + C2_L_PWIDTH, + C2_L_PHEIGHT, + C2_L_PWIDTHB, + C2_L_PHEIGHTB, + C2_L_PBDW, + C2_L_PFULLSCREEN, + C2_L_POVREDIR, + C2_L_PARGB, + C2_L_PFOCUSED, + C2_L_PWMWIN, + C2_L_PBSHAPED, + C2_L_PROUNDED, + C2_L_PCLIENT, + C2_L_PWINDOWTYPE, + C2_L_PLEADER, + C2_L_PNAME, + C2_L_PCLASSG, + C2_L_PCLASSI, + C2_L_PROLE, } predef; - enum c2_l_type { - C2_L_TUNDEFINED, - C2_L_TSTRING, - C2_L_TCARDINAL, - C2_L_TWINDOW, - C2_L_TATOM, - C2_L_TDRAWABLE, - } type; - int format; - enum { C2_L_PTUNDEFINED, - C2_L_PTSTRING, - C2_L_PTINT, + enum { + C2_L_PTUNDEFINED, + C2_L_PTSTRING, + C2_L_PTINT, } ptntype; char *ptnstr; long ptnint; #ifdef CONFIG_REGEX_PCRE - pcre *regex_pcre; - pcre_extra *regex_pcre_extra; + pcre2_code *regex_pcre; + pcre2_match_data *regex_pcre_match; #endif }; /// Initializer for c2_l_t. -#define C2_L_INIT \ - { \ - .neg = false, .op = C2_L_OEXISTS, .match = C2_L_MEXACT, \ - .match_ignorecase = false, .tgt = NULL, .tgtatom = 0, .tgt_onframe = false, \ - .predef = C2_L_PUNDEFINED, .index = 0, .type = C2_L_TUNDEFINED, \ - .format = 0, .ptntype = C2_L_PTUNDEFINED, .ptnstr = NULL, .ptnint = 0, \ +#define C2_L_INIT \ + { \ + .neg = false, .op = C2_L_OEXISTS, .match = C2_L_MEXACT, \ + .match_ignorecase = false, .tgt = NULL, .tgtatom = 0, \ + .target_on_client = false, .predef = C2_L_PUNDEFINED, .index = 0, \ + .ptntype = C2_L_PTUNDEFINED, .ptnstr = NULL, .ptnint = 0, \ } static const c2_l_t leaf_def = C2_L_INIT; @@ -179,71 +227,62 @@ struct _c2_lptr { /// Structure representing a predefined target. typedef struct { const char *name; - enum c2_l_type type; - int format; } c2_predef_t; +// clang-format off // Predefined targets. -static const c2_predef_t C2_PREDEFS[] = { - [C2_L_PID] = {"id", C2_L_TCARDINAL, 0}, - [C2_L_PX] = {"x", C2_L_TCARDINAL, 0}, - [C2_L_PY] = {"y", C2_L_TCARDINAL, 0}, - [C2_L_PX2] = {"x2", C2_L_TCARDINAL, 0}, - [C2_L_PY2] = {"y2", C2_L_TCARDINAL, 0}, - [C2_L_PWIDTH] = {"width", C2_L_TCARDINAL, 0}, - [C2_L_PHEIGHT] = {"height", C2_L_TCARDINAL, 0}, - [C2_L_PWIDTHB] = {"widthb", C2_L_TCARDINAL, 0}, - [C2_L_PHEIGHTB] = {"heightb", C2_L_TCARDINAL, 0}, - [C2_L_PBDW] = {"border_width", C2_L_TCARDINAL, 0}, - [C2_L_PFULLSCREEN] = {"fullscreen", C2_L_TCARDINAL, 0}, - [C2_L_POVREDIR] = {"override_redirect", C2_L_TCARDINAL, 0}, - [C2_L_PARGB] = {"argb", C2_L_TCARDINAL, 0}, - [C2_L_PFOCUSED] = {"focused", C2_L_TCARDINAL, 0}, - [C2_L_PWMWIN] = {"wmwin", C2_L_TCARDINAL, 0}, - [C2_L_PBSHAPED] = {"bounding_shaped", C2_L_TCARDINAL, 0}, - [C2_L_PROUNDED] = {"rounded_corners", C2_L_TCARDINAL, 0}, - [C2_L_PCLIENT] = {"client", C2_L_TWINDOW, 0}, - [C2_L_PWINDOWTYPE] = {"window_type", C2_L_TSTRING, 0}, - [C2_L_PLEADER] = {"leader", C2_L_TWINDOW, 0}, - [C2_L_PNAME] = {"name", C2_L_TSTRING, 0}, - [C2_L_PCLASSG] = {"class_g", C2_L_TSTRING, 0}, - [C2_L_PCLASSI] = {"class_i", C2_L_TSTRING, 0}, - [C2_L_PROLE] = {"role", C2_L_TSTRING, 0}, +static struct { + const char *name; + bool is_string; +} C2_PREDEFS[] = { + [C2_L_PID] = { "id", false, }, + [C2_L_PX] = { "x", false, }, + [C2_L_PY] = { "y", false, }, + [C2_L_PX2] = { "x2", false, }, + [C2_L_PY2] = { "y2", false, }, + [C2_L_PWIDTH] = { "width", false, }, + [C2_L_PHEIGHT] = { "height", false, }, + [C2_L_PWIDTHB] = { "widthb", false, }, + [C2_L_PHEIGHTB] = { "heightb", false, }, + [C2_L_PBDW] = { "border_width", false, }, + [C2_L_PFULLSCREEN] = { "fullscreen", false, }, + [C2_L_POVREDIR] = { "override_redirect", false, }, + [C2_L_PARGB] = { "argb", false, }, + [C2_L_PFOCUSED] = { "focused", false, }, + [C2_L_PWMWIN] = { "wmwin", false, }, + [C2_L_PBSHAPED] = { "bounding_shaped", false, }, + [C2_L_PROUNDED] = { "rounded_corners", false, }, + [C2_L_PCLIENT] = { "client", false, }, + [C2_L_PWINDOWTYPE] = { "window_type", true, }, + [C2_L_PLEADER] = { "leader", false, }, + [C2_L_PNAME] = { "name", true, }, + [C2_L_PCLASSG] = { "class_g", true, }, + [C2_L_PCLASSI] = { "class_i", true, }, + [C2_L_PROLE] = { "role", true, }, }; +// clang-format on -/** - * Get the numeric property value from a win_prop_t. - */ -static inline long winprop_get_int(winprop_t prop, size_t index) { - long tgt = 0; - - if (!prop.nitems || index >= prop.nitems) { - return 0; - } - - switch (prop.format) { - case 8: tgt = *(prop.p8 + index); break; - case 16: tgt = *(prop.p16 + index); break; - case 32: tgt = *(prop.p32 + index); break; - default: assert(0); break; - } - - return tgt; -} +static const char *const c2_pattern_type_names[] = { + [C2_L_PTUNDEFINED] = "undefined", + [C2_L_PTSTRING] = "string", + [C2_L_PTINT] = "number", +}; /** * Compare next word in a string with another string. */ static inline int strcmp_wd(const char *needle, const char *src) { int ret = mstrncmp(needle, src); - if (ret) + if (ret) { return ret; + } char c = src[strlen(needle)]; - if (isalnum((unsigned char)c) || '_' == c) + if (isalnum((unsigned char)c) || '_' == c) { return 1; - else - return 0; + } + + return 0; } /** @@ -257,8 +296,9 @@ static inline attr_unused bool c2_ptr_isempty(const c2_ptr_t p) { * Reset a c2_ptr_t. */ static inline void c2_ptr_reset(c2_ptr_t *pp) { - if (pp) + if (pp) { memcpy(pp, &C2_PTR_NULL, sizeof(c2_ptr_t)); + } } /** @@ -315,6 +355,9 @@ static int c2_parse_legacy(const char *pattern, int offset, c2_ptr_t *presult); static void c2_free(c2_ptr_t p); +static size_t c2_condition_to_str(c2_ptr_t p, char *output, size_t len); +static const char *c2_condition_to_str2(c2_ptr_t ptr); + /** * Wrapper of c2_free(). */ @@ -327,29 +370,23 @@ static inline void c2_freep(c2_ptr_t *pp) { static const char *c2h_dump_str_tgt(const c2_l_t *pleaf); -static const char *c2h_dump_str_type(const c2_l_t *pleaf); - -static void attr_unused c2_dump(c2_ptr_t p); - -static xcb_atom_t c2_get_atom_type(const c2_l_t *pleaf); - -static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_ptr_t cond); - /** * Parse a condition string. */ c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data) { - if (!pattern) + if (!pattern) { return NULL; + } // Parse the pattern c2_ptr_t result = C2_PTR_INIT; int offset = -1; - if (strlen(pattern) >= 2 && ':' == pattern[1]) + if (strlen(pattern) >= 2 && ':' == pattern[1]) { offset = c2_parse_legacy(pattern, 0, &result); - else + } else { offset = c2_parse_grp(pattern, 0, &result, 0); + } if (offset < 0) { c2_freep(&result); @@ -378,6 +415,163 @@ c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data) { } } +/** + * Parse a condition string with a prefix. + */ +c2_lptr_t * +c2_parse_with_prefix(c2_lptr_t **pcondlst, const char *pattern, + void *(*parse_prefix)(const char *input, const char **end, void *), + void (*free_value)(void *), void *user_data) { + char *pattern_start = NULL; + void *val = parse_prefix(pattern, (const char **)&pattern_start, user_data); + if (pattern_start == NULL) { + return NULL; + } + auto ret = c2_parse(pcondlst, pattern_start, val); + if (!ret && free_value) { + free_value(val); + } + return ret; +} + +TEST_CASE(c2_parse) { + char str[1024]; + c2_lptr_t *cond = c2_parse(NULL, "name = \"xterm\"", NULL); + struct atom *atoms = init_mock_atoms(); + struct c2_state *state = c2_state_new(atoms); + TEST_NOTEQUAL(cond, NULL); + TEST_TRUE(!cond->ptr.isbranch); + TEST_NOTEQUAL(cond->ptr.l, NULL); + TEST_EQUAL(cond->ptr.l->op, C2_L_OEQ); + TEST_EQUAL(cond->ptr.l->ptntype, C2_L_PTSTRING); + TEST_STREQUAL(cond->ptr.l->ptnstr, "xterm"); + + size_t len = c2_condition_to_str(cond->ptr, str, sizeof(str)); + TEST_STREQUAL3(str, "name = \"xterm\"", len); + + struct managed_win test_win = { + .name = "xterm", + }; + TEST_TRUE(c2_match(state, &test_win, cond, NULL)); + c2_list_postprocess(state, NULL, cond); + TEST_EQUAL(HASH_COUNT(state->tracked_properties), 0); + c2_state_free(state); + destroy_atoms(atoms); + c2_list_free(&cond, NULL); + + cond = c2_parse(NULL, "argb", NULL); + TEST_NOTEQUAL(cond, NULL); + TEST_TRUE(!cond->ptr.isbranch); + TEST_EQUAL(cond->ptr.l->ptntype, C2_L_PTINT); + c2_list_free(&cond, NULL); + + cond = c2_parse(NULL, "argb = 'b'", NULL); + TEST_EQUAL(cond, NULL); + + cond = c2_parse(NULL, "_GTK_FRAME_EXTENTS@:c", NULL); + TEST_NOTEQUAL(cond, NULL); + TEST_TRUE(!cond->ptr.isbranch); + TEST_NOTEQUAL(cond->ptr.l, NULL); + TEST_EQUAL(cond->ptr.l->op, C2_L_OEXISTS); + TEST_EQUAL(cond->ptr.l->match, C2_L_MEXACT); + TEST_EQUAL(cond->ptr.l->predef, C2_L_PUNDEFINED); + TEST_TRUE(cond->ptr.l->target_on_client); + TEST_NOTEQUAL(cond->ptr.l->tgt, NULL); + TEST_STREQUAL(cond->ptr.l->tgt, "_GTK_FRAME_EXTENTS"); + + atoms = init_mock_atoms(); + state = c2_state_new(atoms); + c2_list_postprocess(state, NULL, cond); + TEST_EQUAL(HASH_COUNT(state->tracked_properties), 1); + HASH_ITER2(state->tracked_properties, prop) { + TEST_EQUAL(prop->key.property, + get_atom_with_nul(state->atoms, "_GTK_FRAME_EXTENTS", NULL)); + TEST_EQUAL(prop->max_indices, 0); + } + c2_state_free(state); + destroy_atoms(atoms); + + len = c2_condition_to_str(cond->ptr, str, sizeof(str)); + TEST_STREQUAL3(str, "_GTK_FRAME_EXTENTS@[0]", len); + c2_list_free(&cond, NULL); + + cond = c2_parse(NULL, "name = \"xterm\" && class_g *= \"XTerm\"", NULL); + TEST_NOTEQUAL(cond, NULL); + TEST_TRUE(cond->ptr.isbranch); + TEST_NOTEQUAL(cond->ptr.b, NULL); + TEST_EQUAL(cond->ptr.b->op, C2_B_OAND); + TEST_NOTEQUAL(cond->ptr.b->opr1.l, NULL); + TEST_NOTEQUAL(cond->ptr.b->opr2.l, NULL); + TEST_EQUAL(cond->ptr.b->opr1.l->op, C2_L_OEQ); + TEST_EQUAL(cond->ptr.b->opr1.l->match, C2_L_MEXACT); + TEST_EQUAL(cond->ptr.b->opr1.l->ptntype, C2_L_PTSTRING); + TEST_EQUAL(cond->ptr.b->opr2.l->op, C2_L_OEQ); + TEST_EQUAL(cond->ptr.b->opr2.l->match, C2_L_MCONTAINS); + TEST_EQUAL(cond->ptr.b->opr2.l->ptntype, C2_L_PTSTRING); + TEST_STREQUAL(cond->ptr.b->opr1.l->tgt, "name"); + TEST_EQUAL(cond->ptr.b->opr1.l->predef, C2_L_PNAME); + TEST_STREQUAL(cond->ptr.b->opr2.l->tgt, "class_g"); + TEST_EQUAL(cond->ptr.b->opr2.l->predef, C2_L_PCLASSG); + + atoms = init_mock_atoms(); + state = c2_state_new(atoms); + len = c2_condition_to_str(cond->ptr, str, sizeof(str)); + TEST_STREQUAL3(str, "(name = \"xterm\" && class_g *= \"XTerm\")", len); + test_win.class_general = "XTerm"; + TEST_TRUE(c2_match(state, &test_win, cond, NULL)); + test_win.class_general = "asdf"; + TEST_TRUE(!c2_match(state, &test_win, cond, NULL)); + c2_list_free(&cond, NULL); + c2_state_free(state); + destroy_atoms(atoms); + + cond = c2_parse(NULL, "_NET_WM_STATE[1]:32a *='_NET_WM_STATE_HIDDEN'", NULL); + TEST_EQUAL(cond->ptr.l->index, 1); + TEST_STREQUAL(cond->ptr.l->tgt, "_NET_WM_STATE"); + TEST_STREQUAL(cond->ptr.l->ptnstr, "_NET_WM_STATE_HIDDEN"); + + len = c2_condition_to_str(cond->ptr, str, sizeof(str)); + TEST_STREQUAL3(str, "_NET_WM_STATE[1] *= \"_NET_WM_STATE_HIDDEN\"", len); + c2_list_free(&cond, NULL); + + cond = c2_parse(NULL, "_NET_WM_STATE[*]:32a*='_NET_WM_STATE_HIDDEN'", NULL); + TEST_EQUAL(cond->ptr.l->index, -1); + + len = c2_condition_to_str(cond->ptr, str, sizeof(str)); + TEST_STREQUAL3(str, "_NET_WM_STATE[*] *= \"_NET_WM_STATE_HIDDEN\"", len); + c2_list_free(&cond, NULL); + + cond = c2_parse(NULL, "!class_i:0s", NULL); + TEST_NOTEQUAL(cond, NULL); + len = c2_condition_to_str(cond->ptr, str, sizeof(str)); + TEST_STREQUAL3(str, "!class_i", len); + c2_list_free(&cond, NULL); + + cond = c2_parse(NULL, "_NET_WM_STATE = '_NET_WM_STATE_HIDDEN'", NULL); + TEST_NOTEQUAL(cond, NULL); + + cond = c2_parse(NULL, "1A:\n1111111111111ar1", NULL); + TEST_EQUAL(cond, NULL); + + cond = c2_parse(NULL, "N [4444444444444: \n", NULL); + TEST_EQUAL(cond, NULL); + + cond = c2_parse(NULL, " x:a=\"b\377\\xCCCCC", NULL); + TEST_EQUAL(cond, NULL); + + cond = c2_parse(NULL, "!!!!!!!((((((!(((((,", NULL); + TEST_EQUAL(cond, NULL); + + const char *rule = "(((role = \"\\\\tg^\\n\\n[\\t\" && role ~?= \"\") && " + "role ~?= \"\\n\\n\\n\\b\\n^\\n*0bon\") && role ~?= " + "\"\\n\\n\\x8a\\b\\n^\\n*0\\n[\\n[\\n\\n\\b\\n\")"; + cond = c2_parse(NULL, rule, NULL); + TEST_NOTEQUAL(cond, NULL); + len = c2_condition_to_str(cond->ptr, str, sizeof(str)); + TEST_STREQUAL3(str, rule, len); + c2_list_free(&cond, NULL); +} + #define c2_error(format, ...) \ do { \ log_error("Pattern \"%s\" pos %d: " format, pattern, offset, ##__VA_ARGS__); \ @@ -397,12 +591,9 @@ c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data) { * @return offset of next character in string */ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int level) { - // Check for recursion levels - if (level > C2_MAX_LEVELS) - c2_error("Exceeded maximum recursion levels."); - - if (!pattern) + if (!pattern) { return -1; + } // Expected end character const char endchar = (offset ? ')' : '\0'); @@ -426,22 +617,30 @@ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int // after encountering a logical operator bool next_expected = true; + // Check for recursion levels + if (level > C2_MAX_LEVELS) { + c2_error("Exceeded maximum recursion levels."); + } + // Parse the pattern character-by-character for (; pattern[offset]; ++offset) { assert(elei <= 2); // Jump over spaces - if (isspace((unsigned char)pattern[offset])) + if (isspace((unsigned char)pattern[offset])) { continue; + } // Handle end of group - if (')' == pattern[offset]) + if (')' == pattern[offset]) { break; + } // Handle "!" if ('!' == pattern[offset]) { - if (!next_expected) + if (!next_expected) { c2_error("Unexpected \"!\"."); + } neg = !neg; continue; @@ -449,8 +648,9 @@ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int // Handle AND and OR if ('&' == pattern[offset] || '|' == pattern[offset]) { - if (next_expected) + if (next_expected) { c2_error("Unexpected logical operator."); + } next_expected = true; if (!mstrncmp("&&", pattern + offset)) { @@ -459,15 +659,17 @@ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int } else if (!mstrncmp("||", pattern + offset)) { ops[elei] = C2_B_OOR; ++offset; - } else + } else { c2_error("Illegal logical operator."); + } continue; } // Parsing an element - if (!next_expected) + if (!next_expected) { c2_error("Unexpected expression."); + } assert(!elei || ops[elei]); @@ -494,21 +696,40 @@ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int // It's a subgroup if it starts with '(' if ('(' == pattern[offset]) { - if ((offset = c2_parse_grp(pattern, offset + 1, pele, level + 1)) < 0) + if ((offset = c2_parse_grp(pattern, offset + 1, pele, level + 1)) < 0) { goto fail; + } } // Otherwise it's a leaf else { - if ((offset = c2_parse_target(pattern, offset, pele)) < 0) + if ((offset = c2_parse_target(pattern, offset, pele)) < 0) { goto fail; + } assert(!pele->isbranch && !c2_ptr_isempty(*pele)); - if ((offset = c2_parse_op(pattern, offset, pele)) < 0) + if ((offset = c2_parse_op(pattern, offset, pele)) < 0) { goto fail; + } - if ((offset = c2_parse_pattern(pattern, offset, pele)) < 0) + if ((offset = c2_parse_pattern(pattern, offset, pele)) < 0) { goto fail; + } + + if (pele->l->predef != C2_L_PUNDEFINED) { + typeof(pele->l->ptntype) predef_type = + C2_PREDEFS[pele->l->predef].is_string ? C2_L_PTSTRING + : C2_L_PTINT; + if (pele->l->op == C2_L_OEXISTS) { + pele->l->ptntype = predef_type; + } else if (pele->l->ptntype != predef_type) { + c2_error("Predefined target %s is a %s, but you " + "are comparing it with a %s", + C2_PREDEFS[pele->l->predef].name, + c2_pattern_type_names[predef_type], + c2_pattern_type_names[pele->l->ptntype]); + } + } } // Decrement offset -- we will increment it in loop update --offset; @@ -516,10 +737,11 @@ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int // Apply negation if (neg) { neg = false; - if (pele->isbranch) + if (pele->isbranch) { pele->b->neg = !pele->b->neg; - else + } else { pele->l->neg = !pele->l->neg; + } } next_expected = false; @@ -528,10 +750,12 @@ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int } // Wrong end character? - if (pattern[offset] && !endchar) + if (pattern[offset] && !endchar) { c2_error("Expected end of string but found '%c'.", pattern[offset]); - if (!pattern[offset] && endchar) + } + if (!pattern[offset] && endchar) { c2_error("Expected '%c' but found end of string.", endchar); + } // Handle end of group if (!elei) { @@ -547,8 +771,9 @@ static int c2_parse_grp(const char *pattern, int offset, c2_ptr_t *presult, int *presult = eles[0]; - if (')' == pattern[offset]) + if (')' == pattern[offset]) { ++offset; + } return offset; @@ -579,8 +804,8 @@ static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult) { // Copy target name out int tgtlen = 0; - for (; pattern[offset] && - (isalnum((unsigned char)pattern[offset]) || '_' == pattern[offset]); + for (; pattern[offset] && (isalnum((unsigned char)pattern[offset]) || + '_' == pattern[offset] || '.' == pattern[offset]); ++offset) { ++tgtlen; } @@ -594,17 +819,15 @@ static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult) { for (int i = 0; i < npredefs; ++i) { if (!strcmp(C2_PREDEFS[i].name, pleaf->tgt)) { pleaf->predef = i; - pleaf->type = C2_PREDEFS[i].type; - pleaf->format = C2_PREDEFS[i].format; break; } } C2H_SKIP_SPACES(); - // Parse target-on-frame flag + // Parse target-on-client flag if ('@' == pattern[offset]) { - pleaf->tgt_onframe = true; + pleaf->target_on_client = true; ++offset; C2H_SKIP_SPACES(); } @@ -635,8 +858,11 @@ static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult) { if (!endptr || pattern + offset == endptr) { c2_error("No index number found after bracket."); } + if (index > INT_MAX) { + c2_error("Index %ld too large.", index); + } - pleaf->index = to_int_checked(index); + pleaf->index = (int)index; offset = to_int_checked(endptr - pattern); C2H_SKIP_SPACES(); @@ -658,86 +884,43 @@ static int c2_parse_target(const char *pattern, int offset, c2_ptr_t *presult) { // Look for format bool hasformat = false; long format = 0; - { - char *endptr = NULL; - format = strtol(pattern + offset, &endptr, 0); - assert(endptr); - if ((hasformat = (endptr && endptr != pattern + offset))) { - offset = to_int_checked(endptr - pattern); - } - C2H_SKIP_SPACES(); + char *endptr = NULL; + format = strtol(pattern + offset, &endptr, 0); + assert(endptr); + hasformat = endptr && endptr != pattern + offset; + if (hasformat) { + offset = to_int_checked(endptr - pattern); } + C2H_SKIP_SPACES(); // Look for type - enum c2_l_type type = C2_L_TUNDEFINED; switch (pattern[offset]) { - case 'w': type = C2_L_TWINDOW; break; - case 'd': type = C2_L_TDRAWABLE; break; - case 'c': type = C2_L_TCARDINAL; break; - case 's': type = C2_L_TSTRING; break; - case 'a': type = C2_L_TATOM; break; + case 'w': + case 'd': + case 'c': + case 's': + case 'a': break; default: c2_error("Invalid type character."); } - if (type) { - if (pleaf->predef != C2_L_PUNDEFINED) { - log_warn("Type specified for a default target " - "will be ignored."); - } else { - if (pleaf->type && type != pleaf->type) { - log_warn("Default type overridden on " - "target."); - } - pleaf->type = type; - } - } + log_warn("Type specifier is deprecated. Type \"%c\" specified on target " + "\"%s\" will be ignored, you can remove it.", + pattern[offset], pleaf->tgt); offset++; C2H_SKIP_SPACES(); - // Default format - if (!pleaf->format) { - switch (pleaf->type) { - case C2_L_TWINDOW: - case C2_L_TDRAWABLE: - case C2_L_TATOM: pleaf->format = 32; break; - case C2_L_TSTRING: pleaf->format = 8; break; - default: break; - } - } - // Write format if (hasformat) { - if (pleaf->predef != C2_L_PUNDEFINED) { - log_warn("Format \"%ld\" specified on a default target " - "will be ignored.", - format); - } else if (pleaf->type == C2_L_TSTRING) { - log_warn("Format \"%ld\" specified on a string target " - "will be ignored.", - format); - } else { - if (pleaf->format && pleaf->format != format) { - log_warn("Default format %d overridden on " - "target.", - pleaf->format); - } - pleaf->format = to_int_checked(format); + log_warn("Format specifier is deprecated. Format \"%ld\" " + "specified on target \"%s\" will be ignored, you can " + "remove it.", + format, pleaf->tgt); + if (format && format != 8 && format != 16 && format != 32) { + c2_error("Invalid format %ld.", format); } } } - - if (!pleaf->type) { - c2_error("Target type cannot be determined."); - } - - // if (!pleaf->predef && !pleaf->format && C2_L_TSTRING != pleaf->type) - // c2_error("Target format cannot be determined."); - - if (pleaf->format && 8 != pleaf->format && 16 != pleaf->format && 32 != pleaf->format) { - c2_error("Invalid format."); - } - return offset; fail: @@ -781,11 +964,11 @@ static int c2_parse_op(const char *pattern, int offset, c2_ptr_t *presult) { // Parse operator while ('=' == pattern[offset] || '>' == pattern[offset] || '<' == pattern[offset]) { - if ('=' == pattern[offset] && C2_L_OGT == pleaf->op) + if ('=' == pattern[offset] && C2_L_OGT == pleaf->op) { pleaf->op = C2_L_OGTEQ; - else if ('=' == pattern[offset] && C2_L_OLT == pleaf->op) + } else if ('=' == pattern[offset] && C2_L_OLT == pleaf->op) { pleaf->op = C2_L_OLTEQ; - else if (pleaf->op) { + } else if (pleaf->op) { c2_error("Duplicate operator."); } else { switch (pattern[offset]) { @@ -800,9 +983,10 @@ static int c2_parse_op(const char *pattern, int offset, c2_ptr_t *presult) { } // Check for problems - if (C2_L_OEQ != pleaf->op && (pleaf->match || pleaf->match_ignorecase)) + if (C2_L_OEQ != pleaf->op && (pleaf->match || pleaf->match_ignorecase)) { c2_error("Exists/greater-than/less-than operators cannot have a " "qualifier."); + } return offset; @@ -852,6 +1036,10 @@ static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult) C2H_SKIP_SPACES(); } + if (raw == true) { + log_warn("Raw string patterns has been deprecated. pos %d", offset); + } + // Check for delimiters if (pattern[offset] == '\"' || pattern[offset] == '\'') { pleaf->ptntype = C2_L_PTSTRING; @@ -886,15 +1074,23 @@ static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult) case 'v': *(ptptnstr++) = '\v'; break; case 'o': case 'x': { - char *tstr = strndup(pattern + offset + 1, 2); + scoped_charp tstr = strndup(pattern + offset + 1, 2); char *pstr = NULL; long val = strtol( tstr, &pstr, ('o' == pattern[offset] ? 8 : 16)); - free(tstr); - if (pstr != &tstr[2] || val <= 0) + if (pstr != &tstr[2] || val <= 0) { c2_error("Invalid octal/hex escape " "sequence."); - *(ptptnstr++) = to_char_checked(val); + } + if (val > 255) { + c2_error("Octal/hex escape sequence out " + "of ASCII range."); + } + if (val > 127) { + // Manual sign extension + val -= 256; + } + *(ptptnstr++) = (char)val; offset += 2; break; } @@ -904,8 +1100,9 @@ static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult) *(ptptnstr++) = pattern[offset]; } } - if (!pattern[offset]) + if (!pattern[offset]) { c2_error("Premature end of pattern string."); + } ++offset; *ptptnstr = '\0'; pleaf->ptnstr = strdup(tptnstr); @@ -914,27 +1111,25 @@ static int c2_parse_pattern(const char *pattern, int offset, c2_ptr_t *presult) C2H_SKIP_SPACES(); - if (!pleaf->ptntype) + if (!pleaf->ptntype) { c2_error("Invalid pattern type."); + } - // Check if the type is correct - if (!(((C2_L_TSTRING == pleaf->type || C2_L_TATOM == pleaf->type) && - C2_L_PTSTRING == pleaf->ptntype) || - ((C2_L_TCARDINAL == pleaf->type || C2_L_TWINDOW == pleaf->type || - C2_L_TDRAWABLE == pleaf->type) && - C2_L_PTINT == pleaf->ptntype))) - c2_error("Pattern type incompatible with target type."); - - if (C2_L_PTINT == pleaf->ptntype && pleaf->match) - c2_error("Integer/boolean pattern cannot have operator qualifiers."); - - if (C2_L_PTINT == pleaf->ptntype && pleaf->match_ignorecase) - c2_error("Integer/boolean pattern cannot have flags."); + if (pleaf->ptntype == C2_L_PTINT) { + if (pleaf->match) { + c2_error("Integer/boolean pattern cannot have operator " + "qualifiers."); + } + if (pleaf->match_ignorecase) { + c2_error("Integer/boolean pattern cannot have flags."); + } + } if (C2_L_PTSTRING == pleaf->ptntype && (C2_L_OGT == pleaf->op || C2_L_OGTEQ == pleaf->op || C2_L_OLT == pleaf->op || - C2_L_OLTEQ == pleaf->op)) + C2_L_OLTEQ == pleaf->op)) { c2_error("String pattern cannot have an arithmetic operator."); + } return offset; @@ -956,22 +1151,17 @@ static int c2_parse_legacy(const char *pattern, int offset, c2_ptr_t *presult) { presult->isbranch = false; presult->l = pleaf; memcpy(pleaf, &leaf_def, sizeof(c2_l_t)); - pleaf->type = C2_L_TSTRING; pleaf->op = C2_L_OEQ; pleaf->ptntype = C2_L_PTSTRING; // Determine the pattern target -#define TGTFILL(pdefid) \ - (pleaf->predef = pdefid, pleaf->type = C2_PREDEFS[pdefid].type, \ - pleaf->format = C2_PREDEFS[pdefid].format) switch (pattern[offset]) { - case 'n': TGTFILL(C2_L_PNAME); break; - case 'i': TGTFILL(C2_L_PCLASSI); break; - case 'g': TGTFILL(C2_L_PCLASSG); break; - case 'r': TGTFILL(C2_L_PROLE); break; + case 'n': pleaf->predef = C2_L_PNAME; break; + case 'i': pleaf->predef = C2_L_PCLASSI; break; + case 'g': pleaf->predef = C2_L_PCLASSG; break; + case 'r': pleaf->predef = C2_L_PROLE; break; default: c2_error("Target \"%c\" invalid.\n", pattern[offset]); } -#undef TGTFILL offset += 2; @@ -1010,36 +1200,37 @@ static int c2_parse_legacy(const char *pattern, int offset, c2_ptr_t *presult) { /** * Do postprocessing on a condition leaf. */ -static bool c2_l_postprocess(session_t *ps, c2_l_t *pleaf) { - // Give a pattern type to a leaf with exists operator, if needed - if (C2_L_OEXISTS == pleaf->op && !pleaf->ptntype) { - pleaf->ptntype = (C2_L_TSTRING == pleaf->type ? C2_L_PTSTRING : C2_L_PTINT); - } - +static bool c2_l_postprocess(struct c2_state *state, xcb_connection_t *c, c2_l_t *pleaf) { // Get target atom if it's not a predefined one if (pleaf->predef == C2_L_PUNDEFINED) { - pleaf->tgtatom = get_atom(ps->atoms, pleaf->tgt); + pleaf->tgtatom = get_atom_with_nul(state->atoms, pleaf->tgt, c); if (!pleaf->tgtatom) { log_error("Failed to get atom for target \"%s\".", pleaf->tgt); return false; } } - // Insert target Atom into atom track list + // Insert target atom into tracked property name list if (pleaf->tgtatom) { - bool found = false; - for (latom_t *platom = ps->track_atom_lst; platom; platom = platom->next) { - if (pleaf->tgtatom == platom->atom) { - found = true; - break; - } - } - if (!found) { - auto pnew = cmalloc(latom_t); - pnew->next = ps->track_atom_lst; - pnew->atom = pleaf->tgtatom; - ps->track_atom_lst = pnew; + struct c2_tracked_property *property; + struct c2_tracked_property_key key = { + .property = pleaf->tgtatom, + .is_on_client = pleaf->target_on_client, + }; + HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), property); + if (property == NULL) { + property = cmalloc(struct c2_tracked_property); + property->key = key; + property->id = HASH_COUNT(state->tracked_properties); + HASH_ADD_KEYPTR(hh, state->tracked_properties, &property->key, + sizeof(property->key), property); + property->max_indices = pleaf->index; + } else if (pleaf->index == -1) { + property->max_indices = -1; + } else if (property->max_indices >= 0 && pleaf->index > property->max_indices) { + property->max_indices = pleaf->index; } + pleaf->target_id = property->id; } // Warn about lower case characters in target name @@ -1056,36 +1247,29 @@ static bool c2_l_postprocess(session_t *ps, c2_l_t *pleaf) { // PCRE patterns if (C2_L_PTSTRING == pleaf->ptntype && C2_L_MPCRE == pleaf->match) { #ifdef CONFIG_REGEX_PCRE - const char *error = NULL; - int erroffset = 0; - int options = 0; + int errorcode = 0; + PCRE2_SIZE erroffset = 0; + unsigned int options = 0; // Ignore case flag - if (pleaf->match_ignorecase) - options |= PCRE_CASELESS; + if (pleaf->match_ignorecase) { + options |= PCRE2_CASELESS; + } // Compile PCRE expression pleaf->regex_pcre = - pcre_compile(pleaf->ptnstr, options, &error, &erroffset, NULL); - if (!pleaf->regex_pcre) { - log_error("Pattern \"%s\": PCRE regular expression parsing " - "failed on " - "offset %d: %s", - pleaf->ptnstr, erroffset, error); + pcre2_compile((PCRE2_SPTR)pleaf->ptnstr, PCRE2_ZERO_TERMINATED, + options, &errorcode, &erroffset, NULL); + if (pleaf->regex_pcre == NULL) { + PCRE2_UCHAR buffer[256]; + pcre2_get_error_message(errorcode, buffer, sizeof(buffer)); + log_error("Pattern \"%s\": PCRE regular expression " + "parsing failed on offset %zu: %s", + pleaf->ptnstr, erroffset, buffer); return false; } -#ifdef CONFIG_REGEX_PCRE_JIT - pleaf->regex_pcre_extra = - pcre_study(pleaf->regex_pcre, PCRE_STUDY_JIT_COMPILE, &error); - if (!pleaf->regex_pcre_extra) { - printf("Pattern \"%s\": PCRE regular expression study failed: %s", - pleaf->ptnstr, error); - } -#endif - - // Free the target string - // free(pleaf->tgt); - // pleaf->tgt = NULL; + pleaf->regex_pcre_match = + pcre2_match_data_create_from_pattern(pleaf->regex_pcre, NULL); #else log_error("PCRE regular expression support not compiled in."); return false; @@ -1095,20 +1279,21 @@ static bool c2_l_postprocess(session_t *ps, c2_l_t *pleaf) { return true; } -static bool c2_tree_postprocess(session_t *ps, c2_ptr_t node) { +static bool c2_tree_postprocess(struct c2_state *state, xcb_connection_t *c, c2_ptr_t node) { if (!node.isbranch) { - return c2_l_postprocess(ps, node.l); + return c2_l_postprocess(state, c, node.l); } - if (!c2_tree_postprocess(ps, node.b->opr1)) - return false; - return c2_tree_postprocess(ps, node.b->opr2); + + return c2_tree_postprocess(state, c, node.b->opr1) && + c2_tree_postprocess(state, c, node.b->opr2); } -bool c2_list_postprocess(session_t *ps, c2_lptr_t *list) { +bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, c2_lptr_t *list) { c2_lptr_t *head = list; while (head) { - if (!c2_tree_postprocess(ps, head->ptr)) + if (!c2_tree_postprocess(state, c, head->ptr)) { return false; + } head = head->next; } return true; @@ -1121,8 +1306,9 @@ static void c2_free(c2_ptr_t p) { if (p.isbranch) { c2_b_t *const pbranch = p.b; - if (!pbranch) + if (!pbranch) { return; + } c2_free(pbranch->opr1); c2_free(pbranch->opr2); @@ -1132,14 +1318,15 @@ static void c2_free(c2_ptr_t p) { else { c2_l_t *const pleaf = p.l; - if (!pleaf) + if (!pleaf) { return; + } free(pleaf->tgt); free(pleaf->ptnstr); #ifdef CONFIG_REGEX_PCRE - pcre_free(pleaf->regex_pcre); - LPCRE_FREE_STUDY(pleaf->regex_pcre_extra); + pcre2_code_free(pleaf->regex_pcre); + pcre2_match_data_free(pleaf->regex_pcre_match); #endif free(pleaf); } @@ -1148,11 +1335,16 @@ static void c2_free(c2_ptr_t p) { /** * Free a condition tree in c2_lptr_t. */ -c2_lptr_t *c2_free_lptr(c2_lptr_t *lp) { - if (!lp) +c2_lptr_t *c2_free_lptr(c2_lptr_t *lp, c2_userdata_free f) { + if (!lp) { return NULL; + } c2_lptr_t *pnext = lp->next; + if (f) { + f(lp->data); + } + lp->data = NULL; c2_free(lp->ptr); free(lp); @@ -1165,415 +1357,438 @@ c2_lptr_t *c2_free_lptr(c2_lptr_t *lp) { static const char *c2h_dump_str_tgt(const c2_l_t *pleaf) { if (pleaf->predef != C2_L_PUNDEFINED) { return C2_PREDEFS[pleaf->predef].name; - } else { - return pleaf->tgt; - } -} - -/** - * Get a string representation of a target. - */ -static const char *c2h_dump_str_type(const c2_l_t *pleaf) { - switch (pleaf->type) { - case C2_L_TWINDOW: return "w"; - case C2_L_TDRAWABLE: return "d"; - case C2_L_TCARDINAL: return "c"; - case C2_L_TSTRING: return "s"; - case C2_L_TATOM: return "a"; - case C2_L_TUNDEFINED: break; } - - return NULL; + return pleaf->tgt; } /** - * Dump a condition tree. + * Dump a condition tree to string. Return the number of characters that + * would have been written if the buffer had been large enough, excluding + * the null terminator. + * null terminator will not be written to the output. */ -static void c2_dump(c2_ptr_t p) { - // For a branch +static size_t c2_condition_to_str(const c2_ptr_t p, char *output, size_t len) { +#define push_char(c) \ + if (offset < len) \ + output[offset] = (c); \ + offset++ +#define push_str(str) \ + do { \ + if (offset < len) { \ + size_t slen = strlen(str); \ + if (offset + slen > len) \ + slen = len - offset; \ + memcpy(output + offset, str, slen); \ + } \ + offset += strlen(str); \ + } while (false) + size_t offset = 0; if (p.isbranch) { + // Branch, i.e. logical operators &&, ||, XOR const c2_b_t *const pbranch = p.b; if (!pbranch) { - return; + return 0; } if (pbranch->neg) { - putchar('!'); + push_char('!'); } - printf("("); - c2_dump(pbranch->opr1); + push_char('('); + if (len > offset) { + offset += c2_condition_to_str(pbranch->opr1, output + offset, + len - offset); + } else { + offset += c2_condition_to_str(pbranch->opr1, NULL, 0); + } switch (pbranch->op) { - case C2_B_OAND: printf(" && "); break; - case C2_B_OOR: printf(" || "); break; - case C2_B_OXOR: printf(" XOR "); break; + case C2_B_OAND: push_str(" && "); break; + case C2_B_OOR: push_str(" || "); break; + case C2_B_OXOR: push_str(" XOR "); break; default: assert(0); break; } - c2_dump(pbranch->opr2); - printf(") "); - } - // For a leaf - else { + if (len > offset) { + offset += c2_condition_to_str(pbranch->opr2, output + offset, + len - offset); + } else { + offset += c2_condition_to_str(pbranch->opr2, NULL, 0); + } + push_str(")"); + } else { + // Leaf node const c2_l_t *const pleaf = p.l; + char number[128]; if (!pleaf) { - return; + return 0; } if (C2_L_OEXISTS == pleaf->op && pleaf->neg) { - putchar('!'); + push_char('!'); } // Print target name, type, and format - { - printf("%s", c2h_dump_str_tgt(pleaf)); - if (pleaf->tgt_onframe) { - putchar('@'); - } - if (pleaf->predef == C2_L_PUNDEFINED) { - if (pleaf->index < 0) { - printf("[*]"); - } else { - printf("[%d]", pleaf->index); - } + const char *target_str = c2h_dump_str_tgt(pleaf); + push_str(target_str); + if (pleaf->target_on_client) { + push_char('@'); + } + if (pleaf->predef == C2_L_PUNDEFINED) { + if (pleaf->index < 0) { + push_str("[*]"); + } else { + sprintf(number, "[%d]", pleaf->index); + push_str(number); } - printf(":%d%s", pleaf->format, c2h_dump_str_type(pleaf)); + } + + if (C2_L_OEXISTS == pleaf->op) { + return offset; } // Print operator - putchar(' '); + push_char(' '); if (C2_L_OEXISTS != pleaf->op && pleaf->neg) { - putchar('!'); + push_char('!'); } switch (pleaf->match) { case C2_L_MEXACT: break; - case C2_L_MCONTAINS: putchar('*'); break; - case C2_L_MSTART: putchar('^'); break; - case C2_L_MPCRE: putchar('~'); break; - case C2_L_MWILDCARD: putchar('%'); break; + case C2_L_MCONTAINS: push_char('*'); break; + case C2_L_MSTART: push_char('^'); break; + case C2_L_MPCRE: push_char('~'); break; + case C2_L_MWILDCARD: push_char('%'); break; } if (pleaf->match_ignorecase) { - putchar('?'); + push_char('?'); } switch (pleaf->op) { case C2_L_OEXISTS: break; - case C2_L_OEQ: fputs("=", stdout); break; - case C2_L_OGT: fputs(">", stdout); break; - case C2_L_OGTEQ: fputs(">=", stdout); break; - case C2_L_OLT: fputs("<", stdout); break; - case C2_L_OLTEQ: fputs("<=", stdout); break; - } - - if (C2_L_OEXISTS == pleaf->op) { - return; + case C2_L_OEQ: push_str("="); break; + case C2_L_OGT: push_str(">"); break; + case C2_L_OGTEQ: push_str(">="); break; + case C2_L_OLT: push_str("<"); break; + case C2_L_OLTEQ: push_str("<="); break; } // Print pattern - putchar(' '); + push_char(' '); switch (pleaf->ptntype) { - case C2_L_PTINT: printf("%ld", pleaf->ptnint); break; + case C2_L_PTINT: + sprintf(number, "%ld", pleaf->ptnint); + push_str(number); + break; case C2_L_PTSTRING: // TODO(yshui) Escape string before printing out? - printf("\"%s\"", pleaf->ptnstr); + push_char('"'); + for (int i = 0; pleaf->ptnstr[i]; i++) { + switch (pleaf->ptnstr[i]) { + case '\\': push_str("\\\\"); break; + case '"': push_str("\\\""); break; + case '\a': push_str("\\a"); break; + case '\b': push_str("\\b"); break; + case '\f': push_str("\\f"); break; + case '\r': push_str("\\r"); break; + case '\v': push_str("\\v"); break; + case '\t': push_str("\\t"); break; + case '\n': push_str("\\n"); break; + default: + if (isprint(pleaf->ptnstr[i])) { + push_char(pleaf->ptnstr[i]); + } else { + sprintf(number, "\\x%02x", + (unsigned char)pleaf->ptnstr[i]); + push_str(number); + } + break; + } + } + push_char('"'); break; default: assert(0); break; } } +#undef push_char +#undef push_str + return offset; } -/** - * Get the type atom of a condition. - */ -static xcb_atom_t c2_get_atom_type(const c2_l_t *pleaf) { - switch (pleaf->type) { - case C2_L_TCARDINAL: return XCB_ATOM_CARDINAL; - case C2_L_TWINDOW: return XCB_ATOM_WINDOW; - case C2_L_TSTRING: return XCB_ATOM_STRING; - case C2_L_TATOM: return XCB_ATOM_ATOM; - case C2_L_TDRAWABLE: return XCB_ATOM_DRAWABLE; - default: assert(0); break; - } - unreachable; +/// Wrapper of c2_condition_to_str which uses an internal static buffer, and +/// returns a nul terminated string. The returned string is only valid until the +/// next call to this function, and should not be freed. +static const char *c2_condition_to_str2(c2_ptr_t ptr) { + static thread_local char buf[4096]; + auto len = c2_condition_to_str(ptr, buf, sizeof(buf)); + if (len >= sizeof(buf)) { + // Resulting string is too long, clobber the last character with a nul. + buf[sizeof(buf) - 1] = '\0'; + } else { + buf[len] = '\0'; + } + return buf; } -/** - * Match a window against a single leaf window condition. - * - * For internal use. - */ -static inline void c2_match_once_leaf(session_t *ps, const struct managed_win *w, - const c2_l_t *pleaf, bool *pres, bool *perr) { - assert(pleaf); - - const xcb_window_t wid = (pleaf->tgt_onframe ? w->client_win : w->base.id); +const char *c2_lptr_to_str(const c2_lptr_t *ptr) { + return c2_condition_to_str2(ptr->ptr); +} - // Return if wid is missing - if (pleaf->predef == C2_L_PUNDEFINED && !wid) { - return; +/// Get the list of target number values from a struct c2_property_value +static inline const int64_t * +c2_values_get_number_targets(const struct c2_property_value *values, int index, size_t *n) { + auto storage = values->numbers; + if (values->length > ARR_SIZE(values->numbers)) { + storage = values->array; + } + if (index < 0) { + *n = values->length; + return storage; } + if ((size_t)index < values->length) { + *n = 1; + return &storage[index]; + } + *n = 0; + return NULL; +} - const int idx = (pleaf->index < 0 ? 0 : pleaf->index); +static inline bool c2_int_op(const c2_l_t *leaf, int64_t target) { + switch (leaf->op) { + case C2_L_OEXISTS: return leaf->predef != C2_L_PUNDEFINED ? target : true; + case C2_L_OEQ: return target == leaf->ptnint; + case C2_L_OGT: return target > leaf->ptnint; + case C2_L_OGTEQ: return target >= leaf->ptnint; + case C2_L_OLT: return target < leaf->ptnint; + case C2_L_OLTEQ: return target <= leaf->ptnint; + } + unreachable(); +} - switch (pleaf->ptntype) { - // Deal with integer patterns - case C2_L_PTINT: { - long *targets = NULL; - long *targets_free = NULL; - size_t ntargets = 0; +static bool c2_match_once_leaf_int(const struct managed_win *w, const c2_l_t *leaf) { + const xcb_window_t wid = (leaf->target_on_client ? w->client_win : w->base.id); - // Get the value + // Get the value + if (leaf->predef != C2_L_PUNDEFINED) { // A predefined target - long predef_target = 0; - if (pleaf->predef != C2_L_PUNDEFINED) { - *perr = false; - switch (pleaf->predef) { - case C2_L_PID: predef_target = wid; break; - case C2_L_PX: predef_target = w->g.x; break; - case C2_L_PY: predef_target = w->g.y; break; - case C2_L_PX2: predef_target = w->g.x + w->widthb; break; - case C2_L_PY2: predef_target = w->g.y + w->heightb; break; - case C2_L_PWIDTH: predef_target = w->g.width; break; - case C2_L_PHEIGHT: predef_target = w->g.height; break; - case C2_L_PWIDTHB: predef_target = w->widthb; break; - case C2_L_PHEIGHTB: predef_target = w->heightb; break; - case C2_L_PBDW: predef_target = w->g.border_width; break; - case C2_L_PFULLSCREEN: - predef_target = win_is_fullscreen(ps, w); - break; - case C2_L_POVREDIR: predef_target = w->a.override_redirect; break; - case C2_L_PARGB: predef_target = win_has_alpha(w); break; - case C2_L_PFOCUSED: - predef_target = win_is_focused_raw(ps, w); - break; - case C2_L_PWMWIN: predef_target = w->wmwin; break; - case C2_L_PBSHAPED: predef_target = w->bounding_shaped; break; - case C2_L_PROUNDED: predef_target = w->rounded_corners; break; - case C2_L_PCLIENT: predef_target = w->client_win; break; - case C2_L_PLEADER: predef_target = w->leader; break; - default: - *perr = true; - assert(0); - break; - } - ntargets = 1; - targets = &predef_target; - } - // A raw window property - else { - int word_count = 1; - if (pleaf->index < 0) { - // Get length of property in 32-bit multiples - auto prop_info = x_get_prop_info(ps->c, wid, pleaf->tgtatom); - word_count = to_int_checked((prop_info.length + 4 - 1) / 4); - } - winprop_t prop = x_get_prop_with_offset( - ps->c, wid, pleaf->tgtatom, idx, word_count, - c2_get_atom_type(pleaf), pleaf->format); - - ntargets = (pleaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); - if (ntargets > 0) { - targets = targets_free = ccalloc(ntargets, long); - *perr = false; - for (size_t i = 0; i < ntargets; ++i) { - targets[i] = winprop_get_int(prop, i); - } - } - free_winprop(&prop); + int64_t predef_target = 0; + switch (leaf->predef) { + case C2_L_PID: predef_target = wid; break; + case C2_L_PX: predef_target = w->g.x; break; + case C2_L_PY: predef_target = w->g.y; break; + case C2_L_PX2: predef_target = w->g.x + w->widthb; break; + case C2_L_PY2: predef_target = w->g.y + w->heightb; break; + case C2_L_PWIDTH: predef_target = w->g.width; break; + case C2_L_PHEIGHT: predef_target = w->g.height; break; + case C2_L_PWIDTHB: predef_target = w->widthb; break; + case C2_L_PHEIGHTB: predef_target = w->heightb; break; + case C2_L_PBDW: predef_target = w->g.border_width; break; + case C2_L_PFULLSCREEN: predef_target = w->is_fullscreen; break; + case C2_L_PARGB: predef_target = win_has_alpha(w); break; + case C2_L_PFOCUSED: predef_target = win_is_focused_raw(w); break; + case C2_L_PWMWIN: predef_target = win_is_wmwin(w); break; + case C2_L_PBSHAPED: predef_target = w->bounding_shaped; break; + case C2_L_PROUNDED: predef_target = w->rounded_corners; break; + case C2_L_PCLIENT: predef_target = w->client_win; break; + case C2_L_PLEADER: predef_target = w->leader; break; + case C2_L_POVREDIR: + // When user wants to check override-redirect, they almost always + // want to check the client window, not the frame window. We + // don't track the override-redirect state of the client window + // directly, however we can assume if a window has a window + // manager frame around it, it's not override-redirect. + predef_target = + w->a.override_redirect && (w->client_win == w->base.id || + w->client_win == XCB_WINDOW_NONE); + break; + default: unreachable(); } + return c2_int_op(leaf, predef_target); + } - if (*perr) { - goto fail_int; - } + // A raw window property + auto values = &w->c2_state.values[leaf->target_id]; + assert(!values->needs_update); + if (!values->valid) { + log_verbose("Property %s not found on window %#010x (%s)", leaf->tgt, + w->client_win, w->name); + return false; + } - // Do comparison - bool res = false; - for (size_t i = 0; i < ntargets; ++i) { - long tgt = targets[i]; - switch (pleaf->op) { - case C2_L_OEXISTS: - res = (pleaf->predef != C2_L_PUNDEFINED ? tgt : true); - break; - case C2_L_OEQ: res = (tgt == pleaf->ptnint); break; - case C2_L_OGT: res = (tgt > pleaf->ptnint); break; - case C2_L_OGTEQ: res = (tgt >= pleaf->ptnint); break; - case C2_L_OLT: res = (tgt < pleaf->ptnint); break; - case C2_L_OLTEQ: res = (tgt <= pleaf->ptnint); break; - default: *perr = true; assert(0); - } - if (res) { - break; - } - } - *pres = res; + if (values->type == C2_PROPERTY_TYPE_STRING) { + log_error("Property %s is not an integer", leaf->tgt); + return false; + } - fail_int: - // Free property values after usage, if necessary - if (targets_free) { - free(targets_free); + size_t ntargets = 0; + auto targets = c2_values_get_number_targets(values, leaf->index, &ntargets); + for (size_t i = 0; i < ntargets; i++) { + if (c2_int_op(leaf, targets[i])) { + return true; } - } break; - // String patterns - case C2_L_PTSTRING: { - const char **targets = NULL; - const char **targets_free = NULL; - const char **targets_free_inner = NULL; - size_t ntargets = 0; + } + return false; +} - // A predefined target - const char *predef_target = NULL; - if (pleaf->predef != C2_L_PUNDEFINED) { - switch (pleaf->predef) { - case C2_L_PWINDOWTYPE: - predef_target = WINTYPES[w->window_type]; - break; - case C2_L_PNAME: predef_target = w->name; break; - case C2_L_PCLASSG: predef_target = w->class_general; break; - case C2_L_PCLASSI: predef_target = w->class_instance; break; - case C2_L_PROLE: predef_target = w->role; break; - default: assert(0); break; - } - ntargets = 1; - targets = &predef_target; - } - // An atom type property, convert it to string - else if (pleaf->type == C2_L_TATOM) { - int word_count = 1; - if (pleaf->index < 0) { - // Get length of property in 32-bit multiples - auto prop_info = x_get_prop_info(ps->c, wid, pleaf->tgtatom); - word_count = to_int_checked((prop_info.length + 4 - 1) / 4); - } - winprop_t prop = x_get_prop_with_offset( - ps->c, wid, pleaf->tgtatom, idx, word_count, - c2_get_atom_type(pleaf), pleaf->format); - - ntargets = (pleaf->index < 0 ? prop.nitems : min2(prop.nitems, 1)); - targets = targets_free = (const char **)ccalloc(2 * ntargets, char *); - targets_free_inner = targets + ntargets; - - for (size_t i = 0; i < ntargets; ++i) { - xcb_atom_t atom = (xcb_atom_t)winprop_get_int(prop, i); - if (atom) { - xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply( - ps->c, xcb_get_atom_name(ps->c, atom), NULL); - if (reply) { - targets[i] = targets_free_inner[i] = strndup( - xcb_get_atom_name_name(reply), - (size_t)xcb_get_atom_name_name_length(reply)); - free(reply); - } - } - } - free_winprop(&prop); +static bool c2_string_op(const c2_l_t *leaf, const char *target) { + if (leaf->op == C2_L_OEXISTS) { + return true; + } + if (leaf->op != C2_L_OEQ) { + log_error("Unsupported operator %d for string comparison.", leaf->op); + assert(leaf->op == C2_L_OEQ); + return false; + } + if (leaf->match == C2_L_MPCRE) { +#ifdef CONFIG_REGEX_PCRE + assert(strlen(target) <= INT_MAX); + assert(leaf->regex_pcre); + return (pcre2_match(leaf->regex_pcre, (PCRE2_SPTR)target, strlen(target), + 0, 0, leaf->regex_pcre_match, NULL) > 0); +#else + log_error("PCRE regular expression support not compiled in."); + assert(leaf->match != C2_L_MPCRE); + return false; +#endif + } + if (leaf->match_ignorecase) { + switch (leaf->match) { + case C2_L_MEXACT: return !strcasecmp(target, leaf->ptnstr); + case C2_L_MCONTAINS: return strcasestr(target, leaf->ptnstr); + case C2_L_MSTART: + return !strncasecmp(target, leaf->ptnstr, strlen(leaf->ptnstr)); + case C2_L_MWILDCARD: return !fnmatch(leaf->ptnstr, target, FNM_CASEFOLD); + default: unreachable(); } - // Not an atom type, just fetch the string list - else { - char **strlst = NULL; - int nstr = 0; - if (wid_get_text_prop(ps, wid, pleaf->tgtatom, &strlst, &nstr)) { - if (pleaf->index < 0 && nstr > 0 && strlen(strlst[0]) > 0) { - ntargets = to_u32_checked(nstr); - targets = (const char **)strlst; - } else if (nstr > idx) { - ntargets = 1; - targets = (const char **)strlst + idx; - } - } - if (strlst) { - targets_free = (const char **)strlst; - } + } else { + switch (leaf->match) { + case C2_L_MEXACT: return !strcmp(target, leaf->ptnstr); + case C2_L_MCONTAINS: return strstr(target, leaf->ptnstr); + case C2_L_MSTART: + return !strncmp(target, leaf->ptnstr, strlen(leaf->ptnstr)); + case C2_L_MWILDCARD: return !fnmatch(leaf->ptnstr, target, 0); + default: unreachable(); } + } + unreachable(); +} + +static bool c2_match_once_leaf_string(struct atom *atoms, const struct managed_win *w, + const c2_l_t *leaf) { - if (ntargets == 0) { - goto fail_str; + // A predefined target + const char *predef_target = NULL; + if (leaf->predef != C2_L_PUNDEFINED) { + switch (leaf->predef) { + case C2_L_PWINDOWTYPE: + predef_target = WINTYPES[w->window_type].name; + break; + case C2_L_PNAME: predef_target = w->name; break; + case C2_L_PCLASSG: predef_target = w->class_general; break; + case C2_L_PCLASSI: predef_target = w->class_instance; break; + case C2_L_PROLE: predef_target = w->role; break; + default: unreachable(); } - for (size_t i = 0; i < ntargets; ++i) { - if (!targets[i]) { - goto fail_str; - } + if (!predef_target) { + return false; } - *perr = false; + log_verbose("Matching against predefined target %s", predef_target); + return c2_string_op(leaf, predef_target); + } + + auto values = &w->c2_state.values[leaf->target_id]; + assert(!values->needs_update); + if (!values->valid) { + log_verbose("Property %s not found on window %#010x (%s)", leaf->tgt, + w->client_win, w->name); + return false; + } + + if (values->type == C2_PROPERTY_TYPE_ATOM) { + size_t ntargets = 0; + auto targets = c2_values_get_number_targets(values, leaf->index, &ntargets); - // Actual matching - bool res = false; for (size_t i = 0; i < ntargets; ++i) { - const char *tgt = targets[i]; - switch (pleaf->op) { - case C2_L_OEXISTS: res = true; break; - case C2_L_OEQ: - switch (pleaf->match) { - case C2_L_MEXACT: - if (pleaf->match_ignorecase) { - res = !strcasecmp(tgt, pleaf->ptnstr); - } else { - res = !strcmp(tgt, pleaf->ptnstr); - } - break; - case C2_L_MCONTAINS: - if (pleaf->match_ignorecase) { - res = strcasestr(tgt, pleaf->ptnstr); - } else { - res = strstr(tgt, pleaf->ptnstr); - } - break; - case C2_L_MSTART: - if (pleaf->match_ignorecase) { - res = !strncasecmp(tgt, pleaf->ptnstr, - strlen(pleaf->ptnstr)); - } else { - res = !strncmp(tgt, pleaf->ptnstr, - strlen(pleaf->ptnstr)); - } - break; - case C2_L_MWILDCARD: { - int flags = 0; - if (pleaf->match_ignorecase) { - flags |= FNM_CASEFOLD; - } - res = !fnmatch(pleaf->ptnstr, tgt, flags); - } break; - case C2_L_MPCRE: -#ifdef CONFIG_REGEX_PCRE - assert(strlen(tgt) <= INT_MAX); - res = (pcre_exec(pleaf->regex_pcre, - pleaf->regex_pcre_extra, tgt, - (int)strlen(tgt), 0, 0, NULL, 0) >= 0); -#else - assert(0); -#endif - break; - } - break; - default: *perr = true; assert(0); - } - if (res) { - break; + auto atom = (xcb_atom_t)targets[i]; + const char *atom_name = get_atom_name_cached(atoms, atom); + log_verbose("(%zu/%zu) Atom %u is %s", i, ntargets, atom, atom_name); + assert(atom_name != NULL); + if (atom_name && c2_string_op(leaf, atom_name)) { + return true; } } - *pres = res; + return false; + } - fail_str: - // Free the string after usage, if necessary - if (targets_free_inner) { - for (size_t i = 0; i < ntargets; ++i) { - if (targets_free_inner[i]) { - free((void *)targets_free_inner[i]); - } + if (values->type != C2_PROPERTY_TYPE_STRING) { + log_verbose("Property %s is not a string", leaf->tgt); + return false; + } + + // Not an atom type, value is a list of nul separated strings + if (leaf->index < 0) { + size_t offset = 0; + while (offset < values->length) { + if (c2_string_op(leaf, values->string + offset)) { + return true; } + offset += strlen(values->string + offset) + 1; } - // Free property values after usage, if necessary - if (targets_free) { - free(targets_free); + return false; + } + size_t offset = 0; + int index = leaf->index; + while (offset < values->length && index != 0) { + offset += strlen(values->string + offset) + 1; + index -= 1; + } + if (index != 0 || values->length == 0) { + // index is out of bounds + return false; + } + return c2_string_op(leaf, values->string + offset); +} + +/** + * Match a window against a single leaf window condition. + * + * For internal use. + */ +static inline bool +c2_match_once_leaf(struct c2_state *state, const struct managed_win *w, const c2_l_t *leaf) { + assert(leaf); + + const xcb_window_t wid = (leaf->target_on_client ? w->client_win : w->base.id); + + // Return if wid is missing + if (leaf->predef == C2_L_PUNDEFINED && !wid) { + log_debug("Window ID missing."); + return false; + } + + log_verbose("Matching window %#010x (%s) against condition %s", wid, w->name, + c2_condition_to_str2((c2_ptr_t){.l = (c2_l_t *)leaf, .isbranch = false})); + + unsigned int pattern_type = leaf->ptntype; + if (pattern_type == C2_L_PTUNDEFINED) { + auto values = &w->c2_state.values[leaf->target_id]; + if (values->type == C2_PROPERTY_TYPE_STRING) { + pattern_type = C2_L_PTSTRING; + } else { + pattern_type = C2_L_PTINT; } - } break; - default: assert(0); break; + } + + switch (pattern_type) { + // Deal with integer patterns + case C2_L_PTINT: return c2_match_once_leaf_int(w, leaf); + // String patterns + case C2_L_PTSTRING: return c2_match_once_leaf_string(state->atoms, w, leaf); + default: unreachable(); } } @@ -1582,71 +1797,58 @@ static inline void c2_match_once_leaf(session_t *ps, const struct managed_win *w * * @return true if matched, false otherwise. */ -static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_ptr_t cond) { +static bool +c2_match_once(struct c2_state *state, const struct managed_win *w, const c2_ptr_t cond) { bool result = false; - bool error = true; - // Handle a branch if (cond.isbranch) { + // Handle a branch (and/or/xor operation) const c2_b_t *pb = cond.b; - if (!pb) + if (!pb) { return false; + } - error = false; + log_verbose("Matching window %#010x (%s) against condition %s", + w->base.id, w->name, c2_condition_to_str2(cond)); switch (pb->op) { case C2_B_OAND: - result = (c2_match_once(ps, w, pb->opr1) && - c2_match_once(ps, w, pb->opr2)); + result = (c2_match_once(state, w, pb->opr1) && + c2_match_once(state, w, pb->opr2)); break; case C2_B_OOR: - result = (c2_match_once(ps, w, pb->opr1) || - c2_match_once(ps, w, pb->opr2)); + result = (c2_match_once(state, w, pb->opr1) || + c2_match_once(state, w, pb->opr2)); break; case C2_B_OXOR: - result = (c2_match_once(ps, w, pb->opr1) != - c2_match_once(ps, w, pb->opr2)); + result = (c2_match_once(state, w, pb->opr1) != + c2_match_once(state, w, pb->opr2)); break; - default: error = true; assert(0); + default: unreachable(); } -#ifdef DEBUG_WINMATCH - log_trace("(%#010x): branch: result = %d, pattern = ", w->base.id, result); - c2_dump(cond); - putchar('\n'); -#endif - } - // Handle a leaf - else { + log_debug("(%#010x): branch: result = %d, pattern = %s", w->base.id, + result, c2_condition_to_str2(cond)); + } else { + // A leaf const c2_l_t *pleaf = cond.l; - if (!pleaf) + if (!pleaf) { return false; - - c2_match_once_leaf(ps, w, pleaf, &result, &error); - - // For EXISTS operator, no errors are fatal - if (C2_L_OEXISTS == pleaf->op && error) { - result = false; - error = false; } -#ifdef DEBUG_WINMATCH - log_trace("(%#010x): leaf: result = %d, error = %d, " - "client = %#010x, pattern = ", - w->base.id, result, error, w->client_win); - c2_dump(cond); - putchar('\n'); -#endif + result = c2_match_once_leaf(state, w, pleaf); + + log_debug("(%#010x): leaf: result = %d, client = %#010x, " + "pattern = %s", + w->base.id, result, w->client_win, c2_condition_to_str2(cond)); } // Postprocess the result - if (error) - result = false; - - if (cond.isbranch ? cond.b->neg : cond.l->neg) + if (cond.isbranch ? cond.b->neg : cond.l->neg) { result = !result; + } return result; } @@ -1658,17 +1860,287 @@ static bool c2_match_once(session_t *ps, const struct managed_win *w, const c2_p * @param pdata a place to return the data * @return true if matched, false otherwise. */ -bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condlst, - void **pdata) { - assert(ps->server_grabbed); +bool c2_match(struct c2_state *state, const struct managed_win *w, + const c2_lptr_t *condlst, void **pdata) { // Then go through the whole linked list for (; condlst; condlst = condlst->next) { - if (c2_match_once(ps, w, condlst->ptr)) { - if (pdata) + if (c2_match_once(state, w, condlst->ptr)) { + if (pdata) { *pdata = condlst->data; + } return true; } } return false; } + +/// Iterate over all conditions in a condition linked list. Call the callback for +/// each of the conditions. If the callback returns true, the iteration stops +/// early. +/// +/// Returns whether the iteration was stopped early. +bool c2_list_foreach(const c2_lptr_t *condlist, c2_list_foreach_cb_t cb, void *data) { + for (auto i = condlist; i; i = i->next) { + if (cb(i, data)) { + return true; + } + } + return false; +} + +/// Return user data stored in a condition. +void *c2_list_get_data(const c2_lptr_t *condlist) { + return condlist->data; +} + +struct c2_state *c2_state_new(struct atom *atoms) { + auto ret = ccalloc(1, struct c2_state); + ret->atoms = atoms; + return ret; +} + +void c2_state_free(struct c2_state *state) { + struct c2_tracked_property *property, *tmp; + HASH_ITER(hh, state->tracked_properties, property, tmp) { + HASH_DEL(state->tracked_properties, property); + free(property); + } + free(state->propert_lengths); + free(state->cookies); + free(state); +} + +void c2_window_state_init(const struct c2_state *state, struct c2_window_state *window_state) { + auto property_count = HASH_COUNT(state->tracked_properties); + window_state->values = ccalloc(property_count, struct c2_property_value); + for (size_t i = 0; i < property_count; i++) { + window_state->values[i].needs_update = true; + window_state->values[i].valid = false; + } +} + +void c2_window_state_destroy(const struct c2_state *state, + struct c2_window_state *window_state) { + size_t property_count = HASH_COUNT(state->tracked_properties); + for (size_t i = 0; i < property_count; i++) { + auto values = &window_state->values[i]; + if (values->type == C2_PROPERTY_TYPE_STRING) { + free(window_state->values[i].string); + } else if (values->length > ARR_SIZE(values->numbers)) { + free(window_state->values[i].array); + } + } + free(window_state->values); +} + +void c2_window_state_mark_dirty(const struct c2_state *state, + struct c2_window_state *window_state, xcb_atom_t property, + bool is_on_client) { + struct c2_tracked_property *p; + struct c2_tracked_property_key key = { + .property = property, + .is_on_client = is_on_client, + }; + HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), p); + if (p) { + window_state->values[p->id].needs_update = true; + } +} + +static void +c2_window_state_update_one_from_reply(struct c2_state *state, + struct c2_property_value *value, xcb_atom_t property, + xcb_get_property_reply_t *reply, xcb_connection_t *c) { + auto len = to_u32_checked(xcb_get_property_value_length(reply)); + void *data = xcb_get_property_value(reply); + bool property_is_string = x_is_type_string(state->atoms, reply->type); + value->needs_update = false; + value->valid = false; + if (reply->type == XCB_ATOM_NONE) { + // Property doesn't exist on this window + log_verbose("Property %s doesn't exist on this window", + get_atom_name_cached(state->atoms, property)); + return; + } + if ((property_is_string && reply->format != 8) || + (reply->format != 8 && reply->format != 16 && reply->format != 32)) { + log_error("Invalid property type and format combination: property %s, " + "type: %s, format: %d.", + get_atom_name_cached(state->atoms, property), + get_atom_name_cached(state->atoms, reply->type), reply->format); + return; + } + + log_verbose("Updating property %s, length = %u, format = %d", + get_atom_name_cached(state->atoms, property), len, reply->format); + value->valid = true; + if (len == 0) { + value->length = 0; + return; + } + if (property_is_string) { + bool nul_terminated = ((char *)data)[len - 1] == '\0'; + value->length = len; + value->type = C2_PROPERTY_TYPE_STRING; + if (!nul_terminated) { + value->length += 1; + } + value->string = crealloc(value->string, value->length); + memcpy(value->string, data, len); + if (!nul_terminated) { + value->string[len] = '\0'; + } + } else { + size_t step = reply->format / 8; + bool is_signed = reply->type == XCB_ATOM_INTEGER; + value->length = len * 8 / reply->format; + if (reply->type == XCB_ATOM_ATOM) { + value->type = C2_PROPERTY_TYPE_ATOM; + } else { + value->type = C2_PROPERTY_TYPE_NUMBER; + } + + int64_t *storage = value->numbers; + if (value->length > ARR_SIZE(value->numbers)) { + if (value->capacity < value->length) { + value->array = crealloc(value->array, value->length); + value->capacity = value->length; + } + storage = value->array; + } + for (uint32_t i = 0; i < value->length; i++) { + auto item = (char *)data + i * step; + if (is_signed) { + switch (reply->format) { + case 8: storage[i] = *(int8_t *)item; break; + case 16: storage[i] = *(int16_t *)item; break; + case 32: storage[i] = *(int32_t *)item; break; + default: unreachable(); + } + } else { + switch (reply->format) { + case 8: storage[i] = *(uint8_t *)item; break; + case 16: storage[i] = *(uint16_t *)item; break; + case 32: storage[i] = *(uint32_t *)item; break; + default: unreachable(); + } + } + log_verbose("Property %s[%d] = %" PRId64, + get_atom_name_cached(state->atoms, property), i, + storage[i]); + if (reply->type == XCB_ATOM_ATOM) { + // Prefetch the atom name so it will be available + // during `c2_match`. We don't need the return value here. + get_atom_name(state->atoms, (xcb_atom_t)storage[i], c); + } + } + } +} + +static void c2_window_state_update_from_replies(struct c2_state *state, + struct c2_window_state *window_state, + xcb_connection_t *c, xcb_window_t client_win, + xcb_window_t frame_win, bool refetch) { + HASH_ITER2(state->tracked_properties, p) { + if (!window_state->values[p->id].needs_update) { + continue; + } + xcb_window_t window = p->key.is_on_client ? client_win : frame_win; + xcb_get_property_reply_t *reply = + xcb_get_property_reply(c, state->cookies[p->id], NULL); + if (!reply) { + log_warn("Failed to get property %d for window %#010x, some " + "window rules might not work.", + p->id, window); + window_state->values[p->id].valid = false; + window_state->values[p->id].needs_update = false; + continue; + } + bool property_is_string = x_is_type_string(state->atoms, reply->type); + if (reply->bytes_after > 0 && (property_is_string || p->max_indices < 0)) { + if (!refetch) { + log_warn("Did property %d for window %#010x change while " + "we were fetching it? some window rules might " + "not work.", + p->id, window); + window_state->values[p->id].valid = false; + window_state->values[p->id].needs_update = false; + } else { + state->propert_lengths[p->id] += reply->bytes_after; + state->cookies[p->id] = xcb_get_property( + c, 0, window, p->key.property, XCB_GET_PROPERTY_TYPE_ANY, + 0, (state->propert_lengths[p->id] + 3) / 4); + } + } else { + c2_window_state_update_one_from_reply( + state, &window_state->values[p->id], p->key.property, reply, c); + } + free(reply); + } +} + +void c2_window_state_update(struct c2_state *state, struct c2_window_state *window_state, + xcb_connection_t *c, xcb_window_t client_win, + xcb_window_t frame_win) { + size_t property_count = HASH_COUNT(state->tracked_properties); + if (!state->cookies) { + state->cookies = ccalloc(property_count, xcb_get_property_cookie_t); + } + if (!state->propert_lengths) { + state->propert_lengths = ccalloc(property_count, uint32_t); + } + memset(state->cookies, 0, property_count * sizeof(xcb_get_property_cookie_t)); + + log_verbose("Updating c2 window state for window %#010x (frame %#010x)", + client_win, frame_win); + + // Because we don't know the length of all properties (i.e. if they are string + // properties, or for properties matched with `[*]`). We do this in 3 steps: + // 1. Send requests to all properties we need. Use `max_indices` to determine + // the length, or use 0 if it's unknown. + // 2. From the replies to (1), for properties we know the length of, we update + // the values. For those we don't, use the length information from the + // replies to send a new request with the correct length. + // 3. Update the rest of the properties. + + // Step 1 + HASH_ITER2(state->tracked_properties, p) { + if (!window_state->values[p->id].needs_update) { + continue; + } + uint32_t length = 0; + if (p->max_indices >= 0) { + // length is in 4 bytes units + length = (uint32_t)p->max_indices + 1; + } + + xcb_window_t window = p->key.is_on_client ? client_win : frame_win; + // xcb_get_property long_length is in units of 4-byte, + // so use `ceil(length / 4)`. same below. + state->cookies[p->id] = xcb_get_property( + c, 0, window, p->key.property, XCB_GET_PROPERTY_TYPE_ANY, 0, length); + state->propert_lengths[p->id] = length * 4; + } + + // Step 2 + c2_window_state_update_from_replies(state, window_state, c, client_win, frame_win, true); + // Step 3 + c2_window_state_update_from_replies(state, window_state, c, client_win, frame_win, + false); +} + +bool c2_state_is_property_tracked(struct c2_state *state, xcb_atom_t property) { + struct c2_tracked_property *p; + struct c2_tracked_property_key key = { + .property = property, + .is_on_client = true, + }; + HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), p); + if (p != NULL) { + return true; + } + key.is_on_client = false; + HASH_FIND(hh, state->tracked_properties, &key, sizeof(key), p); + return p != NULL; +} diff --git a/src/c2.h b/src/c2.h index d6b1d3726c..8589ebeede 100644 --- a/src/c2.h +++ b/src/c2.h @@ -12,15 +12,68 @@ #pragma once #include +#include +#include typedef struct _c2_lptr c2_lptr_t; typedef struct session session_t; +struct c2_state; +/// Per-window state used for c2 condition matching. +struct c2_window_state { + /// An array of window properties. Exact how many + /// properties there are is stored inside `struct c2_state`. + struct c2_property_value *values; +}; +struct atom; struct managed_win; +typedef void (*c2_userdata_free)(void *); c2_lptr_t *c2_parse(c2_lptr_t **pcondlst, const char *pattern, void *data); -c2_lptr_t *c2_free_lptr(c2_lptr_t *lp); +/// Parse a condition that has a prefix. The prefix is parsed by `parse_prefix`. If +/// `free_value` is not NULL, it will be called to free the value returned by +/// `parse_prefix` when error occurs. +c2_lptr_t * +c2_parse_with_prefix(c2_lptr_t **pcondlst, const char *pattern, + void *(*parse_prefix)(const char *input, const char **end, void *), + void (*free_value)(void *), void *user_data); -bool c2_match(session_t *ps, const struct managed_win *w, const c2_lptr_t *condlst, void **pdata); +c2_lptr_t *c2_free_lptr(c2_lptr_t *lp, c2_userdata_free f); -bool c2_list_postprocess(session_t *ps, c2_lptr_t *list); +/// Create a new c2_state object. This is used for maintaining the internal state +/// used for c2 condition matching. This state object holds a reference to the +/// pass atom object, thus the atom object should be kept alive as long as the +/// state object is alive. +struct c2_state *c2_state_new(struct atom *atoms); +void c2_state_free(struct c2_state *state); +/// Returns true if value of the property is used in any condition. +bool c2_state_is_property_tracked(struct c2_state *state, xcb_atom_t property); +void c2_window_state_init(const struct c2_state *state, struct c2_window_state *window_state); +void c2_window_state_destroy(const struct c2_state *state, struct c2_window_state *window_state); +void c2_window_state_mark_dirty(const struct c2_state *state, + struct c2_window_state *window_state, xcb_atom_t property, + bool is_on_client); +void c2_window_state_update(struct c2_state *state, struct c2_window_state *window_state, + xcb_connection_t *c, xcb_window_t client_win, + xcb_window_t frame_win); + +bool c2_match(struct c2_state *state, const struct managed_win *w, + const c2_lptr_t *condlst, void **pdata); + +bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, c2_lptr_t *list); +typedef bool (*c2_list_foreach_cb_t)(const c2_lptr_t *cond, void *data); +bool c2_list_foreach(const c2_lptr_t *list, c2_list_foreach_cb_t cb, void *data); +/// Return user data stored in a condition. +void *c2_list_get_data(const c2_lptr_t *condlist); +/// Convert a c2_lptr_t to string. The returned string is only valid until the +/// next call to this function, and should not be freed. +const char *c2_lptr_to_str(const c2_lptr_t *); + +/** + * Destroy a condition list. + */ +static inline void c2_list_free(c2_lptr_t **pcondlst, c2_userdata_free f) { + while ((*pcondlst = c2_free_lptr(*pcondlst, f))) { + } + *pcondlst = NULL; +} diff --git a/src/cache.c b/src/cache.c index 1ffb31c5df..1e356e5212 100644 --- a/src/cache.c +++ b/src/cache.c @@ -1,95 +1,45 @@ #include -#include "compiler.h" -#include "utils.h" #include "cache.h" -struct cache_entry { - char *key; - void *value; - UT_hash_handle hh; -}; - -struct cache { - cache_getter_t getter; - cache_free_t free; - void *user_data; - struct cache_entry *entries; -}; - -void cache_set(struct cache *c, const char *key, void *data) { - struct cache_entry *e = NULL; - HASH_FIND_STR(c->entries, key, e); - CHECK(!e); - - e = ccalloc(1, struct cache_entry); - e->key = strdup(key); - e->value = data; - HASH_ADD_STR(c->entries, key, e); +struct cache_handle *cache_get(struct cache *c, const char *key, size_t keylen) { + struct cache_handle *e; + HASH_FIND(hh, c->entries, key, keylen, e); + return e; } -void *cache_get(struct cache *c, const char *key, int *err) { - struct cache_entry *e; - HASH_FIND_STR(c->entries, key, e); - if (e) { - return e->value; +int cache_get_or_fetch(struct cache *c, const char *key, size_t keylen, + struct cache_handle **value, void *user_data, cache_getter_t getter) { + *value = cache_get(c, key, keylen); + if (*value) { + return 0; } - int tmperr; - if (!err) { - err = &tmperr; + int err = getter(c, key, keylen, value, user_data); + assert(err <= 0); + if (err < 0) { + return err; } + // Add a NUL terminator to make things easier + (*value)->key = ccalloc(keylen + 1, char); + memcpy((*value)->key, key, keylen); - *err = 0; - e = ccalloc(1, struct cache_entry); - e->key = strdup(key); - e->value = c->getter(c->user_data, key, err); - if (*err) { - free(e->key); - free(e); - return NULL; - } - - HASH_ADD_STR(c->entries, key, e); - return e->value; + HASH_ADD_KEYPTR(hh, c->entries, (*value)->key, keylen, *value); + return 1; } -static inline void _cache_invalidate(struct cache *c, struct cache_entry *e) { - if (c->free) { - c->free(c->user_data, e->value); - } +static inline void +cache_invalidate_impl(struct cache *c, struct cache_handle *e, cache_free_t free_fn) { free(e->key); HASH_DEL(c->entries, e); - free(e); -} - -void cache_invalidate(struct cache *c, const char *key) { - struct cache_entry *e; - HASH_FIND_STR(c->entries, key, e); - - if (e) { - _cache_invalidate(c, e); + if (free_fn) { + free_fn(c, e); } } -void cache_invalidate_all(struct cache *c) { - struct cache_entry *e, *tmpe; +void cache_invalidate_all(struct cache *c, cache_free_t free_fn) { + struct cache_handle *e, *tmpe; HASH_ITER(hh, c->entries, e, tmpe) { - _cache_invalidate(c, e); + cache_invalidate_impl(c, e, free_fn); } } - -void *cache_free(struct cache *c) { - void *ret = c->user_data; - cache_invalidate_all(c); - free(c); - return ret; -} - -struct cache *new_cache(void *ud, cache_getter_t getter, cache_free_t f) { - auto c = ccalloc(1, struct cache); - c->user_data = ud; - c->getter = getter; - c->free = f; - return c; -} diff --git a/src/cache.h b/src/cache.h index 3ca054f0fc..c0fa8df240 100644 --- a/src/cache.h +++ b/src/cache.h @@ -1,32 +1,43 @@ #pragma once -struct cache; +#include +#include "utils.h" -typedef void *(*cache_getter_t)(void *user_data, const char *key, int *err); -typedef void (*cache_free_t)(void *user_data, void *data); +#define cache_entry(ptr, type, member) container_of(ptr, type, member) -/// Create a cache with `getter`, and a free function `f` which is used to free the cache -/// value when they are invalidated. -/// -/// `user_data` will be passed to `getter` and `f` when they are called. -struct cache *new_cache(void *user_data, cache_getter_t getter, cache_free_t f); +struct cache; +struct cache_handle; -/// Fetch a value from the cache. If the value doesn't present in the cache yet, the -/// getter will be called, and the returned value will be stored into the cache. -void *cache_get(struct cache *, const char *key, int *err); +/// User-provided function to fetch a value for the cache, when it's not present. +/// Should return 0 if the value is fetched successfully, and a negative number if the +/// value cannot be fetched. Getter doesn't need to initialize fields of `struct +/// cache_handle`. +typedef int (*cache_getter_t)(struct cache *, const char *key, size_t keylen, + struct cache_handle **value, void *user_data); +typedef void (*cache_free_t)(struct cache *, struct cache_handle *value); -/// Invalidate a value in the cache. -void cache_invalidate(struct cache *, const char *key); +struct cache { + struct cache_handle *entries; +}; -/// Invalidate all values in the cache. -void cache_invalidate_all(struct cache *); +static const struct cache CACHE_INIT = {NULL}; -/// Invalidate all values in the cache and free it. Returns the user data passed to -/// `new_cache` -void *cache_free(struct cache *); +struct cache_handle { + char *key; + UT_hash_handle hh; +}; -/// Insert a key-value pair into the cache. Only used for internal testing. Takes -/// ownership of `data` -/// -/// If `key` already exists in the cache, this function will abort the program. -void cache_set(struct cache *c, const char *key, void *data); +/// Get a value from the cache. If the value doesn't present in the cache yet, the +/// getter will be called, and the returned value will be stored into the cache. +/// Returns 0 if the value is already present in the cache, 1 if the value is fetched +/// successfully, and a negative number if the value cannot be fetched. +int cache_get_or_fetch(struct cache *, const char *key, size_t keylen, + struct cache_handle **value, void *user_data, cache_getter_t getter); + +/// Get a value from the cache. If the value doesn't present in the cache, NULL will be +/// returned. +struct cache_handle *cache_get(struct cache *, const char *key, size_t keylen); + +/// Invalidate all values in the cache. After this call, `struct cache` holds no allocated +/// memory, and can be discarded. +void cache_invalidate_all(struct cache *, cache_free_t free_fn); diff --git a/src/common.h b/src/common.h index 193bc2a36a..a612f63f95 100644 --- a/src/common.h +++ b/src/common.h @@ -36,9 +36,9 @@ #include #include #include -#include #include #include +#include #include "uthash_extra.h" #ifdef CONFIG_OPENGL @@ -55,11 +55,12 @@ #include "backend/driver.h" #include "compiler.h" #include "config.h" +#include "list.h" #include "region.h" +#include "render.h" +#include "statistics.h" #include "types.h" #include "utils.h" -#include "list.h" -#include "render.h" #include "win_defs.h" #include "x.h" @@ -69,9 +70,6 @@ #define US_PER_SEC 1000000L #define MS_PER_SEC 1000 -/// @brief Maximum OpenGL FBConfig depth. -#define OPENGL_MAX_DEPTH 32 - /// @brief Maximum OpenGL buffer age. #define CGLX_MAX_BUFFER_AGE 5 @@ -83,11 +81,6 @@ struct glx_session; struct atom; struct conv; -typedef struct _ignore { - struct _ignore *next; - unsigned long sequence; -} ignore_t; - #ifdef CONFIG_OPENGL #ifdef DEBUG_GLX_DEBUG_CONTEXT typedef GLXContext (*f_glXCreateContextAttribsARB)(Display *dpy, GLXFBConfig config, @@ -130,6 +123,26 @@ typedef struct _latom { struct _latom *next; } latom_t; +struct shader_info { + char *key; + char *source; + void *backend_shader; + uint64_t attributes; + UT_hash_handle hh; +}; + +struct damage_ring { + /// Cache a xfixes region so we don't need to allocate it every time. + /// A workaround for yshui/picom#301 + xcb_xfixes_region_t x_region; + /// The region needs to painted on next paint. + int cursor; + /// The region damaged on the last paint. + region_t *damages; + /// Number of damage regions we track + int count; +}; + /// Structure containing all necessary data for a session. typedef struct session { // === Event handlers === @@ -139,13 +152,9 @@ typedef struct session { ev_timer unredir_timer; /// Timer for fading ev_timer fade_timer; - /// Timer for delayed drawing, right now only used by - /// swopti - ev_timer delayed_draw_timer; - /// Use an ev_idle callback for drawing - /// So we only start drawing when events are processed - ev_idle draw_idle; - /// Called everytime we have timeouts or new data on socket, + /// Use an ev_timer callback for drawing + ev_timer draw_timer; + /// Called every time we have timeouts or new data on socket, /// so we can be sure if xcb read from X socket at anytime during event /// handling, we will not left any event unhandled in the queue ev_prepare event_check; @@ -153,6 +162,8 @@ typedef struct session { ev_signal usr1_signal; /// Signal handler for SIGINT ev_signal int_signal; + + // === Backend related === /// backend data backend_t *backend_data; /// backend blur context @@ -163,31 +174,19 @@ typedef struct session { void *file_watch_handle; /// libev mainloop struct ev_loop *loop; + /// Shaders + struct shader_info *shaders; // === Display related === + /// X connection + struct x_connection c; /// Whether the X server is grabbed by us bool server_grabbed; - /// Display in use. - Display *dpy; - /// Previous handler of X errors - XErrorHandler previous_xerror_handler; - /// Default screen. - int scr; - /// XCB connection. - xcb_connection_t *c; - /// Default visual. - xcb_visualid_t vis; - /// Default depth. - int depth; - /// Root window. - xcb_window_t root; - /// Height of root window. - int root_height; /// Width of root window. int root_width; - // Damage of root window. - // Damage root_damage; - /// X Composite overlay window. Used if --paint-on-overlay. + /// Height of root window. + int root_height; + /// X Composite overlay window. xcb_window_t overlay; /// The target window for debug mode xcb_window_t debug_window; @@ -196,7 +195,10 @@ typedef struct session { /// Picture of the root window background. paint_t root_tile_paint; /// The backend data the root pixmap bound to - void *root_image; + image_handle root_image; + /// The root pixmap generation, incremented everytime + /// the root pixmap changes + uint64_t root_image_generation; /// A region of the size of the screen. region_t screen_reg; /// Picture of root window. Destination of painting in no-DBE painting @@ -214,51 +216,75 @@ typedef struct session { /// Custom GLX program used for painting window. // XXX should be in struct glx_session glx_prog_main_t glx_prog_win; - struct glx_fbconfig_info *argb_fbconfig; + struct glx_fbconfig_info argb_fbconfig; #endif /// Sync fence to sync draw operations xcb_sync_fence_t sync_fence; /// Whether we are rendering the first frame after screen is redirected bool first_frame; + /// Whether screen has been turned off + bool screen_is_off; + /// When last MSC event happened, in useconds. + uint64_t last_msc_instant; + /// The last MSC number + uint64_t last_msc; + /// The delay between when the last frame was scheduled to be rendered, and when + /// the render actually started. + uint64_t last_schedule_delay; + /// When do we want our next frame to start rendering. + uint64_t next_render; + /// Whether we can perform frame pacing. + bool frame_pacing; + /// Vblank event scheduler + struct vblank_scheduler *vblank_scheduler; + + /// Render statistics + struct render_statistics render_stats; // === Operation related === /// Flags related to the root window uint64_t root_flags; /// Program options. options_t o; + /// State object for c2. + struct c2_state *c2_state; /// Whether we have hit unredirection timeout. bool tmout_unredir_hit; - /// Whether we need to redraw the screen - bool redraw_needed; - - /// Cache a xfixes region so we don't need to allocate it everytime. - /// A workaround for yshui/picom#301 - xcb_xfixes_region_t damaged_region; - /// The region needs to painted on next paint. - region_t *damage; - /// The region damaged on the last paint. - region_t *damage_ring; - /// Number of damage regions we track - int ndamage; + /// If the backend is busy. This means two things: + /// Either the backend is currently rendering a frame, or a frame has been + /// rendered but has yet to be presented. In either case, we should not start + /// another render right now. As if we start issuing rendering commands now, we + /// will have to wait for either the current render to finish, or the current + /// back buffer to become available again. In either case, we will be wasting + /// time. + bool backend_busy; + /// Whether a render is queued. This generally means there are pending updates + /// to the screen that's neither included in the current render, nor on the + /// screen. + bool render_queued; + // TODO(yshui) remove this after we remove the legacy backends + /// For tracking damage regions + struct damage_ring damage_ring; + // TODO(yshui) move render related fields into separate struct + /// Render planner + struct layout_manager *layout_manager; + /// Render command builder + struct command_builder *command_builder; + struct renderer *renderer; /// Whether all windows are currently redirected. bool redirected; /// Pre-generated alpha pictures. xcb_render_picture_t *alpha_picts; /// Time of last fading. In milliseconds. - long fade_time; - /// Head pointer of the error ignore linked list. - ignore_t *ignore_head; - /// Pointer to the next member of tail element of the error - /// ignore linked list. - ignore_t **ignore_tail; + long long fade_time; // Cached blur convolution kernels. struct x_convolution_kernel **blur_kerns_cache; /// If we should quit - bool quit:1; - // TODO(yshui) use separate flags for dfferent kinds of updates so we don't + bool quit : 1; + // TODO(yshui) use separate flags for different kinds of updates so we don't // waste our time. /// Whether there are pending updates, like window creation, etc. - bool pending_updates:1; + bool pending_updates : 1; // === Expose event related === /// Pointer to an array of XRectangle-s of exposed region. @@ -269,20 +295,7 @@ typedef struct session { /// Index of the next free slot in expose_rects. int n_expose; - // === Window related === - /// A hash table of all windows. - struct win *windows; - /// Windows in their stacking order - struct list_node window_stack; - /// Pointer to win of current active window. Used by - /// EWMH _NET_ACTIVE_WINDOW focus detection. In theory, - /// it's more reliable to store the window ID directly here, just in - /// case the WM does something extraordinary, but caching the pointer - /// means another layer of complexity. - struct managed_win *active_win; - /// Window ID of leader window of currently active window. Used for - /// subsidiary window detection. - xcb_window_t active_leader; + struct wm *wm; // === Shadow/dimming related === /// 1x1 black Picture. @@ -291,17 +304,11 @@ typedef struct session { xcb_render_picture_t cshadow_picture; /// 1x1 white Picture. xcb_render_picture_t white_picture; - /// Gaussian map of shadow. - struct conv *gaussian_map; + /// Backend shadow context. + struct backend_shadow_context *shadow_context; // for shadow precomputation - /// A region in which shadow is not painted on. - region_t shadow_exclude_reg; // === Software-optimization-related === - /// Currently used refresh rate. - int refresh_rate; - /// Interval between refresh in nanoseconds. - long refresh_intv; /// Nanosecond offset of the first painting. long paint_tm_offset; @@ -330,6 +337,8 @@ typedef struct session { int composite_error; /// Major opcode for X Composite extension. int composite_opcode; + /// Whether X DPMS extension exists + bool dpms_exists; /// Whether X Shape extension exists. bool shape_exists; /// Event base number for X Shape extension. @@ -350,12 +359,8 @@ typedef struct session { int glx_event; /// Error base number for X GLX extension. int glx_error; - /// Whether X Xinerama extension exists. - bool xinerama_exists; - /// Xinerama screen regions. - region_t *xinerama_scr_regs; - /// Number of Xinerama screens. - int xinerama_nscrs; + /// Information about monitors. + struct x_monitors monitors; /// Whether X Sync extension exists. bool xsync_exists; /// Event base number for X Sync extension. @@ -367,14 +372,10 @@ typedef struct session { // === Atoms === struct atom *atoms; - /// Array of atoms of all possible window types. - xcb_atom_t atoms_wintypes[NUM_WINTYPES]; - /// Linked list of additional atoms to track. - latom_t *track_atom_lst; #ifdef CONFIG_DBUS // === DBus related === - void *dbus_data; + struct cdbus_data *dbus_data; #endif int (*vsync_wait)(session_t *); @@ -382,8 +383,11 @@ typedef struct session { /// Enumeration for window event hints. typedef enum { WIN_EVMODE_UNKNOWN, WIN_EVMODE_FRAME, WIN_EVMODE_CLIENT } win_evmode_t; - -extern const char *const WINTYPES[NUM_WINTYPES]; +struct wintype_info { + const char *name; + const char *atom; +}; +extern const struct wintype_info WINTYPES[NUM_WINTYPES]; extern session_t *ps_g; void ev_xcb_error(session_t *ps, xcb_generic_error_t *err); @@ -453,7 +457,7 @@ static inline struct timespec get_time_timespec(void) { * Return the painting target window. */ static inline xcb_window_t get_tgt_window(session_t *ps) { - return ps->overlay != XCB_NONE ? ps->overlay : ps->root; + return ps->overlay != XCB_NONE ? ps->overlay : ps->c.screen_info->root; } /** @@ -463,27 +467,6 @@ static inline bool bkend_use_glx(session_t *ps) { return BKEND_GLX == ps->o.backend || BKEND_XR_GLX_HYBRID == ps->o.backend; } -static void set_ignore(session_t *ps, unsigned long sequence) { - if (ps->o.show_all_xerrors) - return; - - auto i = cmalloc(ignore_t); - if (!i) - return; - - i->sequence = sequence; - i->next = 0; - *ps->ignore_tail = i; - ps->ignore_tail = &i->next; -} - -/** - * Ignore X errors caused by given X request. - */ -static inline void set_ignore_cookie(session_t *ps, xcb_void_cookie_t cookie) { - set_ignore(ps, cookie.sequence); -} - /** * Determine if a window has a specific property. * @@ -492,9 +475,9 @@ static inline void set_ignore_cookie(session_t *ps, xcb_void_cookie_t cookie) { * @param atom atom of property to check * @return true if it has the attribute, false otherwise */ -static inline bool wid_has_prop(const session_t *ps, xcb_window_t w, xcb_atom_t atom) { +static inline bool wid_has_prop(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom) { auto r = xcb_get_property_reply( - ps->c, xcb_get_property(ps->c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 0), NULL); + c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 0), NULL); if (!r) { return false; } @@ -510,17 +493,6 @@ static inline bool wid_has_prop(const session_t *ps, xcb_window_t w, xcb_atom_t void force_repaint(session_t *ps); -/** @name DBus handling - */ -///@{ -#ifdef CONFIG_DBUS -/** @name DBus hooks - */ -///@{ -void opts_set_no_fading_openclose(session_t *ps, bool newval); -//!@} -#endif - /** * Set a bool array of all wintypes to true. */ @@ -531,3 +503,24 @@ static inline void wintype_arr_enable(bool arr[]) { arr[i] = true; } } + +static inline void damage_ring_advance(struct damage_ring *ring) { + ring->cursor--; + if (ring->cursor < 0) { + ring->cursor += ring->count; + } + pixman_region32_clear(&ring->damages[ring->cursor]); +} + +static inline void damage_ring_collect(struct damage_ring *ring, region_t *all_region, + region_t *region, int buffer_age) { + if (buffer_age == -1 || buffer_age > ring->count) { + pixman_region32_copy(region, all_region); + } else { + for (int i = 0; i < buffer_age; i++) { + auto curr = (ring->cursor + i) % ring->count; + pixman_region32_union(region, region, &ring->damages[curr]); + } + pixman_region32_intersect(region, region, all_region); + } +} diff --git a/src/compiler.h b/src/compiler.h index f146bd21bc..54ed1ea6bf 100644 --- a/src/compiler.h +++ b/src/compiler.h @@ -7,7 +7,11 @@ #endif // clang-format off -#define auto __auto_type +#if __STDC_VERSION__ <= 201710L +// Polyfill for C23's `auto` and `typeof` +# define auto __auto_type +# define typeof __typeof__ +#endif #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) #define likely_if(x) if (likely(x)) @@ -50,7 +54,7 @@ #else # define attr_warn_unused_result #endif -// An alias for conveninence +// An alias for convenience #define must_use attr_warn_unused_result #if __has_attribute(nonnull) @@ -85,6 +89,12 @@ # define fallthrough() #endif +#if __has_attribute(cleanup) +# define cleanup(func) __attribute__((cleanup(func))) +#else +# error "Compiler is missing cleanup attribute" +#endif + #if __STDC_VERSION__ >= 201112L # define attr_noret _Noreturn #else @@ -95,10 +105,12 @@ # endif #endif -#if defined(__GNUC__) || defined(__clang__) -# define unreachable __builtin_unreachable() -#else -# define unreachable do {} while(0) +#ifndef unreachable +# if defined(__GNUC__) || defined(__clang__) +# define unreachable() assert(false); __builtin_unreachable() +# else +# define unreachable() assert(false); do {} while(0) +# endif #endif #ifndef __has_include diff --git a/src/config.c b/src/config.c index b1abbaf96c..59b1398e1a 100644 --- a/src/config.c +++ b/src/config.c @@ -3,13 +3,23 @@ // Copyright (c) 2013 Richard Grenville #include +#include +#include #include #include #include +#include +#include +#include #include #include +#include +#include +#include #include // for xcb_render_fixed_t, XXX +#include + #include "c2.h" #include "common.h" #include "compiler.h" @@ -23,6 +33,97 @@ #include "config.h" +struct debug_options global_debug_options; + +const char *xdg_config_home(void) { + char *xdgh = getenv("XDG_CONFIG_HOME"); + char *home = getenv("HOME"); + const char *default_dir = "/.config"; + + if (!xdgh) { + if (!home) { + return NULL; + } + + xdgh = mstrjoin(home, default_dir); + } else { + xdgh = strdup(xdgh); + } + + return xdgh; +} + +char **xdg_config_dirs(void) { + char *xdgd = getenv("XDG_CONFIG_DIRS"); + size_t count = 0; + + if (!xdgd) { + xdgd = "/etc/xdg"; + } + + for (int i = 0; xdgd[i]; i++) { + if (xdgd[i] == ':') { + count++; + } + } + + // Store the string and the result pointers together so they can be + // freed together + char **dir_list = cvalloc(sizeof(char *) * (count + 2) + strlen(xdgd) + 1); + auto dirs = strcpy((char *)dir_list + sizeof(char *) * (count + 2), xdgd); + auto path = dirs; + + for (size_t i = 0; i < count; i++) { + dir_list[i] = path; + path = strchr(path, ':'); + *path = '\0'; + path++; + } + dir_list[count] = path; + + size_t fill = 0; + for (size_t i = 0; i <= count; i++) { + if (dir_list[i][0] == '/') { + dir_list[fill] = dir_list[i]; + fill++; + } + } + + dir_list[fill] = NULL; + + return dir_list; +} + +TEST_CASE(xdg_config_dirs) { + auto old_var = getenv("XDG_CONFIG_DIRS"); + if (old_var) { + old_var = strdup(old_var); + } + unsetenv("XDG_CONFIG_DIRS"); + + auto result = xdg_config_dirs(); + TEST_STREQUAL(result[0], "/etc/xdg"); + TEST_EQUAL(result[1], NULL); + free(result); + + setenv("XDG_CONFIG_DIRS", ".:.:/etc/xdg:.:/:", 1); + result = xdg_config_dirs(); + TEST_STREQUAL(result[0], "/etc/xdg"); + TEST_STREQUAL(result[1], "/"); + TEST_EQUAL(result[2], NULL); + free(result); + + setenv("XDG_CONFIG_DIRS", ":", 1); + result = xdg_config_dirs(); + TEST_EQUAL(result[0], NULL); + free(result); + + if (old_var) { + setenv("XDG_CONFIG_DIRS", old_var, 1); + free(old_var); + } +} + /** * Parse a long number. */ @@ -33,8 +134,9 @@ bool parse_long(const char *s, long *dest) { log_error("Invalid number: %s", s); return false; } - while (isspace((unsigned char)*endptr)) + while (isspace((unsigned char)*endptr)) { ++endptr; + } if (*endptr) { log_error("Trailing characters: %s", s); return false; @@ -81,21 +183,20 @@ const char *parse_readnum(const char *src, double *dest) { return pc; } -enum blur_method parse_blur_method(const char *src) { - if (strcmp(src, "kernel") == 0) { - return BLUR_METHOD_KERNEL; - } else if (strcmp(src, "box") == 0) { +int parse_blur_method(const char *src) { + if (strcmp(src, "box") == 0) { return BLUR_METHOD_BOX; - } else if (strcmp(src, "gaussian") == 0) { - return BLUR_METHOD_GAUSSIAN; - } else if (strcmp(src, "dual_kawase") == 0) { - return BLUR_METHOD_DUAL_KAWASE; - } else if (strcmp(src, "kawase") == 0) { - log_warn("Blur method 'kawase' has been renamed to 'dual_kawase'. " - "Interpreted as 'dual_kawase', but this will stop working " - "soon."); + } + if (strcmp(src, "dual_kawase") == 0) { return BLUR_METHOD_DUAL_KAWASE; - } else if (strcmp(src, "none") == 0) { + } + if (strcmp(src, "gaussian") == 0) { + return BLUR_METHOD_GAUSSIAN; + } + if (strcmp(src, "kernel") == 0) { + return BLUR_METHOD_KERNEL; + } + if (strcmp(src, "none") == 0) { return BLUR_METHOD_NONE; } return BLUR_METHOD_INVALID; @@ -108,20 +209,21 @@ enum blur_method parse_blur_method(const char *src) { * @param[out] endptr return where the end of kernel is in the string * @param[out] hasneg whether the kernel has negative values */ -conv *parse_blur_kern(const char *src, const char **endptr, bool *hasneg) { +static conv *parse_blur_kern(const char *src, const char **endptr) { int width = 0, height = 0; - *hasneg = false; const char *pc = NULL; // Get matrix width and height double val = 0.0; - if (src == (pc = parse_readnum(src, &val))) + if (src == (pc = parse_readnum(src, &val))) { goto err1; + } src = pc; width = (int)val; - if (src == (pc = parse_readnum(src, &val))) + if (src == (pc = parse_readnum(src, &val))) { goto err1; + } src = pc; height = (int)val; @@ -134,9 +236,10 @@ conv *parse_blur_kern(const char *src, const char **endptr, bool *hasneg) { log_error("Blur kernel width/height must be odd."); goto err1; } - if (width > 16 || height > 16) + if (width > 16 || height > 16) { log_warn("Blur kernel width/height too large, may slow down" "rendering, and/or consume lots of memory"); + } // Allocate memory conv *matrix = cvalloc(sizeof(conv) + (size_t)(width * height) * sizeof(double)); @@ -153,9 +256,6 @@ conv *parse_blur_kern(const char *src, const char **endptr, bool *hasneg) { goto err2; } src = pc; - if (val < 0) { - *hasneg = true; - } matrix->data[i] = val; } @@ -203,7 +303,7 @@ conv *parse_blur_kern(const char *src, const char **endptr, bool *hasneg) { * @param[out] hasneg whether any of the kernels have negative values * @return the kernels */ -struct conv **parse_blur_kern_lst(const char *src, bool *hasneg, int *count) { +struct conv **parse_blur_kern_lst(const char *src, int *count) { // TODO(yshui) just return a predefined kernels, not parse predefined strings... static const struct { const char *name; @@ -259,11 +359,11 @@ struct conv **parse_blur_kern_lst(const char *src, bool *hasneg, int *count) { }; *count = 0; - *hasneg = false; for (unsigned int i = 0; i < sizeof(CONV_KERN_PREDEF) / sizeof(CONV_KERN_PREDEF[0]); ++i) { - if (!strcmp(CONV_KERN_PREDEF[i].name, src)) - return parse_blur_kern_lst(CONV_KERN_PREDEF[i].kern_str, hasneg, count); + if (!strcmp(CONV_KERN_PREDEF[i].name, src)) { + return parse_blur_kern_lst(CONV_KERN_PREDEF[i].kern_str, count); + } } int nkernels = 1; @@ -281,9 +381,8 @@ struct conv **parse_blur_kern_lst(const char *src, bool *hasneg, int *count) { // Continue parsing until the end of source string i = 0; while (pc && *pc) { - bool tmp_hasneg; assert(i < nkernels); - ret[i] = parse_blur_kern(pc, &pc, &tmp_hasneg); + ret[i] = parse_blur_kern(pc, &pc); if (!ret[i]) { for (int j = 0; j < i; j++) { free(ret[j]); @@ -292,7 +391,6 @@ struct conv **parse_blur_kern_lst(const char *src, bool *hasneg, int *count) { return NULL; } i++; - *hasneg |= tmp_hasneg; } if (i > 1) { @@ -307,211 +405,237 @@ struct conv **parse_blur_kern_lst(const char *src, bool *hasneg, int *count) { return ret; } -/** - * Parse a X geometry. - * - * ps->root_width and ps->root_height must be valid - */ -bool parse_geometry(session_t *ps, const char *src, region_t *dest) { - pixman_region32_clear(dest); +void *parse_numeric_prefix(const char *src, const char **end, void *user_data) { + int *minmax = user_data; + *end = NULL; if (!src) { - return true; - } - if (!ps->root_width || !ps->root_height) { - return true; + return NULL; } - long x = 0, y = 0; - long width = ps->root_width, height = ps->root_height; - long val = 0L; + // Find numeric value char *endptr = NULL; - - src = skip_space(src); - if (!*src) { - goto parse_geometry_end; + long val = strtol(src, &endptr, 0); + if (!endptr || endptr == src) { + log_error("No number specified: %s", src); + return NULL; } - // Parse width - // Must be base 10, because "0x0..." may appear - if (*src != '+' && *src != '-') { - val = strtol(src, &endptr, 10); - assert(endptr); - if (src != endptr) { - if (val < 0) { - log_error("Invalid width: %s", src); - return false; - } - width = val; - src = endptr; - } - src = skip_space(src); - } - - // Parse height - if (*src == 'x') { - ++src; - val = strtol(src, &endptr, 10); - assert(endptr); - if (src != endptr) { - if (val < 0) { - log_error("Invalid height: %s", src); - return false; - } - height = val; - src = endptr; - } - src = skip_space(src); + if (val < minmax[0] || val > minmax[1]) { + log_error("Number not in range (%d <= n <= %d): %s", minmax[0], minmax[1], src); + return NULL; } - // Parse x - if (*src == '+' || *src == '-') { - val = strtol(src, &endptr, 10); - if (endptr && src != endptr) { - x = val; - if (*src == '-') { - x += ps->root_width - width; - } - src = endptr; - } - src = skip_space(src); + // Skip over spaces + while (*endptr && isspace((unsigned char)*endptr)) { + ++endptr; } - - // Parse y - if (*src == '+' || *src == '-') { - val = strtol(src, &endptr, 10); - if (endptr && src != endptr) { - y = val; - if (*src == '-') { - y += ps->root_height - height; - } - src = endptr; - } - src = skip_space(src); + if (':' != *endptr) { + log_error("Number separator (':') not found: %s", src); + return NULL; } + ++endptr; - if (*src) { - log_error("Trailing characters: %s", src); - return false; - } + *end = endptr; -parse_geometry_end: - if (x < INT_MIN || x > INT_MAX || y < INT_MIN || y > INT_MAX) { - log_error("Geometry coordinates exceeded limits: %s", src); - return false; - } - if (width > UINT_MAX || height > UINT_MAX) { - // less than 0 is checked for earlier - log_error("Geometry size exceeded limits: %s", src); - return false; + return (void *)val; +} + +/// Search for auxiliary file under a base directory +static char *locate_auxiliary_file_at(const char *base, const char *scope, const char *file) { + scoped_charp path = mstrjoin(base, scope); + mstrextend(&path, "/"); + mstrextend(&path, file); + if (access(path, O_RDONLY) == 0) { + // Canonicalize path to avoid duplicates + char *abspath = realpath(path, NULL); + return abspath; } - pixman_region32_union_rect(dest, dest, (int)x, (int)y, (uint)width, (uint)height); - return true; + return NULL; } /** - * Parse a list of opacity rules. + * Get a path of an auxiliary file to read, could be a shader file, or any supplementary + * file. + * + * Follows the XDG specification to search for the shader file in configuration locations. + * + * The search order is: + * 1) If an absolute path is given, use it directly. + * 2) Search for the file directly under `include_dir`. + * 3) Search for the file in the XDG configuration directories, under path + * /picom// */ -bool parse_rule_opacity(c2_lptr_t **res, const char *src) { - // Find opacity value - char *endptr = NULL; - long val = strtol(src, &endptr, 0); - if (!endptr || endptr == src) { - log_error("No opacity specified: %s", src); - return false; - } - if (val > 100 || val < 0) { - log_error("Opacity %ld invalid: %s", val, src); - return false; +char *locate_auxiliary_file(const char *scope, const char *path, const char *include_dir) { + if (!path || strlen(path) == 0) { + return NULL; } - // Skip over spaces - while (*endptr && isspace((unsigned char)*endptr)) - ++endptr; - if (':' != *endptr) { - log_error("Opacity terminator not found: %s", src); - return false; + // Filename is absolute path, so try to load from there + if (path[0] == '/') { + if (access(path, O_RDONLY) == 0) { + return realpath(path, NULL); + } } - ++endptr; - // Parse pattern - // I hope 1-100 is acceptable for (void *) - return c2_parse(res, endptr, (void *)val); -} + // First try to load file from the include directory (i.e. relative to the + // config file) + if (include_dir && strlen(include_dir)) { + char *ret = locate_auxiliary_file_at(include_dir, "", path); + if (ret) { + return ret; + } + } -/** - * Add a pattern to a condition linked list. - */ -bool condlst_add(c2_lptr_t **pcondlst, const char *pattern) { - if (!pattern) - return false; + // Fall back to searching in user config directory + scoped_charp picom_scope = mstrjoin("/picom/", scope); + scoped_charp config_home = (char *)xdg_config_home(); + if (config_home) { + char *ret = locate_auxiliary_file_at(config_home, picom_scope, path); + if (ret) { + return ret; + } + } - if (!c2_parse(pcondlst, pattern, NULL)) - exit(1); + // Fall back to searching in system config directory + auto config_dirs = xdg_config_dirs(); + for (int i = 0; config_dirs[i]; i++) { + char *ret = locate_auxiliary_file_at(config_dirs[i], picom_scope, path); + if (ret) { + free(config_dirs); + return ret; + } + } + free(config_dirs); - return true; + return NULL; } -void set_default_winopts(options_t *opt, win_option_mask_t *mask, bool shadow_enable, - bool fading_enable, bool blur_enable) { - // Apply default wintype options. - if (!mask[WINTYPE_DESKTOP].shadow) { - // Desktop windows are always drawn without shadow by default. - mask[WINTYPE_DESKTOP].shadow = true; - opt->wintype_option[WINTYPE_DESKTOP].shadow = false; - } - - // Focused/unfocused state only apply to a few window types, all other windows - // are always considered focused. - const wintype_t nofocus_type[] = {WINTYPE_UNKNOWN, WINTYPE_NORMAL, WINTYPE_UTILITY}; - for (unsigned long i = 0; i < ARR_SIZE(nofocus_type); i++) { - if (!mask[nofocus_type[i]].focus) { - mask[nofocus_type[i]].focus = true; - opt->wintype_option[nofocus_type[i]].focus = false; - } - } - for (unsigned long i = 0; i < NUM_WINTYPES; i++) { - if (!mask[i].shadow) { - mask[i].shadow = true; - opt->wintype_option[i].shadow = shadow_enable; - } - if (!mask[i].fade) { - mask[i].fade = true; - opt->wintype_option[i].fade = fading_enable; - } - if (!mask[i].focus) { - mask[i].focus = true; - opt->wintype_option[i].focus = true; - } - if (!mask[i].blur_background) { - mask[i].blur_background = true; - opt->wintype_option[i].blur_background = blur_enable; - } - if (!mask[i].full_shadow) { - mask[i].full_shadow = true; - opt->wintype_option[i].full_shadow = false; +struct debug_options_entry { + const char *name; + const char **choices; + size_t offset; +}; + +// clang-format off +const char *vblank_scheduler_str[] = { + [VBLANK_SCHEDULER_PRESENT] = "present", + [VBLANK_SCHEDULER_SGI_VIDEO_SYNC] = "sgi_video_sync", + [LAST_VBLANK_SCHEDULER] = NULL +}; +static const struct debug_options_entry debug_options_entries[] = { + {"always_rebind_pixmap", NULL , offsetof(struct debug_options, always_rebind_pixmap)}, + {"smart_frame_pacing" , NULL , offsetof(struct debug_options, smart_frame_pacing)}, + {"force_vblank_sched" , vblank_scheduler_str, offsetof(struct debug_options, force_vblank_scheduler)}, +}; +// clang-format on + +void parse_debug_option_single(char *setting, struct debug_options *debug_options) { + char *equal = strchr(setting, '='); + size_t name_len = equal ? (size_t)(equal - setting) : strlen(setting); + for (size_t i = 0; i < ARR_SIZE(debug_options_entries); i++) { + if (strncmp(setting, debug_options_entries[i].name, name_len) != 0) { + continue; } - if (!mask[i].redir_ignore) { - mask[i].redir_ignore = true; - opt->wintype_option[i].redir_ignore = false; + if (debug_options_entries[i].name[name_len] != '\0') { + continue; } - if (!mask[i].opacity) { - mask[i].opacity = true; - // Opacity is not set to a concrete number here because the - // opacity logic is complicated, and needs an "unset" state - opt->wintype_option[i].opacity = NAN; + auto value = (int *)((void *)debug_options + debug_options_entries[i].offset); + if (equal) { + const char *const arg = equal + 1; + if (debug_options_entries[i].choices != NULL) { + for (size_t j = 0; debug_options_entries[i].choices[j]; j++) { + if (strcmp(arg, debug_options_entries[i].choices[j]) == + 0) { + *value = (int)j; + return; + } + } + } + if (!parse_int(arg, value)) { + log_error("Invalid value for debug option %s: %s, it " + "will be ignored.", + debug_options_entries[i].name, arg); + } + } else if (debug_options_entries[i].choices == NULL) { + *value = 1; + } else { + log_error( + "Missing value for debug option %s, it will be ignored.", setting); } - if (!mask[i].clip_shadow_above) { - mask[i].clip_shadow_above = true; - opt->wintype_option[i].clip_shadow_above = false; + return; + } + log_error("Invalid debug option: %s", setting); +} + +/// Parse debug options from environment variable `PICOM_DEBUG`. +void parse_debug_options(struct debug_options *debug_options) { + const char *debug = getenv("PICOM_DEBUG"); + const struct debug_options default_debug_options = { + .force_vblank_scheduler = LAST_VBLANK_SCHEDULER, + }; + + *debug_options = default_debug_options; + if (!debug) { + return; + } + + scoped_charp debug_copy = strdup(debug); + char *tmp, *needle = strtok_r(debug_copy, ";", &tmp); + while (needle) { + parse_debug_option_single(needle, debug_options); + needle = strtok_r(NULL, ";", &tmp); + } +} + +void *parse_window_shader_prefix(const char *src, const char **end, void *user_data) { + const char *include_dir = user_data; + *end = NULL; + if (!src) { + return NULL; + } + + // Find custom shader terminator + const char *endptr = strchr(src, ':'); + if (!endptr) { + log_error("Custom shader terminator not found: %s", src); + return NULL; + } + + // Parse and create custom shader + scoped_charp untrimed_shader_source = strdup(src); + if (!untrimed_shader_source) { + return NULL; + } + auto source_end = strchr(untrimed_shader_source, ':'); + *source_end = '\0'; + + size_t length; + char *tmp = (char *)trim_both(untrimed_shader_source, &length); + tmp[length] = '\0'; + char *shader_source = NULL; + + if (strcasecmp(tmp, "default") != 0) { + shader_source = locate_auxiliary_file("shaders", tmp, include_dir); + if (!shader_source) { + log_error("Custom shader file \"%s\" not found for rule: %s", tmp, src); + free(shader_source); + return NULL; } } + + *end = endptr + 1; + return shader_source; +} +void *parse_window_shader_prefix_with_cwd(const char *src, const char **end, void *) { + scoped_charp cwd = getcwd(NULL, 0); + return parse_window_shader_prefix(src, end, cwd); } -char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, - bool *fading_enable, bool *hasneg, win_option_mask_t *winopt_mask) { +char *parse_config(options_t *opt, const char *config_file) { // clang-format off *opt = (struct options){ .backend = BKEND_XRENDER, + .legacy_backends = false, .glx_no_stencil = false, .mark_wmwin_focused = false, .mark_ovredir_focused = false, @@ -526,10 +650,10 @@ char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, .benchmark = 0, .benchmark_wid = XCB_NONE, .logpath = NULL, + .log_level = LOG_LEVEL_WARN, - .refresh_rate = 0, - .sw_opti = false, .use_damage = true, + .frame_pacing = true, .shadow_red = 0.0, .shadow_green = 0.0, @@ -540,7 +664,7 @@ char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, .shadow_opacity = .75, .shadow_blacklist = NULL, .shadow_ignore_shaped = false, - .xinerama_shadow_crop = false, + .crop_shadow_to_monitor = false, .shadow_clip_list = NULL, .corner_radius = 0, @@ -567,6 +691,8 @@ char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, .blur_background_blacklist = NULL, .blur_kerns = NULL, .blur_kernel_count = 0, + .window_shader_fg = NULL, + .window_shader_fg_rules = NULL, .inactive_dim = 0.0, .inactive_dim_fixed = false, .invert_color_list = NULL, @@ -586,15 +712,6 @@ char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, // clang-format on char *ret = NULL; -#ifdef CONFIG_LIBCONFIG - ret = parse_config_libconfig(opt, config_file, shadow_enable, fading_enable, - hasneg, winopt_mask); -#else - (void)config_file; - (void)shadow_enable; - (void)fading_enable; - (void)hasneg; - (void)winopt_mask; -#endif + ret = parse_config_libconfig(opt, config_file); return ret; } diff --git a/src/config.h b/src/config.h index 052e38c232..68aef3b3cd 100644 --- a/src/config.h +++ b/src/config.h @@ -15,9 +15,9 @@ #include #include -#ifdef CONFIG_LIBCONFIG +#include "uthash_extra.h" + #include -#endif #include "compiler.h" #include "kernel.h" @@ -34,6 +34,7 @@ enum backend { BKEND_GLX, BKEND_XR_GLX_HYBRID, BKEND_DUMMY, + BKEND_EGL, NUM_BKEND, }; @@ -70,6 +71,33 @@ enum blur_method { typedef struct _c2_lptr c2_lptr_t; +enum vblank_scheduler_type { + /// X Present extension based vblank events + VBLANK_SCHEDULER_PRESENT, + /// GLX_SGI_video_sync based vblank events + VBLANK_SCHEDULER_SGI_VIDEO_SYNC, + /// An invalid scheduler, served as a scheduler count, and + /// as a sentinel value. + LAST_VBLANK_SCHEDULER, +}; + +extern const char *vblank_scheduler_str[]; + +/// Internal, private options for debugging and development use. +struct debug_options { + /// Try to reduce frame latency by using vblank interval and render time + /// estimates. Right now it's not working well across drivers. + int smart_frame_pacing; + /// Override the vblank scheduler chosen by the compositor. + int force_vblank_scheduler; + /// Release then immediately rebind every window pixmap each frame. + /// Useful when being traced under apitrace, to force it to pick up + /// updated contents. WARNING, extremely slow. + int always_rebind_pixmap; +}; + +extern struct debug_options global_debug_options; + /// Structure representing all options. typedef struct options { // === Debugging === @@ -78,12 +106,14 @@ typedef struct options { /// Render to a separate window instead of taking over the screen bool debug_mode; // === General === - /// Use the experimental new backends? - bool experimental_backends; + /// Use the legacy backends? + bool legacy_backends; /// Path to write PID to. char *write_pid_path; /// The backend in use. - enum backend backend; + int backend; + /// Log level. + int log_level; /// Whether to sync X drawing with X Sync fence to avoid certain delay /// issues with GLX backend. bool xrender_sync_fence; @@ -107,7 +137,7 @@ typedef struct options { /// when determining if a window could be unredirected. c2_lptr_t *unredir_if_possible_blacklist; /// Delay before unredirecting screen, in milliseconds. - long unredir_if_possible_delay; + int unredir_if_possible_delay; /// Forced redirection setting through D-Bus. switch_t redirected_force; /// Whether to stop painting. Controlled through D-Bus. @@ -128,12 +158,11 @@ typedef struct options { bool no_x_selection; /// Window type option override. win_option_t wintype_option[NUM_WINTYPES]; + struct win_option_mask wintype_option_mask[NUM_WINTYPES]; + /// Whether to set realtime scheduling policy for the compositor process. + bool use_realtime_scheduling; // === VSync & software optimization === - /// User-specified refresh rate. - int refresh_rate; - /// Whether to enable refresh-rate-based software optimization. - bool sw_opti; /// VSync method to use; bool vsync; /// Whether to use glFinish() instead of glFlush() for (possibly) better @@ -141,6 +170,8 @@ typedef struct options { bool vsync_use_glfinish; /// Whether use damage information to help limit the area to paint bool use_damage; + /// Disable frame pacing + bool frame_pacing; // === Shadow === /// Red, green and blue tone of the shadow. @@ -148,16 +179,15 @@ typedef struct options { int shadow_radius; int shadow_offset_x, shadow_offset_y; double shadow_opacity; - /// argument string to shadow-exclude-reg option - char *shadow_exclude_reg_str; /// Shadow blacklist. A linked list of conditions. c2_lptr_t *shadow_blacklist; /// Whether bounding-shaped window should be ignored. bool shadow_ignore_shaped; - /// Whether to crop shadow to the very Xinerama screen. - bool xinerama_shadow_crop; + /// Whether to crop shadow to the very X RandR monitor. + bool crop_shadow_to_monitor; /// Don't draw shadow over these windows. A linked list of conditions. c2_lptr_t *shadow_clip_list; + bool shadow_enable; // === Fading === /// How much to fade in in a single fading step. @@ -172,10 +202,11 @@ typedef struct options { bool no_fading_destroyed_argb; /// Fading blacklist. A linked list of conditions. c2_lptr_t *fade_blacklist; + bool fading_enable; // === Opacity === /// Default opacity for inactive windows. - /// 32-bit integer with the format of _NET_WM_OPACITY. + /// 32-bit integer with the format of _NET_WM_WINDOW_OPACITY. double inactive_opacity; /// Default opacity for inactive windows. double active_opacity; @@ -185,8 +216,8 @@ typedef struct options { /// Frame opacity. Relative to window opacity, also affects shadow /// opacity. double frame_opacity; - /// Whether to detect _NET_WM_OPACITY on client windows. Used on window - /// managers that don't pass _NET_WM_OPACITY to frame windows. + /// Whether to detect _NET_WM_WINDOW_OPACITY on client windows. Used on window + /// managers that don't pass _NET_WM_WINDOW_OPACITY to frame windows. bool detect_client_opacity; // === Other window processing === @@ -210,6 +241,10 @@ typedef struct options { struct conv **blur_kerns; /// Number of convolution kernels int blur_kernel_count; + /// Custom fragment shader for painting windows + char *window_shader_fg; + /// Rules to change custom fragment shader for painting windows. + c2_lptr_t *window_shader_fg_rules; /// How much to dim an inactive window. 0.0 - 1.0, 0 to disable. double inactive_dim; /// Whether to use fixed inactive dim opacity, instead of deciding @@ -225,6 +260,8 @@ typedef struct options { int corner_radius; /// Rounded corners blacklist. A linked list of conditions. c2_lptr_t *rounded_corners_blacklist; + /// Rounded corner rules. A linked list of conditions. + c2_lptr_t *corner_radius_rules; // === Focus related === /// Whether to try to detect WM windows and mark them as focused. @@ -250,50 +287,54 @@ typedef struct options { // Make transparent windows clip other windows, instead of blending on top of // them bool transparent_clipping; + /// A list of conditions of windows to which transparent clipping + /// should not apply + c2_lptr_t *transparent_clipping_blacklist; + + bool dithered_present; } options_t; extern const char *const BACKEND_STRS[NUM_BKEND + 1]; bool must_use parse_long(const char *, long *); bool must_use parse_int(const char *, int *); -struct conv **must_use parse_blur_kern_lst(const char *, bool *hasneg, int *count); -bool must_use parse_geometry(session_t *, const char *, region_t *); -bool must_use parse_rule_opacity(c2_lptr_t **, const char *); -enum blur_method must_use parse_blur_method(const char *src); +struct conv **must_use parse_blur_kern_lst(const char *, int *count); +/// Parse the path prefix of a c2 rule. Then look for the specified file in the +/// given include directories. The include directories are passed via `user_data`. +void *parse_window_shader_prefix(const char *src, const char **end, void *user_data); +/// Same as `parse_window_shader_prefix`, but the path is relative to the current +/// working directory. `user_data` is ignored. +void *parse_window_shader_prefix_with_cwd(const char *src, const char **end, void *); +void *parse_numeric_prefix(const char *src, const char **end, void *user_data); +char *must_use locate_auxiliary_file(const char *scope, const char *path, + const char *include_dir); +int must_use parse_blur_method(const char *src); +void parse_debug_options(struct debug_options *); -/** - * Add a pattern to a condition linked list. - */ -bool condlst_add(c2_lptr_t **, const char *); +const char *xdg_config_home(void); +char **xdg_config_dirs(void); -#ifdef CONFIG_LIBCONFIG /// Parse a configuration file /// Returns the actually config_file name used, allocated on heap /// Outputs: -/// shadow_enable = whether shaodw is enabled globally +/// shadow_enable = whether shadow is enabled globally /// fading_enable = whether fading is enabled globally /// win_option_mask = whether option overrides for specific window type is set for given /// options /// hasneg = whether the convolution kernel has negative values -char * -parse_config_libconfig(options_t *, const char *config_file, bool *shadow_enable, - bool *fading_enable, bool *hasneg, win_option_mask_t *winopt_mask); -#endif +char *parse_config_libconfig(options_t *, const char *config_file); -void set_default_winopts(options_t *, win_option_mask_t *, bool shadow_enable, - bool fading_enable, bool blur_enable); /// Parse a configuration file is that is enabled, also initialize the winopt_mask with /// default values /// Outputs and returns: /// same as parse_config_libconfig -char *parse_config(options_t *, const char *config_file, bool *shadow_enable, - bool *fading_enable, bool *hasneg, win_option_mask_t *winopt_mask); +char *parse_config(options_t *, const char *config_file); /** * Parse a backend option argument. */ -static inline attr_pure enum backend parse_backend(const char *str) { - for (enum backend i = 0; BACKEND_STRS[i]; ++i) { +static inline attr_pure int parse_backend(const char *str) { + for (int i = 0; BACKEND_STRS[i]; ++i) { if (!strcasecmp(str, BACKEND_STRS[i])) { return i; } diff --git a/src/config_libconfig.c b/src/config_libconfig.c index d9b530ce1c..df9c4961a6 100644 --- a/src/config_libconfig.c +++ b/src/config_libconfig.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2012-2014 Richard Grenville +#include #include #include #include @@ -9,15 +10,13 @@ #include #include +#include "c2.h" #include "common.h" -#include "compiler.h" #include "config.h" #include "err.h" #include "log.h" -#include "options.h" #include "string_utils.h" #include "utils.h" -#include "win.h" #pragma GCC diagnostic error "-Wunused-parameter" @@ -30,102 +29,11 @@ static inline int lcfg_lookup_bool(const config_t *config, const char *path, boo int ival; int ret = config_lookup_bool(config, path, &ival); - if (ret) + if (ret) { *value = ival; - - return ret; -} - -const char *xdg_config_home(void) { - char *xdgh = getenv("XDG_CONFIG_HOME"); - char *home = getenv("HOME"); - const char *default_dir = "/.config"; - - if (!xdgh) { - if (!home) { - return NULL; - } - - xdgh = cvalloc(strlen(home) + strlen(default_dir) + 1); - - strcpy(xdgh, home); - strcat(xdgh, default_dir); - } else { - xdgh = strdup(xdgh); } - return xdgh; -} - -char **xdg_config_dirs(void) { - char *xdgd = getenv("XDG_CONFIG_DIRS"); - size_t count = 0; - - if (!xdgd) { - xdgd = "/etc/xdg"; - } - - for (int i = 0; xdgd[i]; i++) { - if (xdgd[i] == ':') { - count++; - } - } - - // Store the string and the result pointers together so they can be - // freed together - char **dir_list = cvalloc(sizeof(char *) * (count + 2) + strlen(xdgd) + 1); - auto dirs = strcpy((char *)dir_list + sizeof(char *) * (count + 2), xdgd); - auto path = dirs; - - for (size_t i = 0; i < count; i++) { - dir_list[i] = path; - path = strchr(path, ':'); - *path = '\0'; - path++; - } - dir_list[count] = path; - - size_t fill = 0; - for (size_t i = 0; i <= count; i++) { - if (dir_list[i][0] == '/') { - dir_list[fill] = dir_list[i]; - fill++; - } - } - - dir_list[fill] = NULL; - - return dir_list; -} - -TEST_CASE(xdg_config_dirs) { - auto old_var = getenv("XDG_CONFIG_DIRS"); - if (old_var) { - old_var = strdup(old_var); - } - unsetenv("XDG_CONFIG_DIRS"); - - auto result = xdg_config_dirs(); - TEST_STREQUAL(result[0], "/etc/xdg"); - TEST_EQUAL(result[1], NULL); - free(result); - - setenv("XDG_CONFIG_DIRS", ".:.:/etc/xdg:.:/:", 1); - result = xdg_config_dirs(); - TEST_STREQUAL(result[0], "/etc/xdg"); - TEST_STREQUAL(result[1], "/"); - TEST_EQUAL(result[2], NULL); - free(result); - - setenv("XDG_CONFIG_DIRS", ":", 1); - result = xdg_config_dirs(); - TEST_EQUAL(result[0], NULL); - free(result); - - if (old_var) { - setenv("XDG_CONFIG_DIRS", old_var, 1); - free(old_var); - } + return ret; } /// Search for config file under a base directory @@ -163,24 +71,27 @@ FILE *open_config_file(const char *cpath, char **ppath) { if (cpath) { FILE *ret = fopen(cpath, "r"); - if (ret && ppath) + if (ret && ppath) { *ppath = strdup(cpath); + } return ret; } // First search for config file in user config directory auto config_home = xdg_config_home(); - auto ret = open_config_file_at(config_home, ppath); - free((void *)config_home); - if (ret) { - return ret; + if (config_home) { + auto ret = open_config_file_at(config_home, ppath); + free((void *)config_home); + if (ret) { + return ret; + } } // Fall back to legacy config file in user home directory const char *home = getenv("HOME"); if (home && strlen(home)) { auto path = mstrjoin(home, config_filename_legacy); - ret = fopen(path, "r"); + auto ret = fopen(path, "r"); if (ret && ppath) { *ppath = path; } else { @@ -194,7 +105,7 @@ FILE *open_config_file(const char *cpath, char **ppath) { // Fall back to config file in system config directory auto config_dirs = xdg_config_dirs(); for (int i = 0; config_dirs[i]; i++) { - ret = open_config_file_at(config_dirs[i], ppath); + auto ret = open_config_file_at(config_dirs[i], ppath); if (ret) { free(config_dirs); return ret; @@ -208,46 +119,60 @@ FILE *open_config_file(const char *cpath, char **ppath) { /** * Parse a condition list in configuration file. */ -void parse_cfg_condlst(const config_t *pcfg, c2_lptr_t **pcondlst, const char *name) { +bool must_use parse_cfg_condlst(const config_t *pcfg, c2_lptr_t **pcondlst, const char *name) { config_setting_t *setting = config_lookup(pcfg, name); - if (setting) { - // Parse an array of options - if (config_setting_is_array(setting)) { - int i = config_setting_length(setting); - while (i--) - condlst_add(pcondlst, - config_setting_get_string_elem(setting, i)); + if (setting == NULL) { + return true; + } + // Parse an array of options + if (config_setting_is_array(setting)) { + int i = config_setting_length(setting); + while (i--) { + if (!c2_parse(pcondlst, + config_setting_get_string_elem(setting, i), NULL)) { + return false; + } } - // Treat it as a single pattern if it's a string - else if (CONFIG_TYPE_STRING == config_setting_type(setting)) { - condlst_add(pcondlst, config_setting_get_string(setting)); + } + // Treat it as a single pattern if it's a string + else if (CONFIG_TYPE_STRING == config_setting_type(setting)) { + if (!c2_parse(pcondlst, config_setting_get_string(setting), NULL)) { + return false; } } + return true; } /** - * Parse an opacity rule list in configuration file. + * Parse a window corner radius rule list in configuration file. */ -static inline void -parse_cfg_condlst_opct(options_t *opt, const config_t *pcfg, const char *name) { +static inline bool +parse_cfg_condlst_with_prefix(c2_lptr_t **condlst, const config_t *pcfg, const char *name, + void *(*parse_prefix)(const char *, const char **, void *), + void (*free_value)(void *), void *user_data) { config_setting_t *setting = config_lookup(pcfg, name); - if (setting) { - // Parse an array of options - if (config_setting_is_array(setting)) { - int i = config_setting_length(setting); - while (i--) - if (!parse_rule_opacity( - &opt->opacity_rules, - config_setting_get_string_elem(setting, i))) - exit(1); - } - // Treat it as a single pattern if it's a string - else if (config_setting_type(setting) == CONFIG_TYPE_STRING) { - if (!parse_rule_opacity(&opt->opacity_rules, - config_setting_get_string(setting))) - exit(1); + if (setting == NULL) { + return true; + } + // Parse an array of options + if (config_setting_is_array(setting)) { + int i = config_setting_length(setting); + while (i--) { + if (!c2_parse_with_prefix( + condlst, config_setting_get_string_elem(setting, i), + parse_prefix, free_value, user_data)) { + return false; + } + } + } + // Treat it as a single pattern if it's a string + else if (config_setting_type(setting) == CONFIG_TYPE_STRING) { + if (!c2_parse_with_prefix(condlst, config_setting_get_string(setting), + parse_prefix, free_value, user_data)) { + return false; } } + return true; } static inline void parse_wintype_config(const config_t *cfg, const char *member_name, @@ -300,9 +225,12 @@ static inline void parse_wintype_config(const config_t *cfg, const char *member_ * * Returns the actually config_file name */ -char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shadow_enable, - bool *fading_enable, bool *conv_kern_hasneg, - win_option_mask_t *winopt_mask) { +char *parse_config_libconfig(options_t *opt, const char *config_file) { + + const char *deprecation_message = + "option has been deprecated. Please remove it from your configuration file. " + "If you encounter any problems without this feature, please feel free to " + "open a bug report"; char *path = NULL; FILE *f; config_t cfg; @@ -328,15 +256,14 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad config_set_options(&cfg, CONFIG_OPTION_ALLOW_OVERRIDES); #endif { - // dirname() could modify the original string, thus we must pass a - // copy - char *path2 = strdup(path); - char *parent = dirname(path2); + char *abspath = realpath(path, NULL); + char *parent = dirname(abspath); // path2 may be modified - if (parent) + if (parent) { config_set_include_dir(&cfg, parent); + } - free(path2); + free(abspath); } { @@ -355,15 +282,21 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad // Get options from the configuration file. We don't do range checking // right now. It will be done later + // --dbus + lcfg_lookup_bool(&cfg, "dbus", &opt->dbus); + // -D (fade_delta) - if (config_lookup_int(&cfg, "fade-delta", &ival)) + if (config_lookup_int(&cfg, "fade-delta", &ival)) { opt->fade_delta = ival; + } // -I (fade_in_step) - if (config_lookup_float(&cfg, "fade-in-step", &dval)) + if (config_lookup_float(&cfg, "fade-in-step", &dval)) { opt->fade_in_step = normalize_d(dval); + } // -O (fade_out_step) - if (config_lookup_float(&cfg, "fade-out-step", &dval)) + if (config_lookup_float(&cfg, "fade-out-step", &dval)) { opt->fade_out_step = normalize_d(dval); + } // -r (shadow_radius) config_lookup_int(&cfg, "shadow-radius", &opt->shadow_radius); // -o (shadow_opacity) @@ -373,46 +306,38 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad // -t (shadow_offset_y) config_lookup_int(&cfg, "shadow-offset-y", &opt->shadow_offset_y); // -i (inactive_opacity) - if (config_lookup_float(&cfg, "inactive-opacity", &dval)) + if (config_lookup_float(&cfg, "inactive-opacity", &dval)) { opt->inactive_opacity = normalize_d(dval); + } // --active_opacity - if (config_lookup_float(&cfg, "active-opacity", &dval)) + if (config_lookup_float(&cfg, "active-opacity", &dval)) { opt->active_opacity = normalize_d(dval); + } // --corner-radius config_lookup_int(&cfg, "corner-radius", &opt->corner_radius); - // --rounded-corners-exclude - parse_cfg_condlst(&cfg, &opt->rounded_corners_blacklist, "rounded-corners-exclude"); + + if (lcfg_lookup_bool(&cfg, "no-frame-pacing", &bval)) { + opt->frame_pacing = !bval; + } + // -e (frame_opacity) config_lookup_float(&cfg, "frame-opacity", &opt->frame_opacity); // -c (shadow_enable) - if (config_lookup_bool(&cfg, "shadow", &ival)) - *shadow_enable = ival; - // -C (no_dock_shadow) - if (config_lookup_bool(&cfg, "no-dock-shadow", &ival)) { - log_error("Option `no-dock-shadow` has been removed. Please use the " - "wintype option `shadow` of `dock` instead."); - goto err; - } - // -G (no_dnd_shadow) - if (config_lookup_bool(&cfg, "no-dnd-shadow", &ival)) { - log_error("Option `no-dnd-shadow` has been removed. Please use the " - "wintype option `shadow` of `dnd` instead."); - goto err; - }; + lcfg_lookup_bool(&cfg, "shadow", &opt->shadow_enable); // -m (menu_opacity) if (config_lookup_float(&cfg, "menu-opacity", &dval)) { - log_warn("Option `menu-opacity` is deprecated, and will be " - "removed.Please use the " - "wintype option `opacity` of `popup_menu` and `dropdown_menu` " - "instead."); + log_warn("Option `menu-opacity` is deprecated, and will be removed." + "Please use the wintype option `opacity` of `popup_menu`" + "and `dropdown_menu` instead."); opt->wintype_option[WINTYPE_DROPDOWN_MENU].opacity = dval; opt->wintype_option[WINTYPE_POPUP_MENU].opacity = dval; - winopt_mask[WINTYPE_DROPDOWN_MENU].opacity = true; - winopt_mask[WINTYPE_POPUP_MENU].opacity = true; + opt->wintype_option_mask[WINTYPE_DROPDOWN_MENU].opacity = true; + opt->wintype_option_mask[WINTYPE_POPUP_MENU].opacity = true; } // -f (fading_enable) - if (config_lookup_bool(&cfg, "fading", &ival)) - *fading_enable = ival; + if (config_lookup_bool(&cfg, "fading", &ival)) { + opt->fading_enable = ival; + } // --no-fading-open-close lcfg_lookup_bool(&cfg, "no-fading-openclose", &opt->no_fading_openclose); // --no-fading-destroyed-argb @@ -432,8 +357,11 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad opt->shadow_blue = rgb.blue; } // --shadow-exclude-reg - if (config_lookup_string(&cfg, "shadow-exclude-reg", &sval)) - opt->shadow_exclude_reg_str = strdup(sval); + if (config_lookup_string(&cfg, "shadow-exclude-reg", &sval)) { + log_error("shadow-exclude-reg is deprecated. Please use " + "clip-shadow-above for more flexible shadow exclusion."); + goto err; + } // --inactive-opacity-override lcfg_lookup_bool(&cfg, "inactive-opacity-override", &opt->inactive_opacity_override); // --inactive-dim @@ -446,24 +374,25 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad lcfg_lookup_bool(&cfg, "shadow-ignore-shaped", &opt->shadow_ignore_shaped); // --detect-rounded-corners lcfg_lookup_bool(&cfg, "detect-rounded-corners", &opt->detect_rounded_corners); - // --xinerama-shadow-crop - lcfg_lookup_bool(&cfg, "xinerama-shadow-crop", &opt->xinerama_shadow_crop); + // --crop-shadow-to-monitor + if (lcfg_lookup_bool(&cfg, "xinerama-shadow-crop", &opt->crop_shadow_to_monitor)) { + log_warn("xinerama-shadow-crop is deprecated. Use crop-shadow-to-monitor " + "instead."); + } + lcfg_lookup_bool(&cfg, "crop-shadow-to-monitor", &opt->crop_shadow_to_monitor); // --detect-client-opacity lcfg_lookup_bool(&cfg, "detect-client-opacity", &opt->detect_client_opacity); // --refresh-rate - if (config_lookup_int(&cfg, "refresh-rate", &opt->refresh_rate)) { - if (opt->refresh_rate < 0) { - log_warn("Invalid refresh rate %d, fallback to 0", opt->refresh_rate); - opt->refresh_rate = 0; - } + if (config_lookup_int(&cfg, "refresh-rate", &ival)) { + log_warn("The refresh-rate %s", deprecation_message); } // --vsync if (config_lookup_string(&cfg, "vsync", &sval)) { - opt->vsync = parse_vsync(sval); - log_warn("vsync option will take a boolean from now on. \"%s\" is " - "interpreted as \"%s\" for compatibility, but this will stop " - "working soon", - sval, opt->vsync ? "true" : "false"); + bool parsed_vsync = parse_vsync(sval); + log_error("vsync option will take a boolean from now on. \"%s\" in " + "your configuration should be changed to \"%s\"", + sval, parsed_vsync ? "true" : "false"); + goto err; } lcfg_lookup_bool(&cfg, "vsync", &opt->vsync); // --backend @@ -476,11 +405,11 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad } // --log-level if (config_lookup_string(&cfg, "log-level", &sval)) { - auto level = string_to_log_level(sval); - if (level == LOG_LEVEL_INVALID) { + opt->log_level = string_to_log_level(sval); + if (opt->log_level == LOG_LEVEL_INVALID) { log_warn("Invalid log level, defaults to WARN"); } else { - log_set_level_tls(level); + log_set_level_tls(opt->log_level); } } // --log-file @@ -492,7 +421,10 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad opt->logpath = strdup(sval); } // --sw-opti - lcfg_lookup_bool(&cfg, "sw-opti", &opt->sw_opti); + if (lcfg_lookup_bool(&cfg, "sw-opti", &bval)) { + log_error("The sw-opti %s", deprecation_message); + goto err; + } // --use-ewmh-active-win lcfg_lookup_bool(&cfg, "use-ewmh-active-win", &opt->use_ewmh_active_win); // --unredir-if-possible @@ -515,31 +447,38 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad lcfg_lookup_bool(&cfg, "no-ewmh-fullscreen", &opt->no_ewmh_fullscreen); // --transparent-clipping lcfg_lookup_bool(&cfg, "transparent-clipping", &opt->transparent_clipping); - // --shadow-exclude - parse_cfg_condlst(&cfg, &opt->shadow_blacklist, "shadow-exclude"); - // --clip-shadow-above - parse_cfg_condlst(&cfg, &opt->shadow_clip_list, "clip-shadow-above"); - // --fade-exclude - parse_cfg_condlst(&cfg, &opt->fade_blacklist, "fade-exclude"); - // --focus-exclude - parse_cfg_condlst(&cfg, &opt->focus_blacklist, "focus-exclude"); - // --invert-color-include - parse_cfg_condlst(&cfg, &opt->invert_color_list, "invert-color-include"); - // --blur-background-exclude - parse_cfg_condlst(&cfg, &opt->blur_background_blacklist, "blur-background-exclude"); - // --opacity-rule - parse_cfg_condlst_opct(opt, &cfg, "opacity-rule"); - // --unredir-if-possible-exclude - parse_cfg_condlst(&cfg, &opt->unredir_if_possible_blacklist, - "unredir-if-possible-exclude"); + // --dithered_present + lcfg_lookup_bool(&cfg, "dithered-present", &opt->dithered_present); + + if (!parse_cfg_condlst(&cfg, &opt->transparent_clipping_blacklist, + "transparent-clipping-exclude") || + !parse_cfg_condlst(&cfg, &opt->shadow_blacklist, "shadow-exclude") || + !parse_cfg_condlst(&cfg, &opt->shadow_clip_list, "clip-shadow-above") || + !parse_cfg_condlst(&cfg, &opt->fade_blacklist, "fade-exclude") || + !parse_cfg_condlst(&cfg, &opt->focus_blacklist, "focus-exclude") || + !parse_cfg_condlst(&cfg, &opt->invert_color_list, "invert-color-include") || + !parse_cfg_condlst(&cfg, &opt->blur_background_blacklist, "blur-background-exclude") || + !parse_cfg_condlst(&cfg, &opt->unredir_if_possible_blacklist, + "unredir-if-possible-exclude") || + !parse_cfg_condlst(&cfg, &opt->rounded_corners_blacklist, "rounded-corners-exclude") || + !parse_cfg_condlst_with_prefix(&opt->corner_radius_rules, &cfg, "corner-radius-rules", + parse_numeric_prefix, NULL, (int[]){0, INT_MAX}) || + !parse_cfg_condlst_with_prefix(&opt->opacity_rules, &cfg, "opacity-rule", + parse_numeric_prefix, NULL, (int[]){0, 100}) || + !parse_cfg_condlst_with_prefix( + &opt->window_shader_fg_rules, &cfg, "window-shader-fg-rule", + parse_window_shader_prefix, free, (void *)config_get_include_dir(&cfg))) { + goto err; + } + // --blur-method if (config_lookup_string(&cfg, "blur-method", &sval)) { - enum blur_method method = parse_blur_method(sval); + int method = parse_blur_method(sval); if (method >= BLUR_METHOD_INVALID) { log_fatal("Invalid blur method %s", sval); goto err; } - opt->blur_method = method; + opt->blur_method = (enum blur_method)method; } // --blur-size config_lookup_int(&cfg, "blur-size", &opt->blur_radius); @@ -559,8 +498,7 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad lcfg_lookup_bool(&cfg, "blur-background-fixed", &opt->blur_background_fixed); // --blur-kern if (config_lookup_string(&cfg, "blur-kern", &sval)) { - opt->blur_kerns = - parse_blur_kern_lst(sval, conv_kern_hasneg, &opt->blur_kernel_count); + opt->blur_kerns = parse_blur_kern_lst(sval, &opt->blur_kernel_count); if (!opt->blur_kerns) { log_fatal("Cannot parse \"blur-kern\""); goto err; @@ -577,6 +515,7 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad if (config_lookup_string(&cfg, "glx-swap-method", &sval)) { char *endptr; long val = strtol(sval, &endptr, 10); + bool should_remove = true; if (*endptr || !(*sval)) { // sval is not a number, or an empty string val = -1; @@ -584,12 +523,13 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad if (strcmp(sval, "undefined") != 0 && val != 0) { // If not undefined, we will use damage and buffer-age to limit // the rendering area. - opt->use_damage = true; + should_remove = false; } - log_warn("glx-swap-method has been deprecated since v6, your setting " - "\"%s\" should be %s.", - sval, - opt->use_damage ? "replaced by `use-damage = true`" : "removed"); + log_error("glx-swap-method has been removed, your setting " + "\"%s\" should be %s.", + sval, + !should_remove ? "replaced by `use-damage = true`" : "removed"); + goto err; } // --use-damage lcfg_lookup_bool(&cfg, "use-damage", &opt->use_damage); @@ -602,55 +542,41 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad opt->max_brightness = 1.0; } + // --window-shader-fg + if (config_lookup_string(&cfg, "window-shader-fg", &sval)) { + opt->window_shader_fg = + locate_auxiliary_file("shaders", sval, config_get_include_dir(&cfg)); + } + // --glx-use-gpushader4 - if (config_lookup_bool(&cfg, "glx-use-gpushader4", &ival) && ival) { - log_warn("glx-use-gpushader4 is deprecated since v6, please remove it " - "from" - "your config file"); - } - // --xrender-sync - if (config_lookup_bool(&cfg, "xrender-sync", &ival) && ival) { - log_error("Please use xrender-sync-fence instead of xrender-sync."); + if (config_lookup_bool(&cfg, "glx-use-gpushader4", &ival)) { + log_error("glx-use-gpushader4 has been removed, please remove it " + "from your config file"); goto err; } // --xrender-sync-fence lcfg_lookup_bool(&cfg, "xrender-sync-fence", &opt->xrender_sync_fence); - if (lcfg_lookup_bool(&cfg, "clear-shadow", &bval)) + if (lcfg_lookup_bool(&cfg, "clear-shadow", &bval)) { log_warn("\"clear-shadow\" is removed as an option, and is always" " enabled now. Consider removing it from your config file"); - if (lcfg_lookup_bool(&cfg, "paint-on-overlay", &bval)) { - log_error("\"paint-on-overlay\" has been removed as an option, and " - "the feature is enabled whenever possible"); - goto err; } - if (config_lookup_float(&cfg, "alpha-step", &dval)) { - log_error("\"alpha-step\" has been removed, compton now tries to make use" - " of all alpha values"); - goto err; - } - - const char *deprecation_message attr_unused = - "has been removed. If you encounter problems " - "without this feature, please feel free to open a bug report"; - config_setting_t *blur_cfg = config_lookup(&cfg, "blur"); if (blur_cfg) { if (config_setting_lookup_string(blur_cfg, "method", &sval)) { - enum blur_method method = parse_blur_method(sval); + int method = parse_blur_method(sval); if (method >= BLUR_METHOD_INVALID) { log_warn("Invalid blur method %s, ignoring.", sval); } else { - opt->blur_method = method; + opt->blur_method = (enum blur_method)method; } } config_setting_lookup_int(blur_cfg, "size", &opt->blur_radius); if (config_setting_lookup_string(blur_cfg, "kernel", &sval)) { - opt->blur_kerns = parse_blur_kern_lst(sval, conv_kern_hasneg, - &opt->blur_kernel_count); + opt->blur_kerns = parse_blur_kern_lst(sval, &opt->blur_kernel_count); if (!opt->blur_kerns) { log_warn("Failed to parse blur kernel: %s", sval); } @@ -673,13 +599,13 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad // XXX ! Refactor all the wintype_* arrays into a struct for (wintype_t i = 0; i < NUM_WINTYPES; ++i) { - parse_wintype_config(&cfg, WINTYPES[i], &opt->wintype_option[i], - &winopt_mask[i]); + parse_wintype_config(&cfg, WINTYPES[i].name, &opt->wintype_option[i], + &opt->wintype_option_mask[i]); } // Compatibility with the old name for notification windows. parse_wintype_config(&cfg, "notify", &opt->wintype_option[WINTYPE_NOTIFICATION], - &winopt_mask[WINTYPE_NOTIFICATION]); + &opt->wintype_option_mask[WINTYPE_NOTIFICATION]); config_destroy(&cfg); return path; diff --git a/src/dbus.c b/src/dbus.c index 8d6094ef54..f242622bcf 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include #include @@ -24,14 +26,19 @@ #include "list.h" #include "log.h" #include "string_utils.h" +#include "transition.h" #include "types.h" #include "uthash_extra.h" #include "utils.h" #include "win.h" +#include "win_defs.h" +#include "wm.h" #include "dbus.h" struct cdbus_data { + /// Mainloop + struct ev_loop *loop; /// DBus connection. DBusConnection *dbus_conn; /// DBus service name. @@ -68,11 +75,15 @@ typedef uint32_t cdbus_enum_t; #define CDBUS_ERROR_CUSTOM CDBUS_ERROR_PREFIX ".custom" #define CDBUS_ERROR_CUSTOM_S "%s" -#define cdbus_reply_err(ps, srcmsg, err_name, err_format, ...) \ - cdbus_reply_errm((ps), dbus_message_new_error_printf( \ +#define cdbus_reply_err(conn, srcmsg, err_name, err_format, ...) \ + cdbus_reply_errm(conn, dbus_message_new_error_printf( \ (srcmsg), (err_name), (err_format), ##__VA_ARGS__)) +#define PICOM_WINDOW_INTERFACE "picom.Window" +#define PICOM_COMPOSITOR_INTERFACE "picom.Compositor" + static DBusHandlerResult cdbus_process(DBusConnection *conn, DBusMessage *m, void *); +static DBusHandlerResult cdbus_process_windows(DBusConnection *c, DBusMessage *msg, void *ud); static dbus_bool_t cdbus_callback_add_timeout(DBusTimeout *timeout, void *data); @@ -89,13 +100,10 @@ static void cdbus_callback_watch_toggled(DBusWatch *watch, void *data); /** * Initialize D-Bus connection. */ -bool cdbus_init(session_t *ps, const char *uniq) { +struct cdbus_data *cdbus_init(session_t *ps, const char *uniq) { auto cd = cmalloc(struct cdbus_data); cd->dbus_service = NULL; - // Set ps->dbus_data here because add_watch functions need it - ps->dbus_data = cd; - DBusError err = {}; // Initialize @@ -105,8 +113,7 @@ bool cdbus_init(session_t *ps, const char *uniq) { // Use dbus_bus_get_private() so we can fully recycle it ourselves cd->dbus_conn = dbus_bus_get_private(DBUS_BUS_SESSION, &err); if (dbus_error_is_set(&err)) { - log_error("D-Bus connection failed (%s).", err.message); - dbus_error_free(&err); + log_error("D-Bus connection failed."); goto fail; } @@ -141,13 +148,12 @@ bool cdbus_init(session_t *ps, const char *uniq) { DBUS_NAME_FLAG_DO_NOT_QUEUE, &err); if (dbus_error_is_set(&err)) { - log_error("Failed to obtain D-Bus name (%s).", err.message); - dbus_error_free(&err); + log_error("Failed to obtain D-Bus name."); goto fail; } - if (DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER != ret && - DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER != ret) { + if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER && + ret != DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) { log_error("Failed to become the primary owner of requested D-Bus " "name (%d).", ret); @@ -156,9 +162,10 @@ bool cdbus_init(session_t *ps, const char *uniq) { } // Add watch handlers + cd->loop = ps->loop; if (!dbus_connection_set_watch_functions(cd->dbus_conn, cdbus_callback_add_watch, cdbus_callback_remove_watch, - cdbus_callback_watch_toggled, ps, NULL)) { + cdbus_callback_watch_toggled, cd, NULL)) { log_error("Failed to add D-Bus watch functions."); goto fail; } @@ -166,7 +173,7 @@ bool cdbus_init(session_t *ps, const char *uniq) { // Add timeout handlers if (!dbus_connection_set_timeout_functions( cd->dbus_conn, cdbus_callback_add_timeout, cdbus_callback_remove_timeout, - cdbus_callback_timeout_toggled, ps, NULL)) { + cdbus_callback_timeout_toggled, cd, NULL)) { log_error("Failed to add D-Bus timeout functions."); goto fail; } @@ -176,23 +183,29 @@ bool cdbus_init(session_t *ps, const char *uniq) { "type='method_call',interface='" CDBUS_INTERFACE_NAME "'", &err); if (dbus_error_is_set(&err)) { log_error("Failed to add D-Bus match."); - dbus_error_free(&err); goto fail; } - dbus_connection_add_filter(cd->dbus_conn, cdbus_process, ps, NULL); - return true; + dbus_connection_register_object_path( + cd->dbus_conn, CDBUS_OBJECT_NAME, + (DBusObjectPathVTable[]){{NULL, cdbus_process}}, ps); + dbus_connection_register_fallback( + cd->dbus_conn, CDBUS_OBJECT_NAME "/windows", + (DBusObjectPathVTable[]){{NULL, cdbus_process_windows}}, ps); + return cd; fail: - ps->dbus_data = NULL; + if (dbus_error_is_set(&err)) { + log_error("D-Bus error %s: %s", err.name, err.message); + dbus_error_free(&err); + } free(cd->dbus_service); free(cd); - return false; + return NULL; } /** * Destroy D-Bus connection. */ -void cdbus_destroy(session_t *ps) { - struct cdbus_data *cd = ps->dbus_data; +void cdbus_destroy(struct cdbus_data *cd) { if (cd->dbus_conn) { // Release DBus name firstly if (cd->dbus_service) { @@ -236,7 +249,7 @@ cdbus_callback_handle_timeout(EV_P attr_unused, ev_timer *w, int revents attr_un * Callback for adding D-Bus timeout. */ static dbus_bool_t cdbus_callback_add_timeout(DBusTimeout *timeout, void *data) { - session_t *ps = data; + struct cdbus_data *cd = data; auto t = ccalloc(1, ev_dbus_timer); double i = dbus_timeout_get_interval(timeout) / 1000.0; @@ -244,8 +257,9 @@ static dbus_bool_t cdbus_callback_add_timeout(DBusTimeout *timeout, void *data) t->t = timeout; dbus_timeout_set_data(timeout, t, NULL); - if (dbus_timeout_get_enabled(timeout)) - ev_timer_start(ps->loop, &t->w); + if (dbus_timeout_get_enabled(timeout)) { + ev_timer_start(cd->loop, &t->w); + } return true; } @@ -254,11 +268,11 @@ static dbus_bool_t cdbus_callback_add_timeout(DBusTimeout *timeout, void *data) * Callback for removing D-Bus timeout. */ static void cdbus_callback_remove_timeout(DBusTimeout *timeout, void *data) { - session_t *ps = data; + struct cdbus_data *cd = data; ev_dbus_timer *t = dbus_timeout_get_data(timeout); assert(t); - ev_timer_stop(ps->loop, &t->w); + ev_timer_stop(cd->loop, &t->w); free(t); } @@ -266,15 +280,15 @@ static void cdbus_callback_remove_timeout(DBusTimeout *timeout, void *data) { * Callback for toggling a D-Bus timeout. */ static void cdbus_callback_timeout_toggled(DBusTimeout *timeout, void *data) { - session_t *ps = data; + struct cdbus_data *cd = data; ev_dbus_timer *t = dbus_timeout_get_data(timeout); assert(t); - ev_timer_stop(ps->loop, &t->w); + ev_timer_stop(cd->loop, &t->w); if (dbus_timeout_get_enabled(timeout)) { double i = dbus_timeout_get_interval(timeout) / 1000.0; ev_timer_set(&t->w, i, i); - ev_timer_start(ps->loop, &t->w); + ev_timer_start(cd->loop, &t->w); } } @@ -293,10 +307,12 @@ typedef struct ev_dbus_io { void cdbus_io_callback(EV_P attr_unused, ev_io *w, int revents) { ev_dbus_io *dw = (void *)w; DBusWatchFlags flags = 0; - if (revents & EV_READ) + if (revents & EV_READ) { flags |= DBUS_WATCH_READABLE; - if (revents & EV_WRITE) + } + if (revents & EV_WRITE) { flags |= DBUS_WATCH_WRITABLE; + } dbus_watch_handle(dw->dw, flags); while (dbus_connection_dispatch(dw->cd->dbus_conn) != DBUS_DISPATCH_COMPLETE) ; @@ -308,10 +324,12 @@ void cdbus_io_callback(EV_P attr_unused, ev_io *w, int revents) { static inline int cdbus_get_watch_cond(DBusWatch *watch) { const unsigned flags = dbus_watch_get_flags(watch); int condition = 0; - if (flags & DBUS_WATCH_READABLE) + if (flags & DBUS_WATCH_READABLE) { condition |= EV_READ; - if (flags & DBUS_WATCH_WRITABLE) + } + if (flags & DBUS_WATCH_WRITABLE) { condition |= EV_WRITE; + } return condition; } @@ -320,17 +338,18 @@ static inline int cdbus_get_watch_cond(DBusWatch *watch) { * Callback for adding D-Bus watch. */ static dbus_bool_t cdbus_callback_add_watch(DBusWatch *watch, void *data) { - session_t *ps = data; + struct cdbus_data *cd = data; auto w = ccalloc(1, ev_dbus_io); w->dw = watch; - w->cd = ps->dbus_data; + w->cd = cd; ev_io_init(&w->w, cdbus_io_callback, dbus_watch_get_unix_fd(watch), cdbus_get_watch_cond(watch)); // Leave disabled watches alone - if (dbus_watch_get_enabled(watch)) - ev_io_start(ps->loop, &w->w); + if (dbus_watch_get_enabled(watch)) { + ev_io_start(cd->loop, &w->w); + } dbus_watch_set_data(watch, w, NULL); @@ -342,9 +361,9 @@ static dbus_bool_t cdbus_callback_add_watch(DBusWatch *watch, void *data) { * Callback for removing D-Bus watch. */ static void cdbus_callback_remove_watch(DBusWatch *watch, void *data) { - session_t *ps = data; + struct cdbus_data *cd = data; ev_dbus_io *w = dbus_watch_get_data(watch); - ev_io_stop(ps->loop, &w->w); + ev_io_stop(cd->loop, &w->w); free(w); } @@ -352,332 +371,141 @@ static void cdbus_callback_remove_watch(DBusWatch *watch, void *data) { * Callback for toggling D-Bus watch status. */ static void cdbus_callback_watch_toggled(DBusWatch *watch, void *data) { - session_t *ps = data; + struct cdbus_data *cd = data; ev_io *w = dbus_watch_get_data(watch); - if (dbus_watch_get_enabled(watch)) - ev_io_start(ps->loop, w); - else - ev_io_stop(ps->loop, w); + if (dbus_watch_get_enabled(watch)) { + ev_io_start(cd->loop, w); + } else { + ev_io_stop(cd->loop, w); + } } ///@} -/** @name Message argument appending callbacks - */ -///@{ - -/** - * Callback to append a bool argument to a message. - */ -static bool cdbus_apdarg_bool(session_t *ps attr_unused, DBusMessage *msg, const void *data) { - assert(data); - - dbus_bool_t val = *(const bool *)data; - - if (!dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &val, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - return false; - } - - return true; +static bool cdbus_append_boolean(DBusMessage *msg, dbus_bool_t val) { + return dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &val, DBUS_TYPE_INVALID); } -/** - * Callback to append an int32 argument to a message. - */ -static bool cdbus_apdarg_int32(session_t *ps attr_unused, DBusMessage *msg, const void *data) { - if (!dbus_message_append_args(msg, DBUS_TYPE_INT32, data, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - return false; - } - - return true; +static bool cdbus_append_int32(DBusMessage *msg, int32_t val) { + return dbus_message_append_args(msg, DBUS_TYPE_INT32, &val, DBUS_TYPE_INVALID); } -/** - * Callback to append an uint32 argument to a message. - */ -static bool -cdbus_apdarg_uint32(session_t *ps attr_unused, DBusMessage *msg, const void *data) { - if (!dbus_message_append_args(msg, DBUS_TYPE_UINT32, data, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - return false; - } - - return true; +static bool cdbus_append_uint32(DBusMessage *msg, uint32_t val) { + return dbus_message_append_args(msg, DBUS_TYPE_UINT32, &val, DBUS_TYPE_INVALID); } -/** - * Callback to append a double argument to a message. - */ -static bool -cdbus_apdarg_double(session_t *ps attr_unused, DBusMessage *msg, const void *data) { - if (!dbus_message_append_args(msg, DBUS_TYPE_DOUBLE, data, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - return false; - } - - return true; +static bool cdbus_append_double(DBusMessage *msg, double val) { + return dbus_message_append_args(msg, DBUS_TYPE_DOUBLE, &val, DBUS_TYPE_INVALID); } -/** - * Callback to append a Window argument to a message. - */ -static bool cdbus_apdarg_wid(session_t *ps attr_unused, DBusMessage *msg, const void *data) { - assert(data); - cdbus_window_t val = *(const xcb_window_t *)data; - - if (!dbus_message_append_args(msg, CDBUS_TYPE_WINDOW, &val, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - return false; - } - - return true; +static bool cdbus_append_wid(DBusMessage *msg, xcb_window_t val_) { + cdbus_window_t val = val_; + return dbus_message_append_args(msg, CDBUS_TYPE_WINDOW, &val, DBUS_TYPE_INVALID); } -/** - * Callback to append an cdbus_enum_t argument to a message. - */ -static bool cdbus_apdarg_enum(session_t *ps attr_unused, DBusMessage *msg, const void *data) { - assert(data); - if (!dbus_message_append_args(msg, CDBUS_TYPE_ENUM, data, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - return false; - } - - return true; +static bool cdbus_append_enum(DBusMessage *msg, cdbus_enum_t val) { + return dbus_message_append_args(msg, CDBUS_TYPE_ENUM, &val, DBUS_TYPE_INVALID); } -/** - * Callback to append a string argument to a message. - */ -static bool -cdbus_apdarg_string(session_t *ps attr_unused, DBusMessage *msg, const void *data) { - const char *str = data; - if (!str) - str = ""; - - if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &str, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - return false; - } - - return true; +static bool cdbus_append_string(DBusMessage *msg, const char *data) { + const char *str = data ? data : ""; + return dbus_message_append_args(msg, DBUS_TYPE_STRING, &str, DBUS_TYPE_INVALID); } -/** - * Callback to append all window IDs to a message. - */ -static bool cdbus_apdarg_wids(session_t *ps, DBusMessage *msg, const void *data attr_unused) { - // Get the number of wids we are to include - unsigned count = 0; - HASH_ITER2(ps->windows, w) { - assert(!w->destroyed); - ++count; - } +/// Append a window ID to a D-Bus message as a variant +static bool cdbus_append_wid_variant(DBusMessage *msg, xcb_window_t val_) { + cdbus_window_t val = val_; - if (!count) { - // Nothing to append - return true; + DBusMessageIter it, it2; + dbus_message_iter_init_append(msg, &it); + if (!dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, + CDBUS_TYPE_WINDOW_STR, &it2)) { + return false; } - - // Allocate memory for an array of window IDs - auto arr = ccalloc(count, cdbus_window_t); - - // Build the array - cdbus_window_t *pcur = arr; - HASH_ITER2(ps->windows, w) { - assert(!w->destroyed); - *pcur = w->id; - ++pcur; + if (!dbus_message_iter_append_basic(&it2, CDBUS_TYPE_WINDOW, &val)) { + return false; } - assert(pcur == arr + count); - - // Append arguments - if (!dbus_message_append_args(msg, DBUS_TYPE_ARRAY, CDBUS_TYPE_WINDOW, &arr, - count, DBUS_TYPE_INVALID)) { - log_error("Failed to append argument."); - free(arr); + if (!dbus_message_iter_close_container(&it, &it2)) { return false; } - free(arr); return true; } -///@} -/** - * Send a D-Bus signal. - * - * @param ps current session - * @param name signal name - * @param func a function that modifies the built message, to, for example, - * add an argument - * @param data data pointer to pass to the function - */ -static bool cdbus_signal(session_t *ps, const char *name, - bool (*func)(session_t *ps, DBusMessage *msg, const void *data), - const void *data) { - struct cdbus_data *cd = ps->dbus_data; - DBusMessage *msg = NULL; - - // Create a signal - msg = dbus_message_new_signal(CDBUS_OBJECT_NAME, CDBUS_INTERFACE_NAME, name); - if (!msg) { - log_error("Failed to create D-Bus signal."); +/// Append a boolean to a D-Bus message as a variant +static bool cdbus_append_bool_variant(DBusMessage *msg, dbus_bool_t val) { + DBusMessageIter it, it2; + dbus_message_iter_init_append(msg, &it); + if (!dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, &it2)) { return false; } - - // Append arguments onto message - if (func && !func(ps, msg, data)) { - dbus_message_unref(msg); + if (!dbus_message_iter_append_basic(&it2, DBUS_TYPE_BOOLEAN, &val)) { return false; } - - // Send the message and flush the connection - if (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { - log_error("Failed to send D-Bus signal."); - dbus_message_unref(msg); + if (!dbus_message_iter_close_container(&it, &it2)) { return false; } - dbus_connection_flush(cd->dbus_conn); - - // Free the message - dbus_message_unref(msg); return true; } -/** - * Send a signal with a Window ID as argument. - */ -static inline bool cdbus_signal_wid(session_t *ps, const char *name, xcb_window_t wid) { - return cdbus_signal(ps, name, cdbus_apdarg_wid, &wid); -} +/// Append a string to a D-Bus message as a variant +static bool cdbus_append_string_variant(DBusMessage *msg, const char *data) { + const char *str = data ? data : ""; -/** - * Send a D-Bus reply. - * - * @param ps current session - * @param srcmsg original message - * @param func a function that modifies the built message, to, for example, - * add an argument - * @param data data pointer to pass to the function - */ -static bool cdbus_reply(session_t *ps, DBusMessage *srcmsg, - bool (*func)(session_t *ps, DBusMessage *msg, const void *data), - const void *data) { - struct cdbus_data *cd = ps->dbus_data; - DBusMessage *msg = NULL; - - // Create a reply - msg = dbus_message_new_method_return(srcmsg); - if (!msg) { - log_error("Failed to create D-Bus reply."); + DBusMessageIter it, it2; + dbus_message_iter_init_append(msg, &it); + if (!dbus_message_iter_open_container(&it, DBUS_TYPE_VARIANT, + DBUS_TYPE_STRING_AS_STRING, &it2)) { return false; } - - // Append arguments onto message - if (func && !func(ps, msg, data)) { - dbus_message_unref(msg); + if (!dbus_message_iter_append_basic(&it2, DBUS_TYPE_STRING, &str)) { + log_error("Failed to append argument."); return false; } - - // Send the message and flush the connection - if (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { - log_error("Failed to send D-Bus reply."); - dbus_message_unref(msg); + if (!dbus_message_iter_close_container(&it, &it2)) { return false; } - dbus_connection_flush(cd->dbus_conn); - - // Free the message - dbus_message_unref(msg); return true; } -/** - * Send a reply with a bool argument. - */ -static inline bool cdbus_reply_bool(session_t *ps, DBusMessage *srcmsg, bool bval) { - return cdbus_reply(ps, srcmsg, cdbus_apdarg_bool, &bval); -} - -/** - * Send a reply with an int32 argument. - */ -static inline bool cdbus_reply_int32(session_t *ps, DBusMessage *srcmsg, int32_t val) { - return cdbus_reply(ps, srcmsg, cdbus_apdarg_int32, &val); -} - -/** - * Send a reply with an int32 argument, cast from a long. - */ -static inline bool cdbus_reply_int32l(session_t *ps, DBusMessage *srcmsg, long val) { - int32_t tmp = (int32_t)val; - return cdbus_reply(ps, srcmsg, cdbus_apdarg_int32, &tmp); -} - -/** - * Send a reply with an uint32 argument. - */ -static inline bool cdbus_reply_uint32(session_t *ps, DBusMessage *srcmsg, uint32_t val) { - return cdbus_reply(ps, srcmsg, cdbus_apdarg_uint32, &val); -} - -/** - * Send a reply with a double argument. - */ -static inline bool cdbus_reply_double(session_t *ps, DBusMessage *srcmsg, double val) { - return cdbus_reply(ps, srcmsg, cdbus_apdarg_double, &val); -} - -/** - * Send a reply with a wid argument. - */ -static inline bool cdbus_reply_wid(session_t *ps, DBusMessage *srcmsg, xcb_window_t wid) { - return cdbus_reply(ps, srcmsg, cdbus_apdarg_wid, &wid); -} - -/** - * Send a reply with a string argument. - */ -static inline bool cdbus_reply_string(session_t *ps, DBusMessage *srcmsg, const char *str) { - return cdbus_reply(ps, srcmsg, cdbus_apdarg_string, str); +static int cdbus_append_wids_callback(struct win *w, void *data) { + DBusMessageIter *iter = data; + cdbus_window_t wid = w->id; + if (!dbus_message_iter_append_basic(iter, CDBUS_TYPE_WINDOW, &wid)) { + return 1; + } + return 0; } -/** - * Send a reply with a enum argument. - */ -static inline bool cdbus_reply_enum(session_t *ps, DBusMessage *srcmsg, cdbus_enum_t eval) { - return cdbus_reply(ps, srcmsg, cdbus_apdarg_enum, &eval); -} +/// Append all window IDs in the window list of a session to a D-Bus message +static bool cdbus_append_wids(DBusMessage *msg, session_t *ps) { + // Get the number of wids we are to include + unsigned count = wm_num_windows(ps->wm); + if (!count) { + // Nothing to append + return true; + } -/** - * Send a D-Bus error reply. - * - * @param ps current session - * @param msg the new error DBusMessage - */ -static bool cdbus_reply_errm(session_t *ps, DBusMessage *msg) { - struct cdbus_data *cd = ps->dbus_data; - if (!msg) { - log_error("Failed to create D-Bus reply."); + DBusMessageIter it, subit; + dbus_message_iter_init_append(msg, &it); + if (!dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, + DBUS_TYPE_UINT32_AS_STRING, &subit)) { + log_error("Failed to open container."); return false; } - // Send the message and flush the connection - if (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { - log_error("Failed to send D-Bus reply."); - dbus_message_unref(msg); + auto result = wm_foreach(ps->wm, cdbus_append_wids_callback, &subit); + if (!dbus_message_iter_close_container(&it, &subit)) { + log_error("Failed to close container."); + return false; + } + if (result != 0) { + log_error("Failed to append argument."); return false; } - dbus_connection_flush(cd->dbus_conn); - - // Free the message - dbus_message_unref(msg); - return true; } @@ -726,334 +554,378 @@ static bool cdbus_msg_get_arg(DBusMessage *msg, int count, const int type, void /** * Process a list_win D-Bus request. */ -static bool cdbus_process_list_win(session_t *ps, DBusMessage *msg) { - cdbus_reply(ps, msg, cdbus_apdarg_wids, NULL); - - return true; +static DBusHandlerResult +cdbus_process_list_win(session_t *ps, DBusMessage *msg attr_unused, DBusMessage *reply, + DBusError *err attr_unused) { + if (!cdbus_append_wids(reply, ps)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; } /** * Process a win_get D-Bus request. */ -static bool cdbus_process_win_get(session_t *ps, DBusMessage *msg) { - cdbus_window_t wid = XCB_NONE; +static DBusHandlerResult +cdbus_process_window_property_get(session_t *ps, DBusMessage *msg, cdbus_window_t wid, + DBusMessage *reply, DBusError *e) { const char *target = NULL; - DBusError err = {}; + const char *interface = NULL; - if (!dbus_message_get_args(msg, &err, CDBUS_TYPE_WINDOW, &wid, DBUS_TYPE_STRING, + if (reply == NULL) { + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (!dbus_message_get_args(msg, e, DBUS_TYPE_STRING, &interface, DBUS_TYPE_STRING, &target, DBUS_TYPE_INVALID)) { - log_error("Failed to parse argument of \"win_get\" (%s).", err.message); - dbus_error_free(&err); - return false; + log_debug("Failed to parse argument of \"Get\" (%s).", e->message); + dbus_set_error_const(e, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; } - auto w = find_managed_win(ps, wid); + if (interface[0] != '\0' && strcmp(interface, PICOM_WINDOW_INTERFACE) != 0) { + dbus_set_error_const(e, DBUS_ERROR_UNKNOWN_INTERFACE, NULL); + return DBUS_HANDLER_RESULT_HANDLED; + } + + auto w = wm_find_managed(ps->wm, wid); if (!w) { - log_error("Window %#010x not found.", wid); - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); - return true; + log_debug("Window %#010x not found.", wid); + dbus_set_error(e, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); + return DBUS_HANDLER_RESULT_HANDLED; } -#define cdbus_m_win_get_do(tgt, apdarg_func) \ +#define append(tgt, type, expr) \ if (!strcmp(#tgt, target)) { \ - apdarg_func(ps, msg, w->tgt); \ - return true; \ + if (!cdbus_append_##type(reply, expr)) { \ + return DBUS_HANDLER_RESULT_NEED_MEMORY; \ + } \ + return DBUS_HANDLER_RESULT_HANDLED; \ + } +#define append_win_property(name, member, type) append(name, type, w->member) + + append(Mapped, bool_variant, w->state == WSTATE_MAPPED); + append(Id, wid_variant, w->base.id); + append(Type, string_variant, WINTYPES[w->window_type].name); + append(RawFocused, bool_variant, win_is_focused_raw(w)); + append_win_property(ClientWin, client_win, wid_variant); + append_win_property(Leader, leader, wid_variant); + append_win_property(Name, name, string_variant); + + if (!strcmp("Next", target)) { + cdbus_window_t next_id = 0; + if (!list_node_is_last(wm_stack_end(ps->wm), &w->base.stack_neighbour)) { + next_id = list_entry(w->base.stack_neighbour.next, struct win, + stack_neighbour) + ->id; + } + if (!cdbus_append_wid_variant(reply, next_id)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; } - cdbus_m_win_get_do(base.id, cdbus_reply_wid); +#undef append_win_property +#undef append - // next - if (!strcmp("next", target)) { - cdbus_reply_wid( - ps, msg, - (list_node_is_last(&ps->window_stack, &w->base.stack_neighbour) - ? 0 - : list_entry(w->base.stack_neighbour.next, struct win, stack_neighbour) - ->id)); - return true; - } + log_debug(CDBUS_ERROR_BADTGT_S, target); + dbus_set_error(e, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); - // map_state - if (!strcmp("map_state", target)) { - cdbus_reply_bool(ps, msg, w->a.map_state); - return true; - } + return DBUS_HANDLER_RESULT_HANDLED; +} - cdbus_m_win_get_do(mode, cdbus_reply_enum); - cdbus_m_win_get_do(client_win, cdbus_reply_wid); - cdbus_m_win_get_do(ever_damaged, cdbus_reply_bool); - cdbus_m_win_get_do(window_type, cdbus_reply_enum); - cdbus_m_win_get_do(wmwin, cdbus_reply_bool); - cdbus_m_win_get_do(leader, cdbus_reply_wid); - if (!strcmp("focused_raw", target)) { - cdbus_reply_bool(ps, msg, win_is_focused_raw(ps, w)); - return true; - } - cdbus_m_win_get_do(fade_force, cdbus_reply_enum); - cdbus_m_win_get_do(shadow_force, cdbus_reply_enum); - cdbus_m_win_get_do(focused_force, cdbus_reply_enum); - cdbus_m_win_get_do(invert_color_force, cdbus_reply_enum); - cdbus_m_win_get_do(name, cdbus_reply_string); - cdbus_m_win_get_do(class_instance, cdbus_reply_string); - cdbus_m_win_get_do(class_general, cdbus_reply_string); - cdbus_m_win_get_do(role, cdbus_reply_string); - - cdbus_m_win_get_do(opacity, cdbus_reply_double); - cdbus_m_win_get_do(opacity_target, cdbus_reply_double); - cdbus_m_win_get_do(has_opacity_prop, cdbus_reply_bool); - cdbus_m_win_get_do(opacity_prop, cdbus_reply_uint32); - cdbus_m_win_get_do(opacity_is_set, cdbus_reply_bool); - cdbus_m_win_get_do(opacity_set, cdbus_reply_double); - - cdbus_m_win_get_do(frame_opacity, cdbus_reply_double); - if (!strcmp("left_width", target)) { - cdbus_reply_int32(ps, msg, w->frame_extents.left); - return true; +static DBusHandlerResult cdbus_process_reset(session_t *ps, DBusMessage *msg attr_unused, + DBusMessage *reply, DBusError *e attr_unused) { + // Reset the compositor + log_info("picom is resetting..."); + ev_break(ps->loop, EVBREAK_ALL); + if (reply != NULL && !cdbus_append_boolean(reply, true)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; } - if (!strcmp("right_width", target)) { - cdbus_reply_int32(ps, msg, w->frame_extents.right); - return true; + return DBUS_HANDLER_RESULT_HANDLED; +} + +static DBusHandlerResult cdbus_process_repaint(session_t *ps, DBusMessage *msg attr_unused, + DBusMessage *reply, DBusError *e attr_unused) { + // Reset the compositor + force_repaint(ps); + if (reply != NULL && !cdbus_append_boolean(reply, true)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; } - if (!strcmp("top_width", target)) { - cdbus_reply_int32(ps, msg, w->frame_extents.top); - return true; + return DBUS_HANDLER_RESULT_HANDLED; +} + +/** + * Process a win_get D-Bus request. + */ +static DBusHandlerResult +cdbus_process_win_get(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { + cdbus_window_t wid = XCB_NONE; + const char *target = NULL; + + if (reply == NULL) { + return DBUS_HANDLER_RESULT_HANDLED; } - if (!strcmp("bottom_width", target)) { - cdbus_reply_int32(ps, msg, w->frame_extents.bottom); - return true; + + if (!dbus_message_get_args(msg, err, CDBUS_TYPE_WINDOW, &wid, DBUS_TYPE_STRING, + &target, DBUS_TYPE_INVALID)) { + log_debug("Failed to parse argument of \"win_get\" (%s).", err->message); + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; } - cdbus_m_win_get_do(shadow, cdbus_reply_bool); - cdbus_m_win_get_do(invert_color, cdbus_reply_bool); - cdbus_m_win_get_do(blur_background, cdbus_reply_bool); -#undef cdbus_m_win_get_do + auto w = wm_find_managed(ps->wm, wid); - log_error(CDBUS_ERROR_BADTGT_S, target); - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + if (!w) { + log_debug("Window %#010x not found.", wid); + dbus_set_error(err, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); + return DBUS_HANDLER_RESULT_HANDLED; + } - return true; +#define append(tgt, type, expr) \ + if (strcmp(#tgt, target) == 0) { \ + if (!cdbus_append_##type(reply, expr)) { \ + return DBUS_HANDLER_RESULT_NEED_MEMORY; \ + } \ + return DBUS_HANDLER_RESULT_HANDLED; \ + } +#define append_win_property(tgt, type) append(tgt, type, w->tgt) + + if (!strcmp("next", target)) { + xcb_window_t next_id = + list_node_is_last(wm_stack_end(ps->wm), &w->base.stack_neighbour) + ? 0 + : list_entry(w->base.stack_neighbour.next, struct win, stack_neighbour) + ->id; + if (!cdbus_append_wid(reply, next_id)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; + } + + append(id, boolean, w->base.id); + append(map_state, boolean, w->a.map_state); + append(wmwin, boolean, win_is_wmwin(w)); + append(focused_raw, boolean, win_is_focused_raw(w)); + append(opacity, double, animatable_get(&w->opacity)); + append(left_width, int32, w->frame_extents.left); + append(right_width, int32, w->frame_extents.right); + append(top_width, int32, w->frame_extents.top); + append(bottom_width, int32, w->frame_extents.bottom); + + append_win_property(mode, enum); + append_win_property(client_win, wid); + append_win_property(ever_damaged, boolean); + append_win_property(window_type, enum); + append_win_property(leader, wid); + append_win_property(fade_force, enum); + append_win_property(shadow_force, enum); + append_win_property(focused_force, enum); + append_win_property(invert_color_force, enum); + append_win_property(name, string); + append_win_property(class_instance, string); + append_win_property(class_general, string); + append_win_property(role, string); + append_win_property(opacity.target, double); + append_win_property(has_opacity_prop, boolean); + append_win_property(opacity_prop, uint32); + append_win_property(opacity_is_set, boolean); + append_win_property(opacity_set, double); + append_win_property(frame_opacity, double); + append_win_property(shadow, boolean); + append_win_property(invert_color, boolean); + append_win_property(blur_background, boolean); + +#undef append_win_property +#undef append + + log_debug(CDBUS_ERROR_BADTGT_S, target); + dbus_set_error(err, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + return DBUS_HANDLER_RESULT_HANDLED; } /** * Process a win_set D-Bus request. */ -static bool cdbus_process_win_set(session_t *ps, DBusMessage *msg) { +static DBusHandlerResult +cdbus_process_win_set(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { cdbus_window_t wid = XCB_NONE; const char *target = NULL; - DBusError err = {}; - if (!dbus_message_get_args(msg, &err, CDBUS_TYPE_WINDOW, &wid, DBUS_TYPE_STRING, + if (!dbus_message_get_args(msg, err, CDBUS_TYPE_WINDOW, &wid, DBUS_TYPE_STRING, &target, DBUS_TYPE_INVALID)) { - log_error("(): Failed to parse argument of \"win_set\" (%s).", err.message); - dbus_error_free(&err); - return false; + log_debug("Failed to parse argument of \"win_set\" (%s).", err->message); + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; } - auto w = find_managed_win(ps, wid); + auto w = wm_find_managed(ps->wm, wid); if (!w) { - log_error("Window %#010x not found.", wid); - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); - return true; + log_debug("Window %#010x not found.", wid); + dbus_set_error(err, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); + return DBUS_HANDLER_RESULT_HANDLED; } - -#define cdbus_m_win_set_do(tgt, type, real_type) \ - if (!strcmp(MSTR(tgt), target)) { \ - real_type val; \ - if (!cdbus_msg_get_arg(msg, 2, type, &val)) \ - return false; \ - w->tgt = val; \ - goto cdbus_process_win_set_success; \ + cdbus_enum_t val = UNSET; + if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) { + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; } if (!strcmp("shadow_force", target)) { - cdbus_enum_t val = UNSET; - if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) - return false; win_set_shadow_force(ps, w, val); - goto cdbus_process_win_set_success; - } - - if (!strcmp("fade_force", target)) { - cdbus_enum_t val = UNSET; - if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) - return false; + } else if (!strcmp("fade_force", target)) { win_set_fade_force(w, val); - goto cdbus_process_win_set_success; - } - - if (!strcmp("focused_force", target)) { - cdbus_enum_t val = UNSET; - if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) - return false; + } else if (!strcmp("focused_force", target)) { win_set_focused_force(ps, w, val); - goto cdbus_process_win_set_success; - } - - if (!strcmp("invert_color_force", target)) { - cdbus_enum_t val = UNSET; - if (!cdbus_msg_get_arg(msg, 2, CDBUS_TYPE_ENUM, &val)) - return false; + } else if (!strcmp("invert_color_force", target)) { win_set_invert_color_force(ps, w, val); - goto cdbus_process_win_set_success; + } else { + log_debug(CDBUS_ERROR_BADTGT_S, target); + dbus_set_error(err, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + return DBUS_HANDLER_RESULT_HANDLED; } -#undef cdbus_m_win_set_do - - log_error(CDBUS_ERROR_BADTGT_S, target); - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); - return true; - -cdbus_process_win_set_success: - if (!dbus_message_get_no_reply(msg)) - cdbus_reply_bool(ps, msg, true); - return true; + if (reply != NULL && !cdbus_append_boolean(reply, true)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; } /** * Process a find_win D-Bus request. */ -static bool cdbus_process_find_win(session_t *ps, DBusMessage *msg) { - const char *target = NULL; +static DBusHandlerResult +cdbus_process_find_win(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { + if (reply == NULL) { + return DBUS_HANDLER_RESULT_HANDLED; + } - if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) - return false; + const char *target = NULL; + if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) { + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; + } xcb_window_t wid = XCB_NONE; - // Find window by client window if (!strcmp("client", target)) { + // Find window by client window cdbus_window_t client = XCB_NONE; - if (!cdbus_msg_get_arg(msg, 1, CDBUS_TYPE_WINDOW, &client)) - return false; - auto w = find_toplevel(ps, client); + if (!cdbus_msg_get_arg(msg, 1, CDBUS_TYPE_WINDOW, &client)) { + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; + } + auto w = wm_find_by_client(ps->wm, client); if (w) { wid = w->base.id; } - } - // Find focused window - else if (!strcmp("focused", target)) { - if (ps->active_win && ps->active_win->state != WSTATE_UNMAPPED) { - wid = ps->active_win->base.id; + } else if (!strcmp("focused", target)) { + // Find focused window + auto active_win = wm_active_win(ps->wm); + if (active_win && active_win->state != WSTATE_UNMAPPED) { + wid = active_win->base.id; } } else { - log_error(CDBUS_ERROR_BADTGT_S, target); - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); - - return true; + log_debug(CDBUS_ERROR_BADTGT_S, target); + dbus_set_error(err, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + return DBUS_HANDLER_RESULT_HANDLED; } - cdbus_reply_wid(ps, msg, wid); - - return true; + if (reply != NULL && !cdbus_append_wid(reply, wid)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; } /** * Process a opts_get D-Bus request. */ -static bool cdbus_process_opts_get(session_t *ps, DBusMessage *msg) { - const char *target = NULL; - - if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) - return false; - -#define cdbus_m_opts_get_do(tgt, apdarg_func) \ - if (!strcmp(#tgt, target)) { \ - apdarg_func(ps, msg, ps->o.tgt); \ - return true; \ - } - -#define cdbus_m_opts_get_stub(tgt, apdarg_func, ret) \ - if (!strcmp(#tgt, target)) { \ - apdarg_func(ps, msg, ret); \ - return true; \ - } - - // version - if (!strcmp("version", target)) { - cdbus_reply_string(ps, msg, COMPTON_VERSION); - return true; - } - - // pid - if (!strcmp("pid", target)) { - cdbus_reply_int32(ps, msg, getpid()); - return true; - } - - // display - if (!strcmp("display", target)) { - cdbus_reply_string(ps, msg, DisplayString(ps->dpy)); - return true; +static DBusHandlerResult +cdbus_process_opts_get(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { + if (reply == NULL) { + return DBUS_HANDLER_RESULT_HANDLED; } - cdbus_m_opts_get_stub(config_file, cdbus_reply_string, "Unknown"); - cdbus_m_opts_get_do(write_pid_path, cdbus_reply_string); - cdbus_m_opts_get_do(mark_wmwin_focused, cdbus_reply_bool); - cdbus_m_opts_get_do(mark_ovredir_focused, cdbus_reply_bool); - cdbus_m_opts_get_do(detect_rounded_corners, cdbus_reply_bool); - cdbus_m_opts_get_stub(paint_on_overlay, cdbus_reply_bool, ps->overlay != XCB_NONE); - // paint_on_overlay_id: Get ID of the X composite overlay window - if (!strcmp("paint_on_overlay_id", target)) { - cdbus_reply_uint32(ps, msg, ps->overlay); - return true; - } - cdbus_m_opts_get_do(unredir_if_possible, cdbus_reply_bool); - cdbus_m_opts_get_do(unredir_if_possible_delay, cdbus_reply_int32l); - cdbus_m_opts_get_do(redirected_force, cdbus_reply_enum); - cdbus_m_opts_get_do(stoppaint_force, cdbus_reply_enum); - cdbus_m_opts_get_do(logpath, cdbus_reply_string); - - cdbus_m_opts_get_do(refresh_rate, cdbus_reply_int32); - cdbus_m_opts_get_do(sw_opti, cdbus_reply_bool); - cdbus_m_opts_get_do(vsync, cdbus_reply_bool); - if (!strcmp("backend", target)) { - assert(ps->o.backend < sizeof(BACKEND_STRS) / sizeof(BACKEND_STRS[0])); - cdbus_reply_string(ps, msg, BACKEND_STRS[ps->o.backend]); - return true; + const char *target = NULL; + if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) { + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; } + assert((size_t)ps->o.backend < sizeof(BACKEND_STRS) / sizeof(BACKEND_STRS[0])); - cdbus_m_opts_get_do(shadow_red, cdbus_reply_double); - cdbus_m_opts_get_do(shadow_green, cdbus_reply_double); - cdbus_m_opts_get_do(shadow_blue, cdbus_reply_double); - cdbus_m_opts_get_do(shadow_radius, cdbus_reply_int32); - cdbus_m_opts_get_do(shadow_offset_x, cdbus_reply_int32); - cdbus_m_opts_get_do(shadow_offset_y, cdbus_reply_int32); - cdbus_m_opts_get_do(shadow_opacity, cdbus_reply_double); - cdbus_m_opts_get_do(xinerama_shadow_crop, cdbus_reply_bool); - - cdbus_m_opts_get_do(fade_delta, cdbus_reply_int32); - cdbus_m_opts_get_do(fade_in_step, cdbus_reply_double); - cdbus_m_opts_get_do(fade_out_step, cdbus_reply_double); - cdbus_m_opts_get_do(no_fading_openclose, cdbus_reply_bool); - - cdbus_m_opts_get_do(blur_method, cdbus_reply_bool); - cdbus_m_opts_get_do(blur_background_frame, cdbus_reply_bool); - cdbus_m_opts_get_do(blur_background_fixed, cdbus_reply_bool); - - cdbus_m_opts_get_do(inactive_dim, cdbus_reply_double); - cdbus_m_opts_get_do(inactive_dim_fixed, cdbus_reply_bool); - - cdbus_m_opts_get_do(max_brightness, cdbus_reply_double); - - cdbus_m_opts_get_do(use_ewmh_active_win, cdbus_reply_bool); - cdbus_m_opts_get_do(detect_transient, cdbus_reply_bool); - cdbus_m_opts_get_do(detect_client_leader, cdbus_reply_bool); - cdbus_m_opts_get_do(use_damage, cdbus_reply_bool); +#define append(tgt, type, ret) \ + if (!strcmp(#tgt, target)) { \ + if (reply != NULL && !cdbus_append_##type(reply, ret)) { \ + return DBUS_HANDLER_RESULT_NEED_MEMORY; \ + } \ + return DBUS_HANDLER_RESULT_HANDLED; \ + } +#define append_session_option(tgt, type) append(tgt, type, ps->o.tgt) + + append(version, string, PICOM_VERSION); + append(pid, int32, getpid()); + append(display, string, DisplayString(ps->c.dpy)); + append(config_file, string, "Unknown"); + append(paint_on_overlay, boolean, ps->overlay != XCB_NONE); + append(paint_on_overlay_id, uint32, ps->overlay); // Sending ID of the X + // composite overlay + // window + append(unredir_if_possible_delay, int32, (int32_t)ps->o.unredir_if_possible_delay); + append(refresh_rate, int32, 0); + append(sw_opti, boolean, false); + append(backend, string, BACKEND_STRS[ps->o.backend]); + + append_session_option(unredir_if_possible, boolean); + append_session_option(write_pid_path, string); + append_session_option(mark_wmwin_focused, boolean); + append_session_option(mark_ovredir_focused, boolean); + append_session_option(detect_rounded_corners, boolean); + append_session_option(redirected_force, enum); + append_session_option(stoppaint_force, enum); + append_session_option(logpath, string); + append_session_option(vsync, boolean); + append_session_option(shadow_red, double); + append_session_option(shadow_green, double); + append_session_option(shadow_blue, double); + append_session_option(shadow_radius, int32); + append_session_option(shadow_offset_x, int32); + append_session_option(shadow_offset_y, int32); + append_session_option(shadow_opacity, double); + append_session_option(crop_shadow_to_monitor, boolean); + + append_session_option(fade_delta, int32); + append_session_option(fade_in_step, double); + append_session_option(fade_out_step, double); + append_session_option(no_fading_openclose, boolean); + + append_session_option(blur_method, boolean); + append_session_option(blur_background_frame, boolean); + append_session_option(blur_background_fixed, boolean); + + append_session_option(inactive_dim, double); + append_session_option(inactive_dim_fixed, boolean); + + append_session_option(max_brightness, double); + + append_session_option(use_ewmh_active_win, boolean); + append_session_option(detect_transient, boolean); + append_session_option(detect_client_leader, boolean); + append_session_option(use_damage, boolean); #ifdef CONFIG_OPENGL - cdbus_m_opts_get_do(glx_no_stencil, cdbus_reply_bool); - cdbus_m_opts_get_do(glx_no_rebind_pixmap, cdbus_reply_bool); + append_session_option(glx_no_stencil, boolean); + append_session_option(glx_no_rebind_pixmap, boolean); #endif -#undef cdbus_m_opts_get_do -#undef cdbus_m_opts_get_stub +#undef append_session_option +#undef append - log_error(CDBUS_ERROR_BADTGT_S, target); - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + log_debug(CDBUS_ERROR_BADTGT_S, target); + dbus_set_error(err, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); - return true; + return DBUS_HANDLER_RESULT_HANDLED; } // XXX Remove this after header clean up @@ -1062,66 +934,42 @@ void queue_redraw(session_t *ps); /** * Process a opts_set D-Bus request. */ -static bool cdbus_process_opts_set(session_t *ps, DBusMessage *msg) { +static DBusHandlerResult +cdbus_process_opts_set(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { const char *target = NULL; - if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) - return false; - -#define cdbus_m_opts_set_do(tgt, type, real_type) \ - if (!strcmp(#tgt, target)) { \ - real_type val; \ - if (!cdbus_msg_get_arg(msg, 1, type, &val)) \ - return false; \ - ps->o.tgt = val; \ + if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &target)) { + log_error("Failed to parse argument of \"opts_set\" (%s).", err->message); + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); + return DBUS_HANDLER_RESULT_HANDLED; + } + +#define get_msg_arg(type, val) \ + if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_##type, &(val))) { \ + dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); \ + return DBUS_HANDLER_RESULT_HANDLED; \ + }; +#define opts_set_do(tgt, dbus_type, type, expr) \ + if (strcmp(#tgt, target) == 0) { \ + type val; \ + get_msg_arg(dbus_type, val); \ + ps->o.tgt = expr; \ goto cdbus_process_opts_set_success; \ } - // fade_delta - if (!strcmp("fade_delta", target)) { - int32_t val = 0; - if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_INT32, &val)) { - return false; - } - if (val <= 0) { - return false; - } - ps->o.fade_delta = max2(val, 1); - goto cdbus_process_opts_set_success; - } - - // fade_in_step - if (!strcmp("fade_in_step", target)) { - double val = 0.0; - if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_DOUBLE, &val)) - return false; - ps->o.fade_in_step = normalize_d(val); - goto cdbus_process_opts_set_success; - } - - // fade_out_step - if (!strcmp("fade_out_step", target)) { - double val = 0.0; - if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_DOUBLE, &val)) - return false; - ps->o.fade_out_step = normalize_d(val); + if (!strcmp("clear_shadow", target) || !strcmp("track_focus", target)) { goto cdbus_process_opts_set_success; } - // no_fading_openclose - if (!strcmp("no_fading_openclose", target)) { - dbus_bool_t val = FALSE; - if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_BOOLEAN, &val)) - return false; - opts_set_no_fading_openclose(ps, val); - goto cdbus_process_opts_set_success; - } + opts_set_do(fade_delta, INT32, int32_t, max2(val, 1)); + opts_set_do(fade_in_step, DOUBLE, double, normalize_d(val)); + opts_set_do(fade_out_step, DOUBLE, double, normalize_d(val)); + opts_set_do(no_fading_openclose, BOOLEAN, bool, val); + opts_set_do(stoppaint_force, UINT32, cdbus_enum_t, val); - // unredir_if_possible if (!strcmp("unredir_if_possible", target)) { dbus_bool_t val = FALSE; - if (!cdbus_msg_get_arg(msg, 1, DBUS_TYPE_BOOLEAN, &val)) - return false; + get_msg_arg(BOOLEAN, val); if (ps->o.unredir_if_possible != val) { ps->o.unredir_if_possible = val; queue_redraw(ps); @@ -1129,46 +977,32 @@ static bool cdbus_process_opts_set(session_t *ps, DBusMessage *msg) { goto cdbus_process_opts_set_success; } - // clear_shadow - if (!strcmp("clear_shadow", target)) { - goto cdbus_process_opts_set_success; - } - - // track_focus - if (!strcmp("track_focus", target)) { - goto cdbus_process_opts_set_success; - } - - // redirected_force if (!strcmp("redirected_force", target)) { cdbus_enum_t val = UNSET; - if (!cdbus_msg_get_arg(msg, 1, CDBUS_TYPE_ENUM, &val)) - return false; - ps->o.redirected_force = val; - force_repaint(ps); + get_msg_arg(UINT32, val); + if (ps->o.redirected_force != val) { + ps->o.redirected_force = val; + force_repaint(ps); + } goto cdbus_process_opts_set_success; } - - // stoppaint_force - cdbus_m_opts_set_do(stoppaint_force, CDBUS_TYPE_ENUM, cdbus_enum_t); - -#undef cdbus_m_opts_set_do +#undef get_msg_arg log_error(CDBUS_ERROR_BADTGT_S, target); - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); - - return true; + dbus_set_error(err, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); + return DBUS_HANDLER_RESULT_HANDLED; cdbus_process_opts_set_success: - if (!dbus_message_get_no_reply(msg)) - cdbus_reply_bool(ps, msg, true); - return true; + if (reply != NULL && !cdbus_append_boolean(reply, true)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; } /** * Process an Introspect D-Bus request. */ -static bool cdbus_process_introspect(session_t *ps, DBusMessage *msg) { +static DBusHandlerResult cdbus_process_introspect(DBusMessage *reply) { static const char *str_introspect = "\n" " \n" " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" " \n" + " \n" "\n"; - cdbus_reply_string(ps, msg, str_introspect); - - return true; + if (reply != NULL && !cdbus_append_string(reply, str_introspect)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; } ///@} +static int cdbus_process_windows_root_introspect_callback(struct win *w, void *data) { + char **introspect = data; + if (!w->managed) { + return 0; + } + char *tmp = NULL; + if (asprintf(&tmp, "\n", w->id) < 0) { + log_fatal("Failed to allocate memory."); + return 1; + } + mstrextend(introspect, tmp); + free(tmp); + return 0; +} + /** - * Process a message from D-Bus. + * Process an D-Bus Introspect request, for /windows. */ static DBusHandlerResult -cdbus_process(DBusConnection *c attr_unused, DBusMessage *msg, void *ud) { +cdbus_process_windows_root_introspect(session_t *ps, DBusMessage *reply) { + static const char *str_introspect = + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n"; + + if (reply == NULL) { + return DBUS_HANDLER_RESULT_HANDLED; + } + + scoped_charp introspect = NULL; + mstrextend(&introspect, str_introspect); + if (wm_foreach(ps->wm, cdbus_process_windows_root_introspect_callback, &introspect)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + mstrextend(&introspect, ""); + if (!cdbus_append_string(reply, introspect)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; +} + +static bool cdbus_process_window_introspect(DBusMessage *reply) { + // clang-format off + static const char *str_introspect = + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; + // clang-format on + if (reply != NULL && !cdbus_append_string(reply, str_introspect)) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + return DBUS_HANDLER_RESULT_HANDLED; +} + +/// Send a reply or an error message for request `msg`, appropriately based on the +/// `result` and whether `err` is set. Frees the error message and the reply message, and +/// flushes the connection. +static inline DBusHandlerResult +cdbus_send_reply_or_error(DBusConnection *conn, DBusHandlerResult result, + DBusMessage *msg, DBusMessage *reply, DBusError *err) { + if (dbus_error_is_set(err) && reply != NULL) { + // If error is set, send the error instead of the reply + dbus_message_unref(reply); + reply = dbus_message_new_error(msg, err->name, err->message); + if (reply == NULL) { + result = DBUS_HANDLER_RESULT_NEED_MEMORY; + } + } + if (result != DBUS_HANDLER_RESULT_HANDLED && reply != NULL) { + // We shouldn't send a reply if we didn't handle this message + dbus_message_unref(reply); + reply = NULL; + } + if (reply != NULL) { + if (!dbus_connection_send(conn, reply, NULL)) { + result = DBUS_HANDLER_RESULT_NEED_MEMORY; + } + dbus_message_unref(reply); + } + dbus_error_free(err); + dbus_connection_flush(conn); + return result; +} + +/** + * Process a message from D-Bus. + */ +static DBusHandlerResult cdbus_process(DBusConnection *conn, DBusMessage *msg, void *ud) { session_t *ps = ud; - bool handled = false; - -#define cdbus_m_ismethod(method) \ - dbus_message_is_method_call(msg, CDBUS_INTERFACE_NAME, method) - - if (cdbus_m_ismethod("reset")) { - log_info("picom is resetting..."); - ev_break(ps->loop, EVBREAK_ALL); - if (!dbus_message_get_no_reply(msg)) - cdbus_reply_bool(ps, msg, true); - handled = true; - } else if (cdbus_m_ismethod("repaint")) { - force_repaint(ps); - if (!dbus_message_get_no_reply(msg)) - cdbus_reply_bool(ps, msg, true); - handled = true; - } else if (cdbus_m_ismethod("list_win")) { - handled = cdbus_process_list_win(ps, msg); - } else if (cdbus_m_ismethod("win_get")) { - handled = cdbus_process_win_get(ps, msg); - } else if (cdbus_m_ismethod("win_set")) { - handled = cdbus_process_win_set(ps, msg); - } else if (cdbus_m_ismethod("find_win")) { - handled = cdbus_process_find_win(ps, msg); - } else if (cdbus_m_ismethod("opts_get")) { - handled = cdbus_process_opts_get(ps, msg); - } else if (cdbus_m_ismethod("opts_set")) { - handled = cdbus_process_opts_set(ps, msg); - } -#undef cdbus_m_ismethod - else if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Introspectable", - "Introspect")) { - handled = cdbus_process_introspect(ps, msg); - } else if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Peer", "Ping")) { - cdbus_reply(ps, msg, NULL, NULL); - handled = true; - } else if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Peer", - "GetMachineId")) { - char *uuid = dbus_get_local_machine_id(); - if (uuid) { - cdbus_reply_string(ps, msg, uuid); - dbus_free(uuid); - handled = true; + + if (dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameAcquired") || + dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameLost")) { + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_get_type(msg) == DBUS_MESSAGE_TYPE_ERROR) { + log_debug("Error message of path \"%s\" " + "interface \"%s\", member \"%s\", error \"%s\"", + dbus_message_get_path(msg), dbus_message_get_interface(msg), + dbus_message_get_member(msg), dbus_message_get_error_name(msg)); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_get_type(msg) != DBUS_MESSAGE_TYPE_METHOD_CALL) { + log_debug("Illegal message of type \"%s\", path \"%s\" " + "interface \"%s\", member \"%s\"", + cdbus_repr_msgtype(msg), dbus_message_get_path(msg), + dbus_message_get_interface(msg), dbus_message_get_member(msg)); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + DBusMessage *reply = NULL; + DBusError err; + DBusHandlerResult ret = DBUS_HANDLER_RESULT_HANDLED; + const char *interface = dbus_message_get_interface(msg); + const char *member = dbus_message_get_member(msg); + + dbus_error_init(&err); + if (!dbus_message_get_no_reply(msg)) { + reply = dbus_message_new_method_return(msg); + if (reply == NULL) { + log_error("Failed to create reply message."); + return DBUS_HANDLER_RESULT_NEED_MEMORY; } - } else if (dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameAcquired") || - dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameLost")) { - handled = true; + } + + if (dbus_message_is_method_call(msg, DBUS_INTERFACE_INTROSPECTABLE, "Introspect")) { + ret = cdbus_process_introspect(reply); + } else if (dbus_message_is_method_call(msg, DBUS_INTERFACE_PEER, "Ping")) { + // Intentionally left blank + } else if (dbus_message_is_method_call(msg, DBUS_INTERFACE_PEER, "GetMachineId")) { + if (reply != NULL) { + char *uuid = dbus_get_local_machine_id(); + if (uuid) { + if (!cdbus_append_string(reply, uuid)) { + ret = DBUS_HANDLER_RESULT_NEED_MEMORY; + } + dbus_free(uuid); + } else { + ret = DBUS_HANDLER_RESULT_NEED_MEMORY; + } + } + } else if (strcmp(interface, CDBUS_INTERFACE_NAME) != 0) { + dbus_set_error_const(&err, DBUS_ERROR_UNKNOWN_INTERFACE, NULL); } else { - if (DBUS_MESSAGE_TYPE_ERROR == dbus_message_get_type(msg)) { - log_error( - "Error message of path \"%s\" " - "interface \"%s\", member \"%s\", error \"%s\"", - dbus_message_get_path(msg), dbus_message_get_interface(msg), - dbus_message_get_member(msg), dbus_message_get_error_name(msg)); + static const struct { + const char *name; + DBusHandlerResult (*func)(session_t *ps, DBusMessage *msg, + DBusMessage *reply, DBusError *err); + } handlers[] = { + {"reset", cdbus_process_reset}, + {"repaint", cdbus_process_repaint}, + {"list_win", cdbus_process_list_win}, + {"win_get", cdbus_process_win_get}, + {"win_set", cdbus_process_win_set}, + {"find_win", cdbus_process_find_win}, + {"opts_get", cdbus_process_opts_get}, + {"opts_set", cdbus_process_opts_set}, + }; + + size_t i; + for (i = 0; i < ARR_SIZE(handlers); i++) { + if (strcmp(handlers[i].name, member) == 0) { + ret = handlers[i].func(ps, msg, reply, &err); + break; + } + } + if (i >= ARR_SIZE(handlers)) { + log_debug("Unknown method \"%s\".", member); + dbus_set_error_const(&err, CDBUS_ERROR_BADMSG, CDBUS_ERROR_BADMSG_S); + } + } + + return cdbus_send_reply_or_error(conn, ret, msg, reply, &err); +} + +/** + * Process a message from D-Bus, for /windows path. + */ +static DBusHandlerResult +cdbus_process_windows(DBusConnection *conn, DBusMessage *msg, void *ud) { + session_t *ps = ud; + DBusHandlerResult ret = DBUS_HANDLER_RESULT_HANDLED; + const char *path = dbus_message_get_path(msg); + const char *last_segment = strrchr(path, '/'); + DBusError err; + + dbus_error_init(&err); + + if (dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameAcquired") || + dbus_message_is_signal(msg, "org.freedesktop.DBus", "NameLost")) { + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_get_type(msg) == DBUS_MESSAGE_TYPE_ERROR) { + log_debug("Error message of path \"%s\" " + "interface \"%s\", member \"%s\", error \"%s\"", + dbus_message_get_path(msg), dbus_message_get_interface(msg), + dbus_message_get_member(msg), dbus_message_get_error_name(msg)); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_get_type(msg) != DBUS_MESSAGE_TYPE_METHOD_CALL) { + log_debug("Illegal message of type \"%s\", path \"%s\" " + "interface \"%s\", member \"%s\"", + cdbus_repr_msgtype(msg), dbus_message_get_path(msg), + dbus_message_get_interface(msg), dbus_message_get_member(msg)); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + + DBusMessage *reply = NULL; + const char *interface = dbus_message_get_interface(msg); + const char *member = dbus_message_get_member(msg); + if (!dbus_message_get_no_reply(msg)) { + reply = dbus_message_new_method_return(msg); + if (reply == NULL) { + log_error("Failed to create reply message."); + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + } + + if (last_segment == NULL) { + dbus_set_error_const(&err, CDBUS_ERROR_BADMSG, CDBUS_ERROR_BADMSG_S); + goto finished; + } + + bool is_root = strncmp(last_segment, "/windows", 8) == 0; + if (is_root) { + if (strcmp(interface, DBUS_INTERFACE_INTROSPECTABLE) == 0 && + strcmp(member, "Introspect") == 0) { + ret = cdbus_process_windows_root_introspect(ps, reply); } else { - log_error("Illegal message of type \"%s\", path \"%s\" " + log_debug("Illegal message of type \"%s\", path \"%s\" " "interface \"%s\", member \"%s\"", cdbus_repr_msgtype(msg), dbus_message_get_path(msg), dbus_message_get_interface(msg), dbus_message_get_member(msg)); + dbus_set_error_const(&err, CDBUS_ERROR_BADMSG, CDBUS_ERROR_BADMSG_S); } - if (DBUS_MESSAGE_TYPE_METHOD_CALL == dbus_message_get_type(msg) && - !dbus_message_get_no_reply(msg)) - cdbus_reply_err(ps, msg, CDBUS_ERROR_BADMSG, CDBUS_ERROR_BADMSG_S); - handled = true; + goto finished; + } + + char *endptr = NULL; + auto wid = (cdbus_window_t)strtol(last_segment + 1, &endptr, 0); + if (*endptr != '\0') { + log_error("Invalid window ID string \"%s\".", last_segment + 1); + dbus_set_error_const(&err, DBUS_ERROR_INVALID_ARGS, NULL); + } else if (strcmp(interface, DBUS_INTERFACE_INTROSPECTABLE) == 0 && + strcmp(member, "Introspect") == 0) { + ret = cdbus_process_window_introspect(reply); + } else if (strcmp(interface, DBUS_INTERFACE_PROPERTIES) == 0) { + if (strcmp(member, "GetAll") == 0 || strcmp(member, "Set") == 0) { + dbus_set_error_const(&err, DBUS_ERROR_NOT_SUPPORTED, NULL); + } else if (strcmp(member, "Get") == 0) { + ret = cdbus_process_window_property_get(ps, msg, wid, reply, &err); + } else { + log_debug( + "Unexpected member \"%s\" of dbus properties interface.", member); + dbus_set_error_const(&err, DBUS_ERROR_UNKNOWN_METHOD, NULL); + } + } else { + log_debug("Illegal message of type \"%s\", path \"%s\" " + "interface \"%s\", member \"%s\"", + cdbus_repr_msgtype(msg), dbus_message_get_path(msg), + dbus_message_get_interface(msg), dbus_message_get_member(msg)); + dbus_set_error_const(&err, CDBUS_ERROR_BADMSG, CDBUS_ERROR_BADMSG_S); + } + +finished: + return cdbus_send_reply_or_error(conn, ret, msg, reply, &err); +} + +/** + * Send a signal with a Window ID as argument. + * + * @param ps current session + * @param name signal name + * @param wid window ID + */ +static bool cdbus_signal_wid(struct cdbus_data *cd, const char *interface, + const char *name, xcb_window_t wid) { + DBusMessage *msg = dbus_message_new_signal(CDBUS_OBJECT_NAME, interface, name); + if (!msg) { + log_error("Failed to create D-Bus signal."); + return false; + } + + if (!cdbus_append_wid(msg, wid)) { + dbus_message_unref(msg); + return false; } - // If the message could not be processed, and an reply is expected, return - // an empty reply. - if (!handled && DBUS_MESSAGE_TYPE_METHOD_CALL == dbus_message_get_type(msg) && - !dbus_message_get_no_reply(msg)) { - cdbus_reply_err(ps, msg, CDBUS_ERROR_UNKNOWN, CDBUS_ERROR_UNKNOWN_S); - handled = true; + if (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { + log_error("Failed to send D-Bus signal."); + dbus_message_unref(msg); + return false; } - return handled ? DBUS_HANDLER_RESULT_HANDLED : DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + dbus_connection_flush(cd->dbus_conn); + dbus_message_unref(msg); + return true; } /** @name Core callbacks */ ///@{ -void cdbus_ev_win_added(session_t *ps, struct win *w) { - struct cdbus_data *cd = ps->dbus_data; - if (cd->dbus_conn) - cdbus_signal_wid(ps, "win_added", w->id); +void cdbus_ev_win_added(struct cdbus_data *cd, struct win *w) { + if (cd->dbus_conn) { + cdbus_signal_wid(cd, CDBUS_INTERFACE_NAME, "win_added", w->id); + cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinAdded", w->id); + } } -void cdbus_ev_win_destroyed(session_t *ps, struct win *w) { - struct cdbus_data *cd = ps->dbus_data; - if (cd->dbus_conn) - cdbus_signal_wid(ps, "win_destroyed", w->id); +void cdbus_ev_win_destroyed(struct cdbus_data *cd, struct win *w) { + if (cd->dbus_conn) { + cdbus_signal_wid(cd, CDBUS_INTERFACE_NAME, "win_destroyed", w->id); + cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinDestroyed", w->id); + } } -void cdbus_ev_win_mapped(session_t *ps, struct win *w) { - struct cdbus_data *cd = ps->dbus_data; - if (cd->dbus_conn) - cdbus_signal_wid(ps, "win_mapped", w->id); +void cdbus_ev_win_mapped(struct cdbus_data *cd, struct win *w) { + if (cd->dbus_conn) { + cdbus_signal_wid(cd, CDBUS_INTERFACE_NAME, "win_mapped", w->id); + cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinMapped", w->id); + } } -void cdbus_ev_win_unmapped(session_t *ps, struct win *w) { - struct cdbus_data *cd = ps->dbus_data; - if (cd->dbus_conn) - cdbus_signal_wid(ps, "win_unmapped", w->id); +void cdbus_ev_win_unmapped(struct cdbus_data *cd, struct win *w) { + if (cd->dbus_conn) { + cdbus_signal_wid(cd, CDBUS_INTERFACE_NAME, "win_unmapped", w->id); + cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinUnmapped", w->id); + } } -void cdbus_ev_win_focusout(session_t *ps, struct win *w) { - struct cdbus_data *cd = ps->dbus_data; - if (cd->dbus_conn) - cdbus_signal_wid(ps, "win_focusout", w->id); +void cdbus_ev_win_focusout(struct cdbus_data *cd, struct win *w) { + if (cd->dbus_conn) { + cdbus_signal_wid(cd, CDBUS_INTERFACE_NAME, "win_focusout", w->id); + } } -void cdbus_ev_win_focusin(session_t *ps, struct win *w) { - struct cdbus_data *cd = ps->dbus_data; - if (cd->dbus_conn) - cdbus_signal_wid(ps, "win_focusin", w->id); +void cdbus_ev_win_focusin(struct cdbus_data *cd, struct win *w) { + if (cd->dbus_conn) { + cdbus_signal_wid(cd, CDBUS_INTERFACE_NAME, "win_focusin", w->id); + } } //!@} diff --git a/src/dbus.h b/src/dbus.h index 54a58af52c..8541783a09 100644 --- a/src/dbus.h +++ b/src/dbus.h @@ -11,11 +11,12 @@ #include -#include - typedef struct session session_t; struct win; +struct cdbus_data; +#ifdef CONFIG_DBUS +#include /** * Return a string representation of a D-Bus message type. */ @@ -26,29 +27,57 @@ static inline const char *cdbus_repr_msgtype(DBusMessage *msg) { /** * Initialize D-Bus connection. */ -bool cdbus_init(session_t *ps, const char *uniq_name); +struct cdbus_data *cdbus_init(session_t *ps, const char *uniq_name); /** * Destroy D-Bus connection. */ -void cdbus_destroy(session_t *ps); +void cdbus_destroy(struct cdbus_data *cd); /// Generate dbus win_added signal -void cdbus_ev_win_added(session_t *ps, struct win *w); +void cdbus_ev_win_added(struct cdbus_data *cd, struct win *w); /// Generate dbus win_destroyed signal -void cdbus_ev_win_destroyed(session_t *ps, struct win *w); +void cdbus_ev_win_destroyed(struct cdbus_data *cd, struct win *w); /// Generate dbus win_mapped signal -void cdbus_ev_win_mapped(session_t *ps, struct win *w); +void cdbus_ev_win_mapped(struct cdbus_data *cd, struct win *w); /// Generate dbus win_unmapped signal -void cdbus_ev_win_unmapped(session_t *ps, struct win *w); +void cdbus_ev_win_unmapped(struct cdbus_data *cd, struct win *w); /// Generate dbus win_focusout signal -void cdbus_ev_win_focusout(session_t *ps, struct win *w); +void cdbus_ev_win_focusout(struct cdbus_data *cd, struct win *w); /// Generate dbus win_focusin signal -void cdbus_ev_win_focusin(session_t *ps, struct win *w); +void cdbus_ev_win_focusin(struct cdbus_data *cd, struct win *w); + +#else + +static inline void +cdbus_ev_win_unmapped(struct cdbus_data *cd attr_unused, struct win *w attr_unused) { +} + +static inline void +cdbus_ev_win_mapped(struct cdbus_data *cd attr_unused, struct win *w attr_unused) { +} + +static inline void +cdbus_ev_win_destroyed(struct cdbus_data *cd attr_unused, struct win *w attr_unused) { +} + +static inline void +cdbus_ev_win_added(struct cdbus_data *cd attr_unused, struct win *w attr_unused) { +} + +static inline void +cdbus_ev_win_focusout(struct cdbus_data *cd attr_unused, struct win *w attr_unused) { +} + +static inline void +cdbus_ev_win_focusin(struct cdbus_data *cd attr_unused, struct win *w attr_unused) { +} + +#endif // vim: set noet sw=8 ts=8 : diff --git a/src/diagnostic.c b/src/diagnostic.c index d275b1a17d..ce93cfc0c2 100644 --- a/src/diagnostic.c +++ b/src/diagnostic.c @@ -2,21 +2,21 @@ // Copyright (c) 2018 Yuxuan Shui #include -#include #include +#include #include "backend/driver.h" -#include "diagnostic.h" +#include "common.h" #include "config.h" +#include "diagnostic.h" #include "picom.h" -#include "common.h" void print_diagnostics(session_t *ps, const char *config_file, bool compositor_running) { - printf("**Version:** " COMPTON_VERSION "\n"); - //printf("**CFLAGS:** %s\n", "??"); + printf("**Version:** " PICOM_VERSION "\n"); + // printf("**CFLAGS:** %s\n", "??"); printf("\n### Extensions:\n\n"); printf("* Shape: %s\n", ps->shape_exists ? "Yes" : "No"); - printf("* XRandR: %s\n", ps->randr_exists ? "Yes" : "No"); + printf("* RandR: %s\n", ps->randr_exists ? "Yes" : "No"); printf("* Present: %s\n", ps->present_exists ? "Present" : "Not Present"); printf("\n### Misc:\n\n"); printf("* Use Overlay: %s\n", ps->overlay != XCB_NONE ? "Yes" : "No"); @@ -39,7 +39,7 @@ void print_diagnostics(session_t *ps, const char *config_file, bool compositor_r for (int i = 0; i < NUM_BKEND; i++) { if (backend_list[i] && backend_list[i]->diagnostics) { printf("\n### Backend: %s\n\n", BACKEND_STRS[i]); - auto data = backend_list[i]->init(ps); + auto data = backend_list[i]->init(ps, session_get_target_window(ps)); if (!data) { printf(" Cannot initialize this backend\n"); } else { diff --git a/src/event.c b/src/event.c index b022631127..fd671acf49 100644 --- a/src/event.c +++ b/src/event.c @@ -1,14 +1,18 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2019, Yuxuan Shui +#include #include #include #include #include #include +#include +#include #include "atom.h" +#include "c2.h" #include "common.h" #include "compiler.h" #include "config.h" @@ -16,8 +20,11 @@ #include "log.h" #include "picom.h" #include "region.h" +#include "types.h" #include "utils.h" #include "win.h" +#include "win_defs.h" +#include "wm.h" #include "x.h" /// Event handling with X is complicated. Handling events with other events possibly @@ -28,7 +35,7 @@ /// made the query when those events were already in the queue. so the reply you got is /// more up-to-date than the events). Also, handling events when other client are making /// concurrent requests is not good. Because the server states are changing without you -/// knowning them. This is super racy, and can cause lots of potential problems. +/// knowing them. This is super racy, and can cause lots of potential problems. /// /// All of above mandates we do these things: /// 1. Grab server when handling events @@ -46,8 +53,14 @@ /// When top half finished, we enter the render stage, where no server state should be /// queried. All rendering should be done with our internal knowledge of the server state. /// +/// P.S. There is another reason to avoid sending any request to the server as much as +/// possible. To make sure requests are sent, flushes are needed. And `xcb_flush`/`XFlush` +/// functions may read more events from the server into their queues. This is +/// undesirable, see the comments on `handle_queued_x_events` in picom.c for more details. -// TODO(yshui) the things described above +// TODO(yshui) the things described above. This is mostly done, maybe some of +// the functions here is still making unnecessary queries, we need +// to do some auditing to be sure. /** * Get a window's name from window ID. @@ -56,14 +69,14 @@ static inline const char *ev_window_name(session_t *ps, xcb_window_t wid) { char *name = ""; if (wid) { name = "(Failed to get title)"; - if (ps->root == wid) { + if (ps->c.screen_info->root == wid) { name = "(Root window)"; } else if (ps->overlay == wid) { name = "(Overlay)"; } else { - auto w = find_managed_win(ps, wid); + auto w = wm_find_managed(ps->wm, wid); if (!w) { - w = find_toplevel(ps, wid); + w = wm_find_by_client(ps->wm, wid); } if (w && w->name) { @@ -106,7 +119,7 @@ static inline xcb_window_t attr_pure ev_window(session_t *ps, xcb_generic_event_ static inline const char *ev_name(session_t *ps, xcb_generic_event_t *ev) { static char buf[128]; - switch (ev->response_type & 0x7f) { + switch (XCB_EVENT_RESPONSE_TYPE(ev)) { CASESTRRET(FocusIn); CASESTRRET(FocusOut); CASESTRRET(CreateNotify); @@ -121,11 +134,13 @@ static inline const char *ev_name(session_t *ps, xcb_generic_event_t *ev) { CASESTRRET(ClientMessage); } - if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) + if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) { return "Damage"; + } - if (ps->shape_exists && ev->response_type == ps->shape_event) + if (ps->shape_exists && ev->response_type == ps->shape_event) { return "ShapeNotify"; + } if (ps->xsync_exists) { int o = ev->response_type - ps->xsync_event; @@ -181,27 +196,44 @@ static inline void ev_focus_out(session_t *ps, xcb_focus_out_event_t *ev) { } static inline void ev_create_notify(session_t *ps, xcb_create_notify_event_t *ev) { - if (ev->parent == ps->root) { - add_win_top(ps, ev->window); + if (ev->parent == ps->c.screen_info->root) { + wm_stack_add_top(ps->wm, ev->window); + ps->pending_updates = true; + return; + } + + auto w = wm_find_managed(ps->wm, ev->parent); + if (w == NULL) { + // The parent window is not a toplevel window, we don't care about it. + // This can happen if a toplevel is reparented somewhere, and more events + // were generated from it before we can unsubscribe. + return; + } + // A direct child of a toplevel, subscribe to property changes so we can + // know if WM_STATE is set on this window. + wm_subwin_add_and_subscribe(ps->wm, &ps->c, ev->window, ev->parent); + if (w->client_win == XCB_NONE || w->client_win == w->base.id) { + win_set_flags(w, WIN_FLAGS_CLIENT_STALE); + ps->pending_updates = true; } } /// Handle configure event of a regular window static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { - auto w = find_win(ps, ce->window); + auto w = wm_find(ps->wm, ce->window); if (!w) { return; } + wm_stack_move_above(ps->wm, w, ce->above_sibling); + if (!w->managed) { - restack_above(ps, w, ce->above_sibling); return; } auto mw = (struct managed_win *)w; - - restack_above(ps, w, ce->above_sibling); + add_damage_from_win(ps, mw); // We check against pending_g here, because there might have been multiple // configure notifies in this cycle, or the window could receive multiple updates @@ -235,8 +267,8 @@ static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { win_set_flags(mw, WIN_FLAGS_SIZE_STALE); } - // Recalculate which screen this window is on - win_update_screen(ps->xinerama_nscrs, ps->xinerama_scr_regs, mw); + // Recalculate which monitor this window is on + win_update_monitor(&ps->monitors, mw); } // override_redirect flag cannot be changed after window creation, as far @@ -247,7 +279,7 @@ static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { static inline void ev_configure_notify(session_t *ps, xcb_configure_notify_event_t *ev) { log_debug("{ send_event: %d, id: %#010x, above: %#010x, override_redirect: %d }", ev->event, ev->window, ev->above_sibling, ev->override_redirect); - if (ev->window == ps->root) { + if (ev->window == ps->c.screen_info->root) { set_root_flags(ps, ROOT_FLAGS_CONFIGURED); } else { configure_win(ps, ev); @@ -255,25 +287,42 @@ static inline void ev_configure_notify(session_t *ps, xcb_configure_notify_event } static inline void ev_destroy_notify(session_t *ps, xcb_destroy_notify_event_t *ev) { - auto w = find_win(ps, ev->window); - auto mw = find_toplevel(ps, ev->window); + auto subwin = wm_subwin_find(ps->wm, ev->window); + if (subwin) { + wm_subwin_remove(ps->wm, subwin); + } + + auto w = wm_find(ps->wm, ev->window); + auto mw = wm_find_by_client(ps->wm, ev->window); if (mw && mw->client_win == mw->base.id) { // We only want _real_ frame window assert(&mw->base == w); mw = NULL; } + + // A window can't be a client window and a top-level window at the same time, + // so only one of `w` and `mw` can be non-NULL assert(w == NULL || mw == NULL); if (w != NULL) { - auto _ attr_unused = destroy_win_start(ps, w); - } else if (mw != NULL) { - win_unmark_client(ps, mw); + destroy_win_start(ps, w); + if (!w->managed || !win_as_managed(w)->to_paint) { + // If the window wasn't managed, or was already not rendered, + // we don't need to fade it out. + if (w->managed) { + win_skip_fading(win_as_managed(w)); + } + destroy_win_finish(ps, w); + } + return; + } + if (mw != NULL) { + win_unmark_client(mw); win_set_flags(mw, WIN_FLAGS_CLIENT_STALE); ps->pending_updates = true; - } else { - log_debug("Received a destroy notify from an unknown window, %#010x", - ev->window); + return; } + log_debug("Received a destroy notify from an unknown window, %#010x", ev->window); } static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { @@ -281,7 +330,8 @@ static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { // in redirected state. if (ps->overlay && ev->window == ps->overlay && !ps->redirected) { log_debug("Overlay is mapped while we are not redirected"); - auto e = xcb_request_check(ps->c, xcb_unmap_window(ps->c, ps->overlay)); + auto e = xcb_request_check( + ps->c.c, xcb_unmap_window_checked(ps->c.c, ps->overlay)); if (e) { log_error("Failed to unmap the overlay window"); free(e); @@ -290,12 +340,19 @@ static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { return; } - auto w = find_managed_win(ps, ev->window); + auto w = wm_find_managed(ps->wm, ev->window); if (!w) { return; } win_set_flags(w, WIN_FLAGS_MAPPED); + // We set `ever_damaged` to false here, instead of in `map_win_start`, + // because we might receive damage events before that function is called + // (which is called when we handle the `WIN_FLAGS_MAPPED` flag), in + // which case `repair_win` will be called, which uses `ever_damaged` so + // it needs to be correct. This also covers the case where the window is + // unmapped before `map_win_start` is called. + w->ever_damaged = false; // FocusIn/Out may be ignored when the window is unmapped, so we must // recheck focus here @@ -303,7 +360,7 @@ static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { } static inline void ev_unmap_notify(session_t *ps, xcb_unmap_notify_event_t *ev) { - auto w = find_managed_win(ps, ev->window); + auto w = wm_find_managed(ps->wm, ev->window); if (w) { unmap_win_start(ps, w); } @@ -312,89 +369,131 @@ static inline void ev_unmap_notify(session_t *ps, xcb_unmap_notify_event_t *ev) static inline void ev_reparent_notify(session_t *ps, xcb_reparent_notify_event_t *ev) { log_debug("Window %#010x has new parent: %#010x, override_redirect: %d", ev->window, ev->parent, ev->override_redirect); - auto w_top = find_toplevel(ps, ev->window); - if (w_top) { - win_unmark_client(ps, w_top); - win_set_flags(w_top, WIN_FLAGS_CLIENT_STALE); - ps->pending_updates = true; + + auto old_toplevel = wm_find_by_client(ps->wm, ev->window); + auto old_w = wm_find(ps->wm, ev->window); + auto old_subwin = wm_subwin_find(ps->wm, ev->window); + auto new_toplevel = wm_find_managed(ps->wm, ev->parent); + xcb_window_t old_parent = XCB_NONE; + if (old_subwin) { + old_parent = old_subwin->toplevel; + } else if (old_w) { + old_parent = ps->c.screen_info->root; + } + if (old_toplevel && old_toplevel->client_win == old_toplevel->base.id) { + // We only want _real_ frame window, meaning old_toplevel must be + // a parent of `ev->window`. + old_toplevel = NULL; + } + // A window can't be a toplevel and a subwin at the same time + assert(old_w == NULL || old_subwin == NULL); + + log_debug("old toplevel: %p, old subwin: %p, new toplevel: %p, old parent: " + "%#010x, new parent: %#010x, root window: %#010x", + old_w, old_subwin, new_toplevel, old_parent, ev->parent, + ps->c.screen_info->root); + + if (old_w == NULL && old_subwin == NULL && new_toplevel == NULL && + ev->parent != ps->c.screen_info->root) { + // The window is neither a toplevel nor a subwin, and the new parent is + // neither a root nor a toplevel, we don't care about this window. + // This can happen if a window is reparented to somewhere irrelevant, but + // more events from it are generated before we can unsubscribe. + return; } - if (ev->parent == ps->root) { - // X will generate reparent notifiy even if the parent didn't actually - // change (i.e. reparent again to current parent). So we check if that's - // the case - auto w = find_win(ps, ev->window); - if (w) { - // This window has already been reparented to root before, - // so we don't need to create a new window for it, we just need to - // move it to the top - restack_top(ps, w); - } else { - add_win_top(ps, ev->window); - } - } else { - // otherwise, find and destroy the window first - { - auto w = find_win(ps, ev->window); - if (w) { - auto ret = destroy_win_start(ps, w); - if (!ret && w->managed) { - auto mw = (struct managed_win *)w; - CHECK(win_skip_fading(ps, mw)); - } + if (ev->parent == old_parent) { + // Parent unchanged, but if the parent is root, we need to move the window + // to the top of the window stack + if (old_w) { + // root -> root reparent, we just need to move it to the top + log_debug("Restack %#010x (%s) to top", old_w->id, + win_get_name_if_managed(old_w)); + wm_stack_move_to_top(ps->wm, old_w); + if (old_w->managed) { + add_damage_from_win(ps, win_as_managed(old_w)); } } + return; + } - // Reset event mask in case something wrong happens - xcb_change_window_attributes( - ps->c, ev->window, XCB_CW_EVENT_MASK, - (const uint32_t[]){determine_evmask(ps, ev->window, WIN_EVMODE_UNKNOWN)}); - - if (!wid_has_prop(ps, ev->window, ps->atoms->aWM_STATE)) { - log_debug("Window %#010x doesn't have WM_STATE property, it is " - "probably not a client window. But we will listen for " - "property change in case it gains one.", - ev->window); - xcb_change_window_attributes( - ps->c, ev->window, XCB_CW_EVENT_MASK, - (const uint32_t[]){determine_evmask(ps, ev->window, WIN_EVMODE_UNKNOWN) | - XCB_EVENT_MASK_PROPERTY_CHANGE}); - } else { - auto w_real_top = find_managed_window_or_parent(ps, ev->parent); - if (w_real_top && w_real_top->state != WSTATE_UNMAPPED && - w_real_top->state != WSTATE_UNMAPPING) { - log_debug("Mark window %#010x (%s) as having a stale " - "client", - w_real_top->base.id, w_real_top->name); - win_set_flags(w_real_top, WIN_FLAGS_CLIENT_STALE); - ps->pending_updates = true; - } else { - if (!w_real_top) - log_debug("parent %#010x not found", ev->parent); - else { - // Window is not currently mapped, unmark its - // client to trigger a client recheck when it is - // mapped later. - win_unmark_client(ps, w_real_top); - log_debug("parent %#010x (%s) is in state %d", - w_real_top->base.id, w_real_top->name, - w_real_top->state); - } + if (old_toplevel) { + assert(old_subwin != NULL); + assert(old_subwin->toplevel == old_toplevel->base.id); + win_unmark_client(old_toplevel); + win_set_flags(old_toplevel, WIN_FLAGS_CLIENT_STALE); + ps->pending_updates = true; + } + + if (old_w) { + // A toplevel is reparented, so it is no longer a toplevel. We need to + // destroy the existing toplevel. + if (old_w->managed) { + auto mw = (struct managed_win *)old_w; + // Usually, damage for unmapped windows are added in + // `paint_preprocess`, when a window was painted before and isn't + // anymore. But since we are reparenting the window here, we would + // lose track of the `to_paint` information. So we just add damage + // here. + if (mw->to_paint) { + add_damage_from_win(ps, mw); + } + // Emulating what X server does: a destroyed + // window is always unmapped first. + if (mw->state == WSTATE_MAPPED) { + unmap_win_start(ps, mw); } } + destroy_win_start(ps, old_w); + if (old_w->managed) { + win_skip_fading(win_as_managed(old_w)); + } + destroy_win_finish(ps, old_w); + } + + // We need to guarantee a subwin exists iff it has a valid toplevel. + auto new_subwin = old_subwin; + if (new_subwin != NULL && new_toplevel == NULL) { + wm_subwin_remove_and_unsubscribe(ps->wm, &ps->c, new_subwin); + new_subwin = NULL; + } else if (new_subwin == NULL && new_toplevel != NULL) { + new_subwin = + wm_subwin_add_and_subscribe(ps->wm, &ps->c, ev->window, ev->parent); + } + if (new_subwin) { + new_subwin->toplevel = new_toplevel->base.id; + if (new_toplevel->client_win == XCB_NONE || + new_toplevel->client_win == new_toplevel->base.id) { + win_set_flags(new_toplevel, WIN_FLAGS_CLIENT_STALE); + ps->pending_updates = true; + } + } + + if (ev->parent == ps->c.screen_info->root) { + // New parent is root, add a toplevel; + assert(old_w == NULL); + assert(new_toplevel == NULL); + wm_stack_add_top(ps->wm, ev->window); + ps->pending_updates = true; } } static inline void ev_circulate_notify(session_t *ps, xcb_circulate_notify_event_t *ev) { - auto w = find_win(ps, ev->window); + auto w = wm_find(ps->wm, ev->window); - if (!w) + if (!w) { return; + } + log_debug("Moving window %#010x (%s) to the %s", w->id, + win_get_name_if_managed(w), ev->place == PlaceOnTop ? "top" : "bottom"); if (ev->place == PlaceOnTop) { - restack_top(ps, w); + wm_stack_move_to_top(ps->wm, w); } else { - restack_bottom(ps, w); + wm_stack_move_to_bottom(ps->wm, w); + } + if (w->managed) { + add_damage_from_win(ps, win_as_managed(w)); } } @@ -406,7 +505,8 @@ static inline void expose_root(session_t *ps, const rect_t *rects, int nrects) { } static inline void ev_expose(session_t *ps, xcb_expose_event_t *ev) { - if (ev->window == ps->root || (ps->overlay && ev->window == ps->overlay)) { + if (ev->window == ps->c.screen_info->root || + (ps->overlay && ev->window == ps->overlay)) { int more = ev->count + 1; if (ps->n_expose == ps->size_expose) { if (ps->expose_rects) { @@ -432,11 +532,55 @@ static inline void ev_expose(session_t *ps, xcb_expose_event_t *ev) { } } +static inline void ev_subwin_wm_state_changed(session_t *ps, xcb_property_notify_event_t *ev) { + auto subwin = wm_subwin_find(ps->wm, ev->window); + if (!subwin) { + // We only care if a direct child of a toplevel gained/lost WM_STATE + return; + } + + enum tristate old_has_wm_state = subwin->has_wm_state; + subwin->has_wm_state = ev->state == XCB_PROPERTY_DELETE ? TRI_FALSE : TRI_TRUE; + if (old_has_wm_state == subwin->has_wm_state) { + if (subwin->has_wm_state == TRI_FALSE) { + log_warn("Child window %#010x of window %#010x lost WM_STATE a " + "second time?", + ev->window, subwin->toplevel); + } + return; + } + + auto toplevel = wm_find(ps->wm, subwin->toplevel); + BUG_ON(toplevel == NULL); + if (!toplevel->managed) { + return; + } + + auto managed = (struct managed_win *)toplevel; + if (managed->client_win == subwin->id) { + // 1. This window is the client window of its toplevel, and now it lost + // its WM_STATE (implies it must have it before) + assert(subwin->has_wm_state == TRI_FALSE); + win_set_flags(managed, WIN_FLAGS_CLIENT_STALE); + } else if (subwin->has_wm_state == TRI_TRUE) { + // 2. This window is not the client window of its toplevel, and + // now it gained WM_STATE + if (managed->client_win != XCB_NONE && managed->client_win != toplevel->id) { + log_warn("Toplevel %#010x already has a client window %#010x, " + "but another of its child windows %#010x gained " + "WM_STATE.", + toplevel->id, managed->client_win, subwin->id); + } else { + win_set_flags(managed, WIN_FLAGS_CLIENT_STALE); + } + } +} + static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t *ev) { if (unlikely(log_get_level_tls() <= LOG_LEVEL_TRACE)) { // Print out changed atom - xcb_get_atom_name_reply_t *reply = - xcb_get_atom_name_reply(ps->c, xcb_get_atom_name(ps->c, ev->atom), NULL); + xcb_get_atom_name_reply_t *reply = xcb_get_atom_name_reply( + ps->c.c, xcb_get_atom_name(ps->c.c, ev->atom), NULL); const char *name = "?"; int name_len = 1; if (reply) { @@ -448,7 +592,7 @@ static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t free(reply); } - if (ps->root == ev->window) { + if (ps->c.screen_info->root == ev->window) { if (ps->o.use_ewmh_active_win && ps->atoms->a_NET_ACTIVE_WINDOW == ev->atom) { // to update focus ps->pending_updates = true; @@ -459,135 +603,87 @@ static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t } } - // Unconcerned about any other proprties on root window + // Unconcerned about any other properties on root window return; } ps->pending_updates = true; - // If WM_STATE changes + auto w = wm_find_by_client(ps->wm, ev->window); if (ev->atom == ps->atoms->aWM_STATE) { - // Check whether it could be a client window - if (!find_toplevel(ps, ev->window)) { - // Reset event mask anyway - xcb_change_window_attributes(ps->c, ev->window, XCB_CW_EVENT_MASK, - (const uint32_t[]){determine_evmask( - ps, ev->window, WIN_EVMODE_UNKNOWN)}); - - auto w_top = find_managed_window_or_parent(ps, ev->window); - // ev->window might have not been managed yet, in that case w_top - // would be NULL. - if (w_top) { - win_set_flags(w_top, WIN_FLAGS_CLIENT_STALE); - } - } - return; - } - - // If _NET_WM_WINDOW_TYPE changes... God knows why this would happen, but - // there are always some stupid applications. (#144) - if (ev->atom == ps->atoms->a_NET_WM_WINDOW_TYPE) { - struct managed_win *w = NULL; - if ((w = find_toplevel(ps, ev->window))) { - win_set_property_stale(w, ev->atom); - } + ev_subwin_wm_state_changed(ps, ev); } if (ev->atom == ps->atoms->a_NET_WM_BYPASS_COMPOSITOR) { - // Unnecessay until we remove the queue_redraw in ev_handle + // Unnecessary until we remove the queue_redraw in ev_handle queue_redraw(ps); } - // If _NET_WM_OPACITY changes - if (ev->atom == ps->atoms->a_NET_WM_WINDOW_OPACITY) { - auto w = find_managed_win(ps, ev->window) ?: find_toplevel(ps, ev->window); - if (w) { - win_set_property_stale(w, ev->atom); - } - } - - // If frame extents property changes - if (ev->atom == ps->atoms->a_NET_FRAME_EXTENTS) { - auto w = find_toplevel(ps, ev->window); - if (w) { - win_set_property_stale(w, ev->atom); - } - } - - // If name changes - if (ps->atoms->aWM_NAME == ev->atom || ps->atoms->a_NET_WM_NAME == ev->atom) { - auto w = find_toplevel(ps, ev->window); - if (w) { - win_set_property_stale(w, ev->atom); - } - } - - // If class changes - if (ps->atoms->aWM_CLASS == ev->atom) { - auto w = find_toplevel(ps, ev->window); - if (w) { - win_set_property_stale(w, ev->atom); - } + if (w) { + win_set_property_stale(w, ev->atom); } - // If role changes - if (ps->atoms->aWM_WINDOW_ROLE == ev->atom) { - auto w = find_toplevel(ps, ev->window); - if (w) { - win_set_property_stale(w, ev->atom); + if (ev->atom == ps->atoms->a_NET_WM_WINDOW_OPACITY) { + // We already handle if this is set on the client window, check + // if this is set on the frame window as well. + // TODO(yshui) do we really need this? + auto toplevel = wm_find_managed(ps->wm, ev->window); + if (toplevel) { + win_set_property_stale(toplevel, ev->atom); } } - // If _COMPTON_SHADOW changes - if (ps->atoms->a_COMPTON_SHADOW == ev->atom) { - auto w = find_managed_win(ps, ev->window); - if (w) { - win_set_property_stale(w, ev->atom); + // Check for other atoms we are tracking + if (c2_state_is_property_tracked(ps->c2_state, ev->atom)) { + bool change_is_on_client = false; + w = wm_find_managed(ps->wm, ev->window); + if (!w) { + w = wm_find_by_client(ps->wm, ev->window); + change_is_on_client = true; } - } - - // If a leader property changes - if ((ps->o.detect_transient && ps->atoms->aWM_TRANSIENT_FOR == ev->atom) || - (ps->o.detect_client_leader && ps->atoms->aWM_CLIENT_LEADER == ev->atom)) { - auto w = find_toplevel(ps, ev->window); if (w) { - win_set_property_stale(w, ev->atom); - } - } - - // Check for other atoms we are tracking - for (latom_t *platom = ps->track_atom_lst; platom; platom = platom->next) { - if (platom->atom == ev->atom) { - auto w = find_managed_win(ps, ev->window); - if (!w) { - w = find_toplevel(ps, ev->window); - } - if (w) { - // Set FACTOR_CHANGED so rules based on properties will be - // re-evaluated. - // Don't need to set property stale here, since that only - // concerns properties we explicitly check. - win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); - } - break; + c2_window_state_mark_dirty(ps->c2_state, &w->c2_state, ev->atom, + change_is_on_client); + // Set FACTOR_CHANGED so rules based on properties will be + // re-evaluated. + // Don't need to set property stale here, since that only + // concerns properties we explicitly check. + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); } } } static inline void repair_win(session_t *ps, struct managed_win *w) { // Only mapped window can receive damages - assert(win_is_mapped_in_x(w)); + assert(w->state == WSTATE_MAPPED || win_check_flags_all(w, WIN_FLAGS_MAPPED)); region_t parts; pixman_region32_init(&parts); + // If this is the first time this window is damaged, we would redraw the + // whole window, so we don't need to fetch the damage region. But we still need + // to make sure the X server receives the DamageSubtract request, hence the + // `xcb_request_check` here. + // Otherwise, we fetch the damage regions. That means we will receive a reply + // from the X server, which implies it has received our DamageSubtract request. if (!w->ever_damaged) { + auto e = xcb_request_check( + ps->c.c, + xcb_damage_subtract_checked(ps->c.c, w->damage, XCB_NONE, XCB_NONE)); + if (e) { + if (ps->o.show_all_xerrors) { + x_print_error(e->sequence, e->major_code, e->minor_code, + e->error_code); + } + free(e); + } win_extents(w, &parts); - set_ignore_cookie( - ps, xcb_damage_subtract(ps->c, w->damage, XCB_NONE, XCB_NONE)); } else { - set_ignore_cookie( - ps, xcb_damage_subtract(ps->c, w->damage, XCB_NONE, ps->damaged_region)); - x_fetch_region(ps->c, ps->damaged_region, &parts); + auto cookie = xcb_damage_subtract(ps->c.c, w->damage, XCB_NONE, + ps->damage_ring.x_region); + if (!ps->o.show_all_xerrors) { + set_ignore_cookie(&ps->c, cookie); + } + x_fetch_region(&ps->c, ps->damage_ring.x_region, &parts); pixman_region32_translate(&parts, w->g.x + w->g.border_width, w->g.y + w->g.border_width); } @@ -604,11 +700,17 @@ static inline void repair_win(session_t *ps, struct managed_win *w) { } // Remove the part in the damage area that could be ignored + region_t without_ignored; + pixman_region32_init(&without_ignored); if (w->reg_ignore && win_is_region_ignore_valid(ps, w)) { - pixman_region32_subtract(&parts, &parts, w->reg_ignore); + pixman_region32_subtract(&without_ignored, &parts, w->reg_ignore); } - add_damage(ps, &parts); + add_damage(ps, &without_ignored); + pixman_region32_fini(&without_ignored); + + pixman_region32_translate(&parts, -w->g.x, -w->g.y); + pixman_region32_union(&w->damaged, &w->damaged, &parts); pixman_region32_fini(&parts); } @@ -619,7 +721,7 @@ static inline void ev_damage_notify(session_t *ps, xcb_damage_notify_event_t *de return; } */ - auto w = find_managed_win(ps, de->drawable); + auto w = wm_find_managed(ps->wm, de->drawable); if (!w) { return; @@ -629,7 +731,7 @@ static inline void ev_damage_notify(session_t *ps, xcb_damage_notify_event_t *de } static inline void ev_shape_notify(session_t *ps, xcb_shape_notify_event_t *ev) { - auto w = find_managed_win(ps, ev->affected_window); + auto w = wm_find_managed(ps->wm, ev->affected_window); if (!w || w->a.map_state == XCB_MAP_STATE_UNMAPPED) { return; } @@ -662,8 +764,8 @@ ev_selection_clear(session_t *ps, xcb_selection_clear_event_t attr_unused *ev) { } void ev_handle(session_t *ps, xcb_generic_event_t *ev) { - if ((ev->response_type & 0x7f) != KeymapNotify) { - discard_ignore(ps, ev->full_sequence); + if (XCB_EVENT_RESPONSE_TYPE(ev) != KeymapNotify) { + x_discard_pending(&ps->c, ev->full_sequence); } xcb_window_t wid = ev_window(ps, ev); @@ -684,9 +786,10 @@ void ev_handle(session_t *ps, xcb_generic_event_t *ev) { // For even more details, see: // https://bugs.freedesktop.org/show_bug.cgi?id=35945 // https://lists.freedesktop.org/archives/xcb/2011-November/007337.html - auto proc = XESetWireToEvent(ps->dpy, ev->response_type, 0); + auto response_type = XCB_EVENT_RESPONSE_TYPE(ev); + auto proc = XESetWireToEvent(ps->c.dpy, response_type, 0); if (proc) { - XESetWireToEvent(ps->dpy, ev->response_type, proc); + XESetWireToEvent(ps->c.dpy, response_type, proc); XEvent dummy; // Stop Xlib from complaining about lost sequence numbers. @@ -695,39 +798,46 @@ void ev_handle(session_t *ps, xcb_generic_event_t *ev) { // missing sequence numbers. // // We only need the low 16 bits - ev->sequence = (uint16_t)(LastKnownRequestProcessed(ps->dpy) & 0xffff); - proc(ps->dpy, &dummy, (xEvent *)ev); + uint16_t seq = ev->sequence; + ev->sequence = (uint16_t)(LastKnownRequestProcessed(ps->c.dpy) & 0xffff); + proc(ps->c.dpy, &dummy, (xEvent *)ev); + // Restore the sequence number + ev->sequence = seq; } // XXX redraw needs to be more fine grained queue_redraw(ps); + // We intentionally ignore events sent via SendEvent. Those events has the 8th bit + // of response_type set, meaning they will match none of the cases below. switch (ev->response_type) { - case FocusIn: ev_focus_in(ps, (xcb_focus_in_event_t *)ev); break; - case FocusOut: ev_focus_out(ps, (xcb_focus_out_event_t *)ev); break; - case CreateNotify: ev_create_notify(ps, (xcb_create_notify_event_t *)ev); break; - case ConfigureNotify: + case XCB_FOCUS_IN: ev_focus_in(ps, (xcb_focus_in_event_t *)ev); break; + case XCB_FOCUS_OUT: ev_focus_out(ps, (xcb_focus_out_event_t *)ev); break; + case XCB_CREATE_NOTIFY: + ev_create_notify(ps, (xcb_create_notify_event_t *)ev); + break; + case XCB_CONFIGURE_NOTIFY: ev_configure_notify(ps, (xcb_configure_notify_event_t *)ev); break; - case DestroyNotify: + case XCB_DESTROY_NOTIFY: ev_destroy_notify(ps, (xcb_destroy_notify_event_t *)ev); break; - case MapNotify: ev_map_notify(ps, (xcb_map_notify_event_t *)ev); break; - case UnmapNotify: ev_unmap_notify(ps, (xcb_unmap_notify_event_t *)ev); break; - case ReparentNotify: + case XCB_MAP_NOTIFY: ev_map_notify(ps, (xcb_map_notify_event_t *)ev); break; + case XCB_UNMAP_NOTIFY: ev_unmap_notify(ps, (xcb_unmap_notify_event_t *)ev); break; + case XCB_REPARENT_NOTIFY: ev_reparent_notify(ps, (xcb_reparent_notify_event_t *)ev); break; - case CirculateNotify: + case XCB_CIRCULATE_NOTIFY: ev_circulate_notify(ps, (xcb_circulate_notify_event_t *)ev); break; - case Expose: ev_expose(ps, (xcb_expose_event_t *)ev); break; - case PropertyNotify: + case XCB_EXPOSE: ev_expose(ps, (xcb_expose_event_t *)ev); break; + case XCB_PROPERTY_NOTIFY: ev_property_notify(ps, (xcb_property_notify_event_t *)ev); break; - case SelectionClear: + case XCB_SELECTION_CLEAR: ev_selection_clear(ps, (xcb_selection_clear_event_t *)ev); break; - case 0: ev_xcb_error(ps, (xcb_generic_error_t *)ev); break; + case 0: x_handle_error(&ps->c, (xcb_generic_error_t *)ev); break; default: if (ps->shape_exists && ev->response_type == ps->shape_event) { ev_shape_notify(ps, (xcb_shape_notify_event_t *)ev); diff --git a/src/file_watch.c b/src/file_watch.c index faa8f68933..4532fb6173 100644 --- a/src/file_watch.c +++ b/src/file_watch.c @@ -158,7 +158,7 @@ bool file_watch_add(void *_fwr, const char *filename, file_watch_cb_t cb, void * fflags |= NOTE_CLOSE_WRITE; #else // NOTE_WRITE will receive notification more frequent than necessary, so is less - // preferrable + // preferable fflags |= NOTE_WRITE; #endif struct kevent ev = { diff --git a/src/fuzzer/c2.c b/src/fuzzer/c2.c new file mode 100644 index 0000000000..078116d485 --- /dev/null +++ b/src/fuzzer/c2.c @@ -0,0 +1,20 @@ + +#include "c2.h" +#include +#include +#include "config.h" +#include "log.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + log_init_tls(); + if (size == 0) { + return 0; + } + if (data[size - 1] != 0) { + return 0; + } + c2_lptr_t *cond = c2_parse(NULL, (char *)data, NULL); + (void)cond; + (void)size; + return 0; // Values other than 0 and -1 are reserved for future use. +} \ No newline at end of file diff --git a/src/inspect.c b/src/inspect.c new file mode 100644 index 0000000000..94e9e70701 --- /dev/null +++ b/src/inspect.c @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2024 Yuxuan Shui + +#include +#include +#include +#include +#include +#include +#include + +#include "inspect.h" + +#include "atom.h" +#include "c2.h" +#include "common.h" +#include "config.h" +#include "err.h" +#include "log.h" +#include "options.h" +#include "utils.h" +#include "win.h" +#include "win_defs.h" +#include "x.h" + +static struct managed_win * +setup_window(struct x_connection *c, struct atom *atoms, struct options *options, + struct c2_state *state, xcb_window_t target) { + // Pretend we are the compositor, and build up the window state + struct managed_win *w = ccalloc(1, struct managed_win); + w->state = WSTATE_MAPPED; + w->base.id = target; + w->client_win = win_get_client_window(c, NULL, atoms, w); + win_update_wintype(c, atoms, w); + win_update_frame_extents(c, atoms, w, w->client_win, options->frame_opacity); + // TODO(yshui) get leader + win_update_name(c, atoms, w); + win_update_class(c, atoms, w); + win_update_role(c, atoms, w); + + auto geometry_reply = XCB_AWAIT(xcb_get_geometry, c->c, w->base.id); + w->g = (struct win_geometry){ + .x = geometry_reply->x, + .y = geometry_reply->y, + .width = geometry_reply->width, + .height = geometry_reply->height, + }; + free(geometry_reply); + + auto shape_info = xcb_get_extension_data(c->c, &xcb_shape_id); + win_on_win_size_change(w, options->shadow_offset_x, options->shadow_offset_y, + options->shadow_radius); + win_update_bounding_shape(c, w, shape_info->present, options->detect_rounded_corners); + win_update_prop_fullscreen(c, atoms, w); + + // Determine if the window is focused + xcb_window_t wid = XCB_NONE; + if (options->use_ewmh_active_win) { + wid_get_prop_window(c, c->screen_info->root, atoms->a_NET_ACTIVE_WINDOW); + } else { + // Determine the currently focused window so we can apply appropriate + // opacity on it + xcb_get_input_focus_reply_t *reply = + xcb_get_input_focus_reply(c->c, xcb_get_input_focus(c->c), NULL); + + if (reply) { + wid = reply->focus; + free(reply); + } + } + if (wid == w->base.id || wid == w->client_win) { + w->focused = true; + } + + auto attributes_reply = XCB_AWAIT(xcb_get_window_attributes, c->c, w->base.id); + w->a = *attributes_reply; + w->pictfmt = x_get_pictform_for_visual(c, w->a.visual); + free(attributes_reply); + + c2_window_state_init(state, &w->c2_state); + c2_window_state_update(state, &w->c2_state, c->c, w->client_win, w->base.id); + return w; +} + +xcb_window_t select_window(struct x_connection *c) { + xcb_font_t font = x_new_id(c); + xcb_cursor_t cursor = x_new_id(c); + const char font_name[] = "cursor"; + static const uint16_t CROSSHAIR_CHAR = 34; + XCB_AWAIT_VOID(xcb_open_font, c->c, font, sizeof(font_name) - 1, font_name); + XCB_AWAIT_VOID(xcb_create_glyph_cursor, c->c, cursor, font, font, CROSSHAIR_CHAR, + CROSSHAIR_CHAR + 1, 0, 0, 0, 0xffff, 0xffff, 0xffff); + auto grab_reply = XCB_AWAIT( + xcb_grab_pointer, c->c, false, c->screen_info->root, + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE, XCB_GRAB_MODE_SYNC, + XCB_GRAB_MODE_ASYNC, c->screen_info->root, cursor, XCB_CURRENT_TIME); + if (grab_reply->status != XCB_GRAB_STATUS_SUCCESS) { + log_fatal("Failed to grab pointer"); + return 1; + } + free(grab_reply); + + // Let the user pick a window by clicking on it, mostly stolen from + // xprop + xcb_window_t target = XCB_NONE; + int buttons_pressed = 0; + while ((target == XCB_NONE) || (buttons_pressed > 0)) { + XCB_AWAIT_VOID(xcb_allow_events, c->c, XCB_ALLOW_ASYNC_POINTER, + XCB_CURRENT_TIME); + xcb_generic_event_t *ev = xcb_wait_for_event(c->c); + if (!ev) { + log_fatal("Connection to X server lost"); + return 1; + } + switch (XCB_EVENT_RESPONSE_TYPE(ev)) { + case XCB_BUTTON_PRESS: { + xcb_button_press_event_t *e = (xcb_button_press_event_t *)ev; + if (target == XCB_NONE) { + target = e->child; + if (target == XCB_NONE) { + target = e->root; + } + } + buttons_pressed++; + break; + } + case XCB_BUTTON_RELEASE: { + if (buttons_pressed > 0) { + buttons_pressed--; + } + break; + } + default: break; + } + free(ev); + } + XCB_AWAIT_VOID(xcb_ungrab_pointer, c->c, XCB_CURRENT_TIME); + return target; +} + +struct c2_match_state { + struct c2_state *state; + struct managed_win *w; + bool print_value; +}; + +bool c2_match_once_and_log(const c2_lptr_t *cond, void *data) { + struct c2_match_state *state = data; + void *rule_data = NULL; + printf(" %s ... ", c2_lptr_to_str(cond)); + bool matched = c2_match(state->state, state->w, cond, &rule_data); + printf("%s", matched ? "\033[1;32mmatched\033[0m" : "not matched"); + if (state->print_value && matched) { + printf("/%lu", (unsigned long)(intptr_t)rule_data); + state->print_value = false; + } + printf("\n"); + return false; +} + +#define BOLD(str) "\033[1m" str "\033[0m" + +int inspect_main(int argc, char **argv, const char *config_file) { + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { + log_fatal("Can't open display"); + return 1; + } + struct x_connection c; + x_connection_init(&c, dpy); + + xcb_prefetch_extension_data(c.c, &xcb_shape_id); + + char *config_file_to_free = NULL; + struct options options; + config_file = config_file_to_free = parse_config(&options, config_file); + + if (IS_ERR(config_file_to_free)) { + return 1; + } + + // Parse all of the rest command line options + if (!get_cfg(&options, argc, argv)) { + log_fatal("Failed to get configuration, usually mean you have specified " + "invalid options."); + return 1; + } + + auto atoms attr_unused = init_atoms(c.c); + auto state = c2_state_new(atoms); + options_postprocess_c2_lists(state, &c, &options); + + auto target = select_window(&c); + log_info("Target window: %#x", target); + auto w = setup_window(&c, atoms, &options, state, target); + struct c2_match_state match_state = { + .state = state, + .w = w, + }; + printf("Checking " BOLD("transparent-clipping-exclude") ":\n"); + c2_list_foreach(options.transparent_clipping_blacklist, c2_match_once_and_log, + &match_state); + printf("Checking " BOLD("shadow-exclude") ":\n"); + c2_list_foreach(options.shadow_blacklist, c2_match_once_and_log, &match_state); + printf("Checking " BOLD("fade-exclude") ":\n"); + c2_list_foreach(options.fade_blacklist, c2_match_once_and_log, &match_state); + printf("Checking " BOLD("clip-shadow-above") ":\n"); + c2_list_foreach(options.shadow_clip_list, c2_match_once_and_log, &match_state); + printf("Checking " BOLD("focus-exclude") ":\n"); + c2_list_foreach(options.focus_blacklist, c2_match_once_and_log, &match_state); + printf("Checking " BOLD("invert-color-include") ":\n"); + c2_list_foreach(options.invert_color_list, c2_match_once_and_log, &match_state); + printf("Checking " BOLD("blur-background-exclude") ":\n"); + c2_list_foreach(options.blur_background_blacklist, c2_match_once_and_log, &match_state); + printf("Checking " BOLD("unredir-if-possible-exclude") ":\n"); + c2_list_foreach(options.unredir_if_possible_blacklist, c2_match_once_and_log, + &match_state); + printf("Checking " BOLD("rounded-corners-exclude") ":\n"); + c2_list_foreach(options.rounded_corners_blacklist, c2_match_once_and_log, &match_state); + + match_state.print_value = true; + printf("Checking " BOLD("opacity-rule") ":\n"); + c2_list_foreach(options.opacity_rules, c2_match_once_and_log, &match_state); + printf("Checking " BOLD("corner-radius-rule") ":\n"); + c2_list_foreach(options.corner_radius_rules, c2_match_once_and_log, &match_state); + + printf("\nHere are some rule(s) that match this window:\n"); + if (w->name != NULL) { + printf(" name = '%s'\n", w->name); + } + if (w->class_instance != NULL) { + printf(" class_i = '%s'\n", w->class_instance); + } + if (w->class_general != NULL) { + printf(" class_g = '%s'\n", w->class_general); + } + if (w->role != NULL) { + printf(" role = '%s'\n", w->role); + } + if (w->window_type != WINTYPE_UNKNOWN) { + printf(" window_type = '%s'\n", WINTYPES[w->window_type].name); + } + printf(" %sfullscreen\n", w->is_fullscreen ? "" : "! "); + if (w->bounding_shaped) { + printf(" bounding_shaped\n"); + } + printf(" border_width = %d\n", w->g.border_width); + + pixman_region32_fini(&w->bounding_shape); + free(w->name); + free(w->class_instance); + free(w->class_general); + free(w->role); + c2_window_state_destroy(state, &w->c2_state); + free(w); + + log_deinit_tls(); + free(config_file_to_free); + c2_state_free(state); + destroy_atoms(atoms); + options_destroy(&options); + XCloseDisplay(c.dpy); + return 0; +} diff --git a/src/inspect.h b/src/inspect.h new file mode 100644 index 0000000000..eb700737f9 --- /dev/null +++ b/src/inspect.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2024 Yuxuan Shui + +#pragma once +#include + +int inspect_main(int argc, char **argv, const char *config_file); diff --git a/src/kernel.c b/src/kernel.c index 51510452ba..a9865c9335 100644 --- a/src/kernel.c +++ b/src/kernel.c @@ -54,8 +54,9 @@ static inline double attr_const gaussian(double r, double x, double y) { // Formula can be found here: // https://en.wikipedia.org/wiki/Gaussian_blur#Mathematics // Except a special case for r == 0 to produce sharp shadows - if (r == 0) + if (r == 0) { return 1; + } return exp(-0.5 * (x * x + y * y) / (r * r)) / (2 * M_PI * r * r); } @@ -90,15 +91,20 @@ conv *gaussian_kernel(double r, int size) { /// Estimate the element of the sum of the first row in a gaussian kernel with standard /// deviation `r` and size `size`, static inline double estimate_first_row_sum(double size, double r) { + // `factor` is integral of gaussian from -size to size double factor = erf(size / r / sqrt(2)); + // `a` is gaussian at (size, 0) double a = exp(-0.5 * size * size / (r * r)) / sqrt(2 * M_PI) / r; + // The sum of the whole kernel is normalized to 1, i.e. each element is divided by + // factor squared. So the sum of the first row is a * factor / factor^2 = a / + // factor return a / factor; } -/// Pick a suitable gaussian kernel radius for a given kernel size. The returned radius -/// is the maximum possible radius (<= size*2) that satisfies no sum of the rows in -/// the kernel are less than `row_limit` (up to certain precision). -static inline double gaussian_kernel_std_for_size(int size, double row_limit) { +/// Pick a suitable gaussian kernel standard deviation for a given kernel size. The +/// returned radius is the maximum possible radius (<= size*2) that satisfies no sum of +/// the rows in the kernel are less than `row_limit` (up to certain precision). +double gaussian_kernel_std_for_size(double size, double row_limit) { assert(size > 0); if (row_limit >= 1.0 / 2.0 / size) { return size * 2; @@ -121,14 +127,14 @@ static inline double gaussian_kernel_std_for_size(int size, double row_limit) { /// transparent, so the transition from shadow to the background is smooth. /// /// @param[in] shadow_radius the radius of the shadow -conv *gaussian_kernel_autodetect_deviation(int shadow_radius) { +conv *gaussian_kernel_autodetect_deviation(double shadow_radius) { assert(shadow_radius >= 0); - int size = shadow_radius * 2 + 1; + int size = (int)(shadow_radius * 2 + 1); if (shadow_radius == 0) { return gaussian_kernel(0, size); } - double std = gaussian_kernel_std_for_size(shadow_radius, 1.0 / 256.0); + double std = gaussian_kernel_std_for_size(shadow_radius, 0.5 / 256.0); return gaussian_kernel(std, size); } diff --git a/src/kernel.h b/src/kernel.h index 251d127d0f..d1dd2eee85 100644 --- a/src/kernel.h +++ b/src/kernel.h @@ -22,12 +22,15 @@ double attr_pure sum_kernel_normalized(const conv *map, int x, int y, int width, /// `size`. conv *gaussian_kernel(double r, int size); +/// Estimate the best standard deviation for a give kernel size. +double gaussian_kernel_std_for_size(double size, double row_limit); + /// Create a gaussian kernel with auto detected standard deviation. The choosen standard /// deviation tries to make sure the outer most pixels of the shadow are completely /// transparent. /// /// @param[in] shadow_radius the radius of the shadow -conv *gaussian_kernel_autodetect_deviation(int shadow_radius); +conv *gaussian_kernel_autodetect_deviation(double shadow_radius); /// preprocess kernels to make shadow generation faster /// shadow_sum[x*d+y] is the sum of the kernel from (0, 0) to (x, y), inclusive diff --git a/src/list.h b/src/list.h index 19e2c2c277..d63a764611 100644 --- a/src/list.h +++ b/src/list.h @@ -2,18 +2,7 @@ #include #include -/** - * container_of - cast a member of a structure out to the containing structure - * @ptr: the pointer to the member. - * @type: the type of the container struct this is embedded in. - * @member: the name of the member within the struct. - * - */ -#define container_of(ptr, type, member) \ - ({ \ - const __typeof__(((type *)0)->member) *__mptr = (ptr); \ - (type *)((char *)__mptr - offsetof(type, member)); \ - }) +#include "utils.h" struct list_node { struct list_node *next, *prev; diff --git a/src/log.c b/src/log.c index 0b663e7702..35c0fbbf07 100644 --- a/src/log.c +++ b/src/log.c @@ -9,7 +9,7 @@ #include #ifdef CONFIG_OPENGL -#include +#include #include "backend/gl/gl_common.h" #include "backend/gl/glx.h" #endif @@ -68,6 +68,7 @@ log_default_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { static attr_const const char *log_level_to_string(enum log_level level) { switch (level) { case LOG_LEVEL_TRACE: return "TRACE"; + case LOG_LEVEL_VERBOSE: return "VERBOSE"; case LOG_LEVEL_DEBUG: return "DEBUG"; case LOG_LEVEL_INFO: return "INFO"; case LOG_LEVEL_WARN: return "WARN"; @@ -77,17 +78,25 @@ static attr_const const char *log_level_to_string(enum log_level level) { } } -enum log_level string_to_log_level(const char *str) { - if (strcasecmp(str, "TRACE") == 0) +int string_to_log_level(const char *str) { + if (strcasecmp(str, "TRACE") == 0) { return LOG_LEVEL_TRACE; - else if (strcasecmp(str, "DEBUG") == 0) + } + if (strcasecmp(str, "VERBOSE") == 0) { + return LOG_LEVEL_VERBOSE; + } + if (strcasecmp(str, "DEBUG") == 0) { return LOG_LEVEL_DEBUG; - else if (strcasecmp(str, "INFO") == 0) + } + if (strcasecmp(str, "INFO") == 0) { return LOG_LEVEL_INFO; - else if (strcasecmp(str, "WARN") == 0) + } + if (strcasecmp(str, "WARN") == 0) { return LOG_LEVEL_WARN; - else if (strcasecmp(str, "ERROR") == 0) + } + if (strcasecmp(str, "ERROR") == 0) { return LOG_LEVEL_ERROR; + } return LOG_LEVEL_INVALID; } @@ -143,8 +152,9 @@ enum log_level log_get_level(const struct log *l) { attr_printf(4, 5) void log_printf(struct log *l, int level, const char *func, const char *fmt, ...) { assert(level <= LOG_LEVEL_FATAL && level >= 0); - if (level < l->log_level) + if (level < l->log_level) { return; + } char *buf = NULL; va_list args; @@ -228,12 +238,10 @@ struct log_target *null_logger_new(void) { static void null_logger_write(struct log_target *tgt attr_unused, const char *str attr_unused, size_t len attr_unused) { - return; } static void null_logger_writev(struct log_target *tgt attr_unused, const struct iovec *vec attr_unused, int vcnt attr_unused) { - return; } static const struct log_ops null_logger_ops = { @@ -256,7 +264,7 @@ static void file_logger_write(struct log_target *tgt, const char *str, size_t le static void file_logger_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { auto f = (struct file_logger *)tgt; fflush(f->f); - writev(fileno(f->f), vec, vcnt); + ssize_t _ attr_unused = writev(fileno(f->f), vec, vcnt); } static void file_logger_destroy(struct log_target *tgt) { @@ -269,6 +277,7 @@ static void file_logger_destroy(struct log_target *tgt) { static const char *terminal_colorize_begin(enum log_level level) { switch (level) { case LOG_LEVEL_TRACE: return ANSI("30;2"); + case LOG_LEVEL_VERBOSE: case LOG_LEVEL_DEBUG: return ANSI("37;2"); case LOG_LEVEL_INFO: return ANSI("92"); case LOG_LEVEL_WARN: return ANSI("33"); @@ -329,21 +338,14 @@ struct log_target *stderr_logger_new(void) { } #ifdef CONFIG_OPENGL -/// An opengl logger that can be used for logging into opengl debugging tools, -/// such as apitrace -struct gl_string_marker_logger { - struct log_target tgt; - PFNGLSTRINGMARKERGREMEDYPROC gl_string_marker; -}; -static void -gl_string_marker_logger_write(struct log_target *tgt, const char *str, size_t len) { - auto g = (struct gl_string_marker_logger *)tgt; +static void gl_string_marker_logger_write(struct log_target *tgt attr_unused, + const char *str, size_t len) { // strip newlines at the end of the string - while (len > 0 && str[len-1] == '\n') { + while (len > 0 && str[len - 1] == '\n') { len--; } - g->gl_string_marker((GLsizei)len, str); + glStringMarkerGREMEDY((GLsizei)len, str); } static const struct log_ops gl_string_marker_logger_ops = { @@ -352,19 +354,16 @@ static const struct log_ops gl_string_marker_logger_ops = { .destroy = logger_trivial_destroy, }; +/// Create an opengl logger that can be used for logging into opengl debugging tools, +/// such as apitrace struct log_target *gl_string_marker_logger_new(void) { - if (!gl_has_extension("GL_GREMEDY_string_marker")) { + if (!epoxy_has_gl_extension("GL_GREMEDY_string_marker")) { return NULL; } - void *fnptr = glXGetProcAddress((GLubyte *)"glStringMarkerGREMEDY"); - if (!fnptr) - return NULL; - - auto ret = cmalloc(struct gl_string_marker_logger); - ret->tgt.ops = &gl_string_marker_logger_ops; - ret->gl_string_marker = fnptr; - return &ret->tgt; + auto ret = cmalloc(struct log_target); + ret->ops = &gl_string_marker_logger_ops; + return ret; } #else diff --git a/src/log.h b/src/log.h index e40fe3c8f4..e7c6c06c43 100644 --- a/src/log.h +++ b/src/log.h @@ -9,11 +9,19 @@ enum log_level { LOG_LEVEL_INVALID = -1, + /// Very noisy debug messages, many lines per frame. LOG_LEVEL_TRACE = 0, + /// Frequent debug messages, a few lines per frame. + LOG_LEVEL_VERBOSE, + /// Less frequent debug messages. LOG_LEVEL_DEBUG, + /// Informational messages. LOG_LEVEL_INFO, + /// Warnings. LOG_LEVEL_WARN, + /// Errors. LOG_LEVEL_ERROR, + /// Fatal errors. LOG_LEVEL_FATAL, }; @@ -31,6 +39,7 @@ enum log_level { } \ } while (0) #define log_trace(x, ...) LOG_UNLIKELY(TRACE, x, ##__VA_ARGS__) +#define log_verbose(x, ...) LOG_UNLIKELY(VERBOSE, x, ##__VA_ARGS__) #define log_debug(x, ...) LOG_UNLIKELY(DEBUG, x, ##__VA_ARGS__) #define log_info(x, ...) LOG(INFO, x, ##__VA_ARGS__) #define log_warn(x, ...) LOG(WARN, x, ##__VA_ARGS__) @@ -51,7 +60,7 @@ attr_nonnull_all void log_destroy(struct log *); attr_nonnull(1) void log_set_level(struct log *l, int level); attr_pure enum log_level log_get_level(const struct log *l); attr_nonnull_all void log_add_target(struct log *, struct log_target *); -attr_pure enum log_level string_to_log_level(const char *); +attr_pure int string_to_log_level(const char *); /// Remove a previously added log target for a log struct, and destroy it. If the log /// target was never added, nothing happens. void log_remove_target(struct log *l, struct log_target *tgt); diff --git a/src/meson.build b/src/meson.build index 0a882f930b..0513e09613 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,18 +9,22 @@ base_deps = [ srcs = [ files('picom.c', 'win.c', 'c2.c', 'x.c', 'config.c', 'vsync.c', 'utils.c', 'diagnostic.c', 'string_utils.c', 'render.c', 'kernel.c', 'log.c', - 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c') ] + 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'statistics.c', + 'vblank.c', 'transition.c', 'wm.c', 'renderer/layout.c', 'renderer/command_builder.c', + 'renderer/renderer.c', 'renderer/damage.c', 'config_libconfig.c', 'inspect.c') ] picom_inc = include_directories('.') cflags = [] required_xcb_packages = [ - 'xcb-render', 'xcb-damage', 'xcb-randr', 'xcb-sync', 'xcb-composite', - 'xcb-shape', 'xcb-xinerama', 'xcb-xfixes', 'xcb-present', 'xcb-glx', 'xcb' + 'xcb', 'xcb-composite', 'xcb-damage', 'xcb-dpms', 'xcb-glx', 'xcb-present', + 'xcb-randr', 'xcb-render', 'xcb-shape', 'xcb-sync', 'xcb-xfixes' ] +# Some XCB packages are here because their versioning differs (see check below). required_packages = [ - 'x11', 'x11-xcb', 'xcb-renderutil', 'xcb-image', 'xext', 'pixman-1' + 'pixman-1', 'x11', 'x11-xcb', 'xcb-image', 'xcb-renderutil', 'xcb-util', + 'xext', 'threads', ] foreach i : required_packages @@ -30,6 +34,7 @@ endforeach foreach i : required_xcb_packages base_deps += [dependency(i, version: '>=1.12.0', required: true)] endforeach +base_deps += [dependency('libconfig', version: '>=1.4', required: true)] if not cc.has_header('uthash.h') error('Dependency uthash not found') @@ -37,18 +42,9 @@ endif deps = [] -if get_option('config_file') - deps += [dependency('libconfig', version: '>=1.4', required: true)] - - cflags += ['-DCONFIG_LIBCONFIG'] - srcs += [ 'config_libconfig.c' ] -endif if get_option('regex') - pcre = dependency('libpcre', required: true) + pcre = dependency('libpcre2-8', required: true) cflags += ['-DCONFIG_REGEX_PCRE'] - if pcre.version().version_compare('>=8.20') - cflags += ['-DCONFIG_REGEX_PCRE_JIT'] - endif deps += [pcre] endif @@ -58,15 +54,15 @@ if get_option('vsync_drm') endif if get_option('opengl') - cflags += ['-DCONFIG_OPENGL', '-DGL_GLEXT_PROTOTYPES'] - deps += [dependency('gl', required: true)] + cflags += ['-DCONFIG_OPENGL'] + deps += [dependency('epoxy', required: true)] srcs += [ 'opengl.c' ] endif if get_option('dbus') cflags += ['-DCONFIG_DBUS'] deps += [dependency('dbus-1', required: true)] - srcs += [ 'dbus.c' ] + srcs += [ 'dbus.c', 'rtkit.c' ] endif if get_option('xrescheck') @@ -95,3 +91,15 @@ picom = executable('picom', srcs, c_args: cflags, if get_option('unittest') test('picom unittest', picom, args: [ '--unittest' ]) endif + +install_symlink('picom-inspect', install_dir: 'bin', pointing_to: 'picom') + +if cc.has_argument('-fsanitize=fuzzer') + c2_fuzz = executable('c2_fuzz', srcs + ['fuzzer/c2.c'], + c_args: cflags + ['-fsanitize=fuzzer', '-DCONFIG_FUZZER'], + link_args: ['-fsanitize=fuzzer'], + dependencies: [ base_deps, deps, test_h_dep ], + build_by_default: false, + install: false, include_directories: picom_inc + ) +endif diff --git a/src/opengl.c b/src/opengl.c index 5d2d66cb8c..ea09e1cc07 100644 --- a/src/opengl.c +++ b/src/opengl.c @@ -28,6 +28,7 @@ #include "uthash_extra.h" #include "utils.h" #include "win.h" +#include "wm.h" #include "opengl.h" @@ -39,7 +40,7 @@ static inline XVisualInfo *get_visualinfo_from_visual(session_t *ps, xcb_visuali XVisualInfo vreq = {.visualid = visual}; int nitems = 0; - return XGetVisualInfo(ps->dpy, VisualIDMask, &vreq, &nitems); + return XGetVisualInfo(ps->c.dpy, VisualIDMask, &vreq, &nitems); } /** @@ -56,7 +57,7 @@ bool glx_init(session_t *ps, bool need_render) { } // Get XVisualInfo - pvis = get_visualinfo_from_visual(ps, ps->vis); + pvis = get_visualinfo_from_visual(ps, ps->c.screen_info->root_visual); if (!pvis) { log_error("Failed to acquire XVisualInfo for current visual."); goto glx_init_end; @@ -65,20 +66,22 @@ bool glx_init(session_t *ps, bool need_render) { // Ensure the visual is double-buffered if (need_render) { int value = 0; - if (Success != glXGetConfig(ps->dpy, pvis, GLX_USE_GL, &value) || !value) { + if (Success != glXGetConfig(ps->c.dpy, pvis, GLX_USE_GL, &value) || !value) { log_error("Root visual is not a GL visual."); goto glx_init_end; } - if (Success != glXGetConfig(ps->dpy, pvis, GLX_DOUBLEBUFFER, &value) || !value) { + if (Success != glXGetConfig(ps->c.dpy, pvis, GLX_DOUBLEBUFFER, &value) || + !value) { log_error("Root visual is not a double buffered GL visual."); goto glx_init_end; } } // Ensure GLX_EXT_texture_from_pixmap exists - if (need_render && !glxext.has_GLX_EXT_texture_from_pixmap) + if (need_render && !glxext.has_GLX_EXT_texture_from_pixmap) { goto glx_init_end; + } // Initialize GLX data structure if (!ps->psglx) { @@ -112,7 +115,7 @@ bool glx_init(session_t *ps, bool need_render) { if (!psglx->context) { // Get GLX context #ifndef DEBUG_GLX_DEBUG_CONTEXT - psglx->context = glXCreateContext(ps->dpy, pvis, None, GL_TRUE); + psglx->context = glXCreateContext(ps->c.dpy, pvis, None, GL_TRUE); #else { GLXFBConfig fbconfig = get_fbconfig_from_visualinfo(ps, pvis); @@ -134,7 +137,7 @@ bool glx_init(session_t *ps, bool need_render) { static const int attrib_list[] = { GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_DEBUG_BIT_ARB, None}; psglx->context = p_glXCreateContextAttribsARB( - ps->dpy, fbconfig, NULL, GL_TRUE, attrib_list); + ps->c.dpy, fbconfig, NULL, GL_TRUE, attrib_list); } #endif @@ -144,7 +147,7 @@ bool glx_init(session_t *ps, bool need_render) { } // Attach GLX context - if (!glXMakeCurrent(ps->dpy, get_tgt_window(ps), psglx->context)) { + if (!glXMakeCurrent(ps->c.dpy, get_tgt_window(ps), psglx->context)) { log_error("Failed to attach GLX context."); goto glx_init_end; } @@ -177,9 +180,10 @@ bool glx_init(session_t *ps, bool need_render) { // Check GL_ARB_texture_non_power_of_two, requires a GLX context and // must precede FBConfig fetching - if (need_render) + if (need_render) { psglx->has_texture_non_power_of_two = - gl_has_extension("GL_ARB_texture_non_power_of_two"); + epoxy_has_gl_extension("GL_ARB_texture_non_power_of_two"); + } // Render preparations if (need_render) { @@ -199,9 +203,9 @@ bool glx_init(session_t *ps, bool need_render) { } // Clear screen - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClearColor(0.0F, 0.0F, 0.0F, 1.0F); // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - // glXSwapBuffers(ps->dpy, get_tgt_window(ps)); + // glXSwapBuffers(ps->c.dpy, get_tgt_window(ps)); } success = true; @@ -209,15 +213,17 @@ bool glx_init(session_t *ps, bool need_render) { glx_init_end: XFree(pvis); - if (!success) + if (!success) { glx_destroy(ps); + } return success; } static void glx_free_prog_main(glx_prog_main_t *pprogram) { - if (!pprogram) + if (!pprogram) { return; + } if (pprogram->prog) { glDeleteProgram(pprogram->prog); pprogram->prog = 0; @@ -231,11 +237,12 @@ static void glx_free_prog_main(glx_prog_main_t *pprogram) { * Destroy GLX related resources. */ void glx_destroy(session_t *ps) { - if (!ps->psglx) + if (!ps->psglx) { return; + } // Free all GLX resources of windows - win_stack_foreach_managed(w, &ps->window_stack) { + win_stack_foreach_managed(w, wm_stack_end(ps->wm)) { free_win_res_glx(ps, w); } @@ -266,14 +273,14 @@ void glx_destroy(session_t *ps) { // Destroy GLX context if (ps->psglx->context) { - glXMakeCurrent(ps->dpy, None, NULL); - glXDestroyContext(ps->dpy, ps->psglx->context); + glXMakeCurrent(ps->c.dpy, None, NULL); + glXDestroyContext(ps->c.dpy, ps->psglx->context); ps->psglx->context = NULL; } free(ps->psglx); ps->psglx = NULL; - ps->argb_fbconfig = NULL; + ps->argb_fbconfig = (struct glx_fbconfig_info){0}; } /** @@ -372,8 +379,9 @@ bool glx_init_blur(session_t *ps) { double sum = 0.0; for (int j = 0; j < height; ++j) { for (int k = 0; k < width; ++k) { - if (height / 2 == j && width / 2 == k) + if (height / 2 == j && width / 2 == k) { continue; + } double val = kern->data[j * width + k]; if (val == 0) { continue; @@ -690,8 +698,9 @@ bool glx_bind_texture(session_t *ps attr_unused, glx_texture_t **pptex, int x, i */ bool glx_bind_pixmap(session_t *ps, glx_texture_t **pptex, xcb_pixmap_t pixmap, int width, int height, bool repeat, const struct glx_fbconfig_info *fbcfg) { - if (ps->o.backend != BKEND_GLX && ps->o.backend != BKEND_XR_GLX_HYBRID) + if (ps->o.backend != BKEND_GLX && ps->o.backend != BKEND_XR_GLX_HYBRID) { return true; + } if (!pixmap) { log_error("Binding to an empty pixmap %#010x. This can't work.", pixmap); @@ -732,16 +741,11 @@ bool glx_bind_pixmap(session_t *ps, glx_texture_t **pptex, xcb_pixmap_t pixmap, // Retrieve pixmap parameters, if they aren't provided if (!width || !height) { auto r = xcb_get_geometry_reply( - ps->c, xcb_get_geometry(ps->c, pixmap), NULL); + ps->c.c, xcb_get_geometry(ps->c.c, pixmap), NULL); if (!r) { log_error("Failed to query info of pixmap %#010x.", pixmap); return false; } - if (r->depth > OPENGL_MAX_DEPTH) { - log_error("Requested depth %d higher than %d.", depth, - OPENGL_MAX_DEPTH); - return false; - } depth = r->depth; width = r->width; height = r->height; @@ -753,14 +757,15 @@ bool glx_bind_pixmap(session_t *ps, glx_texture_t **pptex, xcb_pixmap_t pixmap, // pixmap-specific parameters, and this may change in the future GLenum tex_tgt = 0; if (GLX_TEXTURE_2D_BIT_EXT & fbcfg->texture_tgts && - ps->psglx->has_texture_non_power_of_two) + ps->psglx->has_texture_non_power_of_two) { tex_tgt = GLX_TEXTURE_2D_EXT; - else if (GLX_TEXTURE_RECTANGLE_BIT_EXT & fbcfg->texture_tgts) + } else if (GLX_TEXTURE_RECTANGLE_BIT_EXT & fbcfg->texture_tgts) { tex_tgt = GLX_TEXTURE_RECTANGLE_EXT; - else if (!(GLX_TEXTURE_2D_BIT_EXT & fbcfg->texture_tgts)) + } else if (!(GLX_TEXTURE_2D_BIT_EXT & fbcfg->texture_tgts)) { tex_tgt = GLX_TEXTURE_RECTANGLE_EXT; - else + } else { tex_tgt = GLX_TEXTURE_2D_EXT; + } log_debug("depth %d, tgt %#x, rgba %d", depth, tex_tgt, (GLX_TEXTURE_FORMAT_RGBA_EXT == fbcfg->texture_fmt)); @@ -773,7 +778,7 @@ bool glx_bind_pixmap(session_t *ps, glx_texture_t **pptex, xcb_pixmap_t pixmap, 0, }; - ptex->glpixmap = glXCreatePixmap(ps->dpy, fbcfg->cfg, pixmap, attrs); + ptex->glpixmap = glXCreatePixmap(ps->c.dpy, fbcfg->cfg, pixmap, attrs); ptex->pixmap = pixmap; ptex->target = (GLX_TEXTURE_2D_EXT == tex_tgt ? GL_TEXTURE_2D : GL_TEXTURE_RECTANGLE); @@ -819,10 +824,11 @@ bool glx_bind_pixmap(session_t *ps, glx_texture_t **pptex, xcb_pixmap_t pixmap, // The specification requires rebinding whenever the content changes... // We can't follow this, too slow. - if (need_release) - glXReleaseTexImageEXT(ps->dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); + if (need_release) { + glXReleaseTexImageEXT(ps->c.dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); + } - glXBindTexImageEXT(ps->dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT, NULL); + glXBindTexImageEXT(ps->c.dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT, NULL); // Cleanup glBindTexture(ptex->target, 0); @@ -840,13 +846,13 @@ void glx_release_pixmap(session_t *ps, glx_texture_t *ptex) { // Release binding if (ptex->glpixmap && ptex->texture) { glBindTexture(ptex->target, ptex->texture); - glXReleaseTexImageEXT(ps->dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); + glXReleaseTexImageEXT(ps->c.dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); glBindTexture(ptex->target, 0); } // Free GLX Pixmap if (ptex->glpixmap) { - glXDestroyPixmap(ps->dpy, ptex->glpixmap); + glXDestroyPixmap(ps->c.dpy, ptex->glpixmap); ptex->glpixmap = 0; } @@ -858,14 +864,16 @@ void glx_release_pixmap(session_t *ps, glx_texture_t *ptex) { */ void glx_set_clip(session_t *ps, const region_t *reg) { // Quit if we aren't using stencils - if (ps->o.glx_no_stencil) + if (ps->o.glx_no_stencil) { return; + } glDisable(GL_STENCIL_TEST); glDisable(GL_SCISSOR_TEST); - if (!reg) + if (!reg) { return; + } int nrects; const rect_t *rects = pixman_region32_rectangles((region_t *)reg, &nrects); @@ -913,8 +921,9 @@ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, // Calculate copy region size glx_blur_cache_t ibc = {.width = 0, .height = 0}; - if (!pbc) + if (!pbc) { pbc = &ibc; + } int mdx = dx, mdy = dy, mwidth = width, mheight = height; // log_trace("%d, %d, %d, %d", mdx, mdy, mwidth, mheight); @@ -941,24 +950,29 @@ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, */ GLenum tex_tgt = GL_TEXTURE_RECTANGLE; - if (ps->psglx->has_texture_non_power_of_two) + if (ps->psglx->has_texture_non_power_of_two) { tex_tgt = GL_TEXTURE_2D; + } // Free textures if size inconsistency discovered - if (mwidth != pbc->width || mheight != pbc->height) + if (mwidth != pbc->width || mheight != pbc->height) { free_glx_bc_resize(ps, pbc); + } // Generate FBO and textures if needed - if (!pbc->textures[0]) + if (!pbc->textures[0]) { pbc->textures[0] = glx_gen_texture(tex_tgt, mwidth, mheight); + } GLuint tex_scr = pbc->textures[0]; - if (more_passes && !pbc->textures[1]) + if (more_passes && !pbc->textures[1]) { pbc->textures[1] = glx_gen_texture(tex_tgt, mwidth, mheight); + } pbc->width = mwidth; pbc->height = mheight; GLuint tex_scr2 = pbc->textures[1]; - if (more_passes && !pbc->fbo) + if (more_passes && !pbc->fbo) { glGenFramebuffers(1, &pbc->fbo); + } const GLuint fbo = pbc->fbo; if (!tex_scr || (more_passes && !tex_scr2)) { @@ -986,7 +1000,7 @@ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, } */ // Texture scaling factor - GLfloat texfac_x = 1.0f, texfac_y = 1.0f; + GLfloat texfac_x = 1.0F, texfac_y = 1.0F; if (tex_tgt == GL_TEXTURE_2D) { texfac_x /= (GLfloat)mwidth; texfac_y /= (GLfloat)mheight; @@ -1019,10 +1033,12 @@ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, } else { glBindFramebuffer(GL_FRAMEBUFFER, 0); glDrawBuffer(GL_BACK); - if (have_scissors) + if (have_scissors) { glEnable(GL_SCISSOR_TEST); - if (have_stencil) + } + if (have_stencil) { glEnable(GL_STENCIL_TEST); + } } // Color negation for testing... @@ -1032,12 +1048,15 @@ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glUseProgram(ppass->prog); - if (ppass->unifm_offset_x >= 0) + if (ppass->unifm_offset_x >= 0) { glUniform1f(ppass->unifm_offset_x, texfac_x); - if (ppass->unifm_offset_y >= 0) + } + if (ppass->unifm_offset_y >= 0) { glUniform1f(ppass->unifm_offset_y, texfac_y); - if (ppass->unifm_factor_center >= 0) + } + if (ppass->unifm_factor_center >= 0) { glUniform1f(ppass->unifm_factor_center, factor_center); + } P_PAINTREG_START(crect) { auto rx = (GLfloat)(crect.x1 - mdx) * texfac_x; @@ -1087,10 +1106,12 @@ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(tex_tgt, 0); glDisable(tex_tgt); - if (have_scissors) + if (have_scissors) { glEnable(GL_SCISSOR_TEST); - if (have_stencil) + } + if (have_stencil) { glEnable(GL_STENCIL_TEST); + } if (&ibc == pbc) { free_glx_bc(ps, pbc); @@ -1104,7 +1125,7 @@ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, // TODO(bhagwan) this is a mess and needs a more consistent way of getting the border // pixel I tried looking for a notify event for XCB_CW_BORDER_PIXEL (in // xcb_create_window()) or a way to get the pixels from xcb_render_picture_t but the -// documentation for the xcb_xrender extension is literaly non existent... +// documentation for the xcb_xrender extension is literally non existent... // // NOTE(yshui) There is no consistent way to get the "border" color of a X window. From // the WM's perspective there are multiple ways to implement window borders. Using @@ -1275,7 +1296,7 @@ bool glx_dim_dst(session_t *ps, int dx, int dy, int width, int height, int z, // considering all those mess in color negation and modulation glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - glColor4f(0.0f, 0.0f, 0.0f, factor); + glColor4f(0.0F, 0.0F, 0.0F, factor); P_PAINTREG_START(crect) { // XXX what does all of these variables mean? @@ -1291,7 +1312,7 @@ bool glx_dim_dst(session_t *ps, int dx, int dy, int width, int height, int z, } P_PAINTREG_END(); - glColor4f(0.0f, 0.0f, 0.0f, 0.0f); + glColor4f(0.0F, 0.0F, 0.0F, 0.0F); glDisable(GL_BLEND); gl_check_err(); @@ -1411,15 +1432,19 @@ bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, glUseProgram(pprogram->prog); struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); - if (pprogram->unifm_opacity >= 0) + if (pprogram->unifm_opacity >= 0) { glUniform1f(pprogram->unifm_opacity, (float)opacity); - if (pprogram->unifm_invert_color >= 0) + } + if (pprogram->unifm_invert_color >= 0) { glUniform1i(pprogram->unifm_invert_color, neg); - if (pprogram->unifm_tex >= 0) + } + if (pprogram->unifm_tex >= 0) { glUniform1i(pprogram->unifm_tex, 0); - if (pprogram->unifm_time >= 0) - glUniform1f(pprogram->unifm_time, (float)ts.tv_sec * 1000.0f + - (float)ts.tv_nsec / 1.0e6f); + } + if (pprogram->unifm_time >= 0) { + glUniform1f(pprogram->unifm_time, (float)ts.tv_sec * 1000.0F + + (float)ts.tv_nsec / 1.0e6F); + } } // log_trace("Draw: %d, %d, %d, %d -> %d, %d (%d, %d) z %d", x, y, width, height, @@ -1459,8 +1484,8 @@ bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, // Invert Y if needed, this may not work as expected, though. I // don't have such a FBConfig to test with. if (!ptex->y_inverted) { - ry = 1.0f - ry; - rye = 1.0f - rye; + ry = 1.0F - ry; + rye = 1.0F - rye; } // log_trace("Rect %d: %f, %f, %f, %f -> %d, %d, %d, %d", ri, rx, @@ -1492,7 +1517,7 @@ bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, // Cleanup glBindTexture(ptex->target, 0); - glColor4f(0.0f, 0.0f, 0.0f, 0.0f); + glColor4f(0.0F, 0.0F, 0.0F, 0.0F); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glDisable(GL_BLEND); glDisable(GL_COLOR_LOGIC_OP); @@ -1505,8 +1530,9 @@ bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, glActiveTexture(GL_TEXTURE0); } - if (has_prog) + if (has_prog) { glUseProgram(0); + } gl_check_err(); diff --git a/src/opengl.h b/src/opengl.h index dcd8697b41..5bf92ed438 100644 --- a/src/opengl.h +++ b/src/opengl.h @@ -18,9 +18,9 @@ #include "render.h" #include "win.h" -#include -#include #include +#include +#include #include #include #include @@ -75,7 +75,7 @@ typedef struct glx_session { glx_round_pass_t *round_passes; } glx_session_t; -/// @brief Wrapper of a binded GLX texture. +/// @brief Wrapper of a bound GLX texture. typedef struct _glx_texture { GLuint texture; GLXPixmap glpixmap; @@ -121,9 +121,9 @@ bool glx_bind_texture(session_t *ps, glx_texture_t **pptex, int x, int y, int wi void glx_paint_pre(session_t *ps, region_t *preg) attr_nonnull(1, 2); /** - * Check if a texture is binded, or is binded to the given pixmap. + * Check if a texture is bound, or is bound to the given pixmap. */ -static inline bool glx_tex_binded(const glx_texture_t *ptex, xcb_pixmap_t pixmap) { +static inline bool glx_tex_bound(const glx_texture_t *ptex, xcb_pixmap_t pixmap) { return ptex && ptex->glpixmap && ptex->texture && (!pixmap || pixmap == ptex->pixmap); } @@ -156,10 +156,11 @@ static inline bool glx_has_context(session_t *ps) { */ static inline bool ensure_glx_context(session_t *ps) { // Create GLX context - if (!glx_has_context(ps)) + if (!glx_has_context(ps)) { glx_init(ps, false); + } - return ps->psglx->context; + return glx_has_context(ps); } /** @@ -227,10 +228,7 @@ static inline void free_texture(session_t *ps, glx_texture_t **pptex) { */ static inline void free_paint_glx(session_t *ps, paint_t *ppaint) { free_texture(ps, &ppaint->ptex); -#ifdef CONFIG_OPENGL - free(ppaint->fbcfg); -#endif - ppaint->fbcfg = NULL; + ppaint->fbcfg = (struct glx_fbconfig_info){0}; } /** diff --git a/src/options.c b/src/options.c index 3f228eaaef..7a502f0f78 100644 --- a/src/options.c +++ b/src/options.c @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include // for xcb_render_fixed_t, XXX @@ -15,463 +17,673 @@ #include "config.h" #include "log.h" #include "options.h" +#include "string_utils.h" #include "utils.h" #include "win.h" +#include "x.h" #pragma GCC diagnostic error "-Wunused-parameter" +struct picom_option; + +struct picom_arg { + const char *name; + ptrdiff_t offset; + + const void *user_data; + bool (*handler)(const struct picom_option *, const struct picom_arg *, + const char *optarg, void *output); +}; + +struct picom_arg_parser { + int (*parse)(const char *); + int invalid_value; +}; + +struct picom_rules_parser { + void *(*parse_prefix)(const char *, const char **end, void *data); + void (*free_value)(void *); + void *user_data; +}; + +struct picom_deprecated_arg { + const char *message; + struct picom_arg inner; + bool error; +}; + +struct picom_option { + const char *long_name; + int has_arg; + struct picom_arg arg; + const char *help; +}; + +static bool set_flag(const struct picom_option * /*opt*/, const struct picom_arg *arg, + const char * /*arg_str*/, void *output) { + *(bool *)(output + arg->offset) = true; + return true; +} + +static bool unset_flag(const struct picom_option * /*opt*/, const struct picom_arg *arg, + const char * /*arg_str*/, void *output) { + *(bool *)(output + arg->offset) = false; + return true; +} + +static bool parse_with(const struct picom_option *opt, const struct picom_arg *arg, + const char *arg_str, void *output) { + const struct picom_arg_parser *parser = arg->user_data; + int *dst = (int *)(output + arg->offset); + *dst = parser->parse(arg_str); + if (*dst == parser->invalid_value) { + log_error("Invalid argument for option `--%s`: %s", opt->long_name, arg_str); + return false; + } + return true; +} + +static bool store_float(const struct picom_option *opt, const struct picom_arg *arg, + const char *arg_str, void *output) { + double *dst = (double *)(output + arg->offset); + const double *minmax = (const double *)arg->user_data; + const char *endptr = NULL; + *dst = strtod_simple(arg_str, &endptr); + if (!endptr || *endptr != '\0') { + log_error("Argument for option `--%s` is not a valid float number: %s", + opt->long_name, arg_str); + return false; + } + *dst = max2(minmax[0], min2(*dst, minmax[1])); + return true; +} + +static bool store_int(const struct picom_option *opt, const struct picom_arg *arg, + const char *arg_str, void *output) { + const int *minmax = (const int *)arg->user_data; + int *dst = (int *)(output + arg->offset); + if (!parse_int(arg_str, dst)) { + log_error("Argument for option `--%s` is not a valid integer: %s", + opt->long_name, arg_str); + return false; + } + *dst = max2(minmax[0], min2(*dst, minmax[1])); + return true; +} + +static bool store_string(const struct picom_option * /*opt*/, const struct picom_arg *arg, + const char *arg_str, void *output) { + char **dst = (char **)(output + arg->offset); + free(*dst); + *dst = strdup(arg_str); + return true; +} + +static bool store_rules(const struct picom_option * /*opt*/, const struct picom_arg *arg, + const char *arg_str, void *output) { + const struct picom_rules_parser *parser = arg->user_data; + auto rules = (c2_lptr_t **)(output + arg->offset); + if (!parser->parse_prefix) { + return c2_parse(rules, arg_str, NULL) != NULL; + } + return c2_parse_with_prefix(rules, arg_str, parser->parse_prefix, + parser->free_value, parser->user_data); +} + +static bool store_fixed_enum(const struct picom_option * /*opt*/, const struct picom_arg *arg, + const char * /*arg_str*/, void *output) { + const int *value = (const int *)arg->user_data; + *(int *)(output + arg->offset) = *value; + return true; +} + +static bool noop(const struct picom_option * /*opt*/, const struct picom_arg * /*arg*/, + const char * /*arg_str*/, void * /*output*/) { + return true; +} + +static bool reject(const struct picom_option * /*opt*/, const struct picom_arg * /*arg*/, + const char * /*arg_str*/, void * /*output*/) { + return false; +} + +static bool say_deprecated(const struct picom_option *opt, const struct picom_arg *arg, + const char *arg_str, void *output) { + const struct picom_deprecated_arg *deprecation = arg->user_data; + enum log_level level = deprecation->error ? LOG_LEVEL_ERROR : LOG_LEVEL_WARN; + log_printf(tls_logger, level, __func__, + "Option `--%s` has been deprecated. Please remove it. %s", + opt->long_name, deprecation->message); + return deprecation->inner.handler(opt, &deprecation->inner, arg_str, output); +} + +#define OFFSET(member) offsetof(struct options, member) + +#define ENABLE(member) \ + no_argument, { \ + .offset = OFFSET(member), .handler = set_flag, \ + } + +#define DISABLE(member) \ + no_argument, { \ + .offset = OFFSET(member), .handler = unset_flag, \ + } + +#define IGNORE(has_arg) \ + has_arg, { \ + .handler = noop, \ + } + +#define REJECT(has_arg) \ + has_arg, { \ + .handler = reject, \ + } + +#define DO(fn) \ + required_argument, { \ + .handler = (fn), \ + } + +#define PARSE_WITH(fn, invalid, member) \ + required_argument, { \ + .offset = OFFSET(member), .handler = parse_with, \ + .user_data = (struct picom_arg_parser[]){{ \ + .invalid_value = (invalid), \ + .parse = (fn), \ + }}, \ + } + +#define FLOAT(member, min, max) \ + required_argument, { \ + .offset = OFFSET(member), .handler = store_float, \ + .user_data = (double[]){min, max}, \ + } + +#define INTEGER(member, min, max) \ + required_argument, { \ + .offset = OFFSET(member), .handler = store_int, \ + .user_data = (int[]){min, max}, \ + } + +#define NAMED_STRING(member, name_) \ + required_argument, { \ + .offset = OFFSET(member), .handler = store_string, .name = (name_) \ + } + +#define STRING(member) NAMED_STRING(member, NULL) + +#define NAMED_RULES(member, name_, ...) \ + required_argument, { \ + .offset = OFFSET(member), .handler = store_rules, .name = (name_), \ + .user_data = (struct picom_rules_parser[]) { \ + __VA_ARGS__ \ + } \ + } +#define NUMERIC_RULES(member, value, min, max) \ + NAMED_RULES(member, value ":COND", \ + {.parse_prefix = parse_numeric_prefix, .user_data = (int[]){min, max}}) +#define RULES(member) NAMED_RULES(member, "COND", {}) + +#define FIXED(member, value) \ + no_argument, { \ + .offset = OFFSET(member), .handler = store_fixed_enum, \ + .user_data = (int[]){value}, \ + } + +#define SAY_DEPRECATED_(error_, msg, has_arg, ...) \ + has_arg, { \ + .handler = say_deprecated, .user_data = (struct picom_deprecated_arg[]) { \ + {.message = (msg), .inner = __VA_ARGS__, .error = error_}, \ + } \ + } +#define SAY_DEPRECATED(error_, msg, ...) SAY_DEPRECATED_(error_, msg, __VA_ARGS__) + +#define WARN_DEPRECATED(...) \ + SAY_DEPRECATED_(false, \ + "If you encounter problems without this feature, please " \ + "feel free to open a bug report.", \ + __VA_ARGS__) + +#define WARN_DEPRECATED_ENABLED(...) \ + SAY_DEPRECATED_(false, "Its functionality will always be enabled. ", __VA_ARGS__) + +#define ERROR_DEPRECATED(has_arg) SAY_DEPRECATED(true, "", REJECT(has_arg)) + +static bool +store_shadow_color(const struct picom_option * /*opt*/, const struct picom_arg * /*arg*/, + const char *arg_str, void *output) { + struct options *opt = (struct options *)output; + struct color rgb; + rgb = hex_to_rgb(arg_str); + opt->shadow_red = rgb.red; + opt->shadow_green = rgb.green; + opt->shadow_blue = rgb.blue; + return true; +} + +static bool +handle_menu_opacity(const struct picom_option * /*opt*/, const struct picom_arg * /*arg*/, + const char *arg_str, void *output) { + struct options *opt = (struct options *)output; + const char *endptr = NULL; + double tmp = max2(0.0, min2(1.0, strtod_simple(arg_str, &endptr))); + if (!endptr || *endptr != '\0') { + return false; + } + opt->wintype_option_mask[WINTYPE_DROPDOWN_MENU].opacity = true; + opt->wintype_option_mask[WINTYPE_POPUP_MENU].opacity = true; + opt->wintype_option[WINTYPE_POPUP_MENU].opacity = tmp; + opt->wintype_option[WINTYPE_DROPDOWN_MENU].opacity = tmp; + return true; +} + +static bool +store_blur_kern(const struct picom_option * /*opt*/, const struct picom_arg * /*arg*/, + const char *arg_str, void *output) { + struct options *opt = (struct options *)output; + opt->blur_kerns = parse_blur_kern_lst(arg_str, &opt->blur_kernel_count); + return opt->blur_kerns != NULL; +} + +static bool +store_benchmark_wid(const struct picom_option * /*opt*/, const struct picom_arg * /*arg*/, + const char *arg_str, void *output) { + struct options *opt = (struct options *)output; + const char *endptr = NULL; + opt->benchmark_wid = (xcb_window_t)strtoul(arg_str, (char **)&endptr, 0); + if (!endptr || *endptr != '\0') { + log_error("Invalid window ID for `--benchmark-wid`: %s", arg_str); + return false; + } + return true; +} + +#define WINDOW_SHADER_RULE \ + { .parse_prefix = parse_window_shader_prefix_with_cwd, .free_value = free, } + +#ifdef CONFIG_OPENGL +#define BACKENDS "xrender, glx" +#else +#define BACKENDS "xrender" +#endif + +// clang-format off +static const struct option *longopts = NULL; +static const struct picom_option picom_options[] = { + // As you can see, aligning this table is difficult... + + // Rejected options, we shouldn't be able to reach `get_cfg` when these are set + ['h'] = {"help" , REJECT(no_argument), "Print this help message and exit."}, + [318] = {"version", REJECT(no_argument), "Print version number and exit."}, + + // Ignored options, these are already handled by `get_early_cfg` + [314] = {"show-all-xerrors", IGNORE(no_argument)}, + ['b'] = {"daemon" , IGNORE(no_argument) , "Daemonize process."}, + [256] = {"config" , IGNORE(required_argument), "Path to the configuration file."}, + + // Simple flags + ['c'] = {"shadow" , ENABLE(shadow_enable) , "Enabled client-side shadows on windows."}, + ['f'] = {"fading" , ENABLE(fading_enable) , "Fade windows in/out when opening/closing and when opacity changes, " + "unless --no-fading-openclose is used."}, + [262] = {"mark-wmwin-focused" , ENABLE(mark_wmwin_focused) , "Try to detect WM windows and mark them as active."}, + [264] = {"mark-ovredir-focused" , ENABLE(mark_ovredir_focused) , "Mark windows that have no WM frame as active."}, + [265] = {"no-fading-openclose" , ENABLE(no_fading_openclose) , "Do not fade on window open/close."}, + [266] = {"shadow-ignore-shaped" , ENABLE(shadow_ignore_shaped) , "Do not paint shadows on shaped windows. (Deprecated, use --shadow-exclude " + "\'bounding_shaped\' or --shadow-exclude \'bounding_shaped && " + "!rounded_corners\' instead.)"}, + [268] = {"detect-client-opacity" , ENABLE(detect_client_opacity) , "Detect _NET_WM_WINDOW_OPACITY on client windows, useful for window " + "managers not passing _NET_WM_WINDOW_OPACITY of client windows to frame"}, + [270] = {"vsync" , ENABLE(vsync) , "Enable VSync"}, + [271] = {"crop-shadow-to-monitor" , ENABLE(crop_shadow_to_monitor) , "Crop shadow of a window fully on a particular monitor to that monitor. " + "This is currently implemented using the X RandR extension"}, + [276] = {"use-ewmh-active-win" , ENABLE(use_ewmh_active_win) , "Use _NET_WM_ACTIVE_WINDOW on the root window to determine which window is " + "focused instead of using FocusIn/Out events"}, + [278] = {"unredir-if-possible" , ENABLE(unredir_if_possible) , "Unredirect all windows if a full-screen opaque window is detected, to " + "maximize performance for full-screen applications."}, + [280] = {"inactive-dim-fixed" , ENABLE(inactive_dim_fixed) , "Use fixed inactive dim value."}, + [281] = {"detect-transient" , ENABLE(detect_transient) , "Use WM_TRANSIENT_FOR to group windows, and consider windows in the same " + "group focused at the same time."}, + [282] = {"detect-client-leader" , ENABLE(detect_client_leader) , "Use WM_CLIENT_LEADER to group windows, and consider windows in the same group " + "focused at the same time. This usually means windows from the same application " + "will be considered focused or unfocused at the same time. WM_TRANSIENT_FOR has " + "higher priority if --detect-transient is enabled, too."}, + [284] = {"blur-background-frame" , ENABLE(blur_background_frame) , "Blur background of windows when the window frame is not opaque. Implies " + "--blur-background."}, + [285] = {"blur-background-fixed" , ENABLE(blur_background_fixed) , "Use fixed blur strength instead of adjusting according to window opacity."}, +#ifdef CONFIG_DBUS + [286] = {"dbus" , ENABLE(dbus) , "Enable remote control via D-Bus. See the D-BUS API section in the man page " + "for more details."}, +#endif + [311] = {"vsync-use-glfinish" , ENABLE(vsync_use_glfinish)}, + [313] = {"xrender-sync-fence" , ENABLE(xrender_sync_fence) , "Additionally use X Sync fence to sync clients' draw calls. Needed on " + "nvidia-drivers with GLX backend for some users."}, + [315] = {"no-fading-destroyed-argb" , ENABLE(no_fading_destroyed_argb) , "Do not fade destroyed ARGB windows with WM frame. Workaround bugs in Openbox, " + "Fluxbox, etc."}, + [316] = {"force-win-blend" , ENABLE(force_win_blend) , "Force all windows to be painted with blending. Useful if you have a custom " + "shader that could turn opaque pixels transparent."}, + [319] = {"no-x-selection" , ENABLE(no_x_selection)}, + [323] = {"use-damage" , ENABLE(use_damage) , "Render only the damaged (changed) part of the screen"}, + [324] = {"no-use-damage" , DISABLE(use_damage) , "Disable the use of damage information. This cause the whole screen to be" + "redrawn every time, instead of the part of the screen that has actually " + "changed. Potentially degrades the performance, but might fix some artifacts."}, + [260] = {"inactive-opacity-override", ENABLE(inactive_opacity_override), "Inactive opacity set by -i overrides value of _NET_WM_WINDOW_OPACITY."}, + [267] = {"detect-rounded-corners" , ENABLE(detect_rounded_corners) , "Try to detect windows with rounded corners and don't consider them shaped " + "windows. Affects --shadow-ignore-shaped, --unredir-if-possible, and " + "possibly others. You need to turn this on manually if you want to match " + "against rounded_corners in conditions."}, + [298] = {"glx-no-rebind-pixmap" , ENABLE(glx_no_rebind_pixmap)}, + [291] = {"glx-no-stencil" , ENABLE(glx_no_stencil)}, + [325] = {"no-vsync" , DISABLE(vsync) , "Disable VSync"}, + [327] = {"transparent-clipping" , ENABLE(transparent_clipping) , "Make transparent windows clip other windows like non-transparent windows do, " + "instead of blending on top of them"}, + [339] = {"dithered-present" , ENABLE(dithered_present) , "Use higher precision during rendering, and apply dither when presenting the " + "rendered screen. Reduces banding artifacts, but might cause performance " + "degradation. Only works with OpenGL."}, + [341] = {"no-frame-pacing" , DISABLE(frame_pacing) , "Disable frame pacing. This might increase the latency."}, + [733] = {"legacy-backends" , ENABLE(legacy_backends) , "Use deprecated version of the backends."}, + [800] = {"monitor-repaint" , ENABLE(monitor_repaint) , "Highlight the updated area of the screen. For debugging."}, + [801] = {"diagnostics" , ENABLE(print_diagnostics) , "Print diagnostic information"}, + [802] = {"debug-mode" , ENABLE(debug_mode) , "Render into a separate window, and don't take over the screen. Useful when " + "you want to attach a debugger to picom"}, + [803] = {"no-ewmh-fullscreen" , ENABLE(no_ewmh_fullscreen) , "Do not use EWMH to detect fullscreen windows. Reverts to checking if a " + "window is fullscreen based only on its size and coordinates."}, + [804] = {"realtime" , ENABLE(use_realtime_scheduling) , "Enable realtime scheduling. This might reduce latency, but might also cause " + "other issues. Disable this if you see the compositor being killed."}, + + // Flags that takes an argument + ['r'] = {"shadow-radius" , INTEGER(shadow_radius, 0, INT_MAX) , "The blur radius for shadows. (default 12)"}, + ['o'] = {"shadow-opacity" , FLOAT(shadow_opacity, 0, 1) , "The translucency for shadows. (default .75)"}, + ['l'] = {"shadow-offset-x" , INTEGER(shadow_offset_x, INT_MIN, INT_MAX) , "The left offset for shadows. (default -15)"}, + ['t'] = {"shadow-offset-y" , INTEGER(shadow_offset_y, INT_MIN, INT_MAX) , "The top offset for shadows. (default -15)"}, + ['I'] = {"fade-in-step" , FLOAT(fade_in_step, 0, 1) , "Opacity change between steps while fading in. (default 0.028)"}, + ['O'] = {"fade-out-step" , FLOAT(fade_out_step, 0, 1) , "Opacity change between steps while fading out. (default 0.03)"}, + ['D'] = {"fade-delta" , INTEGER(fade_delta, 1, INT_MAX) , "The time between steps in a fade in milliseconds. (default 10)"}, + ['i'] = {"inactive-opacity" , FLOAT(inactive_opacity, 0, 1) , "Opacity of inactive windows. (0.0 - 1.0)"}, + ['e'] = {"frame-opacity" , FLOAT(frame_opacity, 0, 1) , "Opacity of window titlebars and borders. (0.0 - 1.0)"}, + [257] = {"shadow-red" , FLOAT(shadow_red, 0, 1) , "Red color value of shadow (0.0 - 1.0, defaults to 0)."}, + [258] = {"shadow-green" , FLOAT(shadow_green, 0, 1) , "Green color value of shadow (0.0 - 1.0, defaults to 0)."}, + [259] = {"shadow-blue" , FLOAT(shadow_blue, 0, 1) , "Blue color value of shadow (0.0 - 1.0, defaults to 0)."}, + [261] = {"inactive-dim" , FLOAT(inactive_dim, 0, 1) , "Dim inactive windows. (0.0 - 1.0, defaults to 0)"}, + [283] = {"blur-background" , FIXED(blur_method, BLUR_METHOD_KERNEL) , "Blur background of semi-transparent / ARGB windows. May impact performance"}, + [290] = {"backend" , PARSE_WITH(parse_backend, NUM_BKEND, backend) , "Backend. Possible values are: " BACKENDS}, + [293] = {"benchmark" , INTEGER(benchmark, 0, INT_MAX) , "Benchmark mode. Repeatedly paint until reaching the specified cycles."}, + [297] = {"active-opacity" , FLOAT(active_opacity, 0, 1) , "Default opacity for active windows. (0.0 - 1.0)"}, + [302] = {"resize-damage" , INTEGER(resize_damage, INT_MIN, INT_MAX)}, // only used by legacy backends + [309] = {"unredir-if-possible-delay" , INTEGER(unredir_if_possible_delay, 0, INT_MAX) , "Delay before unredirecting the window, in milliseconds. Defaults to 0."}, + [310] = {"write-pid-path" , NAMED_STRING(write_pid_path, "PATH") , "Write process ID to a file."}, + [317] = {"glx-fshader-win" , STRING(glx_fshader_win_str)}, + [322] = {"log-file" , STRING(logpath) , "Path to the log file."}, + [326] = {"max-brightness" , FLOAT(max_brightness, 0, 1) , "Dims windows which average brightness is above this threshold. Requires " + "--no-use-damage. (default: 1.0, meaning no dimming)"}, + [329] = {"blur-size" , INTEGER(blur_radius, 0, INT_MAX) , "The radius of the blur kernel for 'box' and 'gaussian' blur method."}, + [330] = {"blur-deviation" , FLOAT(blur_deviation, 0, INFINITY) , "The standard deviation for the 'gaussian' blur method."}, + [331] = {"blur-strength" , INTEGER(blur_strength, 0, INT_MAX) , "The strength level of the 'dual_kawase' blur method."}, + [333] = {"corner-radius" , INTEGER(corner_radius, 0, INT_MAX) , "Sets the radius of rounded window corners. When > 0, the compositor will " + "round the corners of windows. (defaults to 0)."}, + [336] = {"window-shader-fg" , NAMED_STRING(window_shader_fg, "PATH") , "Specify GLSL fragment shader path for rendering window contents. Does not" + " work when `--legacy-backends` is enabled. See man page for more details."}, + [294] = {"benchmark-wid" , DO(store_benchmark_wid) , "Specify window ID to repaint in benchmark mode. If omitted or is 0, the whole" + " screen is repainted."}, + [301] = {"blur-kern" , DO(store_blur_kern) , "Specify the blur convolution kernel, see man page for more details"}, + [332] = {"shadow-color" , DO(store_shadow_color) , "Color of shadow, as a hex RGB string (defaults to #000000)"}, + + // Rules + [263] = {"shadow-exclude" , RULES(shadow_blacklist) , "Exclude conditions for shadows."}, + [279] = {"focus-exclude" , RULES(focus_blacklist) , "Specify a list of conditions of windows that should always be considered focused."}, + [288] = {"invert-color-include" , RULES(invert_color_list) , "Specify a list of conditions of windows that should be painted with " + "inverted color."}, + [296] = {"blur-background-exclude" , RULES(blur_background_blacklist) , "Exclude conditions for background blur."}, + [300] = {"fade-exclude" , RULES(fade_blacklist) , "Exclude conditions for fading."}, + [306] = {"paint-exclude" , RULES(paint_blacklist) , NULL}, + [308] = {"unredir-if-possible-exclude" , RULES(unredir_if_possible_blacklist) , "Conditions of windows that shouldn't be considered full-screen for " + "unredirecting screen."}, + [334] = {"rounded-corners-exclude" , RULES(rounded_corners_blacklist) , "Exclude conditions for rounded corners."}, + [335] = {"clip-shadow-above" , RULES(shadow_clip_list) , "Specify a list of conditions of windows to not paint a shadow over, such " + "as a dock window."}, + [338] = {"transparent-clipping-exclude", RULES(transparent_clipping_blacklist), "Specify a list of conditions of windows that should never have " + "transparent clipping applied. Useful for screenshot tools, where you " + "need to be able to see through transparent parts of the window."}, + + // Rules that are too long to fit in one line + [304] = {"opacity-rule" , NUMERIC_RULES(opacity_rules, "OPACITY", 0, 100), + "Specify a list of opacity rules, see man page for more details"}, + [337] = {"window-shader-fg-rule" , NAMED_RULES(window_shader_fg_rules, "PATH", WINDOW_SHADER_RULE), + "Specify GLSL fragment shader path for rendering window contents using patterns. Pattern should be " + "in the format of SHADER_PATH:PATTERN, similar to --opacity-rule. SHADER_PATH can be \"default\", " + "in which case the default shader will be used. Does not work when --legacy-backends is enabled. See " + "man page for more details"}, + [340] = {"corner-radius-rules" , NUMERIC_RULES(corner_radius_rules, "RADIUS", 0, INT_MAX), + "Window rules for specific rounded corner radii."}, + + // Options that are too long to fit in one line + [321] = {"log-level" , PARSE_WITH(string_to_log_level, LOG_LEVEL_INVALID, log_level), + "Log level, possible values are: trace, debug, info, warn, error"}, + [328] = {"blur-method", PARSE_WITH(parse_blur_method, BLUR_METHOD_INVALID, blur_method), + "The algorithm used for background bluring. Available choices are: 'none' to disable, 'gaussian', " + "'box' or 'kernel' for custom convolution blur with --blur-kern. Note: 'gaussian' and 'box' is not " + "supported by --legacy-backends."}, + + // Deprecated options + [274] = {"sw-opti" , ERROR_DEPRECATED(no_argument)}, + [275] = {"vsync-aggressive" , ERROR_DEPRECATED(no_argument)}, + [277] = {"respect-prop-shadow", ERROR_DEPRECATED(no_argument)}, + [303] = {"glx-use-gpushader4" , ERROR_DEPRECATED(no_argument)}, + [269] = {"refresh-rate" , WARN_DEPRECATED(IGNORE(required_argument))}, + + // Deprecated options with messages +#define CLEAR_SHADOW_DEPRECATION \ + "Shadows are automatically cleared now. If you want to prevent shadow from " \ + "being cleared under certain types of windows, you can use the \"full-shadow\" " \ + "window type option." + +#define MENU_OPACITY_DEPRECATION \ + "Use the wintype option `opacity` of `popup_menu` and `dropdown_menu` instead." + + ['m'] = {"menu-opacity" , SAY_DEPRECATED(false, MENU_OPACITY_DEPRECATION , DO(handle_menu_opacity))}, + ['z'] = {"clear-shadow" , SAY_DEPRECATED(false, CLEAR_SHADOW_DEPRECATION , IGNORE(no_argument))}, + [272] = {"xinerama-shadow-crop", SAY_DEPRECATED(false, "Use --crop-shadow-to-monitor instead.", ENABLE(crop_shadow_to_monitor))}, + [287] = {"logpath" , SAY_DEPRECATED(false, "Use --log-file instead." , STRING(logpath))}, + [289] = {"opengl" , SAY_DEPRECATED(false, "Use --backend=glx instead." , FIXED(backend, BKEND_GLX))}, + [305] = {"shadow-exclude-reg" , SAY_DEPRECATED(true, "Use --clip-shadow-above instead." , REJECT(required_argument))}, + +#undef CLEAR_SHADOW_DEPRECATION +#undef MENU_OPACITY_DEPRECATION +}; +// clang-format on + +static void setup_longopts(void) { + auto opts = ccalloc(ARR_SIZE(picom_options) + 1, struct option); + int option_count = 0; + for (size_t i = 0; i < ARR_SIZE(picom_options); i++) { + if (picom_options[i].arg.handler == NULL) { + continue; + } + opts[option_count].name = picom_options[i].long_name; + opts[option_count].has_arg = picom_options[i].has_arg; + opts[option_count].flag = NULL; + opts[option_count].val = (int)i; + option_count++; + } + longopts = opts; +} + +void print_help(const char *help, size_t indent, size_t curr_indent, size_t line_wrap, + FILE *f) { + if (curr_indent > indent) { + fputs("\n", f); + curr_indent = 0; + } + + if (line_wrap - indent <= 1) { + line_wrap = indent + 2; + } + + size_t pos = 0; + size_t len = strlen(help); + while (pos < len) { + fprintf(f, "%*s", (int)(indent - curr_indent), ""); + curr_indent = 0; + size_t towrite = line_wrap - indent; + while (help[pos] == ' ') { + pos++; + } + if (pos + towrite > len) { + towrite = len - pos; + fwrite(help + pos, 1, towrite, f); + } else { + auto space_break = towrite; + while (space_break > 0 && help[pos + space_break - 1] != ' ') { + space_break--; + } + + bool print_hyphen = false; + if (space_break == 0) { + print_hyphen = true; + towrite--; + } else { + towrite = space_break; + } + + fwrite(help + pos, 1, towrite, f); + + if (print_hyphen) { + fputs("-", f); + } + } + + fputs("\n", f); + pos += towrite; + } +} + /** * Print usage text. */ static void usage(const char *argv0, int ret) { -#define WARNING_DISABLED " (DISABLED AT COMPILE TIME)" - static const char *usage_text = - "picom (" COMPTON_VERSION ")\n" - "Please report bugs to https://github.com/yshui/picom\n\n" - "usage: %s [options]\n" - "Options:\n" - "\n" - "-r radius\n" - " The blur radius for shadows. (default 12)\n" - "\n" - "-o opacity\n" - " The translucency for shadows. (default .75)\n" - "\n" - "-l left-offset\n" - " The left offset for shadows. (default -15)\n" - "\n" - "-t top-offset\n" - " The top offset for shadows. (default -15)\n" - "\n" - "-I fade-in-step\n" - " Opacity change between steps while fading in. (default 0.028)\n" - "\n" - "-O fade-out-step\n" - " Opacity change between steps while fading out. (default 0.03)\n" - "\n" - "-D fade-delta-time\n" - " The time between steps in a fade in milliseconds. (default 10)\n" - "\n" - "-m opacity\n" - " The opacity for menus. (default 1.0)\n" - "\n" - "-c\n" - " Enabled client-side shadows on windows.\n" - "\n" - "-C\n" - " Avoid drawing shadows on dock/panel windows.\n" - "\n" - "-z\n" - " Zero the part of the shadow's mask behind the window.\n" - "\n" - "-f\n" - " Fade windows in/out when opening/closing and when opacity\n" - " changes, unless --no-fading-openclose is used.\n" - "\n" - "-F\n" - " Equals to -f. Deprecated.\n" - "\n" - "-i opacity\n" - " Opacity of inactive windows. (0.1 - 1.0)\n" - "\n" - "-e opacity\n" - " Opacity of window titlebars and borders. (0.1 - 1.0)\n" - "\n" - "-G\n" - " Don't draw shadows on DND windows\n" - "\n" - "-b\n" - " Daemonize process.\n" - "\n" - "--show-all-xerrors\n" - " Show all X errors (for debugging).\n" - "\n" - "--config path\n" - " Look for configuration file at the path. Use /dev/null to avoid\n" - " loading configuration file." -#ifndef CONFIG_LIBCONFIG - WARNING_DISABLED -#endif - "\n\n" - "--write-pid-path path\n" - " Write process ID to a file.\n" - "\n" - "--shadow-color color\n" - " Color of shadow, as a hex RGB string (defaults to #000000)\n" - "\n" - "--shadow-red value\n" - " Red color value of shadow (0.0 - 1.0, defaults to 0).\n" - "\n" - "--shadow-green value\n" - " Green color value of shadow (0.0 - 1.0, defaults to 0).\n" - "\n" - "--shadow-blue value\n" - " Blue color value of shadow (0.0 - 1.0, defaults to 0).\n" - "\n" - "--inactive-opacity-override\n" - " Inactive opacity set by -i overrides value of _NET_WM_OPACITY.\n" - "\n" - "--inactive-dim value\n" - " Dim inactive windows. (0.0 - 1.0, defaults to 0)\n" - "\n" - "--active-opacity opacity\n" - " Default opacity for active windows. (0.0 - 1.0)\n" - "\n" - "--corner-radius value\n" - " Sets the radius of rounded window corners. When > 0, the compositor\n" - " will round the corners of windows. (defaults to 0).\n" - "\n" - "--rounded-corners-exclude condition\n" - " Exclude conditions for rounded corners.\n" - "\n" - "--mark-wmwin-focused\n" - " Try to detect WM windows and mark them as active.\n" - "\n" - "--shadow-exclude condition\n" - " Exclude conditions for shadows.\n" - "\n" - "--fade-exclude condition\n" - " Exclude conditions for fading.\n" - "\n" - "--mark-ovredir-focused\n" - " Mark windows that have no WM frame as active.\n" - "\n" - "--no-fading-openclose\n" - " Do not fade on window open/close.\n" - "\n" - "--no-fading-destroyed-argb\n" - " Do not fade destroyed ARGB windows with WM frame. Workaround of bugs\n" - " in Openbox, Fluxbox, etc.\n" - "\n" - "--shadow-ignore-shaped\n" - " Do not paint shadows on shaped windows. (Deprecated, use\n" - " --shadow-exclude \'bounding_shaped\' or\n" - " --shadow-exclude \'bounding_shaped && !rounded_corners\' instead.)\n" - "\n" - "--detect-rounded-corners\n" - " Try to detect windows with rounded corners and don't consider\n" - " them shaped windows. Affects --shadow-ignore-shaped,\n" - " --unredir-if-possible, and possibly others. You need to turn this\n" - " on manually if you want to match against rounded_corners in\n" - " conditions.\n" - "\n" - "--detect-client-opacity\n" - " Detect _NET_WM_OPACITY on client windows, useful for window\n" - " managers not passing _NET_WM_OPACITY of client windows to frame\n" - " windows.\n" - "\n" - "--refresh-rate val\n" - " Specify refresh rate of the screen. If not specified or 0, we\n" - " will try detecting this with X RandR extension.\n" - "\n" - "--vsync\n" - " Enable VSync\n" - "\n" - "--paint-on-overlay\n" - " Painting on X Composite overlay window.\n" - "\n" - "--use-ewmh-active-win\n" - " Use _NET_WM_ACTIVE_WINDOW on the root window to determine which\n" - " window is focused instead of using FocusIn/Out events.\n" - "\n" - "--unredir-if-possible\n" - " Unredirect all windows if a full-screen opaque window is\n" - " detected, to maximize performance for full-screen windows.\n" - "\n" - "--unredir-if-possible-delay ms\n" - " Delay before unredirecting the window, in milliseconds.\n" - " Defaults to 0.\n" - "\n" - "--unredir-if-possible-exclude condition\n" - " Conditions of windows that shouldn't be considered full-screen\n" - " for unredirecting screen.\n" - "\n" - "--focus-exclude condition\n" - " Specify a list of conditions of windows that should always be\n" - " considered focused.\n" - "\n" - "--inactive-dim-fixed\n" - " Use fixed inactive dim value.\n" - "\n" - "--max-brightness\n" - " Dims windows which average brightness is above this threshold.\n" - " Requires --no-use-damage.\n" - " Default: 1.0 or no dimming.\n" - "\n" - "--detect-transient\n" - " Use WM_TRANSIENT_FOR to group windows, and consider windows in\n" - " the same group focused at the same time.\n" - "\n" - "--detect-client-leader\n" - " Use WM_CLIENT_LEADER to group windows, and consider windows in\n" - " the same group focused at the same time. WM_TRANSIENT_FOR has\n" - " higher priority if --detect-transient is enabled, too.\n" - "\n" - "--blur-method\n" - " The algorithm used for background bluring. Available choices are:\n" - " 'none' to disable, 'gaussian', 'box' or 'kernel' for custom\n" - " convolution blur with --blur-kern.\n" - " Note: 'gaussian' and 'box' require --experimental-backends.\n" - "\n" - "--blur-size\n" - " The radius of the blur kernel for 'box' and 'gaussian' blur method.\n" - "\n" - "--blur-deviation\n" - " The standard deviation for the 'gaussian' blur method.\n" - "\n" - "--blur-strength\n" - " The strength level of the 'dual_kawase' blur method.\n" - "\n" - "--blur-background\n" - " Blur background of semi-transparent / ARGB windows. Bad in\n" - " performance. The switch name may change without prior\n" - " notifications.\n" - "\n" - "--blur-background-frame\n" - " Blur background of windows when the window frame is not opaque.\n" - " Implies --blur-background. Bad in performance. The switch name\n" - " may change.\n" - "\n" - "--blur-background-fixed\n" - " Use fixed blur strength instead of adjusting according to window\n" - " opacity.\n" - "\n" - "--blur-kern matrix\n" - " Specify the blur convolution kernel, with the following format:\n" - " WIDTH,HEIGHT,ELE1,ELE2,ELE3,ELE4,ELE5...\n" - " The element in the center must not be included, it will be forever\n" - " 1.0 or changing based on opacity, depending on whether you have\n" - " --blur-background-fixed.\n" - " A 7x7 Gaussian blur kernel looks like:\n" - " --blur-kern " - "'7,7,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0." - "000102,0.003494,0.029143,0.059106,0.029143,0.003494,0.000102,0.000849,0." - "029143,0.243117,0.493069,0.243117,0.029143,0.000849,0.001723,0.059106,0." - "493069,0.493069,0.059106,0.001723,0.000849,0.029143,0.243117,0.493069,0." - "243117,0.029143,0.000849,0.000102,0.003494,0.029143,0.059106,0.029143,0." - "003494,0.000102,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." - "000003'\n" - " Up to 4 blur kernels may be specified, separated with semicolon, for\n" - " multi-pass blur.\n" - " May also be one the predefined kernels: 3x3box (default), 5x5box,\n" - " 7x7box, 3x3gaussian, 5x5gaussian, 7x7gaussian, 9x9gaussian,\n" - " 11x11gaussian.\n" - "\n" - "--blur-background-exclude condition\n" - " Exclude conditions for background blur.\n" - "\n" - "--resize-damage integer\n" - " Resize damaged region by a specific number of pixels. A positive\n" - " value enlarges it while a negative one shrinks it. Useful for\n" - " fixing the line corruption issues of blur. May or may not\n" - " work with --glx-no-stencil. Shrinking doesn't function correctly.\n" - "\n" - "--invert-color-include condition\n" - " Specify a list of conditions of windows that should be painted with\n" - " inverted color. Resource-hogging, and is not well tested.\n" - "\n" - "--opacity-rule opacity:condition\n" - " Specify a list of opacity rules, in the format \"PERCENT:PATTERN\",\n" - " like \'50:name *= \"Firefox\"'. picom-trans is recommended over\n" - " this. Note we do not distinguish 100%% and unset, and we don't make\n" - " any guarantee about possible conflicts with other programs that set\n" - " _NET_WM_WINDOW_OPACITY on frame or client windows.\n" - "\n" - "--shadow-exclude-reg geometry\n" - " Specify a X geometry that describes the region in which shadow\n" - " should not be painted in, such as a dock window region.\n" - " Use --shadow-exclude-reg \'x10+0-0\', for example, if the 10 pixels\n" - " on the bottom of the screen should not have shadows painted on.\n" - "\n" - "--clip-shadow-above condition\n" - " Specify a list of conditions of windows to not paint a shadow over,\n" - " such as a dock window.\n" - "\n" - "--xinerama-shadow-crop\n" - " Crop shadow of a window fully on a particular Xinerama screen to the\n" - " screen.\n" - "\n" - "--backend backend\n" - " Choose backend. Possible choices are xrender, glx, and\n" - " xr_glx_hybrid." -#ifndef CONFIG_OPENGL - " (GLX BACKENDS DISABLED AT COMPILE TIME)" -#endif - "\n\n" - "--glx-no-stencil\n" - " GLX backend: Avoid using stencil buffer. Might cause issues\n" - " when rendering transparent content. My tests show a 15%% performance\n" - " boost.\n" - "\n" - "--glx-no-rebind-pixmap\n" - " GLX backend: Avoid rebinding pixmap on window damage. Probably\n" - " could improve performance on rapid window content changes, but is\n" - " known to break things on some drivers (LLVMpipe, xf86-video-intel,\n" - " etc.).\n" - "\n" - "--no-use-damage\n" - " Disable the use of damage information. This cause the whole screen to\n" - " be redrawn everytime, instead of the part of the screen that has\n" - " actually changed. Potentially degrades the performance, but might fix\n" - " some artifacts.\n" - "\n" - "--xrender-sync-fence\n" - " Additionally use X Sync fence to sync clients' draw calls. Needed\n" - " on nvidia-drivers with GLX backend for some users.\n" - "\n" - "--force-win-blend\n" - " Force all windows to be painted with blending. Useful if you have a\n" - " --glx-fshader-win that could turn opaque pixels transparent.\n" - "\n" - "--dbus\n" - " Enable remote control via D-Bus. See the D-BUS API section in the\n" - " man page for more details." -#ifndef CONFIG_DBUS - WARNING_DISABLED -#endif - "\n\n" - "--benchmark cycles\n" - " Benchmark mode. Repeatedly paint until reaching the specified cycles.\n" - "\n" - "--benchmark-wid window-id\n" - " Specify window ID to repaint in benchmark mode. If omitted or is 0,\n" - " the whole screen is repainted.\n" - "\n" - "--monitor-repaint\n" - " Highlight the updated area of the screen. For debugging the xrender\n" - " backend only.\n" - "\n" - "--debug-mode\n" - " Render into a separate window, and don't take over the screen. Useful\n" - " when you want to attach a debugger to picom\n" - "\n" - "--no-ewmh-fullscreen\n" - " Do not use EWMH to detect fullscreen windows. Reverts to checking\n" - " if a window is fullscreen based only on its size and coordinates.\n" - "\n" - "--transparent-clipping\n" - " Make transparent windows clip other windows like non-transparent windows\n" - " do, instead of blending on top of them\n"; FILE *f = (ret ? stderr : stdout); - fprintf(f, usage_text, argv0); -#undef WARNING_DISABLED + fprintf(f, "picom (%s)\n", PICOM_VERSION); + fprintf(f, "Standalone X11 compositor\n"); + fprintf(f, "Please report bugs to https://github.com/yshui/picom\n\n"); + + fprintf(f, "Usage: %s [OPTION]...\n\n", argv0); + fprintf(f, "OPTIONS:\n"); + + int line_wrap = 80; + struct winsize window_size = {0}; + if (ioctl(fileno(f), TIOCGWINSZ, &window_size) != -1) { + line_wrap = window_size.ws_col; + } + + size_t help_indent = 0; + for (size_t i = 0; i < ARR_SIZE(picom_options); i++) { + if (picom_options[i].help == NULL) { + // Hide options with no help message. + continue; + } + auto option_len = strlen(picom_options[i].long_name) + 2 + 4; + if (picom_options[i].arg.name) { + option_len += strlen(picom_options[i].arg.name) + 1; + } + if (option_len > help_indent && option_len < 30) { + help_indent = option_len; + } + } + help_indent += 6; + + for (size_t i = 0; i < ARR_SIZE(picom_options); i++) { + if (picom_options[i].help == NULL) { + continue; + } + size_t option_len = 8; + fprintf(f, " "); + if ((i > 'a' && i < 'z') || (i > 'A' && i < 'Z')) { + fprintf(f, "-%c, ", (char)i); + } else { + fprintf(f, " "); + } + fprintf(f, "--%s", picom_options[i].long_name); + option_len += strlen(picom_options[i].long_name) + 2; + if (picom_options[i].arg.name) { + fprintf(f, "=%s", picom_options[i].arg.name); + option_len += strlen(picom_options[i].arg.name) + 1; + } + fprintf(f, " "); + option_len += 2; + print_help(picom_options[i].help, help_indent, option_len, + (size_t)line_wrap, f); + } } -static const char *shortopts = "D:I:O:d:r:o:m:l:t:i:e:hscnfFCaSzGb"; -static const struct option longopts[] = { - {"help", no_argument, NULL, 'h'}, - {"config", required_argument, NULL, 256}, - {"shadow-radius", required_argument, NULL, 'r'}, - {"shadow-opacity", required_argument, NULL, 'o'}, - {"shadow-offset-x", required_argument, NULL, 'l'}, - {"shadow-offset-y", required_argument, NULL, 't'}, - {"fade-in-step", required_argument, NULL, 'I'}, - {"fade-out-step", required_argument, NULL, 'O'}, - {"fade-delta", required_argument, NULL, 'D'}, - {"menu-opacity", required_argument, NULL, 'm'}, - {"shadow", no_argument, NULL, 'c'}, - {"no-dock-shadow", no_argument, NULL, 'C'}, - {"clear-shadow", no_argument, NULL, 'z'}, - {"fading", no_argument, NULL, 'f'}, - {"inactive-opacity", required_argument, NULL, 'i'}, - {"frame-opacity", required_argument, NULL, 'e'}, - {"daemon", no_argument, NULL, 'b'}, - {"no-dnd-shadow", no_argument, NULL, 'G'}, - {"shadow-red", required_argument, NULL, 257}, - {"shadow-green", required_argument, NULL, 258}, - {"shadow-blue", required_argument, NULL, 259}, - {"inactive-opacity-override", no_argument, NULL, 260}, - {"inactive-dim", required_argument, NULL, 261}, - {"mark-wmwin-focused", no_argument, NULL, 262}, - {"shadow-exclude", required_argument, NULL, 263}, - {"mark-ovredir-focused", no_argument, NULL, 264}, - {"no-fading-openclose", no_argument, NULL, 265}, - {"shadow-ignore-shaped", no_argument, NULL, 266}, - {"detect-rounded-corners", no_argument, NULL, 267}, - {"detect-client-opacity", no_argument, NULL, 268}, - {"refresh-rate", required_argument, NULL, 269}, - {"vsync", optional_argument, NULL, 270}, - {"alpha-step", required_argument, NULL, 271}, - {"dbe", no_argument, NULL, 272}, - {"paint-on-overlay", no_argument, NULL, 273}, - {"sw-opti", no_argument, NULL, 274}, - {"vsync-aggressive", no_argument, NULL, 275}, - {"use-ewmh-active-win", no_argument, NULL, 276}, - {"respect-prop-shadow", no_argument, NULL, 277}, - {"unredir-if-possible", no_argument, NULL, 278}, - {"focus-exclude", required_argument, NULL, 279}, - {"inactive-dim-fixed", no_argument, NULL, 280}, - {"detect-transient", no_argument, NULL, 281}, - {"detect-client-leader", no_argument, NULL, 282}, - {"blur-background", no_argument, NULL, 283}, - {"blur-background-frame", no_argument, NULL, 284}, - {"blur-background-fixed", no_argument, NULL, 285}, - {"dbus", no_argument, NULL, 286}, - {"logpath", required_argument, NULL, 287}, - {"invert-color-include", required_argument, NULL, 288}, - {"opengl", no_argument, NULL, 289}, - {"backend", required_argument, NULL, 290}, - {"glx-no-stencil", no_argument, NULL, 291}, - {"benchmark", required_argument, NULL, 293}, - {"benchmark-wid", required_argument, NULL, 294}, - {"blur-background-exclude", required_argument, NULL, 296}, - {"active-opacity", required_argument, NULL, 297}, - {"glx-no-rebind-pixmap", no_argument, NULL, 298}, - {"glx-swap-method", required_argument, NULL, 299}, - {"fade-exclude", required_argument, NULL, 300}, - {"blur-kern", required_argument, NULL, 301}, - {"resize-damage", required_argument, NULL, 302}, - {"glx-use-gpushader4", no_argument, NULL, 303}, - {"opacity-rule", required_argument, NULL, 304}, - {"shadow-exclude-reg", required_argument, NULL, 305}, - {"paint-exclude", required_argument, NULL, 306}, - {"xinerama-shadow-crop", no_argument, NULL, 307}, - {"unredir-if-possible-exclude", required_argument, NULL, 308}, - {"unredir-if-possible-delay", required_argument, NULL, 309}, - {"write-pid-path", required_argument, NULL, 310}, - {"vsync-use-glfinish", no_argument, NULL, 311}, - {"xrender-sync", no_argument, NULL, 312}, - {"xrender-sync-fence", no_argument, NULL, 313}, - {"show-all-xerrors", no_argument, NULL, 314}, - {"no-fading-destroyed-argb", no_argument, NULL, 315}, - {"force-win-blend", no_argument, NULL, 316}, - {"glx-fshader-win", required_argument, NULL, 317}, - {"version", no_argument, NULL, 318}, - {"no-x-selection", no_argument, NULL, 319}, - {"no-name-pixmap", no_argument, NULL, 320}, - {"log-level", required_argument, NULL, 321}, - {"log-file", required_argument, NULL, 322}, - {"use-damage", no_argument, NULL, 323}, - {"no-use-damage", no_argument, NULL, 324}, - {"no-vsync", no_argument, NULL, 325}, - {"max-brightness", required_argument, NULL, 326}, - {"transparent-clipping", no_argument, NULL, 327}, - {"blur-method", required_argument, NULL, 328}, - {"blur-size", required_argument, NULL, 329}, - {"blur-deviation", required_argument, NULL, 330}, - {"blur-strength", required_argument, NULL, 331}, - {"shadow-color", required_argument, NULL, 332}, - {"corner-radius", required_argument, NULL, 333}, - {"rounded-corners-exclude", required_argument, NULL, 334}, - {"clip-shadow-above", required_argument, NULL, 335}, - {"experimental-backends", no_argument, NULL, 733}, - {"monitor-repaint", no_argument, NULL, 800}, - {"diagnostics", no_argument, NULL, 801}, - {"debug-mode", no_argument, NULL, 802}, - {"no-ewmh-fullscreen", no_argument, NULL, 803}, - // Must terminate with a NULL entry - {NULL, 0, NULL, 0}, -}; +static void set_default_winopts(options_t *opt) { + auto mask = opt->wintype_option_mask; + // Apply default wintype options. + if (!mask[WINTYPE_DESKTOP].shadow) { + // Desktop windows are always drawn without shadow by default. + mask[WINTYPE_DESKTOP].shadow = true; + opt->wintype_option[WINTYPE_DESKTOP].shadow = false; + } + + // Focused/unfocused state only apply to a few window types, all other windows + // are always considered focused. + const wintype_t nofocus_type[] = {WINTYPE_UNKNOWN, WINTYPE_NORMAL, WINTYPE_UTILITY}; + for (unsigned long i = 0; i < ARR_SIZE(nofocus_type); i++) { + if (!mask[nofocus_type[i]].focus) { + mask[nofocus_type[i]].focus = true; + opt->wintype_option[nofocus_type[i]].focus = false; + } + } + for (unsigned long i = 0; i < NUM_WINTYPES; i++) { + if (!mask[i].shadow) { + mask[i].shadow = true; + opt->wintype_option[i].shadow = opt->shadow_enable; + } + if (!mask[i].fade) { + mask[i].fade = true; + opt->wintype_option[i].fade = opt->fading_enable; + } + if (!mask[i].focus) { + mask[i].focus = true; + opt->wintype_option[i].focus = true; + } + if (!mask[i].blur_background) { + mask[i].blur_background = true; + opt->wintype_option[i].blur_background = + opt->blur_method != BLUR_METHOD_NONE; + } + if (!mask[i].full_shadow) { + mask[i].full_shadow = true; + opt->wintype_option[i].full_shadow = false; + } + if (!mask[i].redir_ignore) { + mask[i].redir_ignore = true; + opt->wintype_option[i].redir_ignore = false; + } + if (!mask[i].opacity) { + mask[i].opacity = true; + // Opacity is not set to a concrete number here because the + // opacity logic is complicated, and needs an "unset" state + opt->wintype_option[i].opacity = NAN; + } + if (!mask[i].clip_shadow_above) { + mask[i].clip_shadow_above = true; + opt->wintype_option[i].clip_shadow_above = false; + } + } +} + +static const char *shortopts = "D:I:O:r:o:m:l:t:i:e:hcfCzGb"; /// Get config options that are needed to parse the rest of the options /// Return true if we should quit bool get_early_config(int argc, char *const *argv, char **config_file, bool *all_xerrors, bool *fork, int *exit_code) { + setup_longopts(); + int o = 0, longopt_idx = -1; - // Pre-parse the commandline arguments to check for --config and invalid + // Pre-parse the command line arguments to check for --config and invalid // switches - // Must reset optind to 0 here in case we reread the commandline + // Must reset optind to 0 here in case we reread the command line // arguments optind = 1; *config_file = NULL; @@ -485,21 +697,11 @@ bool get_early_config(int argc, char *const *argv, char **config_file, bool *all } else if (o == 'b') { *fork = true; - } else if (o == 'd') { - log_error("-d is removed, please use the DISPLAY " - "environment variable"); - goto err; } else if (o == 314) { *all_xerrors = true; } else if (o == 318) { - printf("%s\n", COMPTON_VERSION); + printf("%s\n", PICOM_VERSION); return true; - } else if (o == 'S') { - log_error("-S is no longer available"); - goto err; - } else if (o == 320) { - log_error("--no-name-pixmap is no longer available"); - goto err; } else if (o == '?' || o == ':') { usage(argv[0], 1); goto err; @@ -522,378 +724,18 @@ bool get_early_config(int argc, char *const *argv, char **config_file, bool *all /** * Process arguments and configuration files. */ -bool get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, - bool fading_enable, bool conv_kern_hasneg, win_option_mask_t *winopt_mask) { - +bool get_cfg(options_t *opt, int argc, char *const *argv) { int o = 0, longopt_idx = -1; - - char *lc_numeric_old = strdup(setlocale(LC_NUMERIC, NULL)); - - // Enforce LC_NUMERIC locale "C" here to make sure dots are recognized - // instead of commas in atof(). - setlocale(LC_NUMERIC, "C"); - - // Parse commandline arguments. Range checking will be done later. - bool failed = false; - const char *deprecation_message attr_unused = - "has been removed. If you encounter problems " - "without this feature, please feel free to " - "open a bug report."; optind = 1; while (-1 != (o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) { - switch (o) { -#define P_CASEBOOL(idx, option) \ - case idx: \ - opt->option = true; \ - break -#define P_CASELONG(idx, option) \ - case idx: \ - if (!parse_long(optarg, &opt->option)) { \ - exit(1); \ - } \ - break -#define P_CASEINT(idx, option) \ - case idx: \ - if (!parse_int(optarg, &opt->option)) { \ - exit(1); \ - } \ - break - - // clang-format off - // Short options - case 318: - case 'h': - // These options should cause us to exit early, - // so assert(false) here - assert(false); - break; - case 'd': - case 'b': - case 'S': - case 314: - case 320: - // These options are handled by get_early_config() - break; - P_CASEINT('D', fade_delta); - case 'I': opt->fade_in_step = normalize_d(atof(optarg)); break; - case 'O': opt->fade_out_step = normalize_d(atof(optarg)); break; - case 'c': shadow_enable = true; break; - case 'C': - log_error("Option `--no-dock-shadow`/`-C` has been removed. Please" - " use the wintype option `shadow` of `dock` instead."); - failed = true; break; - case 'G': - log_error("Option `--no-dnd-shadow`/`-G` has been removed. Please " - "use the wintype option `shadow` of `dnd` instead."); - failed = true; break; - case 'm':; - double tmp; - tmp = normalize_d(atof(optarg)); - winopt_mask[WINTYPE_DROPDOWN_MENU].opacity = true; - winopt_mask[WINTYPE_POPUP_MENU].opacity = true; - opt->wintype_option[WINTYPE_POPUP_MENU].opacity = tmp; - opt->wintype_option[WINTYPE_DROPDOWN_MENU].opacity = tmp; - break; - case 'f': - case 'F': - fading_enable = true; - break; - P_CASEINT('r', shadow_radius); - case 'o': - opt->shadow_opacity = atof(optarg); - break; - P_CASEINT('l', shadow_offset_x); - P_CASEINT('t', shadow_offset_y); - case 'i': - opt->inactive_opacity = normalize_d(atof(optarg)); - break; - case 'e': opt->frame_opacity = atof(optarg); break; - case 'z': - log_warn("clear-shadow is removed, shadows are automatically " - "cleared now. If you want to prevent shadow from been " - "cleared under certain types of windows, you can use " - "the \"full-shadow\" per window type option."); - break; - case 'n': - case 'a': - case 's': - log_error("-n, -a, and -s have been removed."); - failed = true; break; - // Long options - case 256: - // --config - break; - case 332:; - // --shadow-color - struct color rgb; - rgb = hex_to_rgb(optarg); - opt->shadow_red = rgb.red; - opt->shadow_green = rgb.green; - opt->shadow_blue = rgb.blue; - break; - case 257: - // --shadow-red - opt->shadow_red = atof(optarg); - break; - case 258: - // --shadow-green - opt->shadow_green = atof(optarg); - break; - case 259: - // --shadow-blue - opt->shadow_blue = atof(optarg); - break; - P_CASEBOOL(260, inactive_opacity_override); - case 261: - // --inactive-dim - opt->inactive_dim = atof(optarg); - break; - P_CASEBOOL(262, mark_wmwin_focused); - case 263: - // --shadow-exclude - condlst_add(&opt->shadow_blacklist, optarg); - break; - P_CASEBOOL(264, mark_ovredir_focused); - P_CASEBOOL(265, no_fading_openclose); - P_CASEBOOL(266, shadow_ignore_shaped); - P_CASEBOOL(267, detect_rounded_corners); - P_CASEBOOL(268, detect_client_opacity); - P_CASEINT(269, refresh_rate); - case 270: - if (optarg) { - opt->vsync = parse_vsync(optarg); - log_warn("--vsync doesn't take argument anymore. \"%s\" " - "is interpreted as \"%s\" for compatibility, but " - "this will stop working soon", - optarg, opt->vsync ? "true" : "false"); - } else { - opt->vsync = true; - } - break; - case 271: - // --alpha-step - log_error("--alpha-step has been removed, we now tries to " - "make use of all alpha values"); - failed = true; break; - case 272: - log_error("--dbe has been removed"); - failed = true; break; - case 273: - log_error("--paint-on-overlay has been removed, the feature is enabled " - "whenever possible"); - failed = true; break; - P_CASEBOOL(274, sw_opti); - case 275: - // --vsync-aggressive - log_warn("--vsync-aggressive has been deprecated, please remove it" - " from the command line options"); - break; - P_CASEBOOL(276, use_ewmh_active_win); - case 277: - // --respect-prop-shadow - log_warn("--respect-prop-shadow option has been deprecated, its " - "functionality will always be enabled. Please remove it " - "from the command line options"); - break; - P_CASEBOOL(278, unredir_if_possible); - case 279: - // --focus-exclude - condlst_add(&opt->focus_blacklist, optarg); - break; - P_CASEBOOL(280, inactive_dim_fixed); - P_CASEBOOL(281, detect_transient); - P_CASEBOOL(282, detect_client_leader); - case 283: - // --blur_background - opt->blur_method = BLUR_METHOD_KERNEL; - break; - P_CASEBOOL(284, blur_background_frame); - P_CASEBOOL(285, blur_background_fixed); - P_CASEBOOL(286, dbus); - case 287: - log_warn("Please use --log-file instead of --logpath"); - // fallthrough - case 322: - // --logpath, --log-file - free(opt->logpath); - opt->logpath = strdup(optarg); - break; - case 288: - // --invert-color-include - condlst_add(&opt->invert_color_list, optarg); - break; - case 289: - // --opengl - opt->backend = BKEND_GLX; - break; - case 290: - // --backend - opt->backend = parse_backend(optarg); - if (opt->backend >= NUM_BKEND) - exit(1); - break; - P_CASEBOOL(291, glx_no_stencil); - P_CASEINT(293, benchmark); - case 294: - // --benchmark-wid - opt->benchmark_wid = (xcb_window_t)strtol(optarg, NULL, 0); - break; - case 296: - // --blur-background-exclude - condlst_add(&opt->blur_background_blacklist, optarg); - break; - case 297: - // --active-opacity - opt->active_opacity = normalize_d(atof(optarg)); - break; - P_CASEBOOL(298, glx_no_rebind_pixmap); - case 299: { - // --glx-swap-method - char *endptr; - long tmpval = strtol(optarg, &endptr, 10); - bool should_remove = true; - if (*endptr || !(*optarg)) { - // optarg is not a number, or an empty string - tmpval = -1; - } - if (strcmp(optarg, "undefined") != 0 && tmpval != 0) { - // If not undefined, we will use damage and buffer-age to - // limit the rendering area. - opt->use_damage = true; - should_remove = false; - } - log_warn("--glx-swap-method has been deprecated, your setting " - "\"%s\" should be %s.", - optarg, - !should_remove ? "replaced by `--use-damage`" : - "removed"); - break; - } - case 300: - // --fade-exclude - condlst_add(&opt->fade_blacklist, optarg); - break; - case 301: - // --blur-kern - opt->blur_kerns = parse_blur_kern_lst(optarg, &conv_kern_hasneg, - &opt->blur_kernel_count); - if (!opt->blur_kerns) { - exit(1); - } - break; - P_CASEINT(302, resize_damage); - case 303: - // --glx-use-gpushader4 - log_warn("--glx-use-gpushader4 is deprecated since v6." - " Please remove it from command line options."); - break; - case 304: - // --opacity-rule - if (!parse_rule_opacity(&opt->opacity_rules, optarg)) - exit(1); - break; - case 305: - // --shadow-exclude-reg - free(opt->shadow_exclude_reg_str); - opt->shadow_exclude_reg_str = strdup(optarg); - log_warn("--shadow-exclude-reg is deprecated. You are likely " - "better off using --clip-shadow-above anyway"); - break; - case 306: - // --paint-exclude - condlst_add(&opt->paint_blacklist, optarg); - break; - P_CASEBOOL(307, xinerama_shadow_crop); - case 308: - // --unredir-if-possible-exclude - condlst_add(&opt->unredir_if_possible_blacklist, optarg); - break; - P_CASELONG(309, unredir_if_possible_delay); - case 310: - // --write-pid-path - free(opt->write_pid_path); - opt->write_pid_path = strdup(optarg); - if (*opt->write_pid_path != '/') { - log_warn("--write-pid-path is not an absolute path"); - } - break; - P_CASEBOOL(311, vsync_use_glfinish); - case 312: - // --xrender-sync - log_error("Please use --xrender-sync-fence instead of --xrender-sync"); - failed = true; break; - P_CASEBOOL(313, xrender_sync_fence); - P_CASEBOOL(315, no_fading_destroyed_argb); - P_CASEBOOL(316, force_win_blend); - case 317: - opt->glx_fshader_win_str = strdup(optarg); - break; - case 321: { - enum log_level tmp_level = string_to_log_level(optarg); - if (tmp_level == LOG_LEVEL_INVALID) { - log_warn("Invalid log level, defaults to WARN"); - } else { - log_set_level_tls(tmp_level); - } - break; - } - P_CASEBOOL(319, no_x_selection); - P_CASEBOOL(323, use_damage); - case 324: - opt->use_damage = false; - break; - case 325: - opt->vsync = false; - break; - - case 326: - opt->max_brightness = atof(optarg); - break; - P_CASEBOOL(327, transparent_clipping); - case 328: { - // --blur-method - enum blur_method method = parse_blur_method(optarg); - if (method >= BLUR_METHOD_INVALID) { - log_warn("Invalid blur method %s, ignoring.", optarg); - } else { - opt->blur_method = method; - } - break; - } - case 329: - // --blur-size - opt->blur_radius = atoi(optarg); - break; - case 330: - // --blur-deviation - opt->blur_deviation = atof(optarg); - break; - case 331: - // --blur-strength - opt->blur_strength = atoi(optarg); - break; - case 333: - // --cornor-radius - opt->corner_radius = atoi(optarg); - break; - case 334: - // --rounded-corners-exclude - condlst_add(&opt->rounded_corners_blacklist, optarg); - break; - case 335: - // --clip-shadow-above - condlst_add(&opt->shadow_clip_list, optarg); - break; - P_CASEBOOL(733, experimental_backends); - P_CASEBOOL(800, monitor_repaint); - case 801: opt->print_diagnostics = true; break; - P_CASEBOOL(802, debug_mode); - P_CASEBOOL(803, no_ewmh_fullscreen); - default: usage(argv[0], 1); break; -#undef P_CASEBOOL + if (o == '?' || o == ':' || picom_options[o].arg.handler == NULL) { + usage(argv[0], 1); + failed = true; + } else if (!picom_options[o].arg.handler( + &picom_options[o], &picom_options[o].arg, optarg, opt)) { + failed = true; } - // clang-format on if (failed) { // Parsing this option has failed, break the loop @@ -901,59 +743,84 @@ bool get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, } } - // Restore LC_NUMERIC - setlocale(LC_NUMERIC, lc_numeric_old); - free(lc_numeric_old); - if (failed) { return false; } - if (opt->monitor_repaint && opt->backend != BKEND_XRENDER && - !opt->experimental_backends) { + log_set_level_tls(opt->log_level); + if (opt->monitor_repaint && opt->backend != BKEND_XRENDER && opt->legacy_backends) { log_warn("--monitor-repaint has no effect when backend is not xrender"); } - if (opt->experimental_backends && !backend_list[opt->backend]) { - log_error("Backend \"%s\" is not available as part of the experimental " + if (opt->window_shader_fg) { + scoped_charp cwd = getcwd(NULL, 0); + scoped_charp tmp = opt->window_shader_fg; + opt->window_shader_fg = locate_auxiliary_file("shaders", tmp, cwd); + if (!opt->window_shader_fg) { + log_error("Couldn't find the specified window shader file \"%s\"", tmp); + return false; + } + } + + if (opt->write_pid_path && *opt->write_pid_path != '/') { + log_warn("--write-pid-path is not an absolute path"); + } + + if (opt->backend == BKEND_EGL) { + if (opt->legacy_backends) { + log_error("The egl backend is not supported with " + "--legacy-backends"); + return false; + } + log_warn("The egl backend is still experimental, use with care."); + } + + if (!opt->legacy_backends && !backend_list[opt->backend]) { + log_error("Backend \"%s\" is only available as part of the legacy " "backends.", BACKEND_STRS[opt->backend]); return false; } - if (opt->debug_mode && !opt->experimental_backends) { - log_error("Debug mode only works with the experimental backends."); + if (opt->debug_mode && opt->legacy_backends) { + log_error("Debug mode does not work with the legacy backends."); return false; } - if (opt->transparent_clipping && !opt->experimental_backends) { - log_error("Transparent clipping only works with the experimental " + if (opt->transparent_clipping && opt->legacy_backends) { + log_error("Transparent clipping does not work with the legacy " "backends"); return false; } + if (opt->glx_fshader_win_str && !opt->legacy_backends) { + log_warn("--glx-fshader-win has been replaced by " + "\"--window-shader-fg\" for the new backends."); + } + + if (opt->window_shader_fg || opt->window_shader_fg_rules) { + if (opt->backend == BKEND_XRENDER || opt->legacy_backends) { + log_warn(opt->backend == BKEND_XRENDER + ? "Shader interface is not supported by the xrender " + "backend." + : "The new shader interface is not supported by the " + "legacy glx backend. You may want to use " + "--glx-fshader-win instead."); + opt->window_shader_fg = NULL; + c2_list_free(&opt->window_shader_fg_rules, free); + } + } + // Range checking and option assignments - opt->fade_delta = max2(opt->fade_delta, 1); - opt->shadow_radius = max2(opt->shadow_radius, 0); - opt->shadow_red = normalize_d(opt->shadow_red); - opt->shadow_green = normalize_d(opt->shadow_green); - opt->shadow_blue = normalize_d(opt->shadow_blue); - opt->inactive_dim = normalize_d(opt->inactive_dim); - opt->frame_opacity = normalize_d(opt->frame_opacity); - opt->shadow_opacity = normalize_d(opt->shadow_opacity); - opt->refresh_rate = normalize_i_range(opt->refresh_rate, 0, 300); - - opt->max_brightness = normalize_d(opt->max_brightness); if (opt->max_brightness < 1.0) { - if (opt->use_damage) { - log_warn("--max-brightness requires --no-use-damage. Falling " - "back to 1.0"); + if (opt->backend == BKEND_XRENDER || opt->legacy_backends) { + log_warn("--max-brightness is not supported by the %s backend. " + "Falling back to 1.0.", + opt->backend == BKEND_XRENDER ? "xrender" : "legacy glx"); opt->max_brightness = 1.0; - } - - if (!opt->experimental_backends || opt->backend != BKEND_GLX) { - log_warn("--max-brightness requires the experimental glx " - "backend. Falling back to 1.0"); + } else if (opt->use_damage) { + log_warn("--max-brightness requires --no-use-damage. Falling " + "back to 1.0."); opt->max_brightness = 1.0; } } @@ -964,10 +831,7 @@ bool get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, } // Apply default wintype options that are dependent on global options - set_default_winopts(opt, winopt_mask, shadow_enable, fading_enable, - opt->blur_method != BLUR_METHOD_NONE); - - // Other variables determined by options + set_default_winopts(opt); // Determine whether we track window grouping if (opt->detect_transient || opt->detect_client_leader) { @@ -977,8 +841,7 @@ bool get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, // Fill default blur kernel if (opt->blur_method == BLUR_METHOD_KERNEL && (!opt->blur_kerns || !opt->blur_kerns[0])) { - opt->blur_kerns = parse_blur_kern_lst("3x3box", &conv_kern_hasneg, - &opt->blur_kernel_count); + opt->blur_kerns = parse_blur_kern_lst("3x3box", &opt->blur_kernel_count); CHECK(opt->blur_kerns); CHECK(opt->blur_kernel_count); } @@ -995,10 +858,9 @@ bool get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, "capping to 20."); opt->blur_strength = 20; } - if (!opt->experimental_backends) { + if (opt->legacy_backends) { log_warn("Dual-kawase blur is not implemented by the legacy " - "backends, you must use the `experimental-backends` " - "option."); + "backends."); } } @@ -1006,18 +868,68 @@ bool get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, log_warn("Negative --resize-damage will not work correctly."); } - if (opt->backend == BKEND_XRENDER && conv_kern_hasneg) { - log_warn("A convolution kernel with negative values may not work " - "properly under X Render backend."); + if (opt->backend == BKEND_XRENDER) { + for (int i = 0; i < opt->blur_kernel_count; i++) { + auto kernel = opt->blur_kerns[i]; + for (int j = 0; j < kernel->h * kernel->w; j++) { + if (kernel->data[j] < 0) { + log_warn("A convolution kernel with negative " + "values may not work properly under X " + "Render backend."); + goto check_end; + } + } + } + check_end:; } - if (opt->corner_radius > 0 && opt->experimental_backends) { - log_warn("Rounded corner is only supported on legacy backends, it " - "will be disabled"); - opt->corner_radius = 0; + return true; +} + +void options_postprocess_c2_lists(struct c2_state *state, struct x_connection *c, + struct options *option) { + if (!(c2_list_postprocess(state, c->c, option->unredir_if_possible_blacklist) && + c2_list_postprocess(state, c->c, option->paint_blacklist) && + c2_list_postprocess(state, c->c, option->shadow_blacklist) && + c2_list_postprocess(state, c->c, option->shadow_clip_list) && + c2_list_postprocess(state, c->c, option->fade_blacklist) && + c2_list_postprocess(state, c->c, option->blur_background_blacklist) && + c2_list_postprocess(state, c->c, option->invert_color_list) && + c2_list_postprocess(state, c->c, option->window_shader_fg_rules) && + c2_list_postprocess(state, c->c, option->opacity_rules) && + c2_list_postprocess(state, c->c, option->rounded_corners_blacklist) && + c2_list_postprocess(state, c->c, option->corner_radius_rules) && + c2_list_postprocess(state, c->c, option->focus_blacklist) && + c2_list_postprocess(state, c->c, option->transparent_clipping_blacklist))) { + log_error("Post-processing of conditionals failed, some of your rules " + "might not work"); } +} - return true; +void options_destroy(struct options *options) { + // Free blacklists + c2_list_free(&options->shadow_blacklist, NULL); + c2_list_free(&options->shadow_clip_list, NULL); + c2_list_free(&options->fade_blacklist, NULL); + c2_list_free(&options->focus_blacklist, NULL); + c2_list_free(&options->invert_color_list, NULL); + c2_list_free(&options->blur_background_blacklist, NULL); + c2_list_free(&options->opacity_rules, NULL); + c2_list_free(&options->paint_blacklist, NULL); + c2_list_free(&options->unredir_if_possible_blacklist, NULL); + c2_list_free(&options->rounded_corners_blacklist, NULL); + c2_list_free(&options->corner_radius_rules, NULL); + c2_list_free(&options->window_shader_fg_rules, free); + c2_list_free(&options->transparent_clipping_blacklist, NULL); + + free(options->write_pid_path); + free(options->logpath); + + for (int i = 0; i < options->blur_kernel_count; ++i) { + free(options->blur_kerns[i]); + } + free(options->blur_kerns); + free(options->glx_fshader_win_str); } // vim: set noet sw=8 ts=8 : diff --git a/src/options.h b/src/options.h index 08aa15eb82..d3c43759c5 100644 --- a/src/options.h +++ b/src/options.h @@ -30,8 +30,9 @@ bool get_early_config(int argc, char *const *argv, char **config_file, bool *all * Returns: * Whether configuration are processed successfully. */ -bool must_use get_cfg(options_t *opt, int argc, char *const *argv, bool shadow_enable, - bool fading_enable, bool conv_kern_hasneg, - win_option_mask_t *winopt_mask); +bool must_use get_cfg(options_t *opt, int argc, char *const *argv); +void options_postprocess_c2_lists(struct c2_state *state, struct x_connection *c, + struct options *option); +void options_destroy(struct options *options); // vim: set noet sw=8 ts=8: diff --git a/src/picom.c b/src/picom.c index 7127fd79aa..a8afdcdcaa 100644 --- a/src/picom.c +++ b/src/picom.c @@ -1,10 +1,12 @@ // SPDX-License-Identifier: MIT /* - * Compton - a compositor for X11 + * picom - a compositor for X11 * + * Based on `compton` - Copyright (c) 2011-2013, Christopher Jeffrey * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard * - * Copyright (c) 2011-2013, Christopher Jeffrey + * Copyright (c) 2019-2023, Yuxuan Shui + * * See LICENSE-mit for more information. * */ @@ -16,18 +18,25 @@ #include #include #include +#include +#include +#include +#include #include #include +#include +#include #include #include #include +#include #include #include #include #include #include +#include #include -#include #include #include @@ -36,31 +45,37 @@ #include "compiler.h" #include "config.h" #include "err.h" +#include "inspect.h" #include "kernel.h" #include "picom.h" +#include "transition.h" +#include "win_defs.h" +#include "wm.h" #ifdef CONFIG_OPENGL #include "opengl.h" #endif +#include "atom.h" #include "backend/backend.h" #include "c2.h" -#include "config.h" +#include "dbus.h" #include "diagnostic.h" +#include "event.h" +#include "file_watch.h" +#include "list.h" #include "log.h" +#include "options.h" #include "region.h" #include "render.h" +#include "renderer/command_builder.h" +#include "renderer/layout.h" +#include "renderer/renderer.h" +#include "statistics.h" #include "types.h" +#include "uthash_extra.h" #include "utils.h" +#include "vblank.h" #include "win.h" #include "x.h" -#ifdef CONFIG_DBUS -#include "dbus.h" -#endif -#include "atom.h" -#include "event.h" -#include "file_watch.h" -#include "list.h" -#include "options.h" -#include "uthash_extra.h" /// Get session_t pointer from a pointer to a member of session_t #define session_ptr(ptr, member) \ @@ -69,8 +84,6 @@ (session_t *)((char *)__mptr - offsetof(session_t, member)); \ }) -static const long SWOPTI_TOLERANCE = 3000; - static bool must_use redirect_start(session_t *ps); static void unredirect(session_t *ps); @@ -78,10 +91,24 @@ static void unredirect(session_t *ps); // === Global constants === /// Name strings for window types. -const char *const WINTYPES[NUM_WINTYPES] = { - "unknown", "desktop", "dock", "toolbar", "menu", - "utility", "splash", "dialog", "normal", "dropdown_menu", - "popup_menu", "tooltip", "notification", "combo", "dnd", +const struct wintype_info WINTYPES[] = { + [WINTYPE_UNKNOWN] = {"unknown", NULL}, +#define X(name, type) [WINTYPE_##type] = {#name, "_NET_WM_WINDOW_TYPE_" #type} + X(desktop, DESKTOP), + X(dock, DOCK), + X(toolbar, TOOLBAR), + X(menu, MENU), + X(utility, UTILITY), + X(splash, SPLASH), + X(dialog, DIALOG), + X(normal, NORMAL), + X(dropdown_menu, DROPDOWN_MENU), + X(popup_menu, POPUP_MENU), + X(tooltip, TOOLTIP), + X(notification, NOTIFICATION), + X(combo, COMBO), + X(dnd, DND), +#undef X }; // clang-format off @@ -90,6 +117,7 @@ const char *const BACKEND_STRS[] = {[BKEND_XRENDER] = "xrender", [BKEND_GLX] = "glx", [BKEND_XR_GLX_HYBRID] = "xr_glx_hybrid", [BKEND_DUMMY] = "dummy", + [BKEND_EGL] = "egl", NULL}; // clang-format on @@ -111,21 +139,6 @@ void quit(session_t *ps) { ev_break(ps->loop, EVBREAK_ALL); } -/** - * Free Xinerama screen info. - * - * XXX consider moving to x.c - */ -static inline void free_xinerama_info(session_t *ps) { - if (ps->xinerama_scr_regs) { - for (int i = 0; i < ps->xinerama_nscrs; ++i) - pixman_region32_fini(&ps->xinerama_scr_regs[i]); - free(ps->xinerama_scr_regs); - ps->xinerama_scr_regs = NULL; - } - ps->xinerama_nscrs = 0; -} - /** * Get current system clock in milliseconds. */ @@ -135,64 +148,283 @@ static inline int64_t get_time_ms(void) { return (int64_t)tp.tv_sec * 1000 + (int64_t)tp.tv_nsec / 1000000; } -// XXX Move to x.c -void cxinerama_upd_scrs(session_t *ps) { - // XXX Consider deprecating Xinerama, switch to RandR when necessary - free_xinerama_info(ps); - - if (!ps->o.xinerama_shadow_crop || !ps->xinerama_exists) - return; +enum vblank_callback_action check_render_finish(struct vblank_event *e attr_unused, void *ud) { + auto ps = (session_t *)ud; + if (!ps->backend_busy) { + return VBLANK_CALLBACK_DONE; + } + + struct timespec render_time; + bool completed = + ps->backend_data->ops->last_render_time(ps->backend_data, &render_time); + if (!completed) { + // Render hasn't completed yet, we can't start another render. + // Check again at the next vblank. + log_debug("Last render did not complete during vblank, msc: " + "%" PRIu64, + ps->last_msc); + return VBLANK_CALLBACK_AGAIN; + } + + // The frame has been finished and presented, record its render time. + if (global_debug_options.smart_frame_pacing) { + int render_time_us = + (int)(render_time.tv_sec * 1000000L + render_time.tv_nsec / 1000L); + render_statistics_add_render_time_sample( + &ps->render_stats, render_time_us + (int)ps->last_schedule_delay); + log_verbose("Last render call took: %d (gpu) + %d (cpu) us, " + "last_msc: %" PRIu64, + render_time_us, (int)ps->last_schedule_delay, ps->last_msc); + } + ps->backend_busy = false; + return VBLANK_CALLBACK_DONE; +} - xcb_xinerama_is_active_reply_t *active = - xcb_xinerama_is_active_reply(ps->c, xcb_xinerama_is_active(ps->c), NULL); - if (!active || !active->state) { - free(active); - return; +enum vblank_callback_action +collect_vblank_interval_statistics(struct vblank_event *e, void *ud) { + auto ps = (session_t *)ud; + double vblank_interval = NAN; + assert(ps->frame_pacing); + assert(ps->vblank_scheduler); + + if (!global_debug_options.smart_frame_pacing) { + // We don't need to collect statistics if we are not doing smart frame + // pacing. + return VBLANK_CALLBACK_DONE; + } + + // TODO(yshui): this naive method of estimating vblank interval does not handle + // the variable refresh rate case very well. This includes the case + // of a VRR enabled monitor; or a monitor that's turned off, in which + // case the vblank events might slow down or stop all together. + // I tried using DPMS to detect monitor power state, and stop adding + // samples when the monitor is off, but I had a hard time to get it + // working reliably, there are just too many corner cases. + + // Don't add sample again if we already collected statistics for this vblank + if (ps->last_msc < e->msc) { + if (ps->last_msc_instant != 0) { + auto frame_count = e->msc - ps->last_msc; + auto frame_time = + (int)((e->ust - ps->last_msc_instant) / frame_count); + if (frame_count == 1) { + render_statistics_add_vblank_time_sample( + &ps->render_stats, frame_time); + log_trace("Frame count %" PRIu64 ", frame time: %d us, " + "ust: " + "%" PRIu64, + frame_count, frame_time, e->ust); + } else { + log_trace("Frame count %" PRIu64 ", frame time: %d us, " + "msc: " + "%" PRIu64 ", not adding sample.", + frame_count, frame_time, e->ust); + } + } + ps->last_msc_instant = e->ust; + ps->last_msc = e->msc; + } else if (ps->last_msc > e->msc) { + log_warn("PresentCompleteNotify msc is going backwards, last_msc: " + "%" PRIu64 ", current msc: %" PRIu64, + ps->last_msc, e->msc); + ps->last_msc_instant = 0; + ps->last_msc = 0; } - free(active); - auto xinerama_scrs = - xcb_xinerama_query_screens_reply(ps->c, xcb_xinerama_query_screens(ps->c), NULL); - if (!xinerama_scrs) { - return; + vblank_interval = render_statistics_get_vblank_time(&ps->render_stats); + log_trace("Vblank interval estimate: %f us", vblank_interval); + if (vblank_interval == 0) { + // We don't have enough data for vblank interval estimate, schedule + // another vblank event. + return VBLANK_CALLBACK_AGAIN; } + return VBLANK_CALLBACK_DONE; +} - xcb_xinerama_screen_info_t *scrs = - xcb_xinerama_query_screens_screen_info(xinerama_scrs); - ps->xinerama_nscrs = xcb_xinerama_query_screens_screen_info_length(xinerama_scrs); +void schedule_render(session_t *ps, bool triggered_by_vblank); - ps->xinerama_scr_regs = ccalloc(ps->xinerama_nscrs, region_t); - for (int i = 0; i < ps->xinerama_nscrs; ++i) { - const xcb_xinerama_screen_info_t *const s = &scrs[i]; - pixman_region32_init_rect(&ps->xinerama_scr_regs[i], s->x_org, s->y_org, - s->width, s->height); +/// vblank callback scheduled by schedule_render, when a render is ongoing. +/// +/// Check if previously queued render has finished, and reschedule render if it has. +enum vblank_callback_action reschedule_render_at_vblank(struct vblank_event *e, void *ud) { + auto ps = (session_t *)ud; + assert(ps->frame_pacing); + assert(ps->render_queued); + assert(ps->vblank_scheduler); + + log_verbose("Rescheduling render at vblank, msc: %" PRIu64, e->msc); + + collect_vblank_interval_statistics(e, ud); + check_render_finish(e, ud); + + if (ps->backend_busy) { + return VBLANK_CALLBACK_AGAIN; } - free(xinerama_scrs); + + schedule_render(ps, false); + return VBLANK_CALLBACK_DONE; } -/** - * Find matched window. - * - * XXX move to win.c - */ -static inline struct managed_win *find_win_all(session_t *ps, const xcb_window_t wid) { - if (!wid || PointerRoot == wid || wid == ps->root || wid == ps->overlay) - return NULL; +/// How many seconds into the future should we start rendering the next frame. +/// +/// Renders are scheduled like this: +/// +/// 1. queue_redraw() queues a new render by calling schedule_render, if there +/// is no render currently scheduled. i.e. render_queued == false. +/// 2. then, we need to figure out the best time to start rendering. we need to +/// at least know when the next vblank will start, as we can't start render +/// before the current rendered frame is displayed on screen. we have this +/// information from the vblank scheduler, it will notify us when that happens. +/// we might also want to delay the rendering even further to reduce latency, +/// this is discussed below, in FUTURE WORKS. +/// 3. we schedule a render for that target point in time. +/// 4. draw_callback() is called at the schedule time (i.e. when scheduled +/// vblank event is delivered). Backend APIs are called to issue render +/// commands. render_queued is set to false, and backend_busy is set to true. +/// +/// There are some considerations in step 2: +/// +/// First of all, a vblank event being delivered +/// doesn't necessarily mean the frame has been displayed on screen. If a frame +/// takes too long to render, it might miss the current vblank, and will be +/// displayed on screen during one of the subsequent vblanks. So in +/// schedule_render_at_vblank, we ask the backend to see if it has finished +/// rendering. if not, render_queued is unchanged, and another vblank is +/// scheduled; otherwise, draw_callback_impl will be scheduled to be call at +/// an appropriate time. Second, we might not have rendered for the previous vblank, +/// in which case the last vblank event we received could be many frames in the past, +/// so we can't make scheduling decisions based on that. So we always schedule +/// a vblank event when render is queued, and make scheduling decisions when the +/// event is delivered. +/// +/// All of the above is what happens when frame_pacing is true. Otherwise +/// render_in_progress is either QUEUED or IDLE, and queue_redraw will always +/// schedule a render to be started immediately. PresentCompleteNotify will not +/// be received, and handle_end_of_vblank will not be called. +/// +/// The `triggered_by_timer` parameter is used to indicate whether this function +/// is triggered by a steady timer, i.e. we are rendering for each vblank. The +/// other case is when we stop rendering for a while because there is no changes +/// on screen, then something changed and schedule_render is triggered by a +/// DamageNotify. The idea is that when the schedule is triggered by a steady +/// timer, schedule_render will be called at a predictable offset into each +/// vblank. +/// +/// # FUTURE WORKS +/// +/// As discussed in step 2 above, we might want to delay the rendering even +/// further. If we know the time it takes to render a frame, and the interval +/// between vblanks, we can try to schedule the render to start at a point in +/// time that's closer to the next vblank. We should be able to get this +/// information by doing statistics on the render time of previous frames, which +/// is available from the backends; and the interval between vblank events, +/// which is available from the vblank scheduler. +/// +/// The code that does this is already implemented below, but disabled by +/// default. There are several problems with it, see bug #1072. +void schedule_render(session_t *ps, bool triggered_by_vblank attr_unused) { + // If the backend is busy, we will try again at the next vblank. + if (ps->backend_busy) { + // We should never have set backend_busy to true unless frame_pacing is + // enabled. + assert(ps->vblank_scheduler); + assert(ps->frame_pacing); + log_verbose("Backend busy, will reschedule render at next vblank."); + if (!vblank_scheduler_schedule(ps->vblank_scheduler, + reschedule_render_at_vblank, ps)) { + // TODO(yshui): handle error here + abort(); + } + return; + } - auto w = find_managed_win(ps, wid); - if (!w) - w = find_toplevel(ps, wid); - if (!w) - w = find_managed_window_or_parent(ps, wid); - return w; + // By default, we want to schedule render immediately, later in this function we + // might adjust that and move the render later, based on render timing statistics. + double delay_s = 0; + unsigned int divisor = 0; + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + auto now_us = (uint64_t)now.tv_sec * 1000000 + (uint64_t)now.tv_nsec / 1000; + + ps->next_render = now_us; + + if (!ps->frame_pacing || !ps->redirected) { + // If not doing frame pacing, schedule a render immediately; if + // not redirected, we schedule immediately to have a chance to + // redirect. We won't have frame or render timing information + // anyway. + assert(!ev_is_active(&ps->draw_timer)); + goto schedule; + } + + // if global_debug_options.smart_frame_pacing is false, we won't have any render + // time or vblank interval estimates, so we would naturally fallback to schedule + // render immediately. + auto render_budget = render_statistics_get_budget(&ps->render_stats); + auto frame_time = render_statistics_get_vblank_time(&ps->render_stats); + if (frame_time == 0) { + // We don't have enough data for render time estimates, maybe there's + // no frame rendered yet, or the backend doesn't support render timing + // information, schedule render immediately. + log_verbose("Not enough data for render time estimates."); + goto schedule; + } + + if (render_budget >= frame_time) { + // If the estimated render time is already longer than the estimated + // vblank interval, there is no way we can make it. Instead of always + // dropping frames, we try desperately to catch up and schedule a + // render immediately. + log_verbose("Render budget: %u us >= frame time: %" PRIu32 " us", + render_budget, frame_time); + goto schedule; + } + + auto target_frame = (now_us + render_budget - ps->last_msc_instant) / frame_time + 1; + auto const deadline = ps->last_msc_instant + target_frame * frame_time; + unsigned int available = 0; + if (deadline > now_us) { + available = (unsigned int)(deadline - now_us); + } + + if (available > render_budget) { + delay_s = (double)(available - render_budget) / 1000000.0; + ps->next_render = deadline - render_budget; + } + + if (delay_s > 1) { + log_warn("Delay too long: %f s, render_budget: %d us, frame_time: " + "%" PRIu32 " us, now_us: %" PRIu64 " us, next_msc: %" PRIu64 " u" + "s", + delay_s, render_budget, frame_time, now_us, deadline); + } + + log_verbose("Delay: %.6lf s, last_msc: %" PRIu64 ", render_budget: %d, " + "frame_time: %" PRIu32 ", now_us: %" PRIu64 ", next_render: %" PRIu64 + ", next_msc: %" PRIu64 ", divisor: " + "%d", + delay_s, ps->last_msc_instant, render_budget, frame_time, now_us, + ps->next_render, deadline, divisor); + +schedule: + // If the backend is not busy, we just need to schedule the render at the + // specified time; otherwise we need to wait for the next vblank event and + // reschedule. + ps->last_schedule_delay = 0; + assert(!ev_is_active(&ps->draw_timer)); + ev_timer_set(&ps->draw_timer, delay_s, 0); + ev_timer_start(ps->loop, &ps->draw_timer); } void queue_redraw(session_t *ps) { - // If --benchmark is used, redraw is always queued - if (!ps->redraw_needed && !ps->o.benchmark) { - ev_idle_start(ps->loop, &ps->draw_idle); + log_verbose("Queue redraw, render_queued: %d, backend_busy: %d", + ps->render_queued, ps->backend_busy); + + if (ps->render_queued) { + return; } - ps->redraw_needed = true; + ps->render_queued = true; + schedule_render(ps, false); } /** @@ -215,7 +447,9 @@ void add_damage(session_t *ps, const region_t *damage) { } log_trace("Adding damage: "); dump_region(damage); - pixman_region32_union(ps->damage, ps->damage, (region_t *)damage); + + auto cursor = &ps->damage_ring.damages[ps->damage_ring.cursor]; + pixman_region32_union(cursor, cursor, (region_t *)damage); } // === Fading === @@ -227,8 +461,9 @@ void add_damage(session_t *ps, const region_t *damage) { */ static double fade_timeout(session_t *ps) { auto now = get_time_ms(); - if (ps->o.fade_delta + ps->fade_time < now) + if (ps->o.fade_delta + ps->fade_time < now) { return 0; + } auto diff = ps->o.fade_delta + ps->fade_time - now; @@ -243,97 +478,31 @@ static double fade_timeout(session_t *ps) { * @param steps steps of fading * @return whether we are still in fading mode */ -static bool run_fade(session_t *ps, struct managed_win **_w, long steps) { +static bool run_fade(struct managed_win **_w, double delta_sec) { auto w = *_w; - if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { - // We are not fading - assert(w->opacity_target == w->opacity); - return false; - } - - if (!win_should_fade(ps, w)) { - log_debug("Window %#010x %s doesn't need fading", w->base.id, w->name); - w->opacity = w->opacity_target; - } - if (w->opacity == w->opacity_target) { + log_trace("Process fading for window %s (%#010x), ΔT: %fs", w->name, w->base.id, + delta_sec); + if (w->number_of_animations == 0) { // We have reached target opacity. // We don't call win_check_fade_finished here because that could destroy // the window, but we still need the damage info from this window - log_debug("Fading finished for window %#010x %s", w->base.id, w->name); + log_trace("|- was fading but finished"); return false; } - if (steps) { - log_trace("Window %#010x (%s) opacity was: %lf", w->base.id, w->name, - w->opacity); - if (w->opacity < w->opacity_target) { - w->opacity = clamp(w->opacity + ps->o.fade_in_step * (double)steps, - 0.0, w->opacity_target); - } else { - w->opacity = clamp(w->opacity - ps->o.fade_out_step * (double)steps, - w->opacity_target, 1); - } - log_trace("... updated to: %lf", w->opacity); - } + log_trace("|- fading, opacity: %lf", animatable_get(&w->opacity)); + animatable_advance(&w->opacity, delta_sec); + animatable_advance(&w->blur_opacity, delta_sec); + log_trace("|- opacity updated: %lf", animatable_get(&w->opacity)); - // Note even if opacity == opacity_target here, we still want to run preprocess - // one last time to finish state transition. So return true in that case too. + // Note even if the animatable is not animating anymore at this point, we still + // want to run preprocess one last time to finish state transition. So return true + // in that case too. return true; } -// === Error handling === - -void discard_ignore(session_t *ps, unsigned long sequence) { - while (ps->ignore_head) { - if (sequence > ps->ignore_head->sequence) { - ignore_t *next = ps->ignore_head->next; - free(ps->ignore_head); - ps->ignore_head = next; - if (!ps->ignore_head) { - ps->ignore_tail = &ps->ignore_head; - } - } else { - break; - } - } -} - -static int should_ignore(session_t *ps, unsigned long sequence) { - if (ps == NULL) { - // Do not ignore errors until the session has been initialized - return false; - } - discard_ignore(ps, sequence); - return ps->ignore_head && ps->ignore_head->sequence == sequence; -} - // === Windows === -/** - * Determine the event mask for a window. - */ -uint32_t determine_evmask(session_t *ps, xcb_window_t wid, win_evmode_t mode) { - uint32_t evmask = 0; - struct managed_win *w = NULL; - - // Check if it's a mapped frame window - if (mode == WIN_EVMODE_FRAME || - ((w = find_managed_win(ps, wid)) && w->a.map_state == XCB_MAP_STATE_VIEWABLE)) { - evmask |= XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY; - if (!ps->o.use_ewmh_active_win) { - evmask |= XCB_EVENT_MASK_FOCUS_CHANGE; - } - } - - // Check if it's a mapped client window - if (mode == WIN_EVMODE_CLIENT || - ((w = find_toplevel(ps, wid)) && w->a.map_state == XCB_MAP_STATE_VIEWABLE)) { - evmask |= XCB_EVENT_MASK_PROPERTY_CHANGE; - } - - return evmask; -} - /** * Update current active window based on EWMH _NET_ACTIVE_WIN. * @@ -342,9 +511,9 @@ uint32_t determine_evmask(session_t *ps, xcb_window_t wid, win_evmode_t mode) { */ void update_ewmh_active_win(session_t *ps) { // Search for the window - xcb_window_t wid = - wid_get_prop_window(ps->c, ps->root, ps->atoms->a_NET_ACTIVE_WINDOW); - auto w = find_win_all(ps, wid); + xcb_window_t wid = wid_get_prop_window(&ps->c, ps->c.screen_info->root, + ps->atoms->a_NET_ACTIVE_WINDOW); + auto w = wm_find_by_client(ps->wm, wid); // Mark the window focused. No need to unfocus the previous one. if (w) { @@ -368,24 +537,53 @@ static void recheck_focus(session_t *ps) { // Determine the currently focused window so we can apply appropriate // opacity on it - xcb_window_t wid = XCB_NONE; - xcb_get_input_focus_reply_t *reply = - xcb_get_input_focus_reply(ps->c, xcb_get_input_focus(ps->c), NULL); + xcb_generic_error_t *e = NULL; + auto reply = xcb_get_input_focus_reply(ps->c.c, xcb_get_input_focus(ps->c.c), &e); + if (reply == NULL) { + // Not able to get input focus means very not good things... + log_error_x_error(e, "Failed to get focused window."); + free(e); + return; + } + xcb_window_t wid = reply->focus; + free(reply); - if (reply) { - wid = reply->focus; - free(reply); + if (wid == XCB_NONE || wid == XCB_INPUT_FOCUS_POINTER_ROOT || + wid == ps->c.screen_info->root) { + // Focus is not on a toplevel. + return; } - auto w = find_win_all(ps, wid); + // Trace upwards until we reach the toplevel containing the focus window. + while (true) { + auto tree = xcb_query_tree_reply(ps->c.c, xcb_query_tree(ps->c.c, wid), &e); + if (tree == NULL) { + // xcb_query_tree probably fails if you run picom when X is + // somehow initializing (like add it in .xinitrc). In this case + // just leave it alone. + log_error_x_error(e, "Failed to query window tree."); + free(e); + return; + } + + auto parent = tree->parent; + free(tree); + + if (parent == ps->c.screen_info->root) { + break; + } + wid = parent; + } - log_trace("%#010" PRIx32 " (%#010lx \"%s\") focused.", wid, - (w ? w->base.id : XCB_NONE), (w ? w->name : NULL)); + auto w = wm_find_managed(ps->wm, wid); // And we set the focus state here if (w) { + log_debug("%#010" PRIx32 " (%#010" PRIx32 " \"%s\") focused.", wid, + w->base.id, w->name); win_set_focused(ps, w); - return; + } else { + log_warn("Focus window %#010" PRIx32 " not found.", wid); } } @@ -396,41 +594,38 @@ static void rebuild_screen_reg(session_t *ps) { get_screen_region(ps, &ps->screen_reg); } -/** - * Rebuild shadow_exclude_reg. - */ -static void rebuild_shadow_exclude_reg(session_t *ps) { - bool ret = parse_geometry(ps, ps->o.shadow_exclude_reg_str, &ps->shadow_exclude_reg); - if (!ret) - exit(1); -} - /// Free up all the images and deinit the backend static void destroy_backend(session_t *ps) { - win_stack_foreach_managed_safe(w, &ps->window_stack) { + win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { // Wrapping up fading in progress - if (win_skip_fading(ps, w)) { - // `w` is freed by win_skip_fading - continue; - } + win_skip_fading(w); if (ps->backend_data) { // Unmapped windows could still have shadow images, but not pixmap // images assert(!w->win_image || w->state != WSTATE_UNMAPPED); - if (win_check_flags_any(w, WIN_FLAGS_IMAGES_STALE) && - w->state == WSTATE_MAPPED) { - log_warn("Stale flags set for mapped window %#010x " - "during backend destruction", - w->base.id); - assert(false); - } - // Unmapped windows can still have stale flags set, because their - // stale flags aren't handled until they are mapped. - win_clear_flags(w, WIN_FLAGS_IMAGES_STALE); + // In some cases, the window might have PIXMAP_STALE flag set: + // 1. If the window is unmapped. Their stale flags won't be + // handled until they are mapped. + // 2. If we haven't had chance to handle the stale flags. This + // could happen if we received a root ConfigureNotify + // _immidiately_ after we redirected. + win_clear_flags(w, WIN_FLAGS_PIXMAP_STALE); win_release_images(ps->backend_data, w); } free_paint(ps, &w->paint); + + if (w->state == WSTATE_DESTROYED) { + destroy_win_finish(ps, &w->base); + } + } + + HASH_ITER2(ps->shaders, shader) { + if (shader->backend_shader != NULL) { + ps->backend_data->ops->destroy_shader(ps->backend_data, + shader->backend_shader); + shader->backend_shader = NULL; + } } if (ps->backend_data && ps->root_image) { @@ -439,6 +634,10 @@ static void destroy_backend(session_t *ps) { } if (ps->backend_data) { + if (ps->renderer) { + renderer_free(ps->backend_data, ps->renderer); + ps->renderer = NULL; + } // deinit backend if (ps->backend_blur_context) { ps->backend_data->ops->destroy_blur_context( @@ -480,18 +679,36 @@ static bool initialize_blur(session_t *ps) { default: return true; } + enum backend_image_format format = ps->o.dithered_present + ? BACKEND_IMAGE_FORMAT_PIXMAP_HIGH + : BACKEND_IMAGE_FORMAT_PIXMAP; ps->backend_blur_context = ps->backend_data->ops->create_blur_context( - ps->backend_data, ps->o.blur_method, args); + ps->backend_data, ps->o.blur_method, format, args); return ps->backend_blur_context != NULL; } +static int mark_pixmap_stale(struct win *w, void *data) { + struct session *ps = data; + if (!w->managed) { + return 0; + } + auto mw = win_as_managed(w); + assert(mw->state != WSTATE_DESTROYED); + // We need to reacquire image + log_debug("Marking window %#010x (%s) for update after redirection", w->id, mw->name); + win_set_flags(mw, WIN_FLAGS_PIXMAP_STALE); + ps->pending_updates = true; + return 0; +} + /// Init the backend and bind all the window pixmap to backend images static bool initialize_backend(session_t *ps) { - if (ps->o.experimental_backends) { + if (!ps->o.legacy_backends) { assert(!ps->backend_data); // Reinitialize win_data assert(backend_list[ps->o.backend]); - ps->backend_data = backend_list[ps->o.backend]->init(ps); + ps->backend_data = + backend_list[ps->o.backend]->init(ps, session_get_target_window(ps)); if (!ps->backend_data) { log_fatal("Failed to initialize backend, aborting..."); quit(ps); @@ -501,37 +718,56 @@ static bool initialize_backend(session_t *ps) { if (!initialize_blur(ps)) { log_fatal("Failed to prepare for background blur, aborting..."); - ps->backend_data->ops->deinit(ps->backend_data); - ps->backend_data = NULL; - quit(ps); - return false; + goto err; } - // window_stack shouldn't include window that's - // not in the hash table at this point. Since - // there cannot be any fading windows. - HASH_ITER2(ps->windows, _w) { - if (!_w->managed) { - continue; + // Create shaders + HASH_ITER2(ps->shaders, shader) { + assert(shader->backend_shader == NULL); + shader->backend_shader = ps->backend_data->ops->create_shader( + ps->backend_data, shader->source); + if (shader->backend_shader == NULL) { + log_warn("Failed to create shader for shader file %s, " + "this shader will not be used", + shader->key); + } else { + if (ps->backend_data->ops->get_shader_attributes) { + shader->attributes = + ps->backend_data->ops->get_shader_attributes( + ps->backend_data, shader->backend_shader); + } else { + shader->attributes = 0; + } + log_debug("Shader %s has attributes %" PRIu64, + shader->key, shader->attributes); } - auto w = (struct managed_win *)_w; - assert(w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED); - // We need to reacquire image - log_debug("Marking window %#010x (%s) for update after " - "redirection", - w->base.id, w->name); - win_set_flags(w, WIN_FLAGS_IMAGES_STALE); - ps->pending_updates = true; } + + // wm_stack shouldn't include window that's not iterated by wm_foreach at + // this moment. Since there cannot be any fading windows. + wm_foreach(ps->wm, mark_pixmap_stale, ps); + ps->renderer = renderer_new(ps->backend_data, ps->o.shadow_radius, + (struct color){.alpha = ps->o.shadow_opacity, + .red = ps->o.shadow_red, + .green = ps->o.shadow_green, + .blue = ps->o.shadow_blue}, + ps->o.dithered_present); } // The old backends binds pixmap lazily, nothing to do here return true; +err: + ps->backend_data->ops->deinit(ps->backend_data); + ps->backend_data = NULL; + quit(ps); + return false; } /// Handle configure event of the root window static void configure_root(session_t *ps) { - auto r = XCB_AWAIT(xcb_get_geometry, ps->c, ps->root); + // TODO(yshui) re-initializing backend should be done outside of the + // critical section. Probably set a flag and do it in draw_callback_impl. + auto r = XCB_AWAIT(xcb_get_geometry, ps->c.c, ps->c.screen_info->root); if (!r) { log_fatal("Failed to fetch root geometry"); abort(); @@ -541,7 +777,7 @@ static void configure_root(session_t *ps) { bool has_root_change = false; if (ps->redirected) { // On root window changes - if (ps->o.experimental_backends) { + if (!ps->o.legacy_backends) { assert(ps->backend_data); has_root_change = ps->backend_data->ops->root_change != NULL; } else { @@ -559,25 +795,32 @@ static void configure_root(session_t *ps) { ps->root_width = r->width; ps->root_height = r->height; + free(r); rebuild_screen_reg(ps); - rebuild_shadow_exclude_reg(ps); // Invalidate reg_ignore from the top - auto top_w = win_stack_find_next_managed(ps, &ps->window_stack); + auto top_w = wm_stack_next_managed(ps->wm, wm_stack_end(ps->wm)); if (top_w) { rc_region_unref(&top_w->reg_ignore); top_w->reg_ignore_valid = false; } + // Whether a window is fullscreen depends on the new screen + // size. So we need to refresh the fullscreen state of all + // windows. + win_stack_foreach_managed(w, wm_stack_end(ps->wm)) { + win_update_is_fullscreen(ps, w); + } + if (ps->redirected) { - for (int i = 0; i < ps->ndamage; i++) { - pixman_region32_clear(&ps->damage_ring[i]); + for (int i = 0; i < ps->damage_ring.count; i++) { + pixman_region32_clear(&ps->damage_ring.damages[i]); } - ps->damage = ps->damage_ring + ps->ndamage - 1; + ps->damage_ring.cursor = ps->damage_ring.count - 1; #ifdef CONFIG_OPENGL // GLX root change callback - if (BKEND_GLX == ps->o.backend && !ps->o.experimental_backends) { + if (BKEND_GLX == ps->o.backend && ps->o.legacy_backends) { glx_on_root_change(ps); } #endif @@ -603,21 +846,12 @@ static void configure_root(session_t *ps) { } force_repaint(ps); } - return; } static void handle_root_flags(session_t *ps) { if ((ps->root_flags & ROOT_FLAGS_SCREEN_CHANGE) != 0) { - if (ps->o.xinerama_shadow_crop) { - cxinerama_upd_scrs(ps); - } - - if (ps->o.sw_opti && !ps->o.refresh_rate) { - update_refresh_rate(ps); - if (!ps->refresh_rate) { - log_warn("Refresh rate detection failed. swopti will be " - "temporarily disabled"); - } + if (ps->o.crop_shadow_to_monitor && ps->randr_exists) { + x_update_monitors(&ps->c, &ps->monitors); } ps->root_flags &= ~(uint64_t)ROOT_FLAGS_SCREEN_CHANGE; } @@ -628,52 +862,57 @@ static void handle_root_flags(session_t *ps) { } } -static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { +/** + * Go through the window stack and calculate some parameters for rendering. + * + * @return whether the operation succeeded + */ +static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, + struct managed_win **out_bottom) { // XXX need better, more general name for `fade_running`. It really // means if fade is still ongoing after the current frame is rendered struct managed_win *bottom = NULL; *fade_running = false; + *animation = false; + *out_bottom = NULL; // Fading step calculation - long steps = 0L; + int64_t delta_ms = 0L; auto now = get_time_ms(); if (ps->fade_time) { assert(now >= ps->fade_time); - steps = (now - ps->fade_time) / ps->o.fade_delta; - } else { - // Reset fade_time if unset - ps->fade_time = get_time_ms(); - steps = 0L; + delta_ms = now - ps->fade_time; } - ps->fade_time += steps * ps->o.fade_delta; + ps->fade_time = now; - // First, let's process fading - win_stack_foreach_managed_safe(w, &ps->window_stack) { + // First, let's process fading, and animated shaders + // TODO(yshui) check if a window is fully obscured, and if we don't need to + // process fading or animation for it. + win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { const winmode_t mode_old = w->mode; const bool was_painted = w->to_paint; - const double opacity_old = w->opacity; if (win_should_dim(ps, w) != w->dim) { w->dim = win_should_dim(ps, w); add_damage_from_win(ps, w); } - // Run fading - if (run_fade(ps, &w, steps)) { - *fade_running = true; + if (w->fg_shader && (w->fg_shader->attributes & SHADER_ATTRIBUTE_ANIMATED)) { + add_damage_from_win(ps, w); + *animation = true; } // Add window to damaged area if its opacity changes // If was_painted == false, and to_paint is also false, we don't care // If was_painted == false, but to_paint is true, damage will be added in // the loop below - if (was_painted && w->opacity != opacity_old) { + if (was_painted && w->number_of_animations != 0) { add_damage_from_win(ps, w); } - if (win_check_fade_finished(ps, w)) { - // the window has been destroyed because fading finished - continue; + // Run fading + if (run_fade(&w, (double)delta_ms / 1000.0)) { + *fade_running = true; } if (win_has_frame(w)) { @@ -699,11 +938,13 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { // Track whether it's the highest window to paint bool is_highest = true; bool reg_ignore_valid = true; - win_stack_foreach_managed(w, &ps->window_stack) { + win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { __label__ skip_window; bool to_paint = true; // w->to_paint remembers whether this window is painted last time const bool was_painted = w->to_paint; + const double window_opacity = animatable_get(&w->opacity); + const double blur_opacity = animatable_get(&w->blur_opacity); // Destroy reg_ignore if some window above us invalidated it if (!reg_ignore_valid) { @@ -711,47 +952,45 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { } // log_trace("%d %d %s", w->a.map_state, w->ever_damaged, w->name); + log_trace("Checking whether window %#010x (%s) should be painted", + w->base.id, w->name); // Give up if it's not damaged or invisible, or it's unmapped and its // pixmap is gone (for example due to a ConfigureNotify), or when it's // excluded - if (w->state == WSTATE_UNMAPPED || - unlikely(w->base.id == ps->debug_window || - w->client_win == ps->debug_window)) { + if ((w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYED) && + w->number_of_animations == 0) { + if (window_opacity != 0 || blur_opacity != 0) { + log_warn("Window %#010x (%s) is unmapped but still has " + "opacity", + w->base.id, w->name); + } + log_trace("|- is unmapped"); to_paint = false; - } else if (!w->ever_damaged && w->state != WSTATE_UNMAPPING && - w->state != WSTATE_DESTROYING) { - // Unmapping clears w->ever_damaged, but the fact that the window - // is fading out means it must have been damaged when it was still - // mapped (because unmap_win_start will skip fading if it wasn't), - // so we still need to paint it. - log_trace("Window %#010x (%s) will not be painted because it has " - "not received any damages", - w->base.id, w->name); + } else if (unlikely(ps->debug_window != XCB_NONE) && + (w->base.id == ps->debug_window || + w->client_win == ps->debug_window)) { + log_trace("|- is the debug window"); + to_paint = false; + } else if (!w->ever_damaged) { + log_trace("|- has not received any damages"); to_paint = false; } else if (unlikely(w->g.x + w->g.width < 1 || w->g.y + w->g.height < 1 || w->g.x >= ps->root_width || w->g.y >= ps->root_height)) { - log_trace("Window %#010x (%s) will not be painted because it is " - "positioned outside of the screen", - w->base.id, w->name); + log_trace("|- is positioned outside of the screen"); to_paint = false; - } else if (unlikely((double)w->opacity * MAX_ALPHA < 1 && !w->blur_background)) { - /* TODO(yshui) for consistency, even a window has 0 opacity, we - * still probably need to blur its background, so to_paint - * shouldn't be false for them. */ - log_trace("Window %#010x (%s) will not be painted because it has " - "0 opacity", - w->base.id, w->name); + } else if (unlikely(window_opacity * MAX_ALPHA < 1 && + (!w->blur_background || blur_opacity * MAX_ALPHA < 1))) { + // For consistency, even a window has 0 opacity, we would still + // blur its background. (unless it's background is not blurred, or + // the blur opacity is 0) + log_trace("|- has 0 opacity"); to_paint = false; } else if (w->paint_excluded) { - log_trace("Window %#010x (%s) will not be painted because it is " - "excluded from painting", - w->base.id, w->name); + log_trace("|- is excluded from painting"); to_paint = false; } else if (unlikely((w->flags & WIN_FLAGS_IMAGE_ERROR) != 0)) { - log_trace("Window %#010x (%s) will not be painted because it has " - "image errors", - w->base.id, w->name); + log_trace("|- has image errors"); to_paint = false; } // log_trace("%s %d %d %d", w->name, to_paint, w->opacity, @@ -766,13 +1005,12 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { // to_paint will never change after this point if (!to_paint) { + log_trace("|- will not be painted"); goto skip_window; } - log_trace("Window %#010x (%s) will be painted", w->base.id, w->name); - - // Calculate shadow opacity - w->shadow_opacity = ps->o.shadow_opacity * w->opacity * ps->o.frame_opacity; + log_trace("|- will be painted"); + log_verbose("Window %#010x (%s) will be painted", w->base.id, w->name); // Generate ignore region for painting to reduce GPU load if (!w->reg_ignore) { @@ -783,7 +1021,7 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { // we add the window region to the ignored region // Otherwise last_reg_ignore shouldn't change if ((w->mode != WMODE_TRANS && !ps->o.force_win_blend) || - ps->o.transparent_clipping) { + w->transparent_clipping) { // w->mode == WMODE_SOLID or WMODE_FRAME_TRANS region_t *tmp = rc_region_new(); if (w->mode == WMODE_SOLID) { @@ -808,7 +1046,7 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { // is not correctly set. if (ps->o.unredir_if_possible && is_highest) { if (w->mode == WMODE_SOLID && !ps->o.force_win_blend && - win_is_fullscreen(ps, w) && !w->unredir_if_possible_excluded) { + w->is_fullscreen && !w->unredir_if_possible_excluded) { unredir_possible = true; } } @@ -842,6 +1080,13 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { reg_ignore_valid = reg_ignore_valid && w->reg_ignore_valid; w->reg_ignore_valid = true; + if (w->state == WSTATE_DESTROYED && w->number_of_animations == 0) { + // the window should be destroyed because it was destroyed + // by X server and now its animations are finished + destroy_win_finish(ps, &w->base); + w = NULL; + } + // Avoid setting w->to_paint if w is freed if (w) { w->to_paint = to_paint; @@ -873,12 +1118,13 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { ev_timer_stop(ps->loop, &ps->unredir_timer); if (!ps->redirected) { if (!redirect_start(ps)) { - return NULL; + return false; } } } - return bottom; + *out_bottom = bottom; + return true; } void root_damaged(session_t *ps) { @@ -893,14 +1139,46 @@ void root_damaged(session_t *ps) { if (ps->backend_data) { if (ps->root_image) { ps->backend_data->ops->release_image(ps->backend_data, ps->root_image); + ps->root_image = NULL; } - auto pixmap = x_get_root_back_pixmap(ps->c, ps->root, ps->atoms); + auto pixmap = x_get_root_back_pixmap(&ps->c, ps->atoms); if (pixmap != XCB_NONE) { + xcb_get_geometry_reply_t *r = xcb_get_geometry_reply( + ps->c.c, xcb_get_geometry(ps->c.c, pixmap), NULL); + if (!r) { + goto err; + } + + // We used to assume that pixmaps pointed by the root background + // pixmap atoms are owned by the root window and have the same + // depth and hence the same visual that we can use to bind them. + // However, some applications break this assumption, e.g. the + // Xfce's desktop manager xfdesktop that sets the _XROOTPMAP_ID + // atom to a pixmap owned by it that seems to always have 32 bpp + // depth when the common root window's depth is 24 bpp. So use the + // root window's visual only if the root background pixmap's depth + // matches the root window's depth. Otherwise, find a suitable + // visual for the root background pixmap's depth and use it. + // + // We can't obtain a suitable visual for the root background + // pixmap the same way as the win_bind_pixmap function because it + // requires a window and we have only a pixmap. We also can't not + // bind the root background pixmap in case of depth mismatch + // because some options rely on it's content, e.g. + // transparent-clipping. + xcb_visualid_t visual = + r->depth == ps->c.screen_info->root_depth + ? ps->c.screen_info->root_visual + : x_get_visual_for_depth(ps->c.screen_info, r->depth); + free(r); + ps->root_image = ps->backend_data->ops->bind_pixmap( - ps->backend_data, pixmap, x_get_visual_info(ps->c, ps->vis), false); - ps->backend_data->ops->set_image_property( - ps->backend_data, IMAGE_PROPERTY_EFFECTIVE_SIZE, - ps->root_image, (int[]){ps->root_width, ps->root_height}); + ps->backend_data, pixmap, x_get_visual_info(&ps->c, visual)); + ps->root_image_generation += 1; + if (!ps->root_image) { + err: + log_error("Failed to bind root back pixmap"); + } } } @@ -908,25 +1186,6 @@ void root_damaged(session_t *ps) { force_repaint(ps); } -/** - * Xlib error handler function. - */ -static int xerror(Display attr_unused *dpy, XErrorEvent *ev) { - if (!should_ignore(ps_g, ev->serial)) { - x_print_error(ev->serial, ev->request_code, ev->minor_code, ev->error_code); - } - return 0; -} - -/** - * XCB error handler function. - */ -void ev_xcb_error(session_t *ps, xcb_generic_error_t *err) { - if (!should_ignore(ps, err->sequence)) { - x_print_error(err->sequence, err->major_code, err->minor_code, err->error_code); - } -} - /** * Force a full-screen repaint. */ @@ -936,23 +1195,6 @@ void force_repaint(session_t *ps) { add_damage(ps, &ps->screen_reg); } -#ifdef CONFIG_DBUS -/** @name DBus hooks - */ -///@{ - -/** - * Set no_fading_openclose option. - * - * Don't affect fading already in progress - */ -void opts_set_no_fading_openclose(session_t *ps, bool newval) { - ps->o.no_fading_openclose = newval; -} - -//!@} -#endif - /** * Setup window properties, then register us with the compositor selection (_NET_WM_CM_S) * @@ -961,10 +1203,11 @@ void opts_set_no_fading_openclose(session_t *ps, bool newval) { static int register_cm(session_t *ps) { assert(!ps->reg_win); - ps->reg_win = x_new_id(ps->c); + ps->reg_win = x_new_id(&ps->c); auto e = xcb_request_check( - ps->c, xcb_create_window_checked(ps->c, XCB_COPY_FROM_PARENT, ps->reg_win, ps->root, - 0, 0, 1, 1, 0, XCB_NONE, ps->vis, 0, NULL)); + ps->c.c, xcb_create_window_checked(ps->c.c, XCB_COPY_FROM_PARENT, ps->reg_win, + ps->c.screen_info->root, 0, 0, 1, 1, 0, XCB_NONE, + ps->c.screen_info->root_visual, 0, NULL)); if (e) { log_fatal("Failed to create window."); @@ -983,10 +1226,10 @@ static int register_cm(session_t *ps) { // Set names and classes for (size_t i = 0; i < ARR_SIZE(prop_atoms); i++) { e = xcb_request_check( - ps->c, xcb_change_property_checked( - ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, prop_atoms[i], - prop_is_utf8[i] ? ps->atoms->aUTF8_STRING : XCB_ATOM_STRING, - 8, strlen("picom"), "picom")); + ps->c.c, xcb_change_property_checked( + ps->c.c, XCB_PROP_MODE_REPLACE, ps->reg_win, prop_atoms[i], + prop_is_utf8[i] ? ps->atoms->aUTF8_STRING : XCB_ATOM_STRING, + 8, strlen("picom"), "picom")); if (e) { log_error_x_error(e, "Failed to set window property %d", prop_atoms[i]); @@ -996,9 +1239,9 @@ static int register_cm(session_t *ps) { const char picom_class[] = "picom\0picom"; e = xcb_request_check( - ps->c, xcb_change_property_checked(ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, - ps->atoms->aWM_CLASS, XCB_ATOM_STRING, 8, - ARR_SIZE(picom_class), picom_class)); + ps->c.c, xcb_change_property_checked(ps->c.c, XCB_PROP_MODE_REPLACE, ps->reg_win, + ps->atoms->aWM_CLASS, XCB_ATOM_STRING, 8, + ARR_SIZE(picom_class), picom_class)); if (e) { log_error_x_error(e, "Failed to set the WM_CLASS property"); free(e); @@ -1007,15 +1250,15 @@ static int register_cm(session_t *ps) { // Set WM_CLIENT_MACHINE. As per EWMH, because we set _NET_WM_PID, we must also // set WM_CLIENT_MACHINE. { - const auto hostname_max = (unsigned long)sysconf(_SC_HOST_NAME_MAX); + auto const hostname_max = (unsigned long)sysconf(_SC_HOST_NAME_MAX); char *hostname = malloc(hostname_max); if (gethostname(hostname, hostname_max) == 0) { e = xcb_request_check( - ps->c, xcb_change_property_checked( - ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, - ps->atoms->aWM_CLIENT_MACHINE, XCB_ATOM_STRING, 8, - (uint32_t)strlen(hostname), hostname)); + ps->c.c, xcb_change_property_checked( + ps->c.c, XCB_PROP_MODE_REPLACE, ps->reg_win, + ps->atoms->aWM_CLIENT_MACHINE, XCB_ATOM_STRING, + 8, (uint32_t)strlen(hostname), hostname)); if (e) { log_error_x_error(e, "Failed to set the WM_CLIENT_MACHINE" " property"); @@ -1031,16 +1274,15 @@ static int register_cm(session_t *ps) { // Set _NET_WM_PID { auto pid = getpid(); - xcb_change_property(ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, + xcb_change_property(ps->c.c, XCB_PROP_MODE_REPLACE, ps->reg_win, ps->atoms->a_NET_WM_PID, XCB_ATOM_CARDINAL, 32, 1, &pid); } // Set COMPTON_VERSION - e = xcb_request_check( - ps->c, xcb_change_property_checked( - ps->c, XCB_PROP_MODE_REPLACE, ps->reg_win, - get_atom(ps->atoms, "COMPTON_VERSION"), XCB_ATOM_STRING, 8, - (uint32_t)strlen(COMPTON_VERSION), COMPTON_VERSION)); + e = xcb_request_check(ps->c.c, xcb_change_property_checked( + ps->c.c, XCB_PROP_MODE_REPLACE, ps->reg_win, + ps->atoms->aCOMPTON_VERSION, XCB_ATOM_STRING, 8, + (uint32_t)strlen(PICOM_VERSION), PICOM_VERSION)); if (e) { log_error_x_error(e, "Failed to set COMPTON_VERSION."); free(e); @@ -1052,15 +1294,15 @@ static int register_cm(session_t *ps) { xcb_atom_t atom; char *buf = NULL; - if (asprintf(&buf, "%s%d", register_prop, ps->scr) < 0) { + if (asprintf(&buf, "%s%d", register_prop, ps->c.screen) < 0) { log_fatal("Failed to allocate memory"); return -1; } - atom = get_atom(ps->atoms, buf); + atom = get_atom_with_nul(ps->atoms, buf, ps->c.c); free(buf); xcb_get_selection_owner_reply_t *reply = xcb_get_selection_owner_reply( - ps->c, xcb_get_selection_owner(ps->c, atom), NULL); + ps->c.c, xcb_get_selection_owner(ps->c.c, atom), NULL); if (reply && reply->owner != XCB_NONE) { // Another compositor already running @@ -1068,7 +1310,7 @@ static int register_cm(session_t *ps) { return 1; } free(reply); - xcb_set_selection_owner(ps->c, ps->reg_win, atom, 0); + xcb_set_selection_owner(ps->c.c, ps->reg_win, atom, 0); } return 0; @@ -1094,84 +1336,12 @@ static inline bool write_pid(session_t *ps) { return true; } -/** - * Update refresh rate info with X Randr extension. - */ -void update_refresh_rate(session_t *ps) { - xcb_randr_get_screen_info_reply_t *randr_info = xcb_randr_get_screen_info_reply( - ps->c, xcb_randr_get_screen_info(ps->c, ps->root), NULL); - - if (!randr_info) - return; - ps->refresh_rate = randr_info->rate; - free(randr_info); - - if (ps->refresh_rate) - ps->refresh_intv = US_PER_SEC / ps->refresh_rate; - else - ps->refresh_intv = 0; -} - -/** - * Initialize refresh-rated based software optimization. - * - * @return true for success, false otherwise - */ -static bool swopti_init(session_t *ps) { - log_warn("--sw-opti is going to be deprecated. If you get real benefits from " - "using " - "this option, please open an issue to let us know."); - // Prepare refresh rate - // Check if user provides one - ps->refresh_rate = ps->o.refresh_rate; - if (ps->refresh_rate) - ps->refresh_intv = US_PER_SEC / ps->refresh_rate; - - // Auto-detect refresh rate otherwise - if (!ps->refresh_rate && ps->randr_exists) { - update_refresh_rate(ps); - } - - // Turn off vsync_sw if we can't get the refresh rate - if (!ps->refresh_rate) - return false; - - return true; -} - -/** - * Modify a struct timeval timeout value to render at a fixed pace. - * - * @param ps current session - * @param[in,out] ptv pointer to the timeout - */ -static double swopti_handle_timeout(session_t *ps) { - if (!ps->refresh_intv) - return 0; - - // Get the microsecond offset of the time when the we reach the timeout - // I don't think a 32-bit long could overflow here. - long offset = (get_time_timeval().tv_usec - ps->paint_tm_offset) % ps->refresh_intv; - // XXX this formula dones't work if refresh rate is not a whole number - if (offset < 0) - offset += ps->refresh_intv; - - // If the target time is sufficiently close to a refresh time, don't add - // an offset, to avoid certain blocking conditions. - if (offset < SWOPTI_TOLERANCE || offset > ps->refresh_intv - SWOPTI_TOLERANCE) - return 0; - - // Add an offset so we wait until the next refresh after timeout - return (double)(ps->refresh_intv - offset) / 1e6; -} - /** * Initialize X composite overlay window. */ static bool init_overlay(session_t *ps) { - xcb_composite_get_overlay_window_reply_t *reply = - xcb_composite_get_overlay_window_reply( - ps->c, xcb_composite_get_overlay_window(ps->c, ps->root), NULL); + xcb_composite_get_overlay_window_reply_t *reply = xcb_composite_get_overlay_window_reply( + ps->c.c, xcb_composite_get_overlay_window(ps->c.c, ps->c.screen_info->root), NULL); if (reply) { ps->overlay = reply->overlay_win; free(reply); @@ -1181,13 +1351,13 @@ static bool init_overlay(session_t *ps) { if (ps->overlay != XCB_NONE) { // Set window region of the overlay window, code stolen from // compiz-0.8.8 - if (!XCB_AWAIT_VOID(xcb_shape_mask, ps->c, XCB_SHAPE_SO_SET, + if (!XCB_AWAIT_VOID(xcb_shape_mask, ps->c.c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, ps->overlay, 0, 0, 0)) { log_fatal("Failed to set the bounding shape of overlay, giving " "up."); return false; } - if (!XCB_AWAIT_VOID(xcb_shape_rectangles, ps->c, XCB_SHAPE_SO_SET, + if (!XCB_AWAIT_VOID(xcb_shape_rectangles, ps->c.c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, ps->overlay, 0, 0, 0, NULL)) { log_fatal("Failed to set the input shape of overlay, giving up."); @@ -1195,7 +1365,7 @@ static bool init_overlay(session_t *ps) { } // Listen to Expose events on the overlay - xcb_change_window_attributes(ps->c, ps->overlay, XCB_CW_EVENT_MASK, + xcb_change_window_attributes(ps->c.c, ps->overlay, XCB_CW_EVENT_MASK, (const uint32_t[]){XCB_EVENT_MASK_EXPOSURE}); // Retrieve DamageNotify on root window if we are painting on an @@ -1203,7 +1373,7 @@ static bool init_overlay(session_t *ps) { // root_damage = XDamageCreate(ps->dpy, root, XDamageReportNonEmpty); // Unmap the overlay, we will map it when needed in redirect_start - XCB_AWAIT_VOID(xcb_unmap_window, ps->c, ps->overlay); + XCB_AWAIT_VOID(xcb_unmap_window, ps->c.c, ps->overlay); } else { log_error("Cannot get X Composite overlay window. Falling " "back to painting on root window."); @@ -1214,27 +1384,29 @@ static bool init_overlay(session_t *ps) { } static bool init_debug_window(session_t *ps) { - xcb_colormap_t colormap = x_new_id(ps->c); - ps->debug_window = x_new_id(ps->c); + xcb_colormap_t colormap = x_new_id(&ps->c); + ps->debug_window = x_new_id(&ps->c); auto err = xcb_request_check( - ps->c, xcb_create_colormap_checked(ps->c, XCB_COLORMAP_ALLOC_NONE, colormap, - ps->root, ps->vis)); + ps->c.c, xcb_create_colormap_checked(ps->c.c, XCB_COLORMAP_ALLOC_NONE, + colormap, ps->c.screen_info->root, + ps->c.screen_info->root_visual)); if (err) { goto err_out; } err = xcb_request_check( - ps->c, xcb_create_window_checked(ps->c, (uint8_t)ps->depth, ps->debug_window, - ps->root, 0, 0, to_u16_checked(ps->root_width), - to_u16_checked(ps->root_height), 0, - XCB_WINDOW_CLASS_INPUT_OUTPUT, ps->vis, - XCB_CW_COLORMAP, (uint32_t[]){colormap, 0})); + ps->c.c, xcb_create_window_checked( + ps->c.c, (uint8_t)ps->c.screen_info->root_depth, + ps->debug_window, ps->c.screen_info->root, 0, 0, + to_u16_checked(ps->root_width), to_u16_checked(ps->root_height), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, ps->c.screen_info->root_visual, + XCB_CW_COLORMAP, (uint32_t[]){colormap, 0})); if (err) { goto err_out; } - err = xcb_request_check(ps->c, xcb_map_window(ps->c, ps->debug_window)); + err = xcb_request_check(ps->c.c, xcb_map_window_checked(ps->c.c, ps->debug_window)); if (err) { goto err_out; } @@ -1249,17 +1421,23 @@ xcb_window_t session_get_target_window(session_t *ps) { if (ps->o.debug_mode) { return ps->debug_window; } - return ps->overlay != XCB_NONE ? ps->overlay : ps->root; + return ps->overlay != XCB_NONE ? ps->overlay : ps->c.screen_info->root; } +#ifdef CONFIG_DBUS +struct cdbus_data *session_get_cdbus(struct session *ps) { + return ps->dbus_data; +} +#endif + uint8_t session_redirection_mode(session_t *ps) { if (ps->o.debug_mode) { // If the backend is not rendering to the screen, we don't need to // take over the screen. - assert(ps->o.experimental_backends); + assert(!ps->o.legacy_backends); return XCB_COMPOSITE_REDIRECT_AUTOMATIC; } - if (ps->o.experimental_backends && !backend_list[ps->o.backend]->present) { + if (!ps->o.legacy_backends && !backend_list[ps->o.backend]->present) { // if the backend doesn't render anything, we don't need to take over the // screen. return XCB_COMPOSITE_REDIRECT_AUTOMATIC; @@ -1279,46 +1457,84 @@ static bool redirect_start(session_t *ps) { // Map overlay window. Done firstly according to this: // https://bugzilla.gnome.org/show_bug.cgi?id=597014 if (ps->overlay != XCB_NONE) { - xcb_map_window(ps->c, ps->overlay); + xcb_map_window(ps->c.c, ps->overlay); } - bool success = XCB_AWAIT_VOID(xcb_composite_redirect_subwindows, ps->c, ps->root, - session_redirection_mode(ps)); + bool success = XCB_AWAIT_VOID(xcb_composite_redirect_subwindows, ps->c.c, + ps->c.screen_info->root, session_redirection_mode(ps)); if (!success) { log_fatal("Another composite manager is already running " "(and does not handle _NET_WM_CM_Sn correctly)"); return false; } - x_sync(ps->c); + xcb_aux_sync(ps->c.c); if (!initialize_backend(ps)) { return false; } - if (ps->o.experimental_backends) { + if (!ps->o.legacy_backends) { assert(ps->backend_data); - ps->ndamage = ps->backend_data->ops->max_buffer_age; + ps->damage_ring.count = ps->backend_data->ops->max_buffer_age; + ps->layout_manager = + layout_manager_new((unsigned)ps->backend_data->ops->max_buffer_age); } else { - ps->ndamage = maximum_buffer_age(ps); + ps->damage_ring.count = maximum_buffer_age(ps); } - ps->damage_ring = ccalloc(ps->ndamage, region_t); - ps->damage = ps->damage_ring + ps->ndamage - 1; + ps->damage_ring.damages = ccalloc(ps->damage_ring.count, region_t); + ps->damage_ring.cursor = ps->damage_ring.count - 1; - for (int i = 0; i < ps->ndamage; i++) { - pixman_region32_init(&ps->damage_ring[i]); + for (int i = 0; i < ps->damage_ring.count; i++) { + pixman_region32_init(&ps->damage_ring.damages[i]); + } + + ps->frame_pacing = ps->o.frame_pacing && ps->o.vsync; + if ((ps->o.legacy_backends || ps->o.benchmark || !ps->backend_data->ops->last_render_time) && + ps->frame_pacing) { + // Disable frame pacing if we are using a legacy backend or if we are in + // benchmark mode, or if the backend doesn't report render time + log_info("Disabling frame pacing."); + ps->frame_pacing = false; + } + + // Re-detect driver since we now have a backend + ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root); + apply_driver_workarounds(ps, ps->drivers); + + if (ps->present_exists && ps->frame_pacing) { + // Initialize rendering and frame timing statistics, and frame pacing + // states. + ps->last_msc_instant = 0; + ps->last_msc = 0; + ps->last_schedule_delay = 0; + render_statistics_reset(&ps->render_stats); + enum vblank_scheduler_type scheduler_type = + choose_vblank_scheduler(ps->drivers); + if (global_debug_options.force_vblank_scheduler != LAST_VBLANK_SCHEDULER) { + scheduler_type = + (enum vblank_scheduler_type)global_debug_options.force_vblank_scheduler; + } + log_info("Using vblank scheduler: %s.", vblank_scheduler_str[scheduler_type]); + ps->vblank_scheduler = + vblank_scheduler_new(ps->loop, &ps->c, session_get_target_window(ps), + scheduler_type, ps->o.use_realtime_scheduling); + if (!ps->vblank_scheduler) { + return false; + } + vblank_scheduler_schedule(ps->vblank_scheduler, + collect_vblank_interval_statistics, ps); + } else if (ps->frame_pacing) { + log_error("Present extension is not supported, frame pacing disabled."); + ps->frame_pacing = false; } // Must call XSync() here - x_sync(ps->c); + xcb_aux_sync(ps->c.c); ps->redirected = true; ps->first_frame = true; - // Re-detect driver since we now have a backend - ps->drivers = detect_driver(ps->c, ps->backend_data, ps->root); - apply_driver_workarounds(ps, ps->drivers); - root_damaged(ps); // Repaint the whole screen @@ -1336,43 +1552,74 @@ static void unredirect(session_t *ps) { destroy_backend(ps); - xcb_composite_unredirect_subwindows(ps->c, ps->root, session_redirection_mode(ps)); + xcb_composite_unredirect_subwindows(ps->c.c, ps->c.screen_info->root, + session_redirection_mode(ps)); // Unmap overlay window if (ps->overlay != XCB_NONE) { - xcb_unmap_window(ps->c, ps->overlay); + xcb_unmap_window(ps->c.c, ps->overlay); } // Free the damage ring - for (int i = 0; i < ps->ndamage; ++i) { - pixman_region32_fini(&ps->damage_ring[i]); + for (int i = 0; i < ps->damage_ring.count; ++i) { + pixman_region32_fini(&ps->damage_ring.damages[i]); + } + ps->damage_ring.count = 0; + free(ps->damage_ring.damages); + ps->damage_ring.cursor = 0; + ps->damage_ring.damages = NULL; + if (ps->layout_manager) { + layout_manager_free(ps->layout_manager); + ps->layout_manager = NULL; + } + + if (ps->vblank_scheduler) { + vblank_scheduler_free(ps->vblank_scheduler); + ps->vblank_scheduler = NULL; } - ps->ndamage = 0; - free(ps->damage_ring); - ps->damage_ring = ps->damage = NULL; // Must call XSync() here - x_sync(ps->c); + xcb_aux_sync(ps->c.c); ps->redirected = false; log_debug("Screen unredirected."); } -// Handle queued events before we go to sleep +/// Handle queued events before we go to sleep. +/// +/// This function is called by ev_prepare watcher, which is called just before +/// the event loop goes to sleep. X damage events are incremental, which means +/// if we don't handle the ones X server already sent us, we won't get new ones. +/// And if we don't get new ones, we won't render, i.e. we would freeze. libxcb +/// keeps an internal queue of events, so we have to be 100% sure no events are +/// left in that queue before we go to sleep. static void handle_queued_x_events(EV_P attr_unused, ev_prepare *w, int revents attr_unused) { session_t *ps = session_ptr(w, event_check); + // Flush because if we go into sleep when there is still requests in the + // outgoing buffer, they will not be sent for an indefinite amount of + // time. Use XFlush here too, we might still use some Xlib functions + // because OpenGL. + // + // Also note, after we have flushed here, we won't flush again in this + // function before going into sleep. This is because `xcb_flush`/`XFlush` + // may _read_ more events from the server (yes, this is ridiculous, I + // know). And we can't have that, see the comments above this function. + // + // This means if functions called ev_handle need to send some events, + // they need to carefully make sure those events are flushed, one way or + // another. + XFlush(ps->c.dpy); + xcb_flush(ps->c.c); + + if (ps->vblank_scheduler) { + vblank_handle_x_events(ps->vblank_scheduler); + } + xcb_generic_event_t *ev; - while ((ev = xcb_poll_for_queued_event(ps->c))) { + while ((ev = xcb_poll_for_queued_event(ps->c.c))) { ev_handle(ps, ev); free(ev); }; - // Flush because if we go into sleep when there is still - // requests in the outgoing buffer, they will not be sent - // for an indefinite amount of time. - // Use XFlush here too, we might still use some Xlib functions - // because OpenGL. - XFlush(ps->dpy); - xcb_flush(ps->c); - int err = xcb_connection_has_error(ps->c); + int err = xcb_connection_has_error(ps->c.c); if (err) { log_fatal("X11 server connection broke (error %d)", err); exit(1); @@ -1380,12 +1627,16 @@ static void handle_queued_x_events(EV_P attr_unused, ev_prepare *w, int revents } static void handle_new_windows(session_t *ps) { - list_foreach_safe(struct win, w, &ps->window_stack, stack_neighbour) { + list_foreach_safe(struct win, w, wm_stack_end(ps->wm), stack_neighbour) { if (w->is_new) { - auto new_w = fill_win(ps, w); - if (!new_w->managed) { + auto new_w = maybe_allocate_managed_win(ps, w); + if (new_w == w) { continue; } + + assert(new_w->managed); + wm_stack_replace(ps->wm, w, new_w); + auto mw = (struct managed_win *)new_w; if (mw->a.map_state == XCB_MAP_STATE_VIEWABLE) { win_set_flags(mw, WIN_FLAGS_MAPPED); @@ -1395,18 +1646,22 @@ static void handle_new_windows(session_t *ps) { // us to find out. So just blindly mark it damaged mw->ever_damaged = true; } + // Send D-Bus signal + if (ps->o.dbus) { + cdbus_ev_win_added(session_get_cdbus(ps), new_w); + } } } } static void refresh_windows(session_t *ps) { - win_stack_foreach_managed(w, &ps->window_stack) { + win_stack_foreach_managed(w, wm_stack_end(ps->wm)) { win_process_update_flags(ps, w); } } static void refresh_images(session_t *ps) { - win_stack_foreach_managed(w, &ps->window_stack) { + win_stack_foreach_managed(w, wm_stack_end(ps->wm)) { win_process_image_flags(ps, w); } } @@ -1421,6 +1676,8 @@ static void tmout_unredir_callback(EV_P attr_unused, ev_timer *w, int revents at } static void fade_timer_callback(EV_P attr_unused, ev_timer *w, int revents attr_unused) { + // TODO(yshui): do we still need the fade timer? we queue redraw automatically in + // draw_callback_impl if animation is running. session_t *ps = session_ptr(w, fade_timer); queue_redraw(ps); } @@ -1428,9 +1685,10 @@ static void fade_timer_callback(EV_P attr_unused, ev_timer *w, int revents attr_ static void handle_pending_updates(EV_P_ struct session *ps) { if (ps->pending_updates) { log_debug("Delayed handling of events, entering critical section"); - auto e = xcb_request_check(ps->c, xcb_grab_server_checked(ps->c)); + auto e = xcb_request_check(ps->c.c, xcb_grab_server_checked(ps->c.c)); if (e) { log_fatal_x_error(e, "failed to grab x server"); + free(e); return quit(ps); } @@ -1439,7 +1697,7 @@ static void handle_pending_updates(EV_P_ struct session *ps) { // Catching up with X server handle_queued_x_events(EV_A_ & ps->event_check, 0); - // Call fill_win on new windows + // Process new windows, and maybe allocate struct managed_win for them handle_new_windows(ps); // Handle screen changes @@ -1448,24 +1706,18 @@ static void handle_pending_updates(EV_P_ struct session *ps) { // stale. handle_root_flags(ps); - // Process window flags (window mapping) + // Process window flags refresh_windows(ps); - { - auto r = xcb_get_input_focus_reply( - ps->c, xcb_get_input_focus(ps->c), NULL); - if (!ps->active_win || (r && r->focus != ps->active_win->base.id)) { - recheck_focus(ps); - } - free(r); - } + recheck_focus(ps); // Process window flags (stale images) refresh_images(ps); - e = xcb_request_check(ps->c, xcb_ungrab_server_checked(ps->c)); + e = xcb_request_check(ps->c.c, xcb_ungrab_server_checked(ps->c.c)); if (e) { log_fatal_x_error(e, "failed to ungrab x server"); + free(e); return quit(ps); } @@ -1476,8 +1728,27 @@ static void handle_pending_updates(EV_P_ struct session *ps) { } static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { + assert(!ps->backend_busy); + assert(ps->render_queued); + + struct timespec now; + int64_t draw_callback_enter_us; + clock_gettime(CLOCK_MONOTONIC, &now); + + draw_callback_enter_us = (now.tv_sec * 1000000LL + now.tv_nsec / 1000); + if (ps->next_render != 0) { + log_trace("Schedule delay: %" PRIi64 " us", + draw_callback_enter_us - (int64_t)ps->next_render); + } + handle_pending_updates(EV_A_ ps); + int64_t after_handle_pending_updates_us; + clock_gettime(CLOCK_MONOTONIC, &now); + after_handle_pending_updates_us = (now.tv_sec * 1000000LL + now.tv_nsec / 1000); + log_trace("handle_pending_updates took: %" PRIi64 " us", + after_handle_pending_updates_us - draw_callback_enter_us); + if (ps->first_frame) { // If we are still rendering the first frame, if some of the windows are // unmapped/destroyed during the above handle_pending_updates() call, they @@ -1487,14 +1758,17 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { // // Using foreach_safe here since skipping fading can cause window to be // freed if it's destroyed. - win_stack_foreach_managed_safe(w, &ps->window_stack) { - auto _ attr_unused = win_skip_fading(ps, w); + win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { + win_skip_fading(w); + if (w->state == WSTATE_DESTROYED) { + destroy_win_finish(ps, &w->base); + } } } if (ps->o.benchmark) { if (ps->o.benchmark_wid) { - auto w = find_managed_win(ps, ps->o.benchmark_wid); + auto w = wm_find_managed(ps->wm, ps->o.benchmark_wid); if (!w) { log_fatal("Couldn't find specified benchmark window."); exit(1); @@ -1509,8 +1783,14 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { * screen is not redirected. its sole purpose should be to decide whether the * screen should be redirected. */ bool fade_running = false; + bool animation = false; bool was_redirected = ps->redirected; - auto bottom = paint_preprocess(ps, &fade_running); + struct managed_win *bottom = NULL; + if (!paint_preprocess(ps, &fade_running, &animation, &bottom)) { + log_fatal("Pre-render preparation has failed, exiting..."); + exit(1); + } + ps->tmout_unredir_hit = false; if (!was_redirected && ps->redirected) { @@ -1533,17 +1813,57 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { ev_timer_start(EV_A_ & ps->fade_timer); } - // If the screen is unredirected, free all_damage to stop painting + int64_t after_preprocess_us; + clock_gettime(CLOCK_MONOTONIC, &now); + after_preprocess_us = (now.tv_sec * 1000000LL + now.tv_nsec / 1000); + log_trace("paint_preprocess took: %" PRIi64 " us", + after_preprocess_us - after_handle_pending_updates_us); + + // If the screen is unredirected, we don't render anything. + bool did_render = false; if (ps->redirected && ps->o.stoppaint_force != ON) { static int paint = 0; - log_trace("Render start, frame %d", paint); - if (ps->o.experimental_backends) { - paint_all_new(ps, bottom, false); + log_verbose("Render start, frame %d", paint); + if (!ps->o.legacy_backends) { + uint64_t after_damage_us = 0; + now = get_time_timespec(); + auto render_start_us = + (uint64_t)now.tv_sec * 1000000UL + (uint64_t)now.tv_nsec / 1000; + layout_manager_append_layout( + ps->layout_manager, ps->wm, ps->root_image_generation, + (ivec2){.width = ps->root_width, .height = ps->root_height}); + bool succeeded = renderer_render( + ps->renderer, ps->backend_data, ps->root_image, + ps->layout_manager, ps->command_builder, + ps->backend_blur_context, render_start_us, ps->sync_fence, + ps->o.use_damage, ps->o.monitor_repaint, ps->o.force_win_blend, + ps->o.blur_background_frame, ps->o.inactive_dim_fixed, + ps->o.max_brightness, ps->o.inactive_dim, + ps->o.crop_shadow_to_monitor ? &ps->monitors : NULL, + ps->o.wintype_option, &after_damage_us); + if (!succeeded) { + log_fatal("Render failure"); + abort(); + } + did_render = true; + if (ps->next_render > 0) { + log_verbose( + "Render schedule deviation: %ld us (%s) %" PRIu64 + " %" PRIu64, + labs((long)after_damage_us - (long)ps->next_render), + after_damage_us < ps->next_render ? "early" : "late", + after_damage_us, ps->next_render); + ps->last_schedule_delay = 0; + if (after_damage_us > ps->next_render) { + ps->last_schedule_delay = + after_damage_us - ps->next_render; + } + } } else { - paint_all(ps, bottom, false); + paint_all(ps, bottom); } - log_trace("Render end"); + log_verbose("Render end"); ps->first_frame = false; paint++; @@ -1552,71 +1872,58 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { } } + // With frame pacing, we set backend_busy to true after the end of + // vblank. Without frame pacing, we won't be receiving vblank events, so + // we set backend_busy to false here, right after we issue the render + // commands. + // The other case is if we decided there is no change to render, in that + // case no render command is issued, so we also set backend_busy to + // false. + ps->backend_busy = (ps->frame_pacing && did_render); + ps->next_render = 0; + if (!fade_running) { ps->fade_time = 0L; } + ps->render_queued = false; + // TODO(yshui) Investigate how big the X critical section needs to be. There are // suggestions that rendering should be in the critical section as well. - ps->redraw_needed = false; -} - -static void draw_callback(EV_P_ ev_idle *w, int revents) { - // This function is not used if we are using --swopti - session_t *ps = session_ptr(w, draw_idle); - - draw_callback_impl(EV_A_ ps, revents); - - // Don't do painting non-stop unless we are in benchmark mode - if (!ps->o.benchmark) { - ev_idle_stop(EV_A_ & ps->draw_idle); + // Queue redraw if animation is running. This should be picked up by next present + // event. + if (animation) { + queue_redraw(ps); + } + if (ps->vblank_scheduler) { + // Even if we might not want to render during next vblank, we want to keep + // `backend_busy` up to date, so when the next render comes, we can + // immediately know if we can render. + vblank_scheduler_schedule(ps->vblank_scheduler, check_render_finish, ps); } } -static void delayed_draw_timer_callback(EV_P_ ev_timer *w, int revents) { - session_t *ps = session_ptr(w, delayed_draw_timer); - draw_callback_impl(EV_A_ ps, revents); - - // We might have stopped the ev_idle in delayed_draw_callback, - // so we restart it if we are in benchmark mode - if (ps->o.benchmark) - ev_idle_start(EV_A_ & ps->draw_idle); -} +static void draw_callback(EV_P_ ev_timer *w, int revents) { + session_t *ps = session_ptr(w, draw_timer); -static void delayed_draw_callback(EV_P_ ev_idle *w, int revents) { - // This function is only used if we are using --swopti - session_t *ps = session_ptr(w, draw_idle); - assert(ps->redraw_needed); - assert(!ev_is_active(&ps->delayed_draw_timer)); + // The draw timer has to be stopped before calling the draw_callback_impl + // function because it may be set and started there, e.g. when a custom + // animated shader is used. + ev_timer_stop(EV_A_ w); + draw_callback_impl(EV_A_ ps, revents); - double delay = swopti_handle_timeout(ps); - if (delay < 1e-6) { - if (!ps->o.benchmark) { - ev_idle_stop(EV_A_ & ps->draw_idle); - } - return draw_callback_impl(EV_A_ ps, revents); + // Immediately start next frame if we are in benchmark mode. + if (ps->o.benchmark) { + ps->render_queued = true; + ev_timer_set(w, 0, 0); + ev_timer_start(EV_A_ w); } - - // This is a little bit hacky. When we get to this point in code, we need - // to update the screen , but we will only be updating after a delay, So - // we want to stop the ev_idle, so this callback doesn't get call repeatedly - // during the delay, we also want queue_redraw to not restart the ev_idle. - // So we stop ev_idle and leave ps->redraw_needed to be true. (effectively, - // ps->redraw_needed means if redraw is needed or if draw is in progress). - // - // We do this anyway even if we are in benchmark mode. That means we will - // have to restart draw_idle after the draw actually happened when we are in - // benchmark mode. - ev_idle_stop(EV_A_ & ps->draw_idle); - - ev_timer_set(&ps->delayed_draw_timer, delay, 0); - ev_timer_start(EV_A_ & ps->delayed_draw_timer); } static void x_event_callback(EV_P attr_unused, ev_io *w, int revents attr_unused) { session_t *ps = (session_t *)w; - xcb_generic_event_t *ev = xcb_poll_for_event(ps->c); + xcb_generic_event_t *ev = xcb_poll_for_event(ps->c.c); if (ev) { ev_handle(ps, ev); free(ev); @@ -1626,7 +1933,7 @@ static void x_event_callback(EV_P attr_unused, ev_io *w, int revents attr_unused /** * Turn on the program reset flag. * - * This will result in the compostior resetting itself after next paint. + * This will result in the compositor resetting itself after next paint. */ static void reset_enable(EV_P_ ev_signal *w attr_unused, int revents attr_unused) { log_info("picom is resetting..."); @@ -1644,26 +1951,76 @@ static void config_file_change_cb(void *_ps) { reset_enable(ps->loop, NULL, 0); } +static bool load_shader_source(session_t *ps, const char *path) { + if (!path) { + // Using the default shader. + return false; + } + + log_info("Loading shader source from %s", path); + + struct shader_info *shader = NULL; + HASH_FIND_STR(ps->shaders, path, shader); + if (shader) { + log_debug("Shader already loaded, reusing"); + return false; + } + + shader = ccalloc(1, struct shader_info); + shader->key = strdup(path); + HASH_ADD_KEYPTR(hh, ps->shaders, shader->key, strlen(shader->key), shader); + + FILE *f = fopen(path, "r"); + if (!f) { + log_error("Failed to open custom shader file: %s", path); + goto err; + } + struct stat statbuf; + if (fstat(fileno(f), &statbuf) < 0) { + log_error("Failed to access custom shader file: %s", path); + goto err; + } + + auto num_bytes = (size_t)statbuf.st_size; + shader->source = ccalloc(num_bytes + 1, char); + auto read_bytes = fread(shader->source, sizeof(char), num_bytes, f); + if (read_bytes < num_bytes || ferror(f)) { + // This is a difficult to hit error case, review thoroughly. + log_error("Failed to read custom shader at %s. (read %lu bytes, expected " + "%lu bytes)", + path, read_bytes, num_bytes); + goto err; + } + return false; +err: + HASH_DEL(ps->shaders, shader); + if (f) { + fclose(f); + } + free(shader->source); + free(shader->key); + free(shader); + return true; +} + +static bool load_shader_source_for_condition(const c2_lptr_t *cond, void *data) { + return load_shader_source(data, c2_list_get_data(cond)); +} + /** * Initialize a session. * - * @param argc number of commandline arguments - * @param argv commandline arguments + * @param argc number of command line arguments + * @param argv command line arguments * @param dpy the X Display * @param config_file the path to the config file - * @param all_xerros whether we should report all X errors + * @param all_xerrors whether we should report all X errors * @param fork whether we will fork after initialization */ static session_t *session_init(int argc, char **argv, Display *dpy, const char *config_file, bool all_xerrors, bool fork) { static const session_t s_def = { .backend_data = NULL, - .dpy = NULL, - .scr = 0, - .c = NULL, - .vis = 0, - .depth = 0, - .root = XCB_NONE, .root_height = 0, .root_width = 0, // .root_damage = XCB_NONE, @@ -1679,26 +2036,18 @@ static session_t *session_init(int argc, char **argv, Display *dpy, .redirected = false, .alpha_picts = NULL, .fade_time = 0L, - .ignore_head = NULL, - .ignore_tail = NULL, .quit = false, .expose_rects = NULL, .size_expose = 0, .n_expose = 0, - .windows = NULL, - .active_win = NULL, - .active_leader = XCB_NONE, - .black_picture = XCB_NONE, .cshadow_picture = XCB_NONE, .white_picture = XCB_NONE, - .gaussian_map = NULL, + .shadow_context = NULL, - .refresh_rate = 0, - .refresh_intv = 0UL, - .paint_tm_offset = 0L, + .last_msc = 0, #ifdef CONFIG_VSYNC_DRM .drm_fd = -1, @@ -1724,9 +2073,6 @@ static session_t *session_init(int argc, char **argv, Display *dpy, .glx_error = 0, .xrfilter_convolution_exists = false, - .atoms_wintypes = {0}, - .track_atom_lst = NULL, - #ifdef CONFIG_DBUS .dbus_data = NULL, #endif @@ -1742,55 +2088,48 @@ static session_t *session_init(int argc, char **argv, Display *dpy, // Allocate a session and copy default values into it session_t *ps = cmalloc(session_t); *ps = s_def; - list_init_head(&ps->window_stack); ps->loop = EV_DEFAULT; pixman_region32_init(&ps->screen_reg); - ps->ignore_tail = &ps->ignore_head; + // TODO(yshui) investigate what's the best window size + render_statistics_init(&ps->render_stats, 128); ps->o.show_all_xerrors = all_xerrors; // Use the same Display across reset, primarily for resource leak checking - ps->dpy = dpy; - ps->c = XGetXCBConnection(ps->dpy); + x_connection_init(&ps->c, dpy); + // We store width/height from screen_info instead using them directly because they + // can change, see configure_root(). + ps->root_width = ps->c.screen_info->width_in_pixels; + ps->root_height = ps->c.screen_info->height_in_pixels; const xcb_query_extension_reply_t *ext_info; - ps->previous_xerror_handler = XSetErrorHandler(xerror); - - ps->scr = DefaultScreen(ps->dpy); - - auto screen = x_screen_of_display(ps->c, ps->scr); - ps->vis = screen->root_visual; - ps->depth = screen->root_depth; - ps->root = screen->root; - ps->root_width = screen->width_in_pixels; - ps->root_height = screen->height_in_pixels; - // Start listening to events on root earlier to catch all possible // root geometry changes auto e = xcb_request_check( - ps->c, xcb_change_window_attributes_checked( - ps->c, ps->root, XCB_CW_EVENT_MASK, - (const uint32_t[]){XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | - XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | - XCB_EVENT_MASK_PROPERTY_CHANGE})); + ps->c.c, xcb_change_window_attributes_checked( + ps->c.c, ps->c.screen_info->root, XCB_CW_EVENT_MASK, + (const uint32_t[]){XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | + XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | + XCB_EVENT_MASK_PROPERTY_CHANGE})); if (e) { log_error_x_error(e, "Failed to setup root window event mask"); + free(e); } - xcb_prefetch_extension_data(ps->c, &xcb_render_id); - xcb_prefetch_extension_data(ps->c, &xcb_composite_id); - xcb_prefetch_extension_data(ps->c, &xcb_damage_id); - xcb_prefetch_extension_data(ps->c, &xcb_shape_id); - xcb_prefetch_extension_data(ps->c, &xcb_xfixes_id); - xcb_prefetch_extension_data(ps->c, &xcb_randr_id); - xcb_prefetch_extension_data(ps->c, &xcb_xinerama_id); - xcb_prefetch_extension_data(ps->c, &xcb_present_id); - xcb_prefetch_extension_data(ps->c, &xcb_sync_id); - xcb_prefetch_extension_data(ps->c, &xcb_glx_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_render_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_composite_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_damage_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_shape_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_xfixes_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_randr_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_present_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_sync_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_glx_id); + xcb_prefetch_extension_data(ps->c.c, &xcb_dpms_id); - ext_info = xcb_get_extension_data(ps->c, &xcb_render_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_render_id); if (!ext_info || !ext_info->present) { log_fatal("No render extension"); exit(1); @@ -1798,7 +2137,7 @@ static session_t *session_init(int argc, char **argv, Display *dpy, ps->render_event = ext_info->first_event; ps->render_error = ext_info->first_error; - ext_info = xcb_get_extension_data(ps->c, &xcb_composite_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_composite_id); if (!ext_info || !ext_info->present) { log_fatal("No composite extension"); exit(1); @@ -1809,8 +2148,8 @@ static session_t *session_init(int argc, char **argv, Display *dpy, { xcb_composite_query_version_reply_t *reply = xcb_composite_query_version_reply( - ps->c, - xcb_composite_query_version(ps->c, XCB_COMPOSITE_MAJOR_VERSION, + ps->c.c, + xcb_composite_query_version(ps->c.c, XCB_COMPOSITE_MAJOR_VERSION, XCB_COMPOSITE_MINOR_VERSION), NULL); @@ -1822,59 +2161,67 @@ static session_t *session_init(int argc, char **argv, Display *dpy, free(reply); } - ext_info = xcb_get_extension_data(ps->c, &xcb_damage_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_damage_id); if (!ext_info || !ext_info->present) { log_fatal("No damage extension"); exit(1); } ps->damage_event = ext_info->first_event; ps->damage_error = ext_info->first_error; - xcb_discard_reply(ps->c, xcb_damage_query_version(ps->c, XCB_DAMAGE_MAJOR_VERSION, - XCB_DAMAGE_MINOR_VERSION) - .sequence); + xcb_discard_reply(ps->c.c, xcb_damage_query_version(ps->c.c, XCB_DAMAGE_MAJOR_VERSION, + XCB_DAMAGE_MINOR_VERSION) + .sequence); - ext_info = xcb_get_extension_data(ps->c, &xcb_xfixes_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_xfixes_id); if (!ext_info || !ext_info->present) { log_fatal("No XFixes extension"); exit(1); } ps->xfixes_event = ext_info->first_event; ps->xfixes_error = ext_info->first_error; - xcb_discard_reply(ps->c, xcb_xfixes_query_version(ps->c, XCB_XFIXES_MAJOR_VERSION, - XCB_XFIXES_MINOR_VERSION) - .sequence); + xcb_discard_reply(ps->c.c, xcb_xfixes_query_version(ps->c.c, XCB_XFIXES_MAJOR_VERSION, + XCB_XFIXES_MINOR_VERSION) + .sequence); - ps->damaged_region = x_new_id(ps->c); - if (!XCB_AWAIT_VOID(xcb_xfixes_create_region, ps->c, ps->damaged_region, 0, NULL)) { + ps->damage_ring.x_region = x_new_id(&ps->c); + if (!XCB_AWAIT_VOID(xcb_xfixes_create_region, ps->c.c, ps->damage_ring.x_region, + 0, NULL)) { log_fatal("Failed to create a XFixes region"); goto err; } - ext_info = xcb_get_extension_data(ps->c, &xcb_glx_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_glx_id); if (ext_info && ext_info->present) { ps->glx_exists = true; ps->glx_error = ext_info->first_error; ps->glx_event = ext_info->first_event; } + ext_info = xcb_get_extension_data(ps->c.c, &xcb_dpms_id); + ps->dpms_exists = ext_info && ext_info->present; + if (!ps->dpms_exists) { + log_warn("No DPMS extension"); + } + // Parse configuration file - win_option_mask_t winopt_mask[NUM_WINTYPES] = {{0}}; - bool shadow_enabled = false, fading_enable = false, hasneg = false; char *config_file_to_free = NULL; - config_file = config_file_to_free = parse_config( - &ps->o, config_file, &shadow_enabled, &fading_enable, &hasneg, winopt_mask); + config_file = config_file_to_free = parse_config(&ps->o, config_file); if (IS_ERR(config_file_to_free)) { return NULL; } // Parse all of the rest command line options - if (!get_cfg(&ps->o, argc, argv, shadow_enabled, fading_enable, hasneg, winopt_mask)) { + if (!get_cfg(&ps->o, argc, argv)) { log_fatal("Failed to get configuration, usually mean you have specified " "invalid options."); return NULL; } + if (ps->o.window_shader_fg) { + log_debug("Default window shader: \"%s\"", ps->o.window_shader_fg); + } + if (ps->o.logpath) { auto l = file_logger_new(ps->o.logpath); if (l) { @@ -1896,66 +2243,54 @@ static session_t *session_init(int argc, char **argv, Display *dpy, "binary will not be installed in the future."); } - ps->atoms = init_atoms(ps->c); - ps->atoms_wintypes[WINTYPE_UNKNOWN] = 0; -#define SET_WM_TYPE_ATOM(x) \ - ps->atoms_wintypes[WINTYPE_##x] = ps->atoms->a_NET_WM_WINDOW_TYPE_##x - SET_WM_TYPE_ATOM(DESKTOP); - SET_WM_TYPE_ATOM(DOCK); - SET_WM_TYPE_ATOM(TOOLBAR); - SET_WM_TYPE_ATOM(MENU); - SET_WM_TYPE_ATOM(UTILITY); - SET_WM_TYPE_ATOM(SPLASH); - SET_WM_TYPE_ATOM(DIALOG); - SET_WM_TYPE_ATOM(NORMAL); - SET_WM_TYPE_ATOM(DROPDOWN_MENU); - SET_WM_TYPE_ATOM(POPUP_MENU); - SET_WM_TYPE_ATOM(TOOLTIP); - SET_WM_TYPE_ATOM(NOTIFICATION); - SET_WM_TYPE_ATOM(COMBO); - SET_WM_TYPE_ATOM(DND); -#undef SET_WM_TYPE_ATOM + ps->atoms = init_atoms(ps->c.c); + ps->c2_state = c2_state_new(ps->atoms); // Get needed atoms for c2 condition lists - if (!(c2_list_postprocess(ps, ps->o.unredir_if_possible_blacklist) && - c2_list_postprocess(ps, ps->o.paint_blacklist) && - c2_list_postprocess(ps, ps->o.shadow_blacklist) && - c2_list_postprocess(ps, ps->o.shadow_clip_list) && - c2_list_postprocess(ps, ps->o.fade_blacklist) && - c2_list_postprocess(ps, ps->o.blur_background_blacklist) && - c2_list_postprocess(ps, ps->o.invert_color_list) && - c2_list_postprocess(ps, ps->o.opacity_rules) && - c2_list_postprocess(ps, ps->o.rounded_corners_blacklist) && - c2_list_postprocess(ps, ps->o.focus_blacklist))) { - log_error("Post-processing of conditionals failed, some of your rules " - "might not work"); + options_postprocess_c2_lists(ps->c2_state, &ps->c, &ps->o); + + // Load shader source file specified in the shader rules + if (c2_list_foreach(ps->o.window_shader_fg_rules, load_shader_source_for_condition, ps)) { + log_error("Failed to load shader source file for some of the window " + "shader rules"); + } + if (load_shader_source(ps, ps->o.window_shader_fg)) { + log_error("Failed to load window shader source file"); } - ps->gaussian_map = gaussian_kernel_autodetect_deviation(ps->o.shadow_radius); - sum_kernel_preprocess(ps->gaussian_map); + if (log_get_level_tls() <= LOG_LEVEL_DEBUG) { + HASH_ITER2(ps->shaders, shader) { + log_debug("Shader %s:", shader->key); + log_debug("%s", shader->source); + } + } - rebuild_shadow_exclude_reg(ps); + if (ps->o.legacy_backends) { + ps->shadow_context = + (void *)gaussian_kernel_autodetect_deviation(ps->o.shadow_radius); + sum_kernel_preprocess((conv *)ps->shadow_context); + } // Query X Shape - ext_info = xcb_get_extension_data(ps->c, &xcb_shape_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_shape_id); if (ext_info && ext_info->present) { ps->shape_event = ext_info->first_event; ps->shape_error = ext_info->first_error; ps->shape_exists = true; } - ext_info = xcb_get_extension_data(ps->c, &xcb_randr_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_randr_id); if (ext_info && ext_info->present) { ps->randr_exists = true; ps->randr_event = ext_info->first_event; ps->randr_error = ext_info->first_error; } - ext_info = xcb_get_extension_data(ps->c, &xcb_present_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_present_id); if (ext_info && ext_info->present) { auto r = xcb_present_query_version_reply( - ps->c, - xcb_present_query_version(ps->c, XCB_PRESENT_MAJOR_VERSION, + ps->c.c, + xcb_present_query_version(ps->c.c, XCB_PRESENT_MAJOR_VERSION, XCB_PRESENT_MINOR_VERSION), NULL); if (r) { @@ -1965,14 +2300,14 @@ static session_t *session_init(int argc, char **argv, Display *dpy, } // Query X Sync - ext_info = xcb_get_extension_data(ps->c, &xcb_sync_id); + ext_info = xcb_get_extension_data(ps->c.c, &xcb_sync_id); if (ext_info && ext_info->present) { ps->xsync_error = ext_info->first_error; ps->xsync_event = ext_info->first_event; // Need X Sync 3.1 for fences auto r = xcb_sync_initialize_reply( - ps->c, - xcb_sync_initialize(ps->c, XCB_SYNC_MAJOR_VERSION, XCB_SYNC_MINOR_VERSION), + ps->c.c, + xcb_sync_initialize(ps->c.c, XCB_SYNC_MAJOR_VERSION, XCB_SYNC_MINOR_VERSION), NULL); if (r && (r->major_version > 3 || (r->major_version == 3 && r->minor_version >= 1))) { @@ -1983,9 +2318,10 @@ static session_t *session_init(int argc, char **argv, Display *dpy, ps->sync_fence = XCB_NONE; if (ps->xsync_exists) { - ps->sync_fence = x_new_id(ps->c); + ps->sync_fence = x_new_id(&ps->c); e = xcb_request_check( - ps->c, xcb_sync_create_fence(ps->c, ps->root, ps->sync_fence, 0)); + ps->c.c, xcb_sync_create_fence_checked( + ps->c.c, ps->c.screen_info->root, ps->sync_fence, 0)); if (e) { if (ps->o.xrender_sync_fence) { log_error_x_error(e, "Failed to create a XSync fence. " @@ -2003,19 +2339,10 @@ static session_t *session_init(int argc, char **argv, Display *dpy, } // Query X RandR - if ((ps->o.sw_opti && !ps->o.refresh_rate) || ps->o.xinerama_shadow_crop) { - if (!ps->randr_exists) { - log_fatal("No XRandR extension. sw-opti, refresh-rate or " - "xinerama-shadow-crop " - "cannot be enabled."); - goto err; - } - } - - // Query X Xinerama extension - if (ps->o.xinerama_shadow_crop) { - ext_info = xcb_get_extension_data(ps->c, &xcb_xinerama_id); - ps->xinerama_exists = ext_info && ext_info->present; + if (ps->o.crop_shadow_to_monitor && !ps->randr_exists) { + log_fatal("No X RandR extension. crop-shadow-to-monitor cannot be " + "enabled."); + goto err; } rebuild_screen_reg(ps); @@ -2059,7 +2386,7 @@ static session_t *session_init(int argc, char **argv, Display *dpy, // The old backends doesn't have a automatic redirection mode log_info("The compositor is started in automatic redirection mode."); - assert(ps->o.experimental_backends); + assert(!ps->o.legacy_backends); if (backend_list[ps->o.backend]->present) { // If the backend has `present`, we couldn't be in automatic @@ -2071,11 +2398,11 @@ static session_t *session_init(int argc, char **argv, Display *dpy, } } - ps->drivers = detect_driver(ps->c, ps->backend_data, ps->root); + ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root); apply_driver_workarounds(ps, ps->drivers); // Initialize filters, must be preceded by OpenGL context creation - if (!ps->o.experimental_backends && !init_render(ps)) { + if (ps->o.legacy_backends && !init_render(ps)) { log_fatal("Failed to initialize the backend"); exit(1); } @@ -2093,7 +2420,7 @@ static session_t *session_init(int argc, char **argv, Display *dpy, free(config_file_to_free); - if (bkend_use_glx(ps) && !ps->o.experimental_backends) { + if (bkend_use_glx(ps) && ps->o.legacy_backends) { auto gl_logger = gl_string_marker_logger_new(); if (gl_logger) { log_info("Enabling gl string marker"); @@ -2101,25 +2428,13 @@ static session_t *session_init(int argc, char **argv, Display *dpy, } } - if (ps->o.experimental_backends) { - if (ps->o.monitor_repaint && !backend_list[ps->o.backend]->fill) { - log_warn("--monitor-repaint is not supported by the backend, " - "disabling"); - ps->o.monitor_repaint = false; - } - } - - // Initialize software optimization - if (ps->o.sw_opti) - ps->o.sw_opti = swopti_init(ps); - // Monitor screen changes if vsync_sw is enabled and we are using - // an auto-detected refresh rate, or when Xinerama features are enabled - if (ps->randr_exists && - ((ps->o.sw_opti && !ps->o.refresh_rate) || ps->o.xinerama_shadow_crop)) - xcb_randr_select_input(ps->c, ps->root, XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE); - - cxinerama_upd_scrs(ps); + // an auto-detected refresh rate, or when X RandR features are enabled + if (ps->randr_exists && ps->o.crop_shadow_to_monitor) { + xcb_randr_select_input(ps->c.c, ps->c.screen_info->root, + XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE); + x_update_monitors(&ps->c, &ps->monitors); + } { xcb_render_create_picture_value_list_t pa = { @@ -2127,24 +2442,23 @@ static session_t *session_init(int argc, char **argv, Display *dpy, }; ps->root_picture = x_create_picture_with_visual_and_pixmap( - ps->c, ps->vis, ps->root, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); + &ps->c, ps->c.screen_info->root_visual, ps->c.screen_info->root, + XCB_RENDER_CP_SUBWINDOW_MODE, &pa); if (ps->overlay != XCB_NONE) { ps->tgt_picture = x_create_picture_with_visual_and_pixmap( - ps->c, ps->vis, ps->overlay, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); - } else + &ps->c, ps->c.screen_info->root_visual, ps->overlay, + XCB_RENDER_CP_SUBWINDOW_MODE, &pa); + } else { ps->tgt_picture = ps->root_picture; + } } - ev_io_init(&ps->xiow, x_event_callback, ConnectionNumber(ps->dpy), EV_READ); + ev_io_init(&ps->xiow, x_event_callback, ConnectionNumber(ps->c.dpy), EV_READ); ev_io_start(ps->loop, &ps->xiow); ev_init(&ps->unredir_timer, tmout_unredir_callback); - if (ps->o.sw_opti) - ev_idle_init(&ps->draw_idle, delayed_draw_callback); - else - ev_idle_init(&ps->draw_idle, draw_callback); + ev_init(&ps->draw_timer, draw_callback); ev_init(&ps->fade_timer, fade_timer_callback); - ev_init(&ps->delayed_draw_timer, delayed_draw_timer_callback); // Set up SIGUSR1 signal handler to reset program ev_signal_init(&ps->usr1_signal, reset_enable, SIGUSR1); @@ -2176,7 +2490,7 @@ static session_t *session_init(int argc, char **argv, Display *dpy, // functions if (ps->o.dbus) { #ifdef CONFIG_DBUS - cdbus_init(ps, DisplayString(ps->dpy)); + ps->dbus_data = cdbus_init(ps, DisplayString(ps->c.dpy)); if (!ps->dbus_data) { ps->o.dbus = false; } @@ -2186,7 +2500,7 @@ static session_t *session_init(int argc, char **argv, Display *dpy, #endif } - e = xcb_request_check(ps->c, xcb_grab_server_checked(ps->c)); + e = xcb_request_check(ps->c.c, xcb_grab_server_checked(ps->c.c)); if (e) { log_fatal_x_error(e, "Failed to grab X server"); free(e); @@ -2196,15 +2510,15 @@ static session_t *session_init(int argc, char **argv, Display *dpy, ps->server_grabbed = true; // We are going to pull latest information from X server now, events sent by X - // earlier is irrelavant at this point. + // earlier is irrelevant at this point. // A better solution is probably grabbing the server from the very start. But I // think there still could be race condition that mandates discarding the events. - x_discard_events(ps->c); + x_discard_events(&ps->c); - xcb_query_tree_reply_t *query_tree_reply = - xcb_query_tree_reply(ps->c, xcb_query_tree(ps->c, ps->root), NULL); + xcb_query_tree_reply_t *query_tree_reply = xcb_query_tree_reply( + ps->c.c, xcb_query_tree(ps->c.c, ps->c.screen_info->root), NULL); - e = xcb_request_check(ps->c, xcb_ungrab_server(ps->c)); + e = xcb_request_check(ps->c.c, xcb_ungrab_server_checked(ps->c.c)); if (e) { log_fatal_x_error(e, "Failed to ungrab server"); free(e); @@ -2213,6 +2527,7 @@ static session_t *session_init(int argc, char **argv, Display *dpy, ps->server_grabbed = false; + ps->wm = wm_new(); if (query_tree_reply) { xcb_window_t *children; int nchildren; @@ -2221,16 +2536,18 @@ static session_t *session_init(int argc, char **argv, Display *dpy, nchildren = xcb_query_tree_children_length(query_tree_reply); for (int i = 0; i < nchildren; i++) { - add_win_above(ps, children[i], i ? children[i - 1] : XCB_NONE); + wm_stack_add_above(ps->wm, children[i], i ? children[i - 1] : XCB_NONE); } free(query_tree_reply); } log_debug("Initial stack:"); - list_foreach(struct win, w, &ps->window_stack, stack_neighbour) { + list_foreach(struct win, w, wm_stack_end(ps->wm), stack_neighbour) { log_debug("%#010x", w->id); } + ps->command_builder = command_builder_new(); + ps->pending_updates = true; write_pid(ps); @@ -2257,103 +2574,63 @@ static void session_destroy(session_t *ps) { if (ps->redirected) { unredirect(ps); } - -#ifdef CONFIG_OPENGL - free(ps->argb_fbconfig); - ps->argb_fbconfig = NULL; -#endif + command_builder_free(ps->command_builder); + ps->command_builder = NULL; file_watch_destroy(ps->loop, ps->file_watch_handle); ps->file_watch_handle = NULL; // Stop listening to events on root window - xcb_change_window_attributes(ps->c, ps->root, XCB_CW_EVENT_MASK, + xcb_change_window_attributes(ps->c.c, ps->c.screen_info->root, XCB_CW_EVENT_MASK, (const uint32_t[]){0}); #ifdef CONFIG_DBUS // Kill DBus connection if (ps->o.dbus) { assert(ps->dbus_data); - cdbus_destroy(ps); + cdbus_destroy(ps->dbus_data); + ps->dbus_data = NULL; } #endif - // Free window linked list - - list_foreach_safe(struct win, w, &ps->window_stack, stack_neighbour) { - if (!w->destroyed) { - win_ev_stop(ps, w); - HASH_DEL(ps->windows, w); - } - - if (w->managed) { - auto mw = (struct managed_win *)w; - free_win_res(ps, mw); - } - free(w); + win_stack_foreach_managed_safe(w, wm_stack_end(ps->wm)) { + free_win_res(ps, w); } - list_init_head(&ps->window_stack); // Free blacklists - free_wincondlst(&ps->o.shadow_blacklist); - free_wincondlst(&ps->o.shadow_clip_list); - free_wincondlst(&ps->o.fade_blacklist); - free_wincondlst(&ps->o.focus_blacklist); - free_wincondlst(&ps->o.invert_color_list); - free_wincondlst(&ps->o.blur_background_blacklist); - free_wincondlst(&ps->o.opacity_rules); - free_wincondlst(&ps->o.paint_blacklist); - free_wincondlst(&ps->o.unredir_if_possible_blacklist); - free_wincondlst(&ps->o.rounded_corners_blacklist); - - // Free tracked atom list - { - latom_t *next = NULL; - for (latom_t *this = ps->track_atom_lst; this; this = next) { - next = this->next; - free(this); - } - - ps->track_atom_lst = NULL; - } - - // Free ignore linked list - { - ignore_t *next = NULL; - for (ignore_t *ign = ps->ignore_head; ign; ign = next) { - next = ign->next; - - free(ign); - } - - // Reset head and tail - ps->ignore_head = NULL; - ps->ignore_tail = &ps->ignore_head; - } + options_destroy(&ps->o); + c2_state_free(ps->c2_state); // Free tgt_{buffer,picture} and root_picture - if (ps->tgt_buffer.pict == ps->tgt_picture) + if (ps->tgt_buffer.pict == ps->tgt_picture) { ps->tgt_buffer.pict = XCB_NONE; + } - if (ps->tgt_picture == ps->root_picture) - ps->tgt_picture = XCB_NONE; - else - free_picture(ps->c, &ps->tgt_picture); + if (ps->tgt_picture != ps->root_picture) { + x_free_picture(&ps->c, ps->tgt_picture); + } + x_free_picture(&ps->c, ps->root_picture); + ps->tgt_picture = ps->root_picture = XCB_NONE; - free_picture(ps->c, &ps->root_picture); free_paint(ps, &ps->tgt_buffer); pixman_region32_fini(&ps->screen_reg); free(ps->expose_rects); - free(ps->o.write_pid_path); - free(ps->o.logpath); - for (int i = 0; i < ps->o.blur_kernel_count; ++i) { - free(ps->o.blur_kerns[i]); + x_free_monitor_info(&ps->monitors); + + render_statistics_destroy(&ps->render_stats); + + // Release custom window shaders + free(ps->o.window_shader_fg); + struct shader_info *shader, *tmp; + HASH_ITER(hh, ps->shaders, shader, tmp) { + HASH_DEL(ps->shaders, shader); + assert(shader->backend_shader == NULL); + free(shader->source); + free(shader->key); + free(shader); } - free(ps->o.blur_kerns); - free(ps->o.glx_fshader_win_str); - free_xinerama_info(ps); #ifdef CONFIG_VSYNC_DRM // Close file opened for DRM VSync @@ -2365,32 +2642,32 @@ static void session_destroy(session_t *ps) { // Release overlay window if (ps->overlay) { - xcb_composite_release_overlay_window(ps->c, ps->overlay); + xcb_composite_release_overlay_window(ps->c.c, ps->overlay); ps->overlay = XCB_NONE; } if (ps->sync_fence != XCB_NONE) { - xcb_sync_destroy_fence(ps->c, ps->sync_fence); + xcb_sync_destroy_fence(ps->c.c, ps->sync_fence); ps->sync_fence = XCB_NONE; } // Free reg_win if (ps->reg_win != XCB_NONE) { - xcb_destroy_window(ps->c, ps->reg_win); + xcb_destroy_window(ps->c.c, ps->reg_win); ps->reg_win = XCB_NONE; } if (ps->debug_window != XCB_NONE) { - xcb_destroy_window(ps->c, ps->debug_window); + xcb_destroy_window(ps->c.c, ps->debug_window); ps->debug_window = XCB_NONE; } - if (ps->damaged_region != XCB_NONE) { - xcb_xfixes_destroy_region(ps->c, ps->damaged_region); - ps->damaged_region = XCB_NONE; + if (ps->damage_ring.x_region != XCB_NONE) { + xcb_xfixes_destroy_region(ps->c.c, ps->damage_ring.x_region); + ps->damage_ring.x_region = XCB_NONE; } - if (ps->o.experimental_backends) { + if (!ps->o.legacy_backends) { // backend is deinitialized in unredirect() assert(ps->backend_data == NULL); } else { @@ -2405,9 +2682,11 @@ static void session_destroy(session_t *ps) { #endif // Flush all events - x_sync(ps->c); + xcb_aux_sync(ps->c.c); ev_io_stop(ps->loop, &ps->xiow); - free_conv(ps->gaussian_map); + if (ps->o.legacy_backends) { + free_conv((conv *)ps->shadow_context); + } destroy_atoms(ps->atoms); #ifdef DEBUG_XRC @@ -2415,15 +2694,16 @@ static void session_destroy(session_t *ps) { xrc_report_xid(); #endif - XSetErrorHandler(ps->previous_xerror_handler); - // Stop libev event handlers ev_timer_stop(ps->loop, &ps->unredir_timer); ev_timer_stop(ps->loop, &ps->fade_timer); - ev_idle_stop(ps->loop, &ps->draw_idle); + ev_timer_stop(ps->loop, &ps->draw_timer); ev_prepare_stop(ps->loop, &ps->event_check); ev_signal_stop(ps->loop, &ps->usr1_signal); ev_signal_stop(ps->loop, &ps->int_signal); + + wm_free(ps->wm, &ps->c); + free_x_connection(&ps->c); } /** @@ -2432,12 +2712,15 @@ static void session_destroy(session_t *ps) { * @param ps current session */ static void session_run(session_t *ps) { - if (ps->o.sw_opti) - ps->paint_tm_offset = get_time_timeval().tv_usec; + if (ps->o.use_realtime_scheduling) { + set_rr_scheduling(); + } - // In benchmark mode, we want draw_idle handler to always be active + // In benchmark mode, we want draw_timer handler to always be active if (ps->o.benchmark) { - ev_idle_start(ps->loop, &ps->draw_idle); + ps->render_queued = true; + ev_timer_set(&ps->draw_timer, 0, 0); + ev_timer_start(ps->loop, &ps->draw_timer); } else { // Let's draw our first frame! queue_redraw(ps); @@ -2445,10 +2728,16 @@ static void session_run(session_t *ps) { ev_run(ps->loop, 0); } +#ifdef CONFIG_FUZZER +#define PICOM_MAIN(...) no_main(__VA_ARGS__) +#else +#define PICOM_MAIN(...) main(__VA_ARGS__) +#endif + /** * The function that everybody knows. */ -int main(int argc, char **argv) { +int PICOM_MAIN(int argc, char **argv) { // Set locale so window names with special characters are interpreted // correctly setlocale(LC_ALL, ""); @@ -2463,6 +2752,8 @@ int main(int argc, char **argv) { } } + parse_debug_options(&global_debug_options); + int exit_code; char *config_file = NULL; bool all_xerrors = false, need_fork = false; @@ -2470,6 +2761,11 @@ int main(int argc, char **argv) { return exit_code; } + char *exe_name = basename(argv[0]); + if (strcmp(exe_name, "picom-inspect") == 0) { + return inspect_main(argc, argv, config_file); + } + int pfds[2]; if (need_fork) { if (pipe2(pfds, O_CLOEXEC)) { @@ -2491,10 +2787,9 @@ int main(int argc, char **argv) { // Failed to read, the child has most likely died // We can probably waitpid() here. return 1; - } else { - // We are done - return 0; } + // We are done + return 0; } // We are the child close(pfds[0]); @@ -2539,7 +2834,11 @@ int main(int argc, char **argv) { // Notify the parent that we are done. This might cause the parent // to quit, so only do this after setsid() int tmp = 1; - write(pfds[1], &tmp, sizeof tmp); + if (write(pfds[1], &tmp, sizeof tmp) != sizeof tmp) { + log_fatal("Failed to notify parent process"); + ret_code = 1; + break; + } close(pfds[1]); // We only do this once need_fork = false; @@ -2568,3 +2867,11 @@ int main(int argc, char **argv) { return ret_code; } + +#ifdef UNIT_TEST +static void unittest_setup(void) { + log_init_tls(); + // log_add_target_tls(stderr_logger_new()); +} +void (*test_h_unittest_setup)(void) = unittest_setup; +#endif diff --git a/src/picom.h b/src/picom.h index 25f7580604..e83cf4d1ef 100644 --- a/src/picom.h +++ b/src/picom.h @@ -36,19 +36,13 @@ enum root_flags { void add_damage(session_t *ps, const region_t *damage); -uint32_t determine_evmask(session_t *ps, xcb_window_t wid, win_evmode_t mode); - void circulate_win(session_t *ps, xcb_circulate_notify_event_t *ce); -void update_refresh_rate(session_t *ps); - void root_damaged(session_t *ps); -void cxinerama_upd_scrs(session_t *ps); - void queue_redraw(session_t *ps); -void discard_ignore(session_t *ps, unsigned long sequence); +void discard_pending(session_t *ps, uint32_t sequence); void set_root_flags(session_t *ps, uint64_t flags); @@ -58,15 +52,25 @@ xcb_window_t session_get_target_window(session_t *); uint8_t session_redirection_mode(session_t *ps); +#ifdef CONFIG_DBUS +struct cdbus_data *session_get_cdbus(struct session *); +#else +static inline struct cdbus_data *session_get_cdbus(session_t *ps attr_unused) { + return NULL; +} +#endif + /** * Set a switch_t array of all unset wintypes to true. */ static inline void wintype_arr_enable_unset(switch_t arr[]) { wintype_t i; - for (i = 0; i < NUM_WINTYPES; ++i) - if (UNSET == arr[i]) + for (i = 0; i < NUM_WINTYPES; ++i) { + if (UNSET == arr[i]) { arr[i] = ON; + } + } } /** @@ -86,14 +90,6 @@ static inline bool array_wid_exists(const xcb_window_t *arr, int count, xcb_wind return false; } -/** - * Destroy a condition list. - */ -static inline void free_wincondlst(c2_lptr_t **pcondlst) { - while ((*pcondlst = c2_free_lptr(*pcondlst))) - continue; -} - #ifndef CONFIG_OPENGL static inline void free_paint_glx(session_t *ps attr_unused, paint_t *p attr_unused) { } @@ -106,7 +102,7 @@ free_win_res_glx(session_t *ps attr_unused, struct managed_win *w attr_unused) { * Dump an drawable's info. */ static inline void dump_drawable(session_t *ps, xcb_drawable_t drawable) { - auto r = xcb_get_geometry_reply(ps->c, xcb_get_geometry(ps->c, drawable), NULL); + auto r = xcb_get_geometry_reply(ps->c.c, xcb_get_geometry(ps->c.c, drawable), NULL); if (!r) { log_trace("Drawable %#010x: Failed", drawable); return; diff --git a/src/region.h b/src/region.h index bda66e237c..96b9780e91 100644 --- a/src/region.h +++ b/src/region.h @@ -7,6 +7,7 @@ #include #include "log.h" +#include "types.h" #include "utils.h" typedef struct pixman_region32 pixman_region32_t; @@ -17,17 +18,43 @@ typedef pixman_box32_t rect_t; RC_TYPE(region_t, rc_region, pixman_region32_init, pixman_region32_fini, static inline) static inline void dump_region(const region_t *x) { - if (log_get_level_tls() < LOG_LEVEL_TRACE) { + if (log_get_level_tls() > LOG_LEVEL_TRACE) { return; } int nrects; const rect_t *rects = pixman_region32_rectangles((region_t *)x, &nrects); log_trace("nrects: %d", nrects); - for (int i = 0; i < nrects; i++) + for (int i = 0; i < nrects; i++) { log_trace("(%d, %d) - (%d, %d)", rects[i].x1, rects[i].y1, rects[i].x2, rects[i].y2); + } +} + +static inline void log_region_(enum log_level level, const char *func, const region_t *x) { + if (level < log_get_level_tls()) { + return; + } + + int nrects; + const rect_t *rects = pixman_region32_rectangles((region_t *)x, &nrects); + if (nrects == 0) { + log_printf(tls_logger, level, func, "\t(empty)"); + return; + } + for (int i = 0; i < min2(nrects, 3); i++) { + log_printf(tls_logger, level, func, "\t(%d, %d) - (%d, %d)", rects[i].x1, + rects[i].y1, rects[i].x2, rects[i].y2); + } + if (nrects > 3) { + auto extent = pixman_region32_extents(x); + log_printf(tls_logger, level, func, "\t..."); + log_printf(tls_logger, level, func, "\ttotal: (%d, %d) - (%d, %d)", + extent->x1, extent->y1, extent->x2, extent->y2); + } } +#define log_region(level, x) log_region_(LOG_LEVEL_##level, __func__, x) + /// Convert one xcb rectangle to our rectangle type static inline rect_t from_x_rect(const xcb_rectangle_t *rect) { return (rect_t){ @@ -51,8 +78,7 @@ static inline rect_t *from_x_rects(int nrects, const xcb_rectangle_t *rects) { /** * Resize a region. */ -static inline void _resize_region(const region_t *region, region_t *output, int dx, - int dy) { +static inline void _resize_region(const region_t *region, region_t *output, int dx, int dy) { if (!region || !output) { return; } @@ -77,8 +103,7 @@ static inline void _resize_region(const region_t *region, region_t *output, int if (wid <= 0 || hei <= 0) { continue; } - newrects[nnewrects] = - (rect_t){.x1 = x1, .x2 = x2, .y1 = y1, .y2 = y2}; + newrects[nnewrects] = (rect_t){.x1 = x1, .x2 = x2, .y1 = y1, .y2 = y2}; ++nnewrects; } @@ -98,3 +123,49 @@ static inline region_t resize_region(const region_t *region, int dx, int dy) { static inline void resize_region_in_place(region_t *region, int dx, int dy) { return _resize_region(region, region, dx, dy); } + +static inline rect_t region_translate_rect(rect_t rect, ivec2 origin) { + return (rect_t){ + .x1 = rect.x1 + origin.x, + .y1 = rect.y1 + origin.y, + .x2 = rect.x2 + origin.x, + .y2 = rect.y2 + origin.y, + }; +} + +/// Subtract `other`, placed at `origin`, from `region`. +static inline void region_subtract(region_t *region, ivec2 origin, const region_t *other) { + pixman_region32_translate(region, -origin.x, -origin.y); + pixman_region32_subtract(region, region, other); + pixman_region32_translate(region, origin.x, origin.y); +} + +/// Union `region` with `other` placed at `origin`. +static inline void region_union(region_t *region, ivec2 origin, const region_t *other) { + pixman_region32_translate(region, -origin.x, -origin.y); + pixman_region32_union(region, region, other); + pixman_region32_translate(region, origin.x, origin.y); +} + +/// Intersect `region` with `other` placed at `origin`. +static inline void region_intersect(region_t *region, ivec2 origin, const region_t *other) { + pixman_region32_translate(region, -origin.x, -origin.y); + pixman_region32_intersect(region, region, other); + pixman_region32_translate(region, origin.x, origin.y); +} + +/// Calculate the symmetric difference of `region1`, and `region2`, and union the result +/// into `result`. The two input regions has to be in the same coordinate space. +/// +/// @param scratch a region to store temporary results +static inline void +region_symmetric_difference_local(region_t *result, region_t *scratch, + const region_t *region1, const region_t *region2) { + pixman_region32_copy(scratch, region1); + pixman_region32_subtract(scratch, scratch, region2); + pixman_region32_union(result, result, scratch); + + pixman_region32_copy(scratch, region2); + pixman_region32_subtract(scratch, scratch, region1); + pixman_region32_union(result, result, scratch); +} diff --git a/src/render.c b/src/render.c index ac9b40ecd9..f434361233 100644 --- a/src/render.c +++ b/src/render.c @@ -6,11 +6,13 @@ #include #include #include +#include #include #include #include "common.h" #include "options.h" +#include "transition.h" #ifdef CONFIG_OPENGL #include "backend/gl/glx.h" @@ -48,28 +50,29 @@ static inline bool paint_bind_tex(session_t *ps, paint_t *ppaint, int wid, int h bool repeat, int depth, xcb_visualid_t visual, bool force) { #ifdef CONFIG_OPENGL // XXX This is a mess. But this will go away after the backend refactor. - if (!ppaint->pixmap) + if (!ppaint->pixmap) { return false; + } struct glx_fbconfig_info *fbcfg; if (!visual) { assert(depth == 32); - if (!ps->argb_fbconfig) { - ps->argb_fbconfig = - glx_find_fbconfig(ps->dpy, ps->scr, - (struct xvisual_info){.red_size = 8, - .green_size = 8, - .blue_size = 8, - .alpha_size = 8, - .visual_depth = 32}); + if (!ps->argb_fbconfig.cfg) { + glx_find_fbconfig(&ps->c, + (struct xvisual_info){.red_size = 8, + .green_size = 8, + .blue_size = 8, + .alpha_size = 8, + .visual_depth = 32}, + &ps->argb_fbconfig); } - if (!ps->argb_fbconfig) { + if (!ps->argb_fbconfig.cfg) { log_error("Failed to find appropriate FBConfig for 32 bit depth"); return false; } - fbcfg = ps->argb_fbconfig; + fbcfg = &ps->argb_fbconfig; } else { - auto m = x_get_visual_info(ps->c, visual); + auto m = x_get_visual_info(&ps->c, visual); if (m.visual_depth < 0) { return false; } @@ -79,19 +82,20 @@ static inline bool paint_bind_tex(session_t *ps, paint_t *ppaint, int wid, int h return false; } - if (!ppaint->fbcfg) { - ppaint->fbcfg = glx_find_fbconfig(ps->dpy, ps->scr, m); + if (!ppaint->fbcfg.cfg) { + glx_find_fbconfig(&ps->c, m, &ppaint->fbcfg); } - if (!ppaint->fbcfg) { + if (!ppaint->fbcfg.cfg) { log_error("Failed to find appropriate FBConfig for X pixmap"); return false; } - fbcfg = ppaint->fbcfg; + fbcfg = &ppaint->fbcfg; } - if (force || !glx_tex_binded(ppaint->ptex, ppaint->pixmap)) + if (force || !glx_tex_bound(ppaint->ptex, ppaint->pixmap)) { return glx_bind_pixmap(ps, &ppaint->ptex, ppaint->pixmap, wid, hei, repeat, fbcfg); + } #else (void)ps; (void)ppaint; @@ -129,7 +133,7 @@ static int get_buffer_age(session_t *ps) { } if (ps->o.use_damage) { unsigned int val; - glXQueryDrawable(ps->dpy, get_tgt_window(ps), + glXQueryDrawable(ps->c.dpy, get_tgt_window(ps), GLX_BACK_BUFFER_AGE_EXT, &val); return (int)val ?: -1; } @@ -144,7 +148,7 @@ static int get_buffer_age(session_t *ps) { */ static inline void xrfilter_reset(session_t *ps, xcb_render_picture_t p) { #define FILTER "Nearest" - xcb_render_set_picture_filter(ps->c, p, strlen(FILTER), FILTER, 0, NULL); + xcb_render_set_picture_filter(ps->c.c, p, strlen(FILTER), FILTER, 0, NULL); #undef FILTER } @@ -153,7 +157,7 @@ static inline void attr_nonnull(1, 2) set_tgt_clip(session_t *ps, region_t *reg) switch (ps->o.backend) { case BKEND_XRENDER: case BKEND_XR_GLX_HYBRID: - x_set_picture_clip_region(ps->c, ps->tgt_buffer.pict, 0, 0, reg); + x_set_picture_clip_region(&ps->c, ps->tgt_buffer.pict, 0, 0, reg); break; #ifdef CONFIG_OPENGL case BKEND_GLX: glx_set_clip(ps, reg); break; @@ -162,16 +166,6 @@ static inline void attr_nonnull(1, 2) set_tgt_clip(session_t *ps, region_t *reg) } } -/** - * Destroy a Picture. - */ -void free_picture(xcb_connection_t *c, xcb_render_picture_t *p) { - if (*p) { - xcb_render_free_picture(c, *p); - *p = XCB_NONE; - } -} - /** * Free paint_t. */ @@ -179,10 +173,14 @@ void free_paint(session_t *ps, paint_t *ppaint) { #ifdef CONFIG_OPENGL free_paint_glx(ps, ppaint); #endif - free_picture(ps->c, &ppaint->pict); - if (ppaint->pixmap) - xcb_free_pixmap(ps->c, ppaint->pixmap); - ppaint->pixmap = XCB_NONE; + if (ppaint->pict != XCB_NONE) { + x_free_picture(&ps->c, ppaint->pict); + ppaint->pict = XCB_NONE; + } + if (ppaint->pixmap) { + xcb_free_pixmap(ps->c.c, ppaint->pixmap); + ppaint->pixmap = XCB_NONE; + } } uint32_t @@ -253,8 +251,7 @@ void render(session_t *ps, int x, int y, int dx, int dy, int wid, int hei, int f if (alpha_step != 0) { if (cr) { xcb_render_picture_t p_tmp = x_create_picture_with_standard( - ps->c, ps->root, fullwid, fullhei, - XCB_PICT_STANDARD_ARGB_32, 0, 0); + &ps->c, fullwid, fullhei, XCB_PICT_STANDARD_ARGB_32, 0, 0); xcb_render_color_t trans = { .red = 0, .blue = 0, .green = 0, .alpha = 0}; const xcb_rectangle_t rect = { @@ -262,7 +259,7 @@ void render(session_t *ps, int x, int y, int dx, int dy, int wid, int hei, int f .y = 0, .width = to_u16_checked(fullwid), .height = to_u16_checked(fullhei)}; - xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, + xcb_render_fill_rectangles(ps->c.c, XCB_RENDER_PICT_OP_SRC, p_tmp, trans, 1, &rect); uint32_t max_ntraps = to_u32_checked(cr); @@ -272,25 +269,24 @@ void render(session_t *ps, int x, int y, int dx, int dy, int wid, int hei, int f traps, max_ntraps, cr, fullwid, fullhei); xcb_render_trapezoids( - ps->c, XCB_RENDER_PICT_OP_OVER, alpha_pict, p_tmp, - x_get_pictfmt_for_standard(ps->c, XCB_PICT_STANDARD_A_8), + ps->c.c, XCB_RENDER_PICT_OP_OVER, alpha_pict, p_tmp, + x_get_pictfmt_for_standard(&ps->c, XCB_PICT_STANDARD_A_8), 0, 0, n, traps); xcb_render_composite( - ps->c, XCB_RENDER_PICT_OP_OVER, pict, p_tmp, + ps->c.c, XCB_RENDER_PICT_OP_OVER, pict, p_tmp, ps->tgt_buffer.pict, to_i16_checked(x), to_i16_checked(y), to_i16_checked(x), to_i16_checked(y), to_i16_checked(dx), to_i16_checked(dy), to_u16_checked(wid), to_u16_checked(hei)); - xcb_render_free_picture(ps->c, p_tmp); + x_free_picture(&ps->c, p_tmp); } else { xcb_render_picture_t p_tmp = alpha_pict; if (clip) { p_tmp = x_create_picture_with_standard( - ps->c, ps->root, wid, hei, - XCB_PICT_STANDARD_ARGB_32, 0, 0); + &ps->c, wid, hei, XCB_PICT_STANDARD_ARGB_32, 0, 0); xcb_render_color_t black = { .red = 255, .blue = 255, .green = 255, .alpha = 255}; @@ -299,17 +295,18 @@ void render(session_t *ps, int x, int y, int dx, int dy, int wid, int hei, int f .y = 0, .width = to_u16_checked(wid), .height = to_u16_checked(hei)}; - xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, + xcb_render_fill_rectangles(ps->c.c, + XCB_RENDER_PICT_OP_SRC, p_tmp, black, 1, &rect); if (alpha_pict) { xcb_render_composite( - ps->c, XCB_RENDER_PICT_OP_SRC, + ps->c.c, XCB_RENDER_PICT_OP_SRC, alpha_pict, XCB_NONE, p_tmp, 0, 0, 0, 0, 0, 0, to_u16_checked(wid), to_u16_checked(hei)); } xcb_render_composite( - ps->c, XCB_RENDER_PICT_OP_OUT_REVERSE, + ps->c.c, XCB_RENDER_PICT_OP_OUT_REVERSE, clip->pict, XCB_NONE, p_tmp, 0, 0, 0, 0, to_i16_checked(clip->x), to_i16_checked(clip->y), to_u16_checked(wid), to_u16_checked(hei)); @@ -319,12 +316,12 @@ void render(session_t *ps, int x, int y, int dx, int dy, int wid, int hei, int f : XCB_RENDER_PICT_OP_OVER); xcb_render_composite( - ps->c, op, pict, p_tmp, ps->tgt_buffer.pict, + ps->c.c, op, pict, p_tmp, ps->tgt_buffer.pict, to_i16_checked(x), to_i16_checked(y), 0, 0, to_i16_checked(dx), to_i16_checked(dy), to_u16_checked(wid), to_u16_checked(hei)); if (clip) { - xcb_render_free_picture(ps->c, p_tmp); + x_free_picture(&ps->c, p_tmp); } } } @@ -375,15 +372,18 @@ paint_region(session_t *ps, const struct managed_win *w, int x, int y, int wid, static inline bool paint_isvalid(session_t *ps, const paint_t *ppaint) { // Don't check for presence of Pixmap here, because older X Composite doesn't // provide it - if (!ppaint) + if (!ppaint) { return false; + } - if (bkend_use_xrender(ps) && !ppaint->pict) + if (bkend_use_xrender(ps) && !ppaint->pict) { return false; + } #ifdef CONFIG_OPENGL - if (BKEND_GLX == ps->o.backend && !glx_tex_binded(ppaint->ptex, XCB_NONE)) + if (BKEND_GLX == ps->o.backend && !glx_tex_bound(ppaint->ptex, XCB_NONE)) { return false; + } #endif return true; @@ -395,9 +395,9 @@ static inline bool paint_isvalid(session_t *ps, const paint_t *ppaint) { void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) { // Fetch Pixmap if (!w->paint.pixmap) { - w->paint.pixmap = x_new_id(ps->c); - set_ignore_cookie(ps, xcb_composite_name_window_pixmap(ps->c, w->base.id, - w->paint.pixmap)); + w->paint.pixmap = x_new_id(&ps->c); + set_ignore_cookie(&ps->c, xcb_composite_name_window_pixmap( + ps->c.c, w->base.id, w->paint.pixmap)); } xcb_drawable_t draw = w->paint.pixmap; @@ -415,7 +415,7 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) }; w->paint.pict = x_create_picture_with_pictfmt_and_pixmap( - ps->c, w->pictfmt, draw, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); + &ps->c, w->pictfmt->id, draw, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); } // GLX: Build texture @@ -437,13 +437,14 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) const int y = w->g.y; const uint16_t wid = to_u16_checked(w->widthb); const uint16_t hei = to_u16_checked(w->heightb); + const double window_opacity = animatable_get(&w->opacity); xcb_render_picture_t pict = w->paint.pict; // Invert window color, if required if (bkend_use_xrender(ps) && w->invert_color) { xcb_render_picture_t newpict = x_create_picture_with_pictfmt( - ps->c, ps->root, wid, hei, w->pictfmt, 0, NULL); + &ps->c, wid, hei, w->pictfmt->id, w->pictfmt->depth, 0, NULL); if (newpict) { // Apply clipping region to save some CPU if (reg_paint) { @@ -456,34 +457,35 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) pixman_region32_fini(®); } - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, pict, XCB_NONE, - newpict, 0, 0, 0, 0, 0, 0, wid, hei); - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_DIFFERENCE, + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_SRC, pict, + XCB_NONE, newpict, 0, 0, 0, 0, 0, 0, wid, hei); + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_DIFFERENCE, ps->white_picture, XCB_NONE, newpict, 0, 0, 0, 0, 0, 0, wid, hei); // We use an extra PictOpInReverse operation to get correct // pixel alpha. There could be a better solution. - if (win_has_alpha(w)) - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_IN_REVERSE, + if (win_has_alpha(w)) { + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_IN_REVERSE, pict, XCB_NONE, newpict, 0, 0, 0, 0, 0, 0, wid, hei); + } pict = newpict; } } if (w->frame_opacity == 1) { - paint_region(ps, w, 0, 0, wid, hei, w->opacity, reg_paint, pict); + paint_region(ps, w, 0, 0, wid, hei, window_opacity, reg_paint, pict); } else { // Painting parameters const margin_t extents = win_calc_frame_extents(w); - const auto t = extents.top; - const auto l = extents.left; - const auto b = extents.bottom; - const auto r = extents.right; + auto const t = extents.top; + auto const l = extents.left; + auto const b = extents.bottom; + auto const r = extents.right; #define COMP_BDR(cx, cy, cwid, chei) \ - paint_region(ps, w, (cx), (cy), (cwid), (chei), w->frame_opacity * w->opacity, \ - reg_paint, pict) + paint_region(ps, w, (cx), (cy), (cwid), (chei), \ + w->frame_opacity *window_opacity, reg_paint, pict) // Sanitize the margins, in case some broken WM makes // top_width + bottom_width > height in some cases. @@ -494,60 +496,71 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) // ctop = checked top // Make sure top margin is smaller than height int ctop = min2(body_height, t); - if (ctop > 0) + if (ctop > 0) { COMP_BDR(0, 0, wid, ctop); + } body_height -= ctop; - if (body_height <= 0) + if (body_height <= 0) { break; + } // bottom // cbot = checked bottom // Make sure bottom margin is not too large int cbot = min2(body_height, b); - if (cbot > 0) + if (cbot > 0) { COMP_BDR(0, hei - cbot, wid, cbot); + } // Height of window exclude the margin body_height -= cbot; - if (body_height <= 0) + if (body_height <= 0) { break; + } // left int body_width = wid; int cleft = min2(body_width, l); - if (cleft > 0) + if (cleft > 0) { COMP_BDR(0, ctop, cleft, body_height); + } body_width -= cleft; - if (body_width <= 0) + if (body_width <= 0) { break; + } // right int cright = min2(body_width, r); - if (cright > 0) + if (cright > 0) { COMP_BDR(wid - cright, ctop, cright, body_height); + } body_width -= cright; - if (body_width <= 0) + if (body_width <= 0) { break; + } // body paint_region(ps, w, cleft, ctop, body_width, body_height, - w->opacity, reg_paint, pict); + window_opacity, reg_paint, pict); } while (0); } #undef COMP_BDR - if (pict != w->paint.pict) - free_picture(ps->c, &pict); + if (pict != w->paint.pict) { + x_free_picture(&ps->c, pict); + pict = XCB_NONE; + } // Dimming the window if needed if (w->dim) { double dim_opacity = ps->o.inactive_dim; - if (!ps->o.inactive_dim_fixed) - dim_opacity *= w->opacity; + if (!ps->o.inactive_dim_fixed) { + dim_opacity *= window_opacity; + } switch (ps->o.backend) { case BKEND_XRENDER: @@ -569,7 +582,7 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) .height = hei, }; - xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_OVER, + xcb_render_fill_rectangles(ps->c.c, XCB_RENDER_PICT_OP_OVER, ps->tgt_buffer.pict, color, 1, &rect); } break; #ifdef CONFIG_OPENGL @@ -583,8 +596,6 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) } } -extern const char *background_props_str[]; - static bool get_root_tile(session_t *ps) { /* if (ps->o.paint_on_overlay) { @@ -595,20 +606,29 @@ static bool get_root_tile(session_t *ps) { ps->root_tile_fill = false; bool fill = false; - xcb_pixmap_t pixmap = x_get_root_back_pixmap(ps->c, ps->root, ps->atoms); + xcb_pixmap_t pixmap = x_get_root_back_pixmap(&ps->c, ps->atoms); - // Make sure the pixmap we got is valid - if (pixmap && !x_validate_pixmap(ps->c, pixmap)) - pixmap = XCB_NONE; + xcb_get_geometry_reply_t *r; + if (pixmap) { + r = xcb_get_geometry_reply(ps->c.c, xcb_get_geometry(ps->c.c, pixmap), NULL); + } // Create a pixmap if there isn't any - if (!pixmap) { - pixmap = x_create_pixmap(ps->c, (uint8_t)ps->depth, ps->root, 1, 1); + xcb_visualid_t visual; + if (!pixmap || !r) { + pixmap = + x_create_pixmap(&ps->c, (uint8_t)ps->c.screen_info->root_depth, 1, 1); if (pixmap == XCB_NONE) { log_error("Failed to create pixmaps for root tile."); return false; } + visual = ps->c.screen_info->root_visual; fill = true; + } else { + visual = r->depth == ps->c.screen_info->root_depth + ? ps->c.screen_info->root_visual + : x_get_visual_for_depth(ps->c.screen_info, r->depth); + free(r); } // Create Picture @@ -616,7 +636,7 @@ static bool get_root_tile(session_t *ps) { .repeat = true, }; ps->root_tile_paint.pict = x_create_picture_with_visual_and_pixmap( - ps->c, ps->vis, pixmap, XCB_RENDER_CP_REPEAT, &pa); + &ps->c, visual, pixmap, XCB_RENDER_CP_REPEAT, &pa); // Fill pixmap if needed if (fill) { @@ -629,15 +649,16 @@ static bool get_root_tile(session_t *ps) { rect.x = rect.y = 0; rect.width = rect.height = 1; - xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, + xcb_render_fill_rectangles(ps->c.c, XCB_RENDER_PICT_OP_SRC, ps->root_tile_paint.pict, col, 1, &rect); } ps->root_tile_fill = fill; ps->root_tile_paint.pixmap = pixmap; #ifdef CONFIG_OPENGL - if (BKEND_GLX == ps->o.backend) - return paint_bind_tex(ps, &ps->root_tile_paint, 0, 0, true, 0, ps->vis, false); + if (BKEND_GLX == ps->o.backend) { + return paint_bind_tex(ps, &ps->root_tile_paint, 0, 0, true, 0, visual, false); + } #endif return true; @@ -649,8 +670,9 @@ static bool get_root_tile(session_t *ps) { static void paint_root(session_t *ps, const region_t *reg_paint) { // If there is no root tile pixmap, try getting one. // If that fails, give up. - if (!ps->root_tile_paint.pixmap && !get_root_tile(ps)) + if (!ps->root_tile_paint.pixmap && !get_root_tile(ps)) { return; + } paint_region(ps, NULL, 0, 0, ps->root_width, ps->root_height, 1.0, reg_paint, ps->root_tile_paint.pict); @@ -669,16 +691,16 @@ static bool win_build_shadow(session_t *ps, struct managed_win *w, double opacit xcb_render_picture_t shadow_picture = XCB_NONE, shadow_picture_argb = XCB_NONE; xcb_gcontext_t gc = XCB_NONE; - shadow_image = make_shadow(ps->c, ps->gaussian_map, opacity, width, height); + shadow_image = + make_shadow(&ps->c, (conv *)ps->shadow_context, opacity, width, height); if (!shadow_image) { log_error("failed to make shadow"); return XCB_NONE; } - shadow_pixmap = - x_create_pixmap(ps->c, 8, ps->root, shadow_image->width, shadow_image->height); + shadow_pixmap = x_create_pixmap(&ps->c, 8, shadow_image->width, shadow_image->height); shadow_pixmap_argb = - x_create_pixmap(ps->c, 32, ps->root, shadow_image->width, shadow_image->height); + x_create_pixmap(&ps->c, 32, shadow_image->width, shadow_image->height); if (!shadow_pixmap || !shadow_pixmap_argb) { log_error("failed to create shadow pixmaps"); @@ -686,17 +708,18 @@ static bool win_build_shadow(session_t *ps, struct managed_win *w, double opacit } shadow_picture = x_create_picture_with_standard_and_pixmap( - ps->c, XCB_PICT_STANDARD_A_8, shadow_pixmap, 0, NULL); + &ps->c, XCB_PICT_STANDARD_A_8, shadow_pixmap, 0, NULL); shadow_picture_argb = x_create_picture_with_standard_and_pixmap( - ps->c, XCB_PICT_STANDARD_ARGB_32, shadow_pixmap_argb, 0, NULL); - if (!shadow_picture || !shadow_picture_argb) + &ps->c, XCB_PICT_STANDARD_ARGB_32, shadow_pixmap_argb, 0, NULL); + if (!shadow_picture || !shadow_picture_argb) { goto shadow_picture_err; + } - gc = x_new_id(ps->c); - xcb_create_gc(ps->c, gc, shadow_pixmap, 0, NULL); + gc = x_new_id(&ps->c); + xcb_create_gc(ps->c.c, gc, shadow_pixmap, 0, NULL); - xcb_image_put(ps->c, shadow_pixmap, gc, shadow_image, 0, 0, 0); - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, ps->cshadow_picture, + xcb_image_put(ps->c.c, shadow_pixmap, gc, shadow_image, 0, 0, 0); + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_SRC, ps->cshadow_picture, shadow_picture, shadow_picture_argb, 0, 0, 0, 0, 0, 0, shadow_image->width, shadow_image->height); @@ -705,26 +728,32 @@ static bool win_build_shadow(session_t *ps, struct managed_win *w, double opacit assert(!w->shadow_paint.pict); w->shadow_paint.pict = shadow_picture_argb; - xcb_free_gc(ps->c, gc); + xcb_free_gc(ps->c.c, gc); xcb_image_destroy(shadow_image); - xcb_free_pixmap(ps->c, shadow_pixmap); - xcb_render_free_picture(ps->c, shadow_picture); + xcb_free_pixmap(ps->c.c, shadow_pixmap); + x_free_picture(&ps->c, shadow_picture); return true; shadow_picture_err: - if (shadow_image) + if (shadow_image) { xcb_image_destroy(shadow_image); - if (shadow_pixmap) - xcb_free_pixmap(ps->c, shadow_pixmap); - if (shadow_pixmap_argb) - xcb_free_pixmap(ps->c, shadow_pixmap_argb); - if (shadow_picture) - xcb_render_free_picture(ps->c, shadow_picture); - if (shadow_picture_argb) - xcb_render_free_picture(ps->c, shadow_picture_argb); - if (gc) - xcb_free_gc(ps->c, gc); + } + if (shadow_pixmap) { + xcb_free_pixmap(ps->c.c, shadow_pixmap); + } + if (shadow_pixmap_argb) { + xcb_free_pixmap(ps->c.c, shadow_pixmap_argb); + } + if (shadow_picture) { + x_free_picture(&ps->c, shadow_picture); + } + if (shadow_picture_argb) { + x_free_picture(&ps->c, shadow_picture_argb); + } + if (gc) { + xcb_free_gc(ps->c.c, gc); + } return false; } @@ -753,23 +782,22 @@ win_paint_shadow(session_t *ps, struct managed_win *w, region_t *reg_paint) { traps, max_ntraps, w->corner_radius, w->widthb, w->heightb); td = x_create_picture_with_standard( - ps->c, ps->root, w->widthb, w->heightb, - XCB_PICT_STANDARD_ARGB_32, 0, 0); + &ps->c, w->widthb, w->heightb, XCB_PICT_STANDARD_ARGB_32, 0, 0); xcb_render_color_t trans = { .red = 0, .blue = 0, .green = 0, .alpha = 0}; const xcb_rectangle_t rect = {.x = 0, .y = 0, .width = to_u16_checked(w->widthb), .height = to_u16_checked(w->heightb)}; - xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, td, + xcb_render_fill_rectangles(ps->c.c, XCB_RENDER_PICT_OP_SRC, td, trans, 1, &rect); - auto solid = solid_picture(ps->c, ps->root, false, 1, 0, 0, 0); + auto solid = solid_picture(&ps->c, false, 1, 0, 0, 0); xcb_render_trapezoids( - ps->c, XCB_RENDER_PICT_OP_OVER, solid, td, - x_get_pictfmt_for_standard(ps->c, XCB_PICT_STANDARD_A_8), 0, + ps->c.c, XCB_RENDER_PICT_OP_OVER, solid, td, + x_get_pictfmt_for_standard(&ps->c, XCB_PICT_STANDARD_A_8), 0, 0, n, traps); - xcb_render_free_picture(ps->c, solid); + x_free_picture(&ps->c, solid); } else { // Not implemented } @@ -780,12 +808,14 @@ win_paint_shadow(session_t *ps, struct managed_win *w, region_t *reg_paint) { .x = -(w->shadow_dx), .y = -(w->shadow_dy), }; + double shadow_opacity = + animatable_get(&w->opacity) * ps->o.shadow_opacity * ps->o.frame_opacity; render(ps, 0, 0, w->g.x + w->shadow_dx, w->g.y + w->shadow_dy, w->shadow_width, - w->shadow_height, w->widthb, w->heightb, w->shadow_opacity, true, false, 0, + w->shadow_height, w->widthb, w->heightb, shadow_opacity, true, false, 0, w->shadow_paint.pict, w->shadow_paint.ptex, reg_paint, NULL, should_clip ? &clip : NULL); if (td) { - xcb_render_free_picture(ps->c, td); + x_free_picture(&ps->c, td); } } @@ -813,16 +843,17 @@ xr_blur_dst(session_t *ps, xcb_render_picture_t tgt_buffer, int16_t x, int16_t y // Directly copying from tgt_buffer to it does not work, so we create a // Picture in the middle. - xcb_render_picture_t tmp_picture = - x_create_picture_with_visual(ps->c, ps->root, wid, hei, ps->vis, 0, NULL); + xcb_render_picture_t tmp_picture = x_create_picture_with_visual( + &ps->c, wid, hei, ps->c.screen_info->root_visual, 0, NULL); if (!tmp_picture) { log_error("Failed to build intermediate Picture."); return false; } - if (reg_clip && tmp_picture) - x_set_picture_clip_region(ps->c, tmp_picture, 0, 0, reg_clip); + if (reg_clip && tmp_picture) { + x_set_picture_clip_region(&ps->c, tmp_picture, 0, 0, reg_clip); + } xcb_render_picture_t src_pict = tgt_buffer, dst_pict = tmp_picture; for (int i = 0; i < nkernels; ++i) { @@ -836,9 +867,9 @@ xr_blur_dst(session_t *ps, xcb_render_picture_t tgt_buffer, int16_t x, int16_t y // be applied on source picture, to get the nearby pixels outside the // window. xcb_render_set_picture_filter( - ps->c, src_pict, strlen(XRFILTER_CONVOLUTION), XRFILTER_CONVOLUTION, + ps->c.c, src_pict, strlen(XRFILTER_CONVOLUTION), XRFILTER_CONVOLUTION, (uint32_t)(kwid * khei + 2), convolution_blur); - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, src_pict, XCB_NONE, + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_SRC, src_pict, XCB_NONE, dst_pict, (rd_from_tgt ? x : 0), (rd_from_tgt ? y : 0), 0, 0, (rd_from_tgt ? 0 : x), (rd_from_tgt ? 0 : y), wid, hei); @@ -851,11 +882,12 @@ xr_blur_dst(session_t *ps, xcb_render_picture_t tgt_buffer, int16_t x, int16_t y } } - if (src_pict != tgt_buffer) - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_OVER, src_pict, rounded, + if (src_pict != tgt_buffer) { + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_OVER, src_pict, rounded, tgt_buffer, 0, 0, 0, 0, x, y, wid, hei); + } - free_picture(ps->c, &tmp_picture); + x_free_picture(&ps->c, tmp_picture); return true; } @@ -868,15 +900,16 @@ win_blur_background(session_t *ps, struct managed_win *w, xcb_render_picture_t t const region_t *reg_paint) { const int16_t x = w->g.x; const int16_t y = w->g.y; - const auto wid = to_u16_checked(w->widthb); - const auto hei = to_u16_checked(w->heightb); + auto const wid = to_u16_checked(w->widthb); + auto const hei = to_u16_checked(w->heightb); const int cr = w ? w->corner_radius : 0; + const double window_opacity = animatable_get(&w->opacity); double factor_center = 1.0; // Adjust blur strength according to window opacity, to make it appear // better during fading if (!ps->o.blur_background_fixed) { - double pct = 1.0 - w->opacity * (1.0 - 1.0 / 9.0); + double pct = 1.0 - window_opacity * (1.0 - 1.0 / 9.0); factor_center = pct * 8.0 / (1.1 - pct); } @@ -910,23 +943,23 @@ win_blur_background(session_t *ps, struct managed_win *w, xcb_render_picture_t t make_rounded_window_shape(traps, max_ntraps, cr, wid, hei); td = x_create_picture_with_standard( - ps->c, ps->root, wid, hei, XCB_PICT_STANDARD_ARGB_32, 0, 0); + &ps->c, wid, hei, XCB_PICT_STANDARD_ARGB_32, 0, 0); xcb_render_color_t trans = { .red = 0, .blue = 0, .green = 0, .alpha = 0}; const xcb_rectangle_t rect = {.x = 0, .y = 0, .width = to_u16_checked(wid), .height = to_u16_checked(hei)}; - xcb_render_fill_rectangles(ps->c, XCB_RENDER_PICT_OP_SRC, td, + xcb_render_fill_rectangles(ps->c.c, XCB_RENDER_PICT_OP_SRC, td, trans, 1, &rect); - auto solid = solid_picture(ps->c, ps->root, false, 1, 0, 0, 0); + auto solid = solid_picture(&ps->c, false, 1, 0, 0, 0); xcb_render_trapezoids( - ps->c, XCB_RENDER_PICT_OP_OVER, solid, td, - x_get_pictfmt_for_standard(ps->c, XCB_PICT_STANDARD_A_8), 0, + ps->c.c, XCB_RENDER_PICT_OP_OVER, solid, td, + x_get_pictfmt_for_standard(&ps->c, XCB_PICT_STANDARD_A_8), 0, 0, n, traps); - xcb_render_free_picture(ps->c, solid); + x_free_picture(&ps->c, solid); } // Minimize the region we try to blur, if the window itself is not @@ -946,14 +979,14 @@ win_blur_background(session_t *ps, struct managed_win *w, xcb_render_picture_t t xr_blur_dst(ps, tgt_buffer, x, y, wid, hei, ps->blur_kerns_cache, ps->o.blur_kernel_count, ®_blur, td); if (td) { - xcb_render_free_picture(ps->c, td); + x_free_picture(&ps->c, td); } pixman_region32_clear(®_blur); } break; #ifdef CONFIG_OPENGL case BKEND_GLX: // TODO(compton) Handle frame opacity - glx_blur_dst(ps, x, y, wid, hei, (float)ps->psglx->z - 0.5f, + glx_blur_dst(ps, x, y, wid, hei, (float)ps->psglx->z - 0.5F, (float)factor_center, reg_paint, &w->glx_blur_cache); break; #endif @@ -967,12 +1000,12 @@ win_blur_background(session_t *ps, struct managed_win *w, xcb_render_picture_t t /// paint all windows /// region = ?? /// region_real = the damage region -void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { +void paint_all(session_t *ps, struct managed_win *t) { if (ps->o.xrender_sync_fence || (ps->drivers & DRIVER_NVIDIA)) { - if (ps->xsync_exists && !x_fence_sync(ps->c, ps->sync_fence)) { + if (ps->xsync_exists && !x_fence_sync(&ps->c, ps->sync_fence)) { log_error("x_fence_sync failed, xrender-sync-fence will be " "disabled from now on."); - xcb_sync_destroy_fence(ps->c, ps->sync_fence); + xcb_sync_destroy_fence(ps->c.c, ps->sync_fence); ps->sync_fence = XCB_NONE; ps->o.xrender_sync_fence = false; ps->xsync_exists = false; @@ -981,16 +1014,7 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { region_t region; pixman_region32_init(®ion); - int buffer_age = get_buffer_age(ps); - if (buffer_age == -1 || buffer_age > ps->ndamage || ignore_damage) { - pixman_region32_copy(®ion, &ps->screen_reg); - } else { - for (int i = 0; i < get_buffer_age(ps); i++) { - auto curr = ((ps->damage - ps->damage_ring) + i) % ps->ndamage; - pixman_region32_union(®ion, ®ion, &ps->damage_ring[curr]); - } - } - + damage_ring_collect(&ps->damage_ring, &ps->screen_reg, ®ion, get_buffer_age(ps)); if (!pixman_region32_not_empty(®ion)) { return; } @@ -1010,7 +1034,7 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { if (!ps->tgt_buffer.pixmap) { free_paint(ps, &ps->tgt_buffer); ps->tgt_buffer.pixmap = - x_create_pixmap(ps->c, (uint8_t)ps->depth, ps->root, + x_create_pixmap(&ps->c, ps->c.screen_info->root_depth, ps->root_width, ps->root_height); if (ps->tgt_buffer.pixmap == XCB_NONE) { log_fatal("Failed to allocate a screen-sized pixmap for" @@ -1019,18 +1043,20 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { } } - if (BKEND_GLX != ps->o.backend) + if (BKEND_GLX != ps->o.backend) { ps->tgt_buffer.pict = x_create_picture_with_visual_and_pixmap( - ps->c, ps->vis, ps->tgt_buffer.pixmap, 0, 0); + &ps->c, ps->c.screen_info->root_visual, ps->tgt_buffer.pixmap, + 0, 0); + } } if (BKEND_XRENDER == ps->o.backend) { - x_set_picture_clip_region(ps->c, ps->tgt_picture, 0, 0, ®ion); + x_set_picture_clip_region(&ps->c, ps->tgt_picture, 0, 0, ®ion); } #ifdef CONFIG_OPENGL if (bkend_use_glx(ps)) { - ps->psglx->z = 0.0; + ps->psglx->z = 0; } #endif @@ -1066,18 +1092,16 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { // Painting shadow if (w->shadow) { // Lazy shadow building - if (!w->shadow_paint.pixmap) - if (!win_build_shadow(ps, w, 1)) + if (!w->shadow_paint.pixmap) { + if (!win_build_shadow(ps, w, 1)) { log_error("build shadow failed"); + } + } // Shadow doesn't need to be painted underneath the body // of the windows above. Because no one can see it pixman_region32_subtract(®_tmp, ®ion, w->reg_ignore); - // Mask out the region we don't want shadow on - if (pixman_region32_not_empty(&ps->shadow_exclude_reg)) - pixman_region32_subtract(®_tmp, ®_tmp, - &ps->shadow_exclude_reg); if (pixman_region32_not_empty(®_shadow_clip)) { pixman_region32_subtract(®_tmp, ®_tmp, ®_shadow_clip); } @@ -1093,21 +1117,23 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { // needed Doing it here instead of in make_shadow() for // saving GPU power and handling shaped windows (XXX // unconfirmed) - if (!ps->o.wintype_option[w->window_type].full_shadow) + if (!ps->o.wintype_option[w->window_type].full_shadow) { pixman_region32_subtract(®_tmp, ®_tmp, &bshape_no_corners); + } - if (ps->o.xinerama_shadow_crop && w->xinerama_scr >= 0 && - w->xinerama_scr < ps->xinerama_nscrs) - // There can be a window where number of screens - // is updated, but the screen number attached to - // the windows have not. + if (ps->o.crop_shadow_to_monitor && w->randr_monitor >= 0 && + w->randr_monitor < ps->monitors.count) { + // There can be a window where number of monitors is + // updated, but the monitor number attached to the window + // have not. // - // Window screen number will be updated - // eventually, so here we just check to make sure - // we don't access out of bounds. + // Window monitor number will be updated eventually, so + // here we just check to make sure we don't access out of + // bounds. pixman_region32_intersect( ®_tmp, ®_tmp, - &ps->xinerama_scr_regs[w->xinerama_scr]); + &ps->monitors.regions[w->randr_monitor]); + } // Detect if the region is empty before painting if (pixman_region32_not_empty(®_tmp)) { @@ -1117,7 +1143,7 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { } // Only clip shadows above visible windows - if (w->opacity * MAX_ALPHA >= 1) { + if (animatable_get(&w->opacity) * MAX_ALPHA >= 1) { if (w->clip_shadow_above) { // Add window bounds to shadow-clip region pixman_region32_union(®_shadow_clip, ®_shadow_clip, @@ -1147,8 +1173,8 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { if (w->corner_radius > 0 && ps->o.backend == BKEND_GLX) { const int16_t x = w->g.x; const int16_t y = w->g.y; - const auto wid = to_u16_checked(w->widthb); - const auto hei = to_u16_checked(w->heightb); + auto const wid = to_u16_checked(w->widthb); + auto const hei = to_u16_checked(w->heightb); glx_bind_texture(ps, &w->glx_texture_bg, x, y, wid, hei); } #endif @@ -1168,8 +1194,8 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { // Rounded corners for XRender is implemented inside render() // Round window corners if (w->corner_radius > 0 && ps->o.backend == BKEND_GLX) { - const auto wid = to_u16_checked(w->widthb); - const auto hei = to_u16_checked(w->heightb); + auto const wid = to_u16_checked(w->widthb); + auto const hei = to_u16_checked(w->heightb); glx_round_corners_dst(ps, w, w->glx_texture_bg, w->g.x, w->g.y, wid, hei, (float)ps->psglx->z - 0.5F, @@ -1184,11 +1210,7 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { pixman_region32_fini(®_shadow_clip); // Move the head of the damage ring - ps->damage = ps->damage - 1; - if (ps->damage < ps->damage_ring) { - ps->damage = ps->damage_ring + ps->ndamage - 1; - } - pixman_region32_clear(ps->damage); + damage_ring_advance(&ps->damage_ring); // Do this as early as possible set_tgt_clip(ps, &ps->screen_reg); @@ -1196,13 +1218,14 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { if (ps->o.vsync) { // Make sure all previous requests are processed to achieve best // effect - x_sync(ps->c); + xcb_aux_sync(ps->c.c); #ifdef CONFIG_OPENGL if (glx_has_context(ps)) { - if (ps->o.vsync_use_glfinish) + if (ps->o.vsync_use_glfinish) { glFinish(); - else + } else { glFlush(); + } glXWaitX(); } #endif @@ -1223,57 +1246,63 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { // First we create a new picture, and copy content from the buffer // to it - auto pictfmt = x_get_pictform_for_visual(ps->c, ps->vis); + auto pictfmt = x_get_pictform_for_visual( + &ps->c, ps->c.screen_info->root_visual); xcb_render_picture_t new_pict = x_create_picture_with_pictfmt( - ps->c, ps->root, rwidth, rheight, pictfmt, 0, NULL); - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, + &ps->c, rwidth, rheight, pictfmt->id, pictfmt->depth, 0, NULL); + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_SRC, ps->tgt_buffer.pict, XCB_NONE, new_pict, 0, 0, 0, 0, 0, 0, rwidth, rheight); // Next, we set the region of paint and highlight it - x_set_picture_clip_region(ps->c, new_pict, 0, 0, ®ion); - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_OVER, ps->white_picture, + x_set_picture_clip_region(&ps->c, new_pict, 0, 0, ®ion); + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_OVER, ps->white_picture, ps->alpha_picts[MAX_ALPHA / 2], new_pict, 0, 0, 0, 0, 0, 0, rwidth, rheight); // Finally, clear clip regions of new_pict and the screen, and put // the whole thing on screen - x_set_picture_clip_region(ps->c, new_pict, 0, 0, &ps->screen_reg); - x_set_picture_clip_region(ps->c, ps->tgt_picture, 0, 0, &ps->screen_reg); - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, new_pict, + x_set_picture_clip_region(&ps->c, new_pict, 0, 0, &ps->screen_reg); + x_set_picture_clip_region(&ps->c, ps->tgt_picture, 0, 0, + &ps->screen_reg); + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_SRC, new_pict, XCB_NONE, ps->tgt_picture, 0, 0, 0, 0, 0, 0, rwidth, rheight); - xcb_render_free_picture(ps->c, new_pict); - } else - xcb_render_composite(ps->c, XCB_RENDER_PICT_OP_SRC, + x_free_picture(&ps->c, new_pict); + } else { + xcb_render_composite(ps->c.c, XCB_RENDER_PICT_OP_SRC, ps->tgt_buffer.pict, XCB_NONE, ps->tgt_picture, 0, 0, 0, 0, 0, 0, rwidth, rheight); + } break; #ifdef CONFIG_OPENGL case BKEND_XR_GLX_HYBRID: - x_sync(ps->c); - if (ps->o.vsync_use_glfinish) + xcb_aux_sync(ps->c.c); + if (ps->o.vsync_use_glfinish) { glFinish(); - else + } else { glFlush(); + } glXWaitX(); assert(ps->tgt_buffer.pixmap); paint_bind_tex(ps, &ps->tgt_buffer, ps->root_width, ps->root_height, - false, ps->depth, ps->vis, !ps->o.glx_no_rebind_pixmap); - if (ps->o.vsync_use_glfinish) + false, ps->c.screen_info->root_depth, + ps->c.screen_info->root_visual, !ps->o.glx_no_rebind_pixmap); + if (ps->o.vsync_use_glfinish) { glFinish(); - else + } else { glFlush(); + } glXWaitX(); glx_render(ps, ps->tgt_buffer.ptex, 0, 0, 0, 0, ps->root_width, ps->root_height, 0, 1.0, false, false, ®ion, NULL); fallthrough(); - case BKEND_GLX: glXSwapBuffers(ps->dpy, get_tgt_window(ps)); break; + case BKEND_GLX: glXSwapBuffers(ps->c.dpy, get_tgt_window(ps)); break; #endif default: assert(0); } - x_sync(ps->c); + xcb_aux_sync(ps->c.c); #ifdef CONFIG_OPENGL if (glx_has_context(ps)) { @@ -1303,7 +1332,7 @@ void paint_all(session_t *ps, struct managed_win *t, bool ignore_damage) { static bool xr_init_blur(session_t *ps) { // Query filters xcb_render_query_filters_reply_t *pf = xcb_render_query_filters_reply( - ps->c, xcb_render_query_filters(ps->c, get_tgt_window(ps)), NULL); + ps->c.c, xcb_render_query_filters(ps->c.c, get_tgt_window(ps)), NULL); if (pf) { xcb_str_iterator_t iter = xcb_render_query_filters_filters_iterator(pf); for (; iter.rem; xcb_str_next(&iter)) { @@ -1311,8 +1340,9 @@ static bool xr_init_blur(session_t *ps) { char *name = xcb_str_name(iter.data); // Check for the convolution filter if (strlen(XRFILTER_CONVOLUTION) == len && - !memcmp(XRFILTER_CONVOLUTION, name, strlen(XRFILTER_CONVOLUTION))) + !memcmp(XRFILTER_CONVOLUTION, name, strlen(XRFILTER_CONVOLUTION))) { ps->xrfilter_convolution_exists = true; + } } free(pf); } @@ -1336,9 +1366,10 @@ static bool init_alpha_picts(session_t *ps) { for (int i = 0; i <= MAX_ALPHA; ++i) { double o = (double)i / MAX_ALPHA; - ps->alpha_picts[i] = solid_picture(ps->c, ps->root, false, o, 0, 0, 0); - if (ps->alpha_picts[i] == XCB_NONE) + ps->alpha_picts[i] = solid_picture(&ps->c, false, o, 0, 0, 0); + if (ps->alpha_picts[i] == XCB_NONE) { return false; + } } return true; } @@ -1350,12 +1381,13 @@ bool init_render(session_t *ps) { // Initialize OpenGL as early as possible #ifdef CONFIG_OPENGL - glxext_init(ps->dpy, ps->scr); + glxext_init(ps->c.dpy, ps->c.screen); #endif if (bkend_use_glx(ps)) { #ifdef CONFIG_OPENGL - if (!glx_init(ps, true)) + if (!glx_init(ps, true)) { return false; + } #else log_error("GLX backend support not compiled in."); return false; @@ -1370,8 +1402,9 @@ bool init_render(session_t *ps) { // Initialize window GL shader if (BKEND_GLX == ps->o.backend && ps->o.glx_fshader_win_str) { #ifdef CONFIG_OPENGL - if (!glx_load_prog_main(NULL, ps->o.glx_fshader_win_str, &ps->glx_prog_win)) + if (!glx_load_prog_main(NULL, ps->o.glx_fshader_win_str, &ps->glx_prog_win)) { return false; + } #else log_error("GLSL supported not compiled in, can't load " "shader."); @@ -1410,8 +1443,8 @@ bool init_render(session_t *ps) { } } - ps->black_picture = solid_picture(ps->c, ps->root, true, 1, 0, 0, 0); - ps->white_picture = solid_picture(ps->c, ps->root, true, 1, 1, 1, 1); + ps->black_picture = solid_picture(&ps->c, true, 1, 0, 0, 0); + ps->white_picture = solid_picture(&ps->c, true, 1, 1, 1, 1); if (ps->black_picture == XCB_NONE || ps->white_picture == XCB_NONE) { log_error("Failed to create solid xrender pictures."); @@ -1423,7 +1456,7 @@ bool init_render(session_t *ps) { if (ps->o.shadow_red == 0 && ps->o.shadow_green == 0 && ps->o.shadow_blue == 0) { ps->cshadow_picture = ps->black_picture; } else { - ps->cshadow_picture = solid_picture(ps->c, ps->root, true, 1, ps->o.shadow_red, + ps->cshadow_picture = solid_picture(&ps->c, true, 1, ps->o.shadow_red, ps->o.shadow_green, ps->o.shadow_blue); if (ps->cshadow_picture == XCB_NONE) { log_error("Failed to create shadow picture."); @@ -1449,14 +1482,14 @@ bool init_render(session_t *ps) { * Free root tile related things. */ void free_root_tile(session_t *ps) { - free_picture(ps->c, &ps->root_tile_paint.pict); + x_free_picture(&ps->c, ps->root_tile_paint.pict); #ifdef CONFIG_OPENGL free_texture(ps, &ps->root_tile_paint.ptex); #else assert(!ps->root_tile_paint.ptex); #endif if (ps->root_tile_fill) { - xcb_free_pixmap(ps->c, ps->root_tile_paint.pixmap); + xcb_free_pixmap(ps->c.c, ps->root_tile_paint.pixmap); ps->root_tile_paint.pixmap = XCB_NONE; } ps->root_tile_paint.pixmap = XCB_NONE; @@ -1465,25 +1498,26 @@ void free_root_tile(session_t *ps) { void deinit_render(session_t *ps) { // Free alpha_picts - for (int i = 0; i <= MAX_ALPHA; ++i) - free_picture(ps->c, &ps->alpha_picts[i]); + for (int i = 0; i <= MAX_ALPHA; ++i) { + x_free_picture(&ps->c, ps->alpha_picts[i]); + } free(ps->alpha_picts); ps->alpha_picts = NULL; // Free cshadow_picture and black_picture - if (ps->cshadow_picture == ps->black_picture) - ps->cshadow_picture = XCB_NONE; - else - free_picture(ps->c, &ps->cshadow_picture); + if (ps->cshadow_picture != ps->black_picture) { + x_free_picture(&ps->c, ps->cshadow_picture); + } - free_picture(ps->c, &ps->black_picture); - free_picture(ps->c, &ps->white_picture); + x_free_picture(&ps->c, ps->black_picture); + x_free_picture(&ps->c, ps->white_picture); + ps->cshadow_picture = ps->black_picture = ps->white_picture = XCB_NONE; // Free other X resources free_root_tile(ps); #ifdef CONFIG_OPENGL - free(ps->root_tile_paint.fbcfg); + ps->root_tile_paint.fbcfg = (struct glx_fbconfig_info){0}; if (bkend_use_glx(ps)) { glx_destroy(ps); } diff --git a/src/render.h b/src/render.h index 95a46dbee0..62258f0edb 100644 --- a/src/render.h +++ b/src/render.h @@ -21,7 +21,7 @@ typedef struct paint { xcb_render_picture_t pict; glx_texture_t *ptex; #ifdef CONFIG_OPENGL - struct glx_fbconfig_info *fbcfg; + struct glx_fbconfig_info fbcfg; #endif } paint_t; @@ -37,9 +37,7 @@ void render(session_t *ps, int x, int y, int dx, int dy, int w, int h, int fullw const glx_prog_main_t *pprogram, clip_t *clip); void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint); -void paint_all(session_t *ps, struct managed_win *const t, bool ignore_damage); - -void free_picture(xcb_connection_t *c, xcb_render_picture_t *p); +void paint_all(session_t *ps, struct managed_win *const t); void free_paint(session_t *ps, paint_t *ppaint); void free_root_tile(session_t *ps); diff --git a/src/renderer/command_builder.c b/src/renderer/command_builder.c new file mode 100644 index 0000000000..0dc8067c1e --- /dev/null +++ b/src/renderer/command_builder.c @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#include "command_builder.h" + +#include "common.h" +#include "layout.h" +#include "win.h" + +/// Generate commands for rendering the body of the window in `layer`. +/// +/// @param[in] frame_region frame region of the window, in window local coordinates +/// @param[out] cmd output commands, when multiple commands are generated, +/// it's stored in `cmd` going backwards, i.e. cmd - 1, -2, ... +/// @return number of commands generated +static inline unsigned +commands_for_window_body(struct layer *layer, struct backend_command *cmd, + const region_t *frame_region, bool inactive_dim_fixed, + double inactive_dim, double max_brightness) { + auto w = layer->win; + auto mode = win_calc_mode_raw(layer->win); + int border_width = w->g.border_width; + double dim = 0; + if (w->dim) { + dim = inactive_dim; + if (!inactive_dim_fixed) { + dim *= layer->opacity; + } + } + if (border_width == 0) { + // Some WM has borders implemented as WM frames + border_width = min3(w->frame_extents.left, w->frame_extents.right, + w->frame_extents.bottom); + } + ivec2 raw_size = {.width = w->widthb, .height = w->heightb}; + pixman_region32_copy(&cmd->target_mask, &w->bounding_shape); + pixman_region32_translate(&cmd->target_mask, layer->origin.x, layer->origin.y); + if (w->frame_opacity < 1) { + pixman_region32_subtract(&cmd->target_mask, &cmd->target_mask, frame_region); + } + pixman_region32_init(&cmd->opaque_region); + if ((mode == WMODE_SOLID || mode == WMODE_FRAME_TRANS) && layer->opacity == 1.0) { + pixman_region32_copy(&cmd->opaque_region, &cmd->target_mask); + if (mode == WMODE_FRAME_TRANS) { + pixman_region32_subtract(&cmd->opaque_region, &cmd->opaque_region, + frame_region); + } + } + if (w->corner_radius > 0) { + win_region_remove_corners(w, layer->origin, &cmd->opaque_region); + } + cmd->op = BACKEND_COMMAND_BLIT; + cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; + cmd->origin = layer->origin; + cmd->blit = (struct backend_blit_args){ + .border_width = border_width, + .target_mask = &cmd->target_mask, + .corner_radius = w->corner_radius, + .opacity = layer->opacity, + .dim = dim, + .effective_size = raw_size, + .shader = w->fg_shader ? w->fg_shader->backend_shader : NULL, + .color_inverted = w->invert_color, + .source_mask = NULL, + .max_brightness = max_brightness}; + + if (w->frame_opacity == 1 || w->frame_opacity == 0) { + return 1; + } + cmd -= 1; + + pixman_region32_copy(&cmd->target_mask, frame_region); + pixman_region32_init(&cmd->opaque_region); + cmd->op = BACKEND_COMMAND_BLIT; + cmd->origin = layer->origin; + cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; + cmd->blit = cmd[1].blit; + cmd->blit.target_mask = &cmd->target_mask; + cmd->blit.opacity *= w->frame_opacity; + return 2; +} + +/// Generate render command for the shadow in `layer` +/// +/// @param[in] end the end of the commands generated for this `layer`. +static inline unsigned +command_for_shadow(struct layer *layer, struct backend_command *cmd, + const struct win_option *wintype_options, + const struct x_monitors *monitors, const struct backend_command *end) { + auto w = layer->win; + if (!w->shadow) { + return 0; + } + cmd->op = BACKEND_COMMAND_BLIT; + cmd->origin = layer->shadow_origin; + cmd->source = BACKEND_COMMAND_SOURCE_SHADOW; + pixman_region32_clear(&cmd->target_mask); + pixman_region32_union_rect(&cmd->target_mask, &cmd->target_mask, + layer->shadow_origin.x, layer->shadow_origin.y, + (unsigned)layer->shadow_size.width, + (unsigned)layer->shadow_size.height); + log_trace("Calculate shadow for %#010x (%s)", w->base.id, w->name); + log_region(TRACE, &cmd->target_mask); + if (!wintype_options[w->window_type].full_shadow) { + // We need to not draw under the window + // From this command up, until the next WINDOW_START + // should be blits for the current window. + for (auto j = cmd + 1; j != end; j++) { + assert(j->op == BACKEND_COMMAND_BLIT); + assert(j->source == BACKEND_COMMAND_SOURCE_WINDOW); + if (j->blit.corner_radius == 0) { + pixman_region32_subtract( + &cmd->target_mask, &cmd->target_mask, &j->target_mask); + } else { + region_t mask_without_corners; + pixman_region32_init(&mask_without_corners); + pixman_region32_copy(&mask_without_corners, &j->target_mask); + win_region_remove_corners(layer->win, j->origin, + &mask_without_corners); + pixman_region32_subtract(&cmd->target_mask, &cmd->target_mask, + &mask_without_corners); + pixman_region32_fini(&mask_without_corners); + } + } + } + log_region(TRACE, &cmd->target_mask); + if (monitors && w->randr_monitor >= 0 && w->randr_monitor < monitors->count) { + pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, + &monitors->regions[w->randr_monitor]); + } + log_region(TRACE, &cmd->target_mask); + if (w->corner_radius > 0) { + cmd->source_mask.corner_radius = w->corner_radius; + cmd->source_mask.inverted = true; + cmd->source_mask.origin = ivec2_sub(layer->origin, layer->shadow_origin); + } + cmd->blit = (struct backend_blit_args){ + .opacity = layer->shadow_opacity, + .max_brightness = 1, + .source_mask = w->corner_radius > 0 ? &cmd->source_mask : NULL, + .effective_size = layer->shadow_size, + .target_mask = &cmd->target_mask, + }; + pixman_region32_init(&cmd->opaque_region); + return 1; +} + +static inline unsigned +command_for_blur(struct layer *layer, struct backend_command *cmd, + const region_t *frame_region, bool force_blend, bool blur_frame) { + auto w = layer->win; + auto mode = win_calc_mode_raw(w); + if (!w->blur_background || layer->blur_opacity == 0) { + return 0; + } + if (force_blend || mode == WMODE_TRANS || layer->opacity < 1.0) { + pixman_region32_copy(&cmd->target_mask, &w->bounding_shape); + pixman_region32_translate(&cmd->target_mask, layer->origin.x, + layer->origin.y); + } else if (blur_frame && mode == WMODE_FRAME_TRANS) { + pixman_region32_copy(&cmd->target_mask, frame_region); + } else { + return 0; + } + cmd->op = BACKEND_COMMAND_BLUR; + cmd->origin = (ivec2){}; + if (w->corner_radius > 0) { + cmd->source_mask.origin = (ivec2){.x = layer->origin.x, .y = layer->origin.y}; + cmd->source_mask.corner_radius = w->corner_radius; + cmd->source_mask.inverted = false; + } + cmd->blur = (struct backend_blur_args){ + .opacity = layer->blur_opacity, + .target_mask = &cmd->target_mask, + .source_mask = w->corner_radius > 0 ? &cmd->source_mask : NULL, + }; + return 1; +} + +static inline void +command_builder_apply_transparent_clipping(struct layout *layout, region_t *scratch_region) { + // Going from top down, apply transparent-clipping + if (layout->len == 0) { + return; + } + + pixman_region32_clear(scratch_region); + auto end = &layout->commands[layout->number_of_commands - 1]; + auto begin = &layout->commands[layout->first_layer_start - 1]; + auto layer = &layout->layers[layout->len - 1]; + // `layer_start` is one before the first command for this layer + auto layer_start = end - layer->number_of_commands; + for (auto i = end; i != begin; i--) { + if (i == layer_start) { + if (layer->win->transparent_clipping) { + auto win = layer->win; + auto mode = win_calc_mode_raw(layer->win); + region_t tmp; + pixman_region32_init(&tmp); + if (mode == WMODE_TRANS || layer->opacity < 1.0) { + pixman_region32_copy(&tmp, &win->bounding_shape); + } else if (mode == WMODE_FRAME_TRANS) { + win_get_region_frame_local(win, &tmp); + } + pixman_region32_translate(&tmp, layer->origin.x, + layer->origin.y); + pixman_region32_union(scratch_region, scratch_region, &tmp); + pixman_region32_fini(&tmp); + } + layer -= 1; + layer_start -= layer->number_of_commands; + } + + if (i->op == BACKEND_COMMAND_BLUR || + (i->op == BACKEND_COMMAND_BLIT && + i->source != BACKEND_COMMAND_SOURCE_BACKGROUND)) { + pixman_region32_subtract(&i->target_mask, &i->target_mask, + scratch_region); + } + if (i->op == BACKEND_COMMAND_BLIT && + i->source != BACKEND_COMMAND_SOURCE_BACKGROUND) { + pixman_region32_subtract(&i->opaque_region, &i->opaque_region, + scratch_region); + } + } +} +static inline void +command_builder_apply_shadow_clipping(struct layout *layout, region_t *scratch_region) { + // Going from bottom up, apply clipping-shadow-above + pixman_region32_clear(scratch_region); + auto begin = &layout->commands[layout->first_layer_start]; + auto end = &layout->commands[layout->number_of_commands]; + auto layer = layout->layers - 1; + // `layer_end` is one after the last command for this layer + auto layer_end = begin; + bool clip_shadow_above = false; + for (auto i = begin; i != end; i++) { + if (i == layer_end) { + layer += 1; + layer_end += layer->number_of_commands; + clip_shadow_above = layer->win->clip_shadow_above; + } + + if (i->op == BACKEND_COMMAND_BLUR) { + pixman_region32_subtract(scratch_region, scratch_region, + &i->target_mask); + } else if (i->op == BACKEND_COMMAND_BLIT) { + if (i->source == BACKEND_COMMAND_SOURCE_SHADOW) { + pixman_region32_subtract(&i->target_mask, &i->target_mask, + scratch_region); + } else if (i->source == BACKEND_COMMAND_SOURCE_WINDOW && + clip_shadow_above) { + pixman_region32_union(scratch_region, scratch_region, + &i->target_mask); + } + } + } +} + +struct command_builder { + region_t scratch_region; + struct list_node free_command_lists; +}; + +struct command_list { + struct list_node free_list; + unsigned capacity; + struct command_builder *super; + struct backend_command commands[]; +}; + +static struct command_list * +command_builder_command_list_new(struct command_builder *cb, unsigned ncmds) { + const auto size = sizeof(struct command_list) + sizeof(struct backend_command[ncmds]); + struct command_list *list = NULL; + unsigned capacity = 0; + if (!list_is_empty(&cb->free_command_lists)) { + list = list_entry(cb->free_command_lists.next, struct command_list, free_list); + capacity = list->capacity; + list_remove(&list->free_list); + } + if (capacity < ncmds || capacity / 2 > ncmds) { + for (unsigned i = ncmds; i < capacity; i++) { + pixman_region32_fini(&list->commands[i].target_mask); + } + + struct command_list *new_list = realloc(list, size); + allocchk(new_list); + list = new_list; + list_init_head(&list->free_list); + list->capacity = ncmds; + list->super = cb; + + for (unsigned i = capacity; i < ncmds; i++) { + list->commands[i].op = BACKEND_COMMAND_INVALID; + pixman_region32_init(&list->commands[i].target_mask); + } + } + return list; +} + +void command_builder_command_list_free(struct backend_command *cmds) { + if (!cmds) { + return; + } + + auto list = container_of(cmds, struct command_list, commands[0]); + for (unsigned i = 0; i < list->capacity; i++) { + auto cmd = &list->commands[i]; + if (cmd->op == BACKEND_COMMAND_BLIT) { + pixman_region32_fini(&cmd->opaque_region); + } + cmd->op = BACKEND_COMMAND_INVALID; + } + list_insert_after(&list->super->free_command_lists, &list->free_list); +} + +struct command_builder *command_builder_new(void) { + auto cb = ccalloc(1, struct command_builder); + pixman_region32_init(&cb->scratch_region); + list_init_head(&cb->free_command_lists); + return cb; +} + +void command_builder_free(struct command_builder *cb) { + list_foreach_safe(struct command_list, i, &cb->free_command_lists, free_list) { + list_remove(&i->free_list); + for (unsigned j = 0; j < i->capacity; j++) { + pixman_region32_fini(&i->commands[j].target_mask); + } + free(i); + } + + pixman_region32_fini(&cb->scratch_region); + free(cb); +} + +// TODO(yshui) reduce the number of parameters by storing the final effective parameter +// value in `struct managed_win`. +void command_builder_build(struct command_builder *cb, struct layout *layout, bool force_blend, + bool blur_frame, bool inactive_dim_fixed, double max_brightness, + double inactive_dim, const struct x_monitors *monitors, + const struct win_option *wintype_options) { + + unsigned ncmds = 1; + for (unsigned i = 0; i < layout->len; i++) { + auto layer = &layout->layers[i]; + auto mode = win_calc_mode_raw(layer->win); + if (layer->win->blur_background && layer->blur_opacity > 0 && + (force_blend || mode == WMODE_TRANS || layer->opacity < 1.0 || + (blur_frame && mode == WMODE_FRAME_TRANS))) { + // Needs blur + ncmds += 1; + } + if (layer->win->shadow) { + ncmds += 1; + } + if (layer->win->frame_opacity < 1 && layer->win->frame_opacity > 0) { + // Needs to draw the frame separately + ncmds += 1; + } + ncmds += 1; // window body + } + + auto list = command_builder_command_list_new(cb, ncmds); + layout->commands = list->commands; + + auto cmd = &layout->commands[ncmds - 1]; + for (int i = to_int_checked(layout->len) - 1; i >= 0; i--) { + auto layer = &layout->layers[i]; + auto last = cmd; + auto frame_region = win_get_region_frame_local_by_val(layer->win); + pixman_region32_translate(&frame_region, layer->origin.x, layer->origin.y); + + // Add window body + cmd -= commands_for_window_body(layer, cmd, &frame_region, inactive_dim_fixed, + inactive_dim, max_brightness); + + // Add shadow + cmd -= command_for_shadow(layer, cmd, wintype_options, monitors, last + 1); + + // Add blur + cmd -= command_for_blur(layer, cmd, &frame_region, force_blend, blur_frame); + + layer->number_of_commands = (unsigned)(last - cmd); + pixman_region32_fini(&frame_region); + } + + // Command for the desktop background + cmd->op = BACKEND_COMMAND_COPY_AREA; + cmd->source = BACKEND_COMMAND_SOURCE_BACKGROUND; + cmd->origin = (ivec2){}; + pixman_region32_reset( + &cmd->target_mask, + (rect_t[]){{.x1 = 0, .y1 = 0, .x2 = layout->size.width, .y2 = layout->size.height}}); + cmd->copy_area.region = &cmd->target_mask; + assert(cmd == list->commands); + + layout->first_layer_start = 1; + layout->number_of_commands = ncmds; + + command_builder_apply_transparent_clipping(layout, &cb->scratch_region); + command_builder_apply_shadow_clipping(layout, &cb->scratch_region); +} diff --git a/src/renderer/command_builder.h b/src/renderer/command_builder.h new file mode 100644 index 0000000000..4672f315f3 --- /dev/null +++ b/src/renderer/command_builder.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#pragma once + +#include "backend/backend.h" +#include "types.h" + +struct command_builder; +struct layout; + +struct command_builder *command_builder_new(void); +void command_builder_free(struct command_builder *); + +void command_builder_command_list_free(struct backend_command *cmds); + +/// Generate render commands that need to be executed to render the current layout. +/// This function updates `layout->commands` with the list of generated commands, and also +/// the `number_of_commands` field of each of the layers in `layout`. The list of +/// commands must later be freed with `command_builder_command_list_free` +/// It is guaranteed that each of the command's region of operation (e.g. the mask.region +/// argument of blit), will be store in `struct backend_command::mask`. This might not +/// stay true after further passes. +void command_builder_build(struct command_builder *cb, struct layout *layout, bool force_blend, + bool blur_frame, bool inactive_dim_fixed, double max_brightness, + double inactive_dim, const struct x_monitors *monitors, + const struct win_option *wintype_options); diff --git a/src/renderer/damage.c b/src/renderer/damage.c new file mode 100644 index 0000000000..dd17efd693 --- /dev/null +++ b/src/renderer/damage.c @@ -0,0 +1,357 @@ +#include "damage.h" + +#include "layout.h" +#include "region.h" +#include "win.h" +static inline bool attr_unused layer_key_eq(const struct layer_key *a, + const struct layer_key *b) { + if (!a->generation || !b->generation) { + return false; + } + return a->window == b->window && a->generation == b->generation; +} + +/// Compare two layers that contain the same window, return if they are the "same". Same +/// means these two layers are render in the same way at the same position, with the only +/// possible differences being the contents inside the window. +static bool +layer_compare(const struct layer *past_layer, const struct backend_command *past_layer_cmd, + const struct layer *curr_layer, const struct backend_command *curr_layer_cmd) { + if (past_layer->origin.x != curr_layer->origin.x || + past_layer->origin.y != curr_layer->origin.y || + past_layer->size.width != curr_layer->size.width || + past_layer->size.height != curr_layer->size.height) { + // Window moved or size changed + return false; + } + + if (past_layer->shadow_origin.x != curr_layer->shadow_origin.x || + past_layer->shadow_origin.y != curr_layer->shadow_origin.y || + past_layer->shadow_size.width != curr_layer->shadow_size.width || + past_layer->shadow_size.height != curr_layer->shadow_size.height) { + // Shadow moved or size changed + return false; + } + if (past_layer->number_of_commands != curr_layer->number_of_commands) { + // Number of render commands changed. We are being conservative + // here, because even though the number of commands changed, we can still + // try to match them up. For example, maybe this window just has shadow + // disabled, but other commands are still the same. We are not do that + // here, this could be a TODO + // TODO(yshui) match render commands here + return false; + } + + for (unsigned i = 0; i < past_layer->number_of_commands; i++) { + auto cmd1 = &past_layer_cmd[i]; + auto cmd2 = &curr_layer_cmd[i]; + if (cmd1->op != cmd2->op || !ivec2_eq(cmd1->origin, cmd2->origin) || + cmd1->source != cmd2->source) { + return false; + } + } + return true; +} + +/// Add all regions of `layer`'s commands to `region` +static inline void region_union_render_layer(region_t *region, const struct layer *layer, + const struct backend_command *cmds) { + for (auto i = cmds; i < &cmds[layer->number_of_commands]; i++) { + pixman_region32_union(region, region, &i->target_mask); + } +} + +static inline void +command_blit_damage(region_t *damage, region_t *scratch_region, struct backend_command *cmd1, + struct backend_command *cmd2, const struct layout_manager *lm, + unsigned layer_index, unsigned buffer_age) { + // clang-format off + // First part, if any blit argument that would affect the whole image changed + if (cmd1->blit.dim != cmd2->blit.dim || + cmd1->blit.shader != cmd2->blit.shader || + cmd1->blit.opacity != cmd2->blit.opacity || + cmd1->blit.corner_radius != cmd2->blit.corner_radius || + cmd1->blit.max_brightness != cmd2->blit.max_brightness || + cmd1->blit.color_inverted != cmd2->blit.color_inverted || + + // Second part, if round corner is enabled, then border width and effective size + // affect the whole image too. + (cmd1->blit.corner_radius > 0 && + (cmd1->blit.border_width != cmd2->blit.border_width || + !ivec2_eq(cmd1->blit.effective_size, cmd2->blit.effective_size))) + ) + { + pixman_region32_union(damage, damage, &cmd1->target_mask); + pixman_region32_union(damage, damage, &cmd2->target_mask); + return; + } + // clang-format on + + if (cmd1->blit.opacity == 0) { + return; + } + + // Damage from layers below that is covered up by the current layer, won't be + // visible. So remove them. + pixman_region32_subtract(damage, damage, &cmd2->opaque_region); + region_symmetric_difference_local(damage, scratch_region, &cmd1->target_mask, + &cmd2->target_mask); + if (cmd1->source == BACKEND_COMMAND_SOURCE_WINDOW) { + layout_manager_collect_window_damage(lm, layer_index, buffer_age, + scratch_region); + pixman_region32_intersect(scratch_region, scratch_region, &cmd1->target_mask); + pixman_region32_intersect(scratch_region, scratch_region, &cmd2->target_mask); + pixman_region32_union(damage, damage, scratch_region); + } +} + +static inline void command_blur_damage(region_t *damage, region_t *scratch_region, + struct backend_command *cmd1, + struct backend_command *cmd2, ivec2 blur_size) { + if (cmd1->blur.opacity != cmd2->blur.opacity) { + pixman_region32_union(damage, damage, &cmd1->target_mask); + pixman_region32_union(damage, damage, &cmd2->target_mask); + return; + } + if (cmd1->blur.opacity == 0) { + return; + } + region_symmetric_difference_local(damage, scratch_region, &cmd1->target_mask, + &cmd2->target_mask); + + // We need to expand the damage region underneath the blur. Because blur + // "diffuses" the changes from below. + pixman_region32_copy(scratch_region, damage); + resize_region_in_place(scratch_region, blur_size.width, blur_size.height); + pixman_region32_intersect(scratch_region, scratch_region, &cmd2->target_mask); + pixman_region32_union(damage, damage, scratch_region); +} + +/// Do the first step of render planning, collecting damages and calculating which +/// parts of the final screen will be affected by the damages. +void layout_manager_damage(struct layout_manager *lm, unsigned buffer_age, + ivec2 blur_size, region_t *damage) { + log_trace("Damage for buffer age %d", buffer_age); + unsigned past_layer_rank = 0, curr_layer_rank = 0; + auto past_layout = layout_manager_layout(lm, buffer_age); + auto curr_layout = layout_manager_layout(lm, 0); + auto past_layer = &past_layout->layers[past_layer_rank]; + auto curr_layer = &curr_layout->layers[curr_layer_rank]; + auto past_layer_cmd = &past_layout->commands[past_layout->first_layer_start]; + auto curr_layer_cmd = &curr_layout->commands[curr_layout->first_layer_start]; + region_t scratch_region; + pixman_region32_init(&scratch_region); + pixman_region32_clear(damage); + if (past_layout->size.width != curr_layout->size.width || + past_layout->size.height != curr_layout->size.height || + past_layout->root_image_generation != curr_layout->root_image_generation) { + pixman_region32_union_rect(damage, damage, 0, 0, + (unsigned)curr_layout->size.width, + (unsigned)curr_layout->size.height); + return; + } + if (log_get_level_tls() <= LOG_LEVEL_TRACE) { + log_trace("Comparing across %d layouts:", buffer_age); + for (unsigned l = 0; l <= buffer_age; l++) { + log_trace("Layout[%d]: ", -l); + auto layout = layout_manager_layout(lm, l); + for (unsigned i = 0; i < layout->len; i++) { + log_trace( + "\t%#010x %dx%d+%dx%d (prev %d, next %d)", + layout->layers[i].key.window, layout->layers[i].size.width, + layout->layers[i].size.height, + layout->layers[i].origin.x, layout->layers[i].origin.y, + layout->layers[i].prev_rank, layout->layers[i].next_rank); + } + } + } + + // Explanation of what's happening here. We want to get damage by comparing + // `past_layout` and `curr_layout` But windows in them could be different. And + // comparing different windows doesn't really make sense. So we want to "align" + // the layouts so we compare matching windows and skip over non-matching ones. For + // example, say past layout has window "ABCDE"; and in current layout, window C is + // closed, and F is opened: "ABDFE", we want to align them like this: + // ABCD E + // AB DFE + // Note there can be multiple ways of aligning windows, some of them are not + // optimal. For example, in layout "ABCDEFG", if we move B to after F: "ACDEFBG", + // we want to align them like this: + // ABCDEF G + // A CDEFBG + // not like this: + // A BCDEFG + // ACDEFB G + // + // This is the classic Longest Common Sequence (LCS) problem, but we are not doing + // a full LCS algorithm here. Since damage is calculated every frame, there is + // likely not a lot of changes between the two layouts. We use a simple linear + // time greedy approximation that should work well enough in those cases. + + for (;; past_layer_rank += 1, curr_layer_rank += 1, + past_layer_cmd += past_layer->number_of_commands, + curr_layer_cmd += curr_layer->number_of_commands, past_layer += 1, + curr_layer += 1) { + int past_layer_curr_rank = -1, curr_layer_past_rank = -1; + unsigned past_layer_rank_target = past_layer_rank, + curr_layer_rank_target = curr_layer_rank; + log_region(TRACE, damage); + + // Skip layers in the past layout doesn't contain a window that has a + // match in the remaining layers of the current layout; and vice versa. + while (past_layer_rank_target < past_layout->len) { + past_layer_curr_rank = + layer_next_rank(lm, buffer_age, past_layer_rank_target); + if (past_layer_curr_rank >= (int)curr_layer_rank) { + break; + } + past_layer_rank_target++; + }; + while (curr_layer_rank_target < curr_layout->len) { + curr_layer_past_rank = + layer_prev_rank(lm, buffer_age, curr_layer_rank_target); + if (curr_layer_past_rank >= (int)past_layer_rank) { + break; + } + curr_layer_rank_target++; + }; + + // past_layer_curr_rank/curr_layer_past_rank can be -1 + if (past_layer_curr_rank >= (int)curr_layer_rank || + curr_layer_past_rank >= (int)past_layer_rank) { + // Now past_layer_current_rank and current_layer_past_rank both + // have a matching layer in the other layout. We check which side + // has less layers to skip. + assert((unsigned)curr_layer_past_rank >= past_layer_rank_target); + assert((unsigned)past_layer_curr_rank >= curr_layer_rank_target); + // How many layers will be skipped on either side if we move + // past_layer_rank to past_layer_rank_target. And vice versa. + auto skipped_using_past_target = + past_layer_rank_target - past_layer_rank + + ((unsigned)past_layer_curr_rank - curr_layer_rank); + auto skipped_using_curr_target = + curr_layer_rank_target - curr_layer_rank + + ((unsigned)curr_layer_past_rank - past_layer_rank); + if (skipped_using_curr_target < skipped_using_past_target) { + past_layer_rank_target = (unsigned)curr_layer_past_rank; + } else { + curr_layer_rank_target = (unsigned)past_layer_curr_rank; + } + } + + // For the skipped layers, we need to add them to the damage region. + for (; past_layer_rank < past_layer_rank_target; past_layer_rank++) { + region_union_render_layer(damage, past_layer, past_layer_cmd); + past_layer_cmd += past_layer->number_of_commands; + past_layer += 1; + } + for (; curr_layer_rank < curr_layer_rank_target; curr_layer_rank++) { + region_union_render_layer(damage, curr_layer, curr_layer_cmd); + curr_layer_cmd += curr_layer->number_of_commands; + curr_layer += 1; + } + + if (past_layer_rank >= past_layout->len || curr_layer_rank >= curr_layout->len) { + // No more matching layers left. + assert(past_layer_rank >= past_layout->len && + curr_layer_rank >= curr_layout->len); + break; + } + + assert(layer_key_eq(&past_layer->key, &curr_layer->key)); + log_trace("%#010x == %#010x %s", past_layer->key.window, + curr_layer->key.window, curr_layer->win->name); + + if (!layer_compare(past_layer, past_layer_cmd, curr_layer, curr_layer_cmd)) { + region_union_render_layer(damage, curr_layer, curr_layer_cmd); + region_union_render_layer(damage, past_layer, past_layer_cmd); + continue; + } + + // Layers are otherwise identical besides the window content. We will + // process their render command and add appropriate damage. + log_trace("Adding window damage"); + for (struct backend_command *cmd1 = past_layer_cmd, *cmd2 = curr_layer_cmd; + cmd1 < past_layer_cmd + past_layer->number_of_commands; cmd1++, cmd2++) { + switch (cmd1->op) { + case BACKEND_COMMAND_BLIT: + command_blit_damage(damage, &scratch_region, cmd1, cmd2, + lm, curr_layer_rank, buffer_age); + break; + case BACKEND_COMMAND_BLUR: + command_blur_damage(damage, &scratch_region, cmd1, cmd2, + blur_size); + break; + default: assert(false); + } + } + } + pixman_region32_fini(&scratch_region); +} + +void commands_cull_with_damage(struct layout *layout, const region_t *damage, + ivec2 blur_size, region_t *culled_mask) { + // This may sound silly, and probably actually is. Why do GPU's job on the CPU? + // Isn't the GPU supposed to be the one that does culling, depth testing etc.? + // + // Well, the things is the compositor is a bit special which makes this a bit + // hard. First of all, each window is its own texture. If we bundle them in one + // draw call, we might run into texture unit limits. If we don't bundle them, + // then because we draw things bottom up, depth testing is pointless. Maybe we + // can draw consecutive opaque windows top down with depth test, which will work + // on OpenGL. But xrender won't like it. So that would be backend specific. + // + // Which is to say, there might be better way of utilizing the GPU for this, but + // that will be complicated. And being a compositor makes doing this on CPU + // easier, we only need to handle a dozen axis aligned rectangles, not hundreds of + // thousands of triangles. So this is what we are stuck with for now. + region_t scratch_region, tmp; + pixman_region32_init(&scratch_region); + pixman_region32_init(&tmp); + // scratch_region stores the visible damage region of the screen at the current + // layer. at the top most layer, all of damage is visible + pixman_region32_copy(&scratch_region, damage); + for (int i = to_int_checked(layout->number_of_commands - 1); i >= 0; i--) { + auto cmd = &layout->commands[i]; + pixman_region32_copy(&culled_mask[i], &cmd->target_mask); + pixman_region32_intersect(&culled_mask[i], &culled_mask[i], &scratch_region); + switch (cmd->op) { + case BACKEND_COMMAND_BLIT: + pixman_region32_subtract(&scratch_region, &scratch_region, + &cmd->opaque_region); + cmd->blit.target_mask = &culled_mask[i]; + break; + case BACKEND_COMMAND_COPY_AREA: + pixman_region32_subtract(&scratch_region, &scratch_region, + &cmd->target_mask); + cmd->copy_area.region = &culled_mask[i]; + break; + case BACKEND_COMMAND_BLUR: + // To render blur, the layers below must render pixels surrounding + // the blurred area in this layer. + pixman_region32_copy(&tmp, &scratch_region); + pixman_region32_intersect(&tmp, &tmp, &cmd->target_mask); + resize_region_in_place(&tmp, blur_size.width, blur_size.height); + pixman_region32_union(&scratch_region, &scratch_region, &tmp); + cmd->blur.target_mask = &culled_mask[i]; + break; + case BACKEND_COMMAND_INVALID: assert(false); + } + } + pixman_region32_fini(&tmp); + pixman_region32_fini(&scratch_region); +} + +void commands_uncull(struct layout *layout) { + for (auto i = layout->commands; + i != &layout->commands[layout->number_of_commands]; i++) { + switch (i->op) { + case BACKEND_COMMAND_BLIT: i->blit.target_mask = &i->target_mask; break; + case BACKEND_COMMAND_BLUR: i->blur.target_mask = &i->target_mask; break; + case BACKEND_COMMAND_COPY_AREA: + i->copy_area.region = &i->target_mask; + break; + case BACKEND_COMMAND_INVALID: assert(false); + } + } +} diff --git a/src/renderer/damage.h b/src/renderer/damage.h new file mode 100644 index 0000000000..6be5d07671 --- /dev/null +++ b/src/renderer/damage.h @@ -0,0 +1,39 @@ +#pragma once + +#include "types.h" + +typedef struct pixman_region32 region_t; +struct layout; +struct layout_manager; +struct backend_mask; +/// Remove unnecessary parts of the render commands. +/// +/// After this call, the commands' regions of operations no longer point to their `mask` +/// fields. they point to `culled_mask` instead. The values of their `mask` fields are +/// retained, so later the commands can be "un-culled". +/// +/// @param culled_mask use to stored the culled masks, must be have space to store at +/// least `layout->number_of_commands` elements. They MUST be +/// initialized before calling this function. These masks MUST NOT be +/// freed until you call `commands_uncull`. +void commands_cull_with_damage(struct layout *layout, const region_t *damage, + ivec2 blur_size, region_t *culled_mask); + +/// Un-do the effect of `commands_cull_with_damage` +void commands_uncull(struct layout *layout); + +/// Calculate damage of the screen for the last `buffer_age` layouts. Assuming the +/// current, yet to be rendered frame is numbered frame 0, the previous frame is numbered +/// frame -1, and so on. This function returns the region of the screen that will be +/// different between frame `-buffer_age` and frame 0. The region is in screen +/// coordinates. `buffer_age` is at least 1, and must be less than the `max_buffer_age` +/// passed to the `layout_manager_new` that was used to create `lm`. +/// +/// The layouts you want to calculate damage for must already have commands built for +/// them. `blur_size` is the size of the background blur, and is assumed to not change +/// over time. +/// +/// Note `layout_manager_damage` cannot take desktop background change into +/// account. +void layout_manager_damage(struct layout_manager *lm, unsigned buffer_age, + ivec2 blur_size, region_t *damage); diff --git a/src/renderer/layout.c b/src/renderer/layout.c new file mode 100644 index 0000000000..d8a3eece53 --- /dev/null +++ b/src/renderer/layout.c @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#include +#include + +#include "command_builder.h" +#include "common.h" +#include "list.h" +#include "region.h" +#include "types.h" +#include "utils.h" +#include "win.h" +#include "wm.h" + +#include "layout.h" +struct layer_index { + UT_hash_handle hh; + struct layer_key key; + unsigned index; + struct list_node free_list; +}; +struct layout_manager { + unsigned max_buffer_age; + /// Index of the most recent layout in `layouts`. + unsigned current; + /// Mapping from window to its index in the current layout. + struct layer_index *layer_indices; + struct list_node free_indices; + + // internal + /// Scratch region used for calculations, to avoid repeated allocations. + region_t scratch_region; + /// Current and past layouts, at most `max_buffer_age` layouts are stored. + struct layout layouts[]; +}; + +/// Compute layout of a layer from a window. Returns false if the window is not +/// visible / should not be rendered. `out_layer` is modified either way. +static bool layer_from_window(struct layer *out_layer, struct managed_win *w, ivec2 size) { + bool to_paint = false; + if (!w->ever_damaged || w->paint_excluded) { + goto out; + } + + out_layer->origin = (ivec2){.x = w->g.x, .y = w->g.y}; + out_layer->size = (ivec2){.width = w->widthb, .height = w->heightb}; + if (w->shadow) { + out_layer->shadow_origin = + (ivec2){.x = w->g.x + w->shadow_dx, .y = w->g.y + w->shadow_dy}; + out_layer->shadow_size = + (ivec2){.width = w->shadow_width, .height = w->shadow_height}; + } else { + out_layer->shadow_origin = (ivec2){}; + out_layer->shadow_size = (ivec2){}; + } + if (out_layer->size.width <= 0 || out_layer->size.height <= 0) { + goto out; + } + if (out_layer->size.height + out_layer->origin.y <= 0 || + out_layer->size.width + out_layer->origin.x <= 0 || + out_layer->origin.y >= size.height || out_layer->origin.x >= size.width) { + goto out; + } + + out_layer->opacity = (float)animatable_get(&w->opacity); + out_layer->blur_opacity = (float)animatable_get(&w->blur_opacity); + out_layer->shadow_opacity = + (float)(out_layer->opacity * w->shadow_opacity * w->frame_opacity); + if (out_layer->opacity == 0 && out_layer->blur_opacity == 0) { + goto out; + } + + pixman_region32_copy(&out_layer->damaged, &w->damaged); + pixman_region32_translate(&out_layer->damaged, out_layer->origin.x, + out_layer->origin.y); + // TODO(yshui) Is there a better way to handle shaped windows? Shaped windows can + // have a very large number of rectangles in their shape, we don't want to handle + // that and slow ourselves down. so we treat them as transparent and just use + // their extent rectangle. + out_layer->is_opaque = + !win_has_alpha(w) && out_layer->opacity == 1.0F && !w->bounding_shaped; + out_layer->is_clipping = w->transparent_clipping; + out_layer->next_rank = -1; + out_layer->prev_rank = -1; + out_layer->key = + (struct layer_key){.window = w->base.id, .generation = w->base.generation}; + out_layer->win = w; + to_paint = true; + +out: + pixman_region32_clear(&w->damaged); + return to_paint; +} + +static void layout_deinit(struct layout *layout) { + for (unsigned i = 0; i < layout->len; i++) { + pixman_region32_fini(&layout->layers[i].damaged); + } + free(layout->layers); + command_builder_command_list_free(layout->commands); + *layout = (struct layout){}; +} + +struct layout_manager *layout_manager_new(unsigned max_buffer_age) { + struct layout_manager *planner = malloc( + sizeof(struct layout_manager) + (max_buffer_age + 1) * sizeof(struct layout)); + planner->max_buffer_age = max_buffer_age + 1; + planner->current = 0; + planner->layer_indices = NULL; + list_init_head(&planner->free_indices); + pixman_region32_init(&planner->scratch_region); + for (unsigned i = 0; i <= max_buffer_age; i++) { + planner->layouts[i] = (struct layout){}; + } + return planner; +} + +void layout_manager_free(struct layout_manager *lm) { + for (unsigned i = 0; i < lm->max_buffer_age; i++) { + layout_deinit(&lm->layouts[i]); + } + struct layer_index *index, *tmp; + HASH_ITER(hh, lm->layer_indices, index, tmp) { + HASH_DEL(lm->layer_indices, index); + free(index); + } + list_foreach_safe(struct layer_index, i, &lm->free_indices, free_list) { + list_remove(&i->free_list); + free(i); + } + pixman_region32_fini(&lm->scratch_region); + free(lm); +} + +// ## Layout manager Concepts +// +// - "layer", because windows form a stack, it's easy to think of the final screen as +// a series of layers stacked on top of each other. Each layer is the same size as +// the screen, and contains a single window positioned somewhere in the layer. Other +// parts of the layer are transparent. +// When talking about "screen at a certain layer", we mean the result you would get +// if you stack all layers from the bottom up to that certain layer, ignoring any layers +// above. + +void layout_manager_append_layout(struct layout_manager *lm, struct wm *wm, + uint64_t root_pixmap_generation, ivec2 size) { + auto prev_layout = &lm->layouts[lm->current]; + lm->current = (lm->current + 1) % lm->max_buffer_age; + auto layout = &lm->layouts[lm->current]; + command_builder_command_list_free(layout->commands); + layout->root_image_generation = root_pixmap_generation; + + unsigned nlayers = wm_stack_num_managed_windows(wm); + if (nlayers > layout->capacity) { + struct layer *new_layers = + realloc(layout->layers, nlayers * sizeof(struct layer)); + BUG_ON(new_layers == NULL); + for (unsigned i = layout->capacity; i < nlayers; i++) { + pixman_region32_init(&new_layers[i].damaged); + } + layout->capacity = nlayers; + layout->layers = new_layers; + } + + layout->size = size; + + unsigned rank = 0; + struct layer_index *index, *next_index; + for (struct list_node *cursor = wm_stack_end(wm)->prev; + cursor != wm_stack_end(wm); cursor = cursor->prev) { + auto w = list_entry(cursor, struct win, stack_neighbour); + if (!w->managed) { + continue; + } + if (!layer_from_window(&layout->layers[rank], (struct managed_win *)w, size)) { + continue; + } + + HASH_FIND(hh, lm->layer_indices, &layout->layers[rank].key, + sizeof(layout->layers[rank].key), index); + if (index) { + prev_layout->layers[index->index].next_rank = (int)rank; + layout->layers[rank].prev_rank = (int)index->index; + } + rank++; + assert(rank <= nlayers); + } + layout->len = rank; + + // Update indices. If a layer exist in both prev_layout and current layout, + // we could update the index using next_rank; if a layer no longer exist in + // current layout, we remove it from the indices. + HASH_ITER(hh, lm->layer_indices, index, next_index) { + if (prev_layout->layers[index->index].next_rank == -1) { + HASH_DEL(lm->layer_indices, index); + list_insert_after(&lm->free_indices, &index->free_list); + } else { + index->index = (unsigned)prev_layout->layers[index->index].next_rank; + } + } + // And finally, if a layer in current layout didn't exist in prev_layout, add a + // new index for it. + for (unsigned i = 0; i < layout->len; i++) { + if (layout->layers[i].prev_rank != -1) { + continue; + } + if (!list_is_empty(&lm->free_indices)) { + index = + list_entry(lm->free_indices.next, struct layer_index, free_list); + list_remove(&index->free_list); + } else { + index = cmalloc(struct layer_index); + } + index->key = layout->layers[i].key; + index->index = i; + HASH_ADD(hh, lm->layer_indices, key, sizeof(index->key), index); + } +} + +struct layout *layout_manager_layout(struct layout_manager *lm, unsigned age) { + if (age >= lm->max_buffer_age) { + assert(false); + return NULL; + } + return &lm->layouts[(lm->current + lm->max_buffer_age - age) % lm->max_buffer_age]; +} + +void layout_manager_collect_window_damage(const struct layout_manager *lm, unsigned index, + unsigned buffer_age, region_t *damage) { + auto curr = lm->current; + auto layer = &lm->layouts[curr].layers[index]; + for (unsigned i = 0; i < buffer_age; i++) { + pixman_region32_union(damage, damage, &layer->damaged); + curr = (curr + lm->max_buffer_age - 1) % lm->max_buffer_age; + assert(layer->prev_rank >= 0); + layer = &lm->layouts[curr].layers[layer->prev_rank]; + } +} + +unsigned layout_manager_max_buffer_age(const struct layout_manager *lm) { + return lm->max_buffer_age - 1; +} + +int layer_prev_rank(struct layout_manager *lm, unsigned buffer_age, unsigned index_) { + int index = to_int_checked(index_); + unsigned layout = lm->current; + while (buffer_age--) { + index = lm->layouts[layout].layers[index].prev_rank; + if (index < 0) { + break; + } + layout = (layout + lm->max_buffer_age - 1) % lm->max_buffer_age; + } + return index; +} + +int layer_next_rank(struct layout_manager *lm, unsigned buffer_age, unsigned index_) { + int index = to_int_checked(index_); + unsigned layout = (lm->current + lm->max_buffer_age - buffer_age) % lm->max_buffer_age; + while (buffer_age--) { + index = lm->layouts[layout].layers[index].next_rank; + if (index < 0) { + break; + } + layout = (layout + 1) % lm->max_buffer_age; + } + return index; +} diff --git a/src/renderer/layout.h b/src/renderer/layout.h new file mode 100644 index 0000000000..a58849e901 --- /dev/null +++ b/src/renderer/layout.h @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui +#pragma once +#include +#include +#include +#include "backend/backend.h" +#include "region.h" +#include "types.h" + +struct layer_key { + /// Window generation, (see `struct wm::generation` for explanation of what a + /// generation is) + uint64_t generation; + /// Window ID + xcb_window_t window; + uint32_t pad; // explicit padding because this will be used as hash table + // key +}; + +static_assert(sizeof(struct layer_key) == 16, "layer_key has implicit padding"); + +/// A layer to be rendered in a render layout +struct layer { + /// Window that will be rendered in this layer + struct layer_key key; + /// The window, this is only valid for the current layout. Once + /// a frame has passed, windows could have been freed. + struct managed_win *win; + /// Damaged region of this layer, in screen coordinates + region_t damaged; + /// Origin (the top left outmost corner) of the window in screen coordinates + ivec2 origin; + /// Size of the window + ivec2 size; + /// Origin of the shadow in screen coordinates + ivec2 shadow_origin; + /// Size of the shadow + ivec2 shadow_size; + /// Opacity of this window + float opacity; + /// Opacity of the background blur of this window + float blur_opacity; + /// Opacity of this window's shadow + float shadow_opacity; + + /// How many commands are needed to render this layer + unsigned number_of_commands; + + /// Rank of this layer in the previous frame, -1 if this window + /// appears in this frame for the first time + int prev_rank; + /// Rank of this layer in the next frame, -1 if this window is + /// removed in the next frame + int next_rank; + + /// Is this window completely opaque? + bool is_opaque; + /// Is this window clipping the windows beneath it? + bool is_clipping; + + // TODO(yshui) make opaqueness/blur finer grained maybe? to support + // things like blur-background-frame + // region_t opaque_region; + // region_t blur_region; + + // TODO(yshui) support cropping + /// x and y offset for cropping. Anything to the top or + /// left of the crop point will be cropped out. + // uint32_t crop_x, crop_y; +}; + +/// Layout of windows at a specific frame +struct layout { + ivec2 size; + /// The root image generation, see `struct session::root_image_generation` + uint64_t root_image_generation; + /// Number of layers in `layers` + unsigned len; + /// Capacity of `layers` + unsigned capacity; + /// Layers as a flat array, from bottom to top in stack order. + struct layer *layers; + /// Number of commands in `commands` + unsigned number_of_commands; + /// Where does the commands for the bottom most layer start. + /// Any commands before that is for the desktop background. + unsigned first_layer_start; + /// Commands that are needed to render this layout. Commands + /// are recorded in the same order as the layers they correspond to. Each layer + /// can have 0 or more commands associated with it. + struct backend_command *commands; +}; + +struct wm; +struct layout_manager; + +/// Compute the layout of windows to be rendered in the current frame, and append it to +/// the end of layout manager's ring buffer. The layout manager has a ring buffer of +/// layouts, with its size chosen at creation time. Calling this will push at new layout +/// at the end of the ring buffer, and remove the oldest layout if the buffer is full. +void layout_manager_append_layout(struct layout_manager *lm, struct wm *wm, + uint64_t root_image_generation, ivec2 size); +/// Get the layout `age` frames into the past. Age `0` is the most recently appended +/// layout. +struct layout *layout_manager_layout(struct layout_manager *lm, unsigned age); +void layout_manager_free(struct layout_manager *lm); +/// Create a new render lm with a ring buffer for `max_buffer_age` layouts. +struct layout_manager *layout_manager_new(unsigned max_buffer_age); +/// Collect damage from the window for the past `buffer_age` frames. +void layout_manager_collect_window_damage(const struct layout_manager *lm, unsigned index, + unsigned buffer_age, region_t *damage); +/// Find where layer at `index` was `buffer_age` frames ago. +int layer_prev_rank(struct layout_manager *lm, unsigned buffer_age, unsigned index_); +/// Find layer that was at `index` `buffer_age` aga in the current layout. +int layer_next_rank(struct layout_manager *lm, unsigned buffer_age, unsigned index_); +unsigned layout_manager_max_buffer_age(const struct layout_manager *lm); diff --git a/src/renderer/renderer.c b/src/renderer/renderer.c new file mode 100644 index 0000000000..68130a9c12 --- /dev/null +++ b/src/renderer/renderer.c @@ -0,0 +1,620 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#include "renderer.h" + +#include +#include + +#include "../picom.h" +#include "backend/backend.h" +#include "backend/backend_common.h" +#include "command_builder.h" +#include "damage.h" +#include "layout.h" + +struct renderer { + /// Intermediate image to hold what will be presented to the back buffer. + image_handle back_image; + /// 1x1 white image + image_handle white_image; + /// 1x1 black image + image_handle black_image; + /// 1x1 image with the monitor repaint color + image_handle monitor_repaint_pixel; + /// Copy of back images before they were tainted by monitor repaint + image_handle *monitor_repaint_copy; + /// Regions painted over by monitor repaint + region_t *monitor_repaint_region; + /// Current frame index in ring buffer + int frame_index; + int max_buffer_age; + /// 1x1 shadow colored xrender picture + xcb_render_picture_t shadow_pixel; + ivec2 canvas_size; + /// Format to use for back_image and intermediate images + enum backend_image_format format; + struct color shadow_color; + int shadow_radius; + void *shadow_blur_context; + struct conv *shadow_kernel; + + region_t *culled_masks; + size_t culled_masks_capacity; +}; + +static void renderer_reallocate_culled_masks(struct renderer *r, size_t capacity) { + if (capacity <= r->culled_masks_capacity && + capacity > max2(1, r->culled_masks_capacity / 2)) { + return; + } + for (size_t i = capacity; i < r->culled_masks_capacity; i++) { + pixman_region32_fini(&r->culled_masks[i]); + } + void *new_storage = NULL; + if (capacity > 0) { + new_storage = realloc(r->culled_masks, sizeof(region_t[capacity])); + allocchk(new_storage); + } else { + free(r->culled_masks); + } + r->culled_masks = new_storage; + for (size_t i = r->culled_masks_capacity; i < capacity; i++) { + pixman_region32_init(&r->culled_masks[i]); + } + r->culled_masks_capacity = capacity; +} + +void renderer_free(struct backend_base *backend, struct renderer *r) { + if (r->white_image) { + backend->ops->release_image(backend, r->white_image); + } + if (r->black_image) { + backend->ops->release_image(backend, r->black_image); + } + if (r->back_image) { + backend->ops->release_image(backend, r->back_image); + } + if (r->monitor_repaint_pixel) { + backend->ops->release_image(backend, r->monitor_repaint_pixel); + } + if (r->shadow_blur_context) { + backend->ops->destroy_blur_context(backend, r->shadow_blur_context); + } + if (r->shadow_kernel) { + free_conv(r->shadow_kernel); + } + if (r->shadow_pixel) { + x_free_picture(backend->c, r->shadow_pixel); + } + if (r->monitor_repaint_region) { + for (int i = 0; i < r->max_buffer_age; i++) { + pixman_region32_fini(&r->monitor_repaint_region[i]); + } + free(r->monitor_repaint_region); + } + if (r->monitor_repaint_copy) { + for (int i = 0; i < r->max_buffer_age; i++) { + backend->ops->release_image(backend, r->monitor_repaint_copy[i]); + } + free(r->monitor_repaint_copy); + } + renderer_reallocate_culled_masks(r, 0); + free(r); +} + +static bool +renderer_init(struct renderer *renderer, struct backend_base *backend, + double shadow_radius, struct color shadow_color, bool dithered_present) { + auto has_high_precision = + backend->ops->is_format_supported(backend, BACKEND_IMAGE_FORMAT_PIXMAP_HIGH); + renderer->format = has_high_precision && dithered_present + ? BACKEND_IMAGE_FORMAT_PIXMAP_HIGH + : BACKEND_IMAGE_FORMAT_PIXMAP; + renderer->back_image = NULL; + renderer->white_image = + backend->ops->new_image(backend, renderer->format, (ivec2){1, 1}); + if (!renderer->white_image || !backend->ops->clear(backend, renderer->white_image, + (struct color){1, 1, 1, 1})) { + return false; + } + renderer->black_image = + backend->ops->new_image(backend, renderer->format, (ivec2){1, 1}); + if (!renderer->black_image || !backend->ops->clear(backend, renderer->black_image, + (struct color){0, 0, 0, 1})) { + return false; + } + renderer->canvas_size = (ivec2){0, 0}; + if (shadow_radius > 0) { + struct gaussian_blur_args args = { + .size = (int)shadow_radius, + .deviation = gaussian_kernel_std_for_size(shadow_radius, 0.5 / 256.0), + }; + renderer->shadow_blur_context = backend->ops->create_blur_context( + backend, BLUR_METHOD_GAUSSIAN, BACKEND_IMAGE_FORMAT_MASK, &args); + if (!renderer->shadow_blur_context) { + log_error("Failed to create shadow blur context"); + return false; + } + renderer->shadow_radius = (int)shadow_radius; + renderer->shadow_color = shadow_color; + renderer->shadow_pixel = + solid_picture(backend->c, true, shadow_color.alpha, shadow_color.red, + shadow_color.green, shadow_color.blue); + if (renderer->shadow_pixel == XCB_NONE) { + log_error("Failed to create shadow pixel"); + return false; + } + renderer->shadow_kernel = gaussian_kernel_autodetect_deviation(shadow_radius); + if (!renderer->shadow_kernel) { + log_error("Failed to create common shadow context"); + return false; + } + sum_kernel_preprocess(renderer->shadow_kernel); + } + renderer->max_buffer_age = backend->ops->max_buffer_age + 1; + return true; +} + +struct renderer *renderer_new(struct backend_base *backend, double shadow_radius, + struct color shadow_color, bool dithered_present) { + auto renderer = ccalloc(1, struct renderer); + if (!renderer_init(renderer, backend, shadow_radius, shadow_color, dithered_present)) { + renderer_free(backend, renderer); + return NULL; + } + + return renderer; +} + +static inline bool +renderer_set_root_size(struct renderer *r, struct backend_base *backend, ivec2 root_size) { + if (r->canvas_size.width == root_size.width && + r->canvas_size.height == root_size.height) { + return true; + } + if (r->back_image) { + backend->ops->release_image(backend, r->back_image); + } + if (r->monitor_repaint_copy) { + for (int i = 0; i < r->max_buffer_age; i++) { + backend->ops->release_image(backend, r->monitor_repaint_copy[i]); + } + free(r->monitor_repaint_copy); + r->monitor_repaint_copy = NULL; + } + r->back_image = backend->ops->new_image(backend, r->format, root_size); + if (r->back_image != NULL) { + r->canvas_size = root_size; + return true; + } + r->canvas_size = (ivec2){0, 0}; + return false; +} + +static bool +renderer_bind_mask(struct renderer *r, struct backend_base *backend, struct managed_win *w) { + ivec2 size = {.width = w->widthb, .height = w->heightb}; + bool succeeded = false; + auto image = backend->ops->new_image(backend, BACKEND_IMAGE_FORMAT_MASK, size); + if (!image || !backend->ops->clear(backend, image, (struct color){0, 0, 0, 0})) { + log_error("Failed to create mask image"); + goto err; + } + + auto bound_region_local = win_get_bounding_shape_global_by_val(w); + pixman_region32_translate(&bound_region_local, -w->g.x, -w->g.y); + succeeded = backend->ops->copy_area(backend, (ivec2){0, 0}, (image_handle)image, + r->white_image, &bound_region_local); + pixman_region32_fini(&bound_region_local); + if (!succeeded) { + log_error("Failed to fill the mask"); + goto err; + } + w->mask_image = image; + image = NULL; + +err: + if (image != NULL) { + backend->ops->release_image(backend, image); + } + return succeeded; +} + +image_handle renderer_shadow_from_mask(struct renderer *r, struct backend_base *backend, + image_handle mask, int corner_radius, ivec2 mask_size) { + image_handle normalized_mask_image = NULL, shadow_image = NULL, + shadow_color_pixel = NULL; + bool succeeded = false; + int radius = r->shadow_radius; + + log_trace("Generating shadow from mask, mask %p, color (%f, %f, %f, %f)", mask, + r->shadow_color.red, r->shadow_color.green, r->shadow_color.blue, + r->shadow_color.alpha); + + // Apply the properties on the mask image and blit the result into a larger + // image, each side larger by `2 * radius` so there is space for blurring. + normalized_mask_image = backend->ops->new_image( + backend, BACKEND_IMAGE_FORMAT_MASK, + (ivec2){mask_size.width + 2 * radius, mask_size.height + 2 * radius}); + if (!normalized_mask_image || !backend->ops->clear(backend, normalized_mask_image, + (struct color){0, 0, 0, 0})) { + log_error("Failed to create mask image"); + goto out; + } + { + region_t target_mask; + struct backend_mask_image mask_args = { + .image = mask, + .origin = {0, 0}, + .corner_radius = corner_radius, + .inverted = false, + }; + struct backend_blit_args args = { + .source_image = r->white_image, + .opacity = 1, + .source_mask = &mask_args, + .target_mask = &target_mask, + .shader = NULL, + .color_inverted = false, + .effective_size = mask_size, + .dim = 0, + .corner_radius = 0, + .border_width = 0, + .max_brightness = 1, + }; + pixman_region32_init_rect(&target_mask, radius, radius, + (unsigned)mask_size.width, + (unsigned)mask_size.height); + succeeded = backend->ops->blit(backend, (ivec2){radius, radius}, + normalized_mask_image, &args); + pixman_region32_fini(&target_mask); + if (!succeeded) { + log_error("Failed to blit for shadow generation"); + goto out; + } + } + // Then we blur the normalized mask image + if (r->shadow_blur_context != NULL) { + region_t target_mask; + struct backend_blur_args args = { + .source_image = normalized_mask_image, + .target_mask = &target_mask, + .opacity = 1, + .blur_context = r->shadow_blur_context, + }; + pixman_region32_init_rect(&target_mask, 0, 0, + (unsigned)(mask_size.width + 2 * radius), + (unsigned)(mask_size.height + 2 * radius)); + succeeded = + backend->ops->blur(backend, (ivec2){0, 0}, normalized_mask_image, &args); + pixman_region32_fini(&target_mask); + if (!succeeded) { + log_error("Failed to blur for shadow generation"); + goto out; + } + } + // Finally, we blit with this mask to colorize the shadow + succeeded = false; + shadow_image = backend->ops->new_image( + backend, BACKEND_IMAGE_FORMAT_PIXMAP, + (ivec2){mask_size.width + 2 * radius, mask_size.height + 2 * radius}); + if (!shadow_image || + !backend->ops->clear(backend, shadow_image, (struct color){0, 0, 0, 0})) { + log_error("Failed to allocate shadow image"); + goto out; + } + + shadow_color_pixel = + backend->ops->new_image(backend, BACKEND_IMAGE_FORMAT_PIXMAP, (ivec2){1, 1}); + if (!shadow_color_pixel || + !backend->ops->clear(backend, shadow_color_pixel, r->shadow_color)) { + log_error("Failed to create shadow color image"); + goto out; + } + + const ivec2 shadow_size = + ivec2_add(mask_size, (ivec2){.width = 2 * radius, .height = 2 * radius}); + region_t target_mask; + struct backend_mask_image mask_args = { + .image = (image_handle)normalized_mask_image, + .origin = {0, 0}, + .corner_radius = 0, + .inverted = false, + }; + struct backend_blit_args args = { + .source_image = shadow_color_pixel, + .opacity = 1, + .source_mask = &mask_args, + .target_mask = &target_mask, + .shader = NULL, + .color_inverted = false, + .effective_size = shadow_size, + .dim = 0, + .corner_radius = 0, + .border_width = 0, + .max_brightness = 1, + }; + pixman_region32_init_rect(&target_mask, 0, 0, (unsigned)shadow_size.width, + (unsigned)shadow_size.height); + succeeded = backend->ops->blit(backend, (ivec2){0, 0}, shadow_image, &args); + pixman_region32_fini(&target_mask); + +out: + if (normalized_mask_image) { + backend->ops->release_image(backend, normalized_mask_image); + } + if (shadow_color_pixel) { + backend->ops->release_image(backend, shadow_color_pixel); + } + if (!succeeded && shadow_image) { + log_error("Failed to draw shadow image"); + backend->ops->release_image(backend, shadow_image); + shadow_image = NULL; + } + return shadow_image; +} + +static bool renderer_bind_shadow(struct renderer *r, struct backend_base *backend, + struct managed_win *w) { + if (backend->ops->quirks(backend) & BACKEND_QUIRK_SLOW_BLUR) { + xcb_pixmap_t shadow = XCB_NONE; + xcb_render_picture_t pict = XCB_NONE; + + if (!build_shadow(backend->c, r->shadow_color.alpha, w->widthb, w->heightb, + (void *)r->shadow_kernel, r->shadow_pixel, &shadow, &pict)) { + return false; + } + + auto visual = + x_get_visual_for_standard(backend->c, XCB_PICT_STANDARD_ARGB_32); + w->shadow_image = backend->ops->bind_pixmap( + backend, shadow, x_get_visual_info(backend->c, visual)); + } else { + if (!w->mask_image && !renderer_bind_mask(r, backend, w)) { + return false; + } + w->shadow_image = renderer_shadow_from_mask( + r, backend, w->mask_image, w->corner_radius, + (ivec2){.width = w->widthb, .height = w->heightb}); + } + if (!w->shadow_image) { + log_error("Failed to create shadow"); + return false; + } + return true; +} + +/// Go through the list of commands and replace symbolic image references with real +/// images. Allocate images for windows when necessary. +static bool renderer_prepare_commands(struct renderer *r, struct backend_base *backend, + void *blur_context, image_handle root_image, + struct layout *layout) { + auto end = &layout->commands[layout->number_of_commands]; + auto cmds = layout->commands; + // These assertions are the limitation of this renderer. If we expand its + // capabilities, we might remove these. + assert(cmds[0].op == BACKEND_COMMAND_COPY_AREA && + cmds[0].source == BACKEND_COMMAND_SOURCE_BACKGROUND); + cmds[0].copy_area.source_image = root_image ?: r->black_image; + assert(layout->first_layer_start == 1); + + auto layer = layout->layers - 1; + auto layer_end = &layout->commands[layout->first_layer_start]; + for (auto cmd = &cmds[1]; cmd != end; cmd++) { + if (cmd == layer_end) { + layer += 1; + assert(layer->number_of_commands > 0); + layer_end = cmd + layer->number_of_commands; + log_trace("Prepare commands for layer %#010x @ %#010x (%s)", + layer->win->base.id, layer->win->client_win, + layer->win->name); + } + + auto w = layer->win; + switch (cmd->op) { + case BACKEND_COMMAND_BLIT: + assert(cmd->source != BACKEND_COMMAND_SOURCE_BACKGROUND); + if (cmd->source == BACKEND_COMMAND_SOURCE_SHADOW) { + if (w->shadow_image == NULL && + !renderer_bind_shadow(r, backend, w)) { + return false; + } + cmd->blit.source_image = w->shadow_image; + } else if (cmd->source == BACKEND_COMMAND_SOURCE_WINDOW) { + assert(w->win_image); + cmd->blit.source_image = w->win_image; + } + if (cmd->blit.source_mask != NULL) { + if (w->mask_image == NULL && + !renderer_bind_mask(r, backend, w)) { + return false; + } + cmd->source_mask.image = w->mask_image; + } + break; + case BACKEND_COMMAND_BLUR: + cmd->blur.blur_context = blur_context; + cmd->blur.source_image = r->back_image; + if (cmd->blur.source_mask != NULL) { + if (w->mask_image == NULL && + !renderer_bind_mask(r, backend, w)) { + return false; + } + cmd->source_mask.image = w->mask_image; + } + break; + default: + case BACKEND_COMMAND_COPY_AREA: + case BACKEND_COMMAND_INVALID: assert(false); + } + } + return true; +} + +void renderer_ensure_monitor_repaint_ready(struct renderer *r, struct backend_base *backend) { + if (!r->monitor_repaint_pixel) { + r->monitor_repaint_pixel = backend->ops->new_image( + backend, BACKEND_IMAGE_FORMAT_PIXMAP, (ivec2){1, 1}); + BUG_ON(!r->monitor_repaint_pixel); + backend->ops->clear(backend, r->monitor_repaint_pixel, + (struct color){.alpha = 0.5, .red = 0.5}); + } + if (!r->monitor_repaint_copy) { + r->monitor_repaint_copy = ccalloc(r->max_buffer_age, image_handle); + for (int i = 0; i < r->max_buffer_age; i++) { + r->monitor_repaint_copy[i] = backend->ops->new_image( + backend, BACKEND_IMAGE_FORMAT_PIXMAP, + (ivec2){.width = r->canvas_size.width, + .height = r->canvas_size.height}); + BUG_ON(!r->monitor_repaint_copy[i]); + } + } + if (!r->monitor_repaint_region) { + r->monitor_repaint_region = ccalloc(r->max_buffer_age, region_t); + for (int i = 0; i < r->max_buffer_age; i++) { + pixman_region32_init(&r->monitor_repaint_region[i]); + } + } +} + +/// @return true if a frame is rendered, false if this frame is skipped. +bool renderer_render(struct renderer *r, struct backend_base *backend, + image_handle root_image, struct layout_manager *lm, + struct command_builder *cb, void *blur_context, + uint64_t render_start_us, xcb_sync_fence_t xsync_fence, + bool use_damage, bool monitor_repaint, bool force_blend, + bool blur_frame, bool inactive_dim_fixed, double max_brightness, + double inactive_dim, const struct x_monitors *monitors, + const struct win_option *wintype_options, uint64_t *after_damage_us) { + if (xsync_fence != XCB_NONE) { + // Trigger the fence but don't immediately wait on it. Let it run + // concurrent with our CPU tasks to save time. + set_cant_fail_cookie(backend->c, + xcb_sync_trigger_fence(backend->c->c, xsync_fence)); + } + // TODO(yshui) In some cases we can render directly into the back buffer, and + // don't need the intermediate back_image. Several conditions need to be met: no + // dithered present; no blur, with blur we will render areas that's just for blur + // and can't be presented; + auto layout = layout_manager_layout(lm, 0); + if (!renderer_set_root_size(r, backend, + (ivec2){layout->size.width, layout->size.height})) { + log_error("Failed to allocate back image"); + return false; + } + + if (monitor_repaint) { + renderer_ensure_monitor_repaint_ready(r, backend); + } + + command_builder_build(cb, layout, force_blend, blur_frame, inactive_dim_fixed, + max_brightness, inactive_dim, monitors, wintype_options); + if (log_get_level_tls() <= LOG_LEVEL_TRACE) { + auto layer = layout->layers - 1; + auto layer_end = &layout->commands[layout->first_layer_start]; + auto end = &layout->commands[layout->number_of_commands]; + log_trace("Desktop background"); + for (auto i = layout->commands; i != end; i++) { + if (i == layer_end) { + layer += 1; + layer_end += layer->number_of_commands; + log_trace("Layer for window %#010x @ %#010x (%s)", + layer->win->base.id, layer->win->client_win, + layer->win->name); + } + log_backend_command(TRACE, *i); + } + } + region_t screen_region, damage_region; + pixman_region32_init_rect(&screen_region, 0, 0, (unsigned)r->canvas_size.width, + (unsigned)r->canvas_size.height); + pixman_region32_init(&damage_region); + pixman_region32_copy(&damage_region, &screen_region); + ivec2 blur_size = {}; + if (backend->ops->get_blur_size && blur_context) { + backend->ops->get_blur_size(blur_context, &blur_size.width, &blur_size.height); + } + auto buffer_age = + (use_damage || monitor_repaint) ? backend->ops->buffer_age(backend) : 0; + if (buffer_age > 0 && (unsigned)buffer_age <= layout_manager_max_buffer_age(lm)) { + layout_manager_damage(lm, (unsigned)buffer_age, blur_size, &damage_region); + } + + renderer_reallocate_culled_masks(r, layout->number_of_commands); + commands_cull_with_damage(layout, &damage_region, blur_size, r->culled_masks); + + auto now = get_time_timespec(); + *after_damage_us = (uint64_t)now.tv_sec * 1000000UL + (uint64_t)now.tv_nsec / 1000; + log_trace("Getting damage took %" PRIu64 " us", *after_damage_us - render_start_us); + + if (!renderer_prepare_commands(r, backend, blur_context, root_image, layout)) { + log_error("Failed to prepare render commands"); + return false; + } + + if (xsync_fence != XCB_NONE) { + set_cant_fail_cookie( + backend->c, xcb_sync_await_fence(backend->c->c, 1, &xsync_fence)); + // Making sure the wait is completed by receiving a response from the X + // server + xcb_aux_sync(backend->c->c); + set_cant_fail_cookie(backend->c, + xcb_sync_reset_fence(backend->c->c, xsync_fence)); + } + + if (backend->ops->prepare) { + backend->ops->prepare(backend, &layout->commands[0].target_mask); + } + + if (monitor_repaint && buffer_age <= r->max_buffer_age) { + // Restore the area of back buffer that was tainted by monitor repaint + int past_frame = + (r->frame_index + r->max_buffer_age - buffer_age) % r->max_buffer_age; + backend->ops->copy_area(backend, (ivec2){}, backend->ops->back_buffer(backend), + r->monitor_repaint_copy[past_frame], + &r->monitor_repaint_region[past_frame]); + } + + if (!backend_execute(backend, r->back_image, layout->number_of_commands, + layout->commands)) { + log_error("Failed to complete execution of the render commands"); + return false; + } + + if (monitor_repaint) { + // Keep a copy of un-tainted back image + backend->ops->copy_area(backend, (ivec2){}, + r->monitor_repaint_copy[r->frame_index], + r->back_image, &damage_region); + pixman_region32_copy(&r->monitor_repaint_region[r->frame_index], &damage_region); + + struct backend_blit_args blit = { + .source_image = r->monitor_repaint_pixel, + .max_brightness = 1, + .opacity = 1, + .effective_size = r->canvas_size, + .source_mask = NULL, + .target_mask = &damage_region, + }; + log_trace("Blit for monitor repaint"); + backend->ops->blit(backend, (ivec2){}, r->back_image, &blit); + } + + if (backend->ops->present) { + backend->ops->copy_area_quantize(backend, (ivec2){}, + backend->ops->back_buffer(backend), + r->back_image, &damage_region); + backend->ops->present(backend); + } + + // "Un-cull" the render commands, so later damage calculation using those commands + // will not use culled regions. + commands_uncull(layout); + + pixman_region32_fini(&screen_region); + pixman_region32_fini(&damage_region); + + r->frame_index = (r->frame_index + 1) % r->max_buffer_age; + return true; +} diff --git a/src/renderer/renderer.h b/src/renderer/renderer.h new file mode 100644 index 0000000000..7354b99873 --- /dev/null +++ b/src/renderer/renderer.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#pragma once +#include +#include +#include "types.h" + +struct renderer; +struct layout_manager; +struct backend_base; +struct command_builder; +typedef struct image_handle *image_handle; +struct x_monitors; +struct wm; +struct win_option; +typedef struct pixman_region32 region_t; + +void renderer_free(struct backend_base *backend, struct renderer *r); +struct renderer *renderer_new(struct backend_base *backend, double shadow_radius, + struct color shadow_color, bool dithered_present); +bool renderer_render(struct renderer *r, struct backend_base *backend, + image_handle root_image, struct layout_manager *lm, + struct command_builder *cb, void *blur_context, + uint64_t render_start_us, xcb_sync_fence_t xsync_fence, + bool use_damage, bool monitor_repaint, bool force_blend, + bool blur_frame, bool inactive_dim_fixed, double max_brightness, + double inactive_dim, const struct x_monitors *monitors, + const struct win_option *wintype_options, uint64_t *after_damage_us); diff --git a/src/rtkit.c b/src/rtkit.c new file mode 100644 index 0000000000..ecb89a252e --- /dev/null +++ b/src/rtkit.c @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2009 Lennart Poettering +// Copyright 2010 David Henningsson +// Copyright (c) Yuxuan Shui + +// Imported from https://github.com/heftig/rtkit/blob/master/rtkit.c + +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#elif defined(__NetBSD__) +#include +#elif defined(__FreeBSD__) +#include +#elif defined(__DragonFly__) +#include +#endif + +#include "log.h" +#include "rtkit.h" +#include "utils.h" + +#define RTKIT_SERVICE_NAME "org.freedesktop.RealtimeKit1" +#define RTKIT_OBJECT_PATH "/org/freedesktop/RealtimeKit1" +#define RTKIT_INTERFACE "org.freedesktop.RealtimeKit1" + +static inline long compat_gettid(void) { + long ret = -1; +#if defined(__linux__) + ret = (pid_t)syscall(SYS_gettid); +#elif defined(__NetBSD__) + ret = _lwp_self(); +#elif defined(__FreeBSD__) + long lwpid; + thr_self(&lwpid); + ret = lwpid; +#elif defined(__DragonFly__) + ret = lwp_gettid(); +#endif + return ret; +} + +static bool +rtkit_get_int_property(DBusConnection *connection, const char *propname, long long *propval) { + DBusMessage *m = NULL, *r = NULL; + DBusMessageIter iter, subiter; + dbus_int64_t i64; + dbus_int32_t i32; + DBusError error; + int current_type; + int ret = 0; + const char *interfacestr = RTKIT_INTERFACE; + + dbus_error_init(&error); + + m = dbus_message_new_method_call(RTKIT_SERVICE_NAME, RTKIT_OBJECT_PATH, + "org.freedesktop.DBus.Properties", "Get"); + if (!m) { + ret = -ENOMEM; + goto finish; + } + + if (!dbus_message_append_args(m, DBUS_TYPE_STRING, &interfacestr, + DBUS_TYPE_STRING, &propname, DBUS_TYPE_INVALID)) { + ret = -ENOMEM; + goto finish; + } + + r = dbus_connection_send_with_reply_and_block(connection, m, -1, &error); + if (!r) { + goto finish; + } + + if (dbus_set_error_from_message(&error, r)) { + goto finish; + } + + ret = -EBADMSG; + dbus_message_iter_init(r, &iter); + while ((current_type = dbus_message_iter_get_arg_type(&iter)) != DBUS_TYPE_INVALID) { + if (current_type == DBUS_TYPE_VARIANT) { + dbus_message_iter_recurse(&iter, &subiter); + + while ((current_type = dbus_message_iter_get_arg_type(&subiter)) != + DBUS_TYPE_INVALID) { + + if (current_type == DBUS_TYPE_INT32) { + dbus_message_iter_get_basic(&subiter, &i32); + *propval = i32; + ret = 0; + } + + if (current_type == DBUS_TYPE_INT64) { + dbus_message_iter_get_basic(&subiter, &i64); + *propval = i64; + ret = 0; + } + + dbus_message_iter_next(&subiter); + } + } + dbus_message_iter_next(&iter); + } + +finish: + if (m) { + dbus_message_unref(m); + } + if (r) { + dbus_message_unref(r); + } + if (dbus_error_is_set(&error)) { + log_debug("Couldn't get property %s from rtkit: (dbus) %s", propname, + error.message); + dbus_error_free(&error); + return false; + } + if (ret != 0) { + log_debug("Couldn't get property %s from rtkit: %s", propname, strerror(-ret)); + return false; + } + + return true; +} + +static bool rtkit_get_rttime_usec_max(DBusConnection *connection, long long *retval) { + return rtkit_get_int_property(connection, "RTTimeUSecMax", retval); +} + +static inline void free_dbus_connection(DBusConnection **connection) { + if (*connection) { + dbus_connection_close(*connection); + dbus_connection_unref(*connection); + *connection = NULL; + } +} + +static inline void free_dbus_message(DBusMessage **message) { + if (*message) { + dbus_message_unref(*message); + *message = NULL; + } +} + +bool rtkit_make_realtime(long thread, int priority) { + cleanup(free_dbus_message) DBusMessage *m = NULL; + cleanup(free_dbus_message) DBusMessage *r = NULL; + dbus_uint64_t u64; + dbus_uint32_t u32; + DBusError error; + int ret = 0; + long long rttime_usec_max = 0; + bool succeeded = true; + + dbus_error_init(&error); + + cleanup(free_dbus_connection) DBusConnection *connection = + dbus_bus_get_private(DBUS_BUS_SYSTEM, &error); + if (dbus_error_is_set(&error)) { + log_info("Couldn't get system bus: %s", error.message); + dbus_error_free(&error); + return false; + } + dbus_connection_set_exit_on_disconnect(connection, false); + + if (thread == 0) { + thread = compat_gettid(); + } + + if (!rtkit_get_rttime_usec_max(connection, &rttime_usec_max)) { + log_debug("Couldn't get RTTimeUSecMax from rtkit."); + return false; + } + if (rttime_usec_max <= 0) { + log_debug("Unreasonable RTTimeUSecMax from rtkit: %lld", rttime_usec_max); + return false; + } + +#if defined(RLIMIT_RTTIME) + struct rlimit old_rlim, new_rlim; + // For security reasons, rtkit requires us to set RLIMIT_RTTIME before it will + // give us realtime priority. + if (getrlimit(RLIMIT_RTTIME, &old_rlim) != 0) { + log_debug("Couldn't get RLIMIT_RTTIME."); + return false; + } + new_rlim = old_rlim; + new_rlim.rlim_cur = min3(new_rlim.rlim_max, (rlim_t)rttime_usec_max, 100000); // 100ms + new_rlim.rlim_max = new_rlim.rlim_cur; + if (setrlimit(RLIMIT_RTTIME, &new_rlim) != 0) { + log_debug("Couldn't set RLIMIT_RTTIME."); + return false; + } +#endif + + m = dbus_message_new_method_call(RTKIT_SERVICE_NAME, RTKIT_OBJECT_PATH, + RTKIT_INTERFACE, "MakeThreadRealtime"); + if (!m) { + ret = -ENOMEM; + goto finish; + } + + u64 = (dbus_uint64_t)thread; + u32 = (dbus_uint32_t)priority; + + if (!dbus_message_append_args(m, DBUS_TYPE_UINT64, &u64, DBUS_TYPE_UINT32, &u32, + DBUS_TYPE_INVALID)) { + ret = -ENOMEM; + goto finish; + } + + r = dbus_connection_send_with_reply_and_block(connection, m, -1, &error); + if (!r) { + goto finish; + } + + if (dbus_set_error_from_message(&error, r)) { + goto finish; + } + + ret = 0; + +finish: + if (dbus_error_is_set(&error)) { + log_info("Couldn't make thread realtime with rtkit: (dbus) %s", error.message); + dbus_error_free(&error); + succeeded = false; + } else if (ret != 0) { + log_info("Couldn't make thread realtime with rtkit: %s", strerror(-ret)); + succeeded = false; + } +#if defined(RLIMIT_RTTIME) + if (!succeeded) { + // Restore RLIMIT_RTTIME + setrlimit(RLIMIT_RTTIME, &old_rlim); + } +#endif + + return succeeded; +} diff --git a/src/rtkit.h b/src/rtkit.h new file mode 100644 index 0000000000..a5b04e885a --- /dev/null +++ b/src/rtkit.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#pragma once +#include +#include + +#ifdef CONFIG_DBUS + +#include + +bool rtkit_make_realtime(long thread, int priority); + +#else + +static inline bool rtkit_make_realtime(pid_t thread attr_unused, int priority attr_unused) { + return false; +} + +#endif diff --git a/src/statistics.c b/src/statistics.c new file mode 100644 index 0000000000..1a3b335fc4 --- /dev/null +++ b/src/statistics.c @@ -0,0 +1,85 @@ +//! Rendering statistics +//! +//! Tracks how long it takes to render a frame, for measuring performance, and for pacing +//! the frames. + +#include "statistics.h" +#include "log.h" +#include "utils.h" + +void render_statistics_init(struct render_statistics *rs, int window_size) { + *rs = (struct render_statistics){0}; + + rolling_window_init(&rs->render_times, window_size); + rolling_quantile_init_with_tolerance(&rs->render_time_quantile, window_size, + /* q */ 0.98, /* tolerance */ 0.01); +} + +void render_statistics_add_vblank_time_sample(struct render_statistics *rs, int time_us) { + auto sample_sd = sqrt(cumulative_mean_and_var_get_var(&rs->vblank_time_us)); + auto current_estimate = render_statistics_get_vblank_time(rs); + if (current_estimate != 0 && fabs((double)time_us - current_estimate) > sample_sd * 3) { + // Deviated from the mean by more than 3 sigma (p < 0.003) + log_debug("vblank time outlier: %d %f %f", time_us, rs->vblank_time_us.mean, + cumulative_mean_and_var_get_var(&rs->vblank_time_us)); + // An outlier sample, this could mean things like refresh rate changes, so + // we reset the statistics. This could also be benign, but we like to be + // cautious. + cumulative_mean_and_var_init(&rs->vblank_time_us); + } + + if (rs->vblank_time_us.mean != 0) { + auto nframes_in_10_seconds = + (unsigned int)(10. * 1000000. / rs->vblank_time_us.mean); + if (rs->vblank_time_us.n > 20 && rs->vblank_time_us.n > nframes_in_10_seconds) { + // We collected 10 seconds worth of samples, we assume the + // estimated refresh rate is stable. We will still reset the + // statistics if we get an outlier sample though, see above. + return; + } + } + cumulative_mean_and_var_update(&rs->vblank_time_us, time_us); +} + +void render_statistics_add_render_time_sample(struct render_statistics *rs, int time_us) { + int oldest; + if (rolling_window_push_back(&rs->render_times, time_us, &oldest)) { + rolling_quantile_pop_front(&rs->render_time_quantile, oldest); + } + + rolling_quantile_push_back(&rs->render_time_quantile, time_us); +} + +/// How much time budget we should give to the backend for rendering, in microseconds. +unsigned int render_statistics_get_budget(struct render_statistics *rs) { + if (rs->render_times.nelem < rs->render_times.window_size) { + // No valid render time estimates yet. Assume maximum budget. + return UINT_MAX; + } + + // N-th percentile of render times, see render_statistics_init for N. + auto render_time_percentile = + rolling_quantile_estimate(&rs->render_time_quantile, &rs->render_times); + return (unsigned int)render_time_percentile; +} + +unsigned int render_statistics_get_vblank_time(struct render_statistics *rs) { + if (rs->vblank_time_us.n <= 20 || rs->vblank_time_us.mean < 100) { + // Not enough samples yet, or the vblank time is too short to be + // meaningful. Assume maximum budget. + return 0; + } + return (unsigned int)rs->vblank_time_us.mean; +} + +void render_statistics_reset(struct render_statistics *rs) { + rolling_window_reset(&rs->render_times); + rolling_quantile_reset(&rs->render_time_quantile); + rs->vblank_time_us = (struct cumulative_mean_and_var){0}; +} + +void render_statistics_destroy(struct render_statistics *rs) { + render_statistics_reset(rs); + rolling_window_destroy(&rs->render_times); + rolling_quantile_destroy(&rs->render_time_quantile); +} diff --git a/src/statistics.h b/src/statistics.h new file mode 100644 index 0000000000..a111486111 --- /dev/null +++ b/src/statistics.h @@ -0,0 +1,30 @@ +#pragma once + +#include "utils.h" + +#define NTIERS (3) + +struct render_statistics { + /// Rolling window of rendering times (in us) and the tiers they belong to. + /// We keep track of the tiers because the vblank time estimate can change over + /// time. + struct rolling_window render_times; + /// Estimate the 95-th percentile of rendering times + struct rolling_quantile render_time_quantile; + /// Time between each vblanks + struct cumulative_mean_and_var vblank_time_us; +}; + +void render_statistics_init(struct render_statistics *rs, int window_size); +void render_statistics_reset(struct render_statistics *rs); +void render_statistics_destroy(struct render_statistics *rs); + +void render_statistics_add_vblank_time_sample(struct render_statistics *rs, int time_us); +void render_statistics_add_render_time_sample(struct render_statistics *rs, int time_us); + +/// How much time budget we should give to the backend for rendering, in microseconds. +unsigned int render_statistics_get_budget(struct render_statistics *rs); + +/// Return the measured vblank interval in microseconds. Returns 0 if not enough +/// samples have been collected yet. +unsigned int render_statistics_get_vblank_time(struct render_statistics *rs); diff --git a/src/string_utils.c b/src/string_utils.c index 65af0f209f..b683fdc585 100644 --- a/src/string_utils.c +++ b/src/string_utils.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui +#include #include #include @@ -85,6 +86,8 @@ TEST_CASE(mstrextend) { /// Parse a floating point number of form (+|-)?[0-9]*(\.[0-9]*) double strtod_simple(const char *src, const char **end) { double neg = 1; + bool succeeded = false; + *end = src; if (*src == '-') { neg = -1; src++; @@ -95,6 +98,7 @@ double strtod_simple(const char *src, const char **end) { double ret = 0; while (*src >= '0' && *src <= '9') { ret = ret * 10 + (*src - '0'); + succeeded = true; src++; } @@ -104,13 +108,17 @@ double strtod_simple(const char *src, const char **end) { while (*src >= '0' && *src <= '9') { frac += mult * (*src - '0'); mult *= 0.1; + succeeded = true; src++; } ret += frac; } - *end = src; - return ret * neg; + if (succeeded) { + *end = src; + return ret * neg; + } + return NAN; } TEST_CASE(strtod_simple) { @@ -126,4 +134,63 @@ TEST_CASE(strtod_simple) { result = strtod_simple("+.5", &end); TEST_EQUAL(result, 0.5); TEST_EQUAL(*end, '\0'); + + result = strtod_simple("+.", &end); + TEST_TRUE(safe_isnan(result)); + TEST_EQUAL(*end, '+'); +} + +const char *trim_both(const char *src, size_t *length) { + size_t i = 0; + while (isspace(src[i])) { + i++; + } + size_t j = strlen(src) - 1; + while (j > i && isspace(src[j])) { + j--; + } + *length = j - i + 1; + return src + i; +} + +TEST_CASE(trim_both) { + size_t length; + const char *str = trim_both(" \t\n\r\f", &length); + TEST_EQUAL(length, 0); + TEST_EQUAL(*str, '\0'); + + str = trim_both(" asdfas ", &length); + TEST_EQUAL(length, 6); + TEST_STRNEQUAL(str, "asdfas", length); + + str = trim_both(" asdf asdf ", &length); + TEST_EQUAL(length, 9); + TEST_STRNEQUAL(str, "asdf asdf", length); +} + +static int vasnprintf(char **strp, size_t *capacity, const char *fmt, va_list args) { + va_list copy; + va_copy(copy, args); + int needed = vsnprintf(*strp, *capacity, fmt, copy); + va_end(copy); + + if ((size_t)needed + 1 > *capacity) { + char *new_str = malloc((size_t)needed + 1); + allocchk(new_str); + free(*strp); + *strp = new_str; + *capacity = (size_t)needed + 1; + } else { + return needed; + } + + return vsnprintf(*strp, *capacity, fmt, args); +} + +int asnprintf(char **strp, size_t *capacity, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + int ret = vasnprintf(strp, capacity, fmt, args); + va_end(args); + return ret; } diff --git a/src/string_utils.h b/src/string_utils.h index 38febde6a3..970f227ea1 100644 --- a/src/string_utils.h +++ b/src/string_utils.h @@ -2,7 +2,9 @@ // Copyright (c) Yuxuan Shui #pragma once #include +#include #include +#include #include "compiler.h" @@ -11,6 +13,7 @@ char *mstrjoin(const char *src1, const char *src2); char *mstrjoin3(const char *src1, const char *src2, const char *src3); void mstrextend(char **psrc1, const char *src2); +const char *trim_both(const char *src, size_t *length); /// Parse a floating point number of form (+|-)?[0-9]*(\.[0-9]*) double strtod_simple(const char *, const char **); @@ -23,8 +26,9 @@ static inline int uitostr(unsigned int n, char *buf) { ret++; } - if (ret == 0) + if (ret == 0) { ret = 1; + } int pos = ret; while (pos--) { @@ -35,20 +39,35 @@ static inline int uitostr(unsigned int n, char *buf) { } static inline const char *skip_space_const(const char *src) { - if (!src) + if (!src) { return NULL; - while (*src && isspace((unsigned char)*src)) + } + while (*src && isspace((unsigned char)*src)) { src++; + } return src; } static inline char *skip_space_mut(char *src) { - if (!src) + if (!src) { return NULL; - while (*src && isspace((unsigned char)*src)) + } + while (*src && isspace((unsigned char)*src)) { src++; + } return src; } #define skip_space(x) \ - _Generic((x), char * : skip_space_mut, const char * : skip_space_const)(x) + _Generic((x), char *: skip_space_mut, const char *: skip_space_const)(x) + +static inline bool starts_with(const char *str, const char *needle, bool ignore_case) { + if (ignore_case) { + return strncasecmp(str, needle, strlen(needle)) == 0; + } + return strncmp(str, needle, strlen(needle)) == 0; +} + +/// Similar to `asprintf`, but it reuses the allocated memory pointed to by `*strp`, and +/// reallocates it if it's not big enough. +int asnprintf(char **strp, size_t *capacity, const char *fmt, ...); diff --git a/src/transition.c b/src/transition.c new file mode 100644 index 0000000000..fc03142709 --- /dev/null +++ b/src/transition.c @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#include +#include +#include + +#include "compiler.h" +#include "transition.h" +#include "utils.h" + +double animatable_get_progress(const struct animatable *a) { + if (a->duration > 0) { + return a->elapsed / a->duration; + } + return 1; +} + +/// Get the current value of an `animatable`. +double animatable_get(const struct animatable *a) { + if (a->duration > 0) { + assert(a->elapsed < a->duration); + double t = a->curve->sample(a->curve, animatable_get_progress(a)); + return (1 - t) * a->start + t * a->target; + } + return a->target; +} + +/// Advance the animation by a given number of steps. +void animatable_advance(struct animatable *a, double elapsed) { + if (a->duration == 0 || elapsed <= 0) { + return; + } + + assert(a->elapsed < a->duration); + if (elapsed >= a->duration - a->elapsed) { + a->elapsed = a->duration; + } else { + a->elapsed += elapsed; + } + + if (a->elapsed == a->duration) { + a->start = a->target; + a->duration = 0; + a->elapsed = 0; + a->curve->free(a->curve); + a->curve = NULL; + if (a->callback) { + a->callback(TRANSITION_COMPLETED, a->callback_data); + a->callback = NULL; + a->callback_data = NULL; + } + } +} + +/// Returns whether an `animatable` is currently animating. +bool animatable_is_animating(const struct animatable *a) { + assert(a->duration == 0 || a->elapsed < a->duration); + return a->duration != 0; +} + +/// Cancel the current animation of an `animatable`. This stops the animation and +/// the `animatable` will retain its current value. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_interrupt(struct animatable *a) { + if (a->duration == 0) { + return false; + } + + a->start = animatable_get(a); + a->target = a->start; + a->duration = 0; + a->elapsed = 0; + a->curve->free(a->curve); + a->curve = NULL; + if (a->callback) { + a->callback(TRANSITION_INTERRUPTED, a->callback_data); + a->callback = NULL; + a->callback_data = NULL; + } + return true; +} + +/// Cancel the current animation of an `animatable` and set its value to its target. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_skip(struct animatable *a) { + if (a->duration == 0) { + return false; + } + + a->start = a->target; + a->duration = 0; + a->elapsed = 0; + a->curve->free(a->curve); + a->curve = NULL; + if (a->callback) { + a->callback(TRANSITION_SKIPPED, a->callback_data); + a->callback = NULL; + a->callback_data = NULL; + } + return true; +} + +/// Change the target value of an `animatable`. +/// If the `animatable` is already animating, the animation will be canceled first. +bool animatable_set_target(struct animatable *a, double target, double duration, + const struct curve *curve, transition_callback_fn cb, void *data) { + animatable_interrupt(a); + if (duration == 0 || a->start == target) { + a->start = target; + a->target = target; + curve->free(curve); + return false; + } + + a->target = target; + a->duration = duration; + a->elapsed = 0; + a->callback = cb; + a->callback_data = data; + a->curve = curve; + return true; +} + +/// Create a new animatable. +struct animatable animatable_new(double value) { + struct animatable ret = { + .start = value, + .target = value, + .duration = 0, + .elapsed = 0, + }; + return ret; +} + +static double curve_sample_linear(const struct curve *this attr_unused, double progress) { + return progress; +} + +static void noop_free(const struct curve *this attr_unused) { +} + +const struct curve *curve_new_linear(void) { + static const struct curve ret = { + .sample = curve_sample_linear, + .free = noop_free, + }; + return &ret; +} diff --git a/src/transition.h b/src/transition.h new file mode 100644 index 0000000000..e7c20efc20 --- /dev/null +++ b/src/transition.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#pragma once +#include +#include "compiler.h" + +struct animatable; +enum transition_event; + +/// Callback when the transition state changes. Callback might be called by: +/// - `animatable_set_target` generates TRANSITION_COMPLETED when the specified duration +/// is 0. also generates TRANSITION_CANCELLED if the animatable was already animating. +/// - `animatable_cancel` generates TRANSITION_CANCELED +/// - `animatable_early_stop` generates TRANSITION_STOPPED_EARLY +/// - `animatable_step` generates TRANSITION_COMPLETED when the animation is completed. +/// Callback is guaranteed to be called exactly once for each `animatable_set_target` +/// call, unless an animatable is freed before the transition is completed. +typedef void (*transition_callback_fn)(enum transition_event event, void *data); + +enum transition_event { + TRANSITION_COMPLETED, + TRANSITION_INTERRUPTED, + TRANSITION_SKIPPED, +}; + +/// The base type for step_state. +struct step_state_base { + /// The current value of the `animatable`. + /// If the `animatable` is not animated, this equals to `animatable->target`. + double current; +}; + +struct curve { + /// The interpolator function for an animatable. This function should calculate + /// the current value of the `animatable` based on its `start`, `target`, + /// `duration` and `progress`. + double (*sample)(const struct curve *this, double progress); + /// Free the interpolator. + void (*free)(const struct curve *this); +}; + +/// An animatable value +struct animatable { + /// The starting value. + /// When this `animatable` is not animated, this is the current value. + double start; + /// The target value. + /// If the `animatable` is not animated, this equals to `start`. + double target; + /// The animation duration in unspecified units. + /// If the `animatable` is not animated, this is 0. + double duration; + /// The current progress of the animation in the same units as `duration`. + /// If the `animatable` is not animated, this is 0. + double elapsed; + + transition_callback_fn callback; + void *callback_data; + + /// The function for calculating the current value. If + /// `step_state` is not NULL, the `step` function is used; + /// otherwise, the `interpolator` function is used. + /// The interpolator function. + const struct curve *curve; +}; + +// =============================== API =============================== + +/// Get the current value of an `animatable`. +double animatable_get(const struct animatable *a); +/// Get the animation progress as a percentage of the total duration. +double animatable_get_progress(const struct animatable *a); +/// Advance the animation by a given amount. `elapsed` cannot be negative. +void animatable_advance(struct animatable *a, double elapsed); +/// Returns whether an `animatable` is currently animating. +bool animatable_is_animating(const struct animatable *a); +/// Interrupt the current animation of an `animatable`. This stops the animation and +/// the `animatable` will retain its current value. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_interrupt(struct animatable *a); +/// Skip the current animation of an `animatable` and set its value to its target. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_skip(struct animatable *a); +/// Change the target value of an `animatable`. Specify a duration, an interpolator +/// function, and a callback function. +/// +/// If the `animatable` is already animating, the animation will be canceled first. +/// +/// Note, In some cases this function does not start the animation, for example, if the +/// target value is the same as the current value of the animatable, or if the duration is +/// 0. If the animation is not started, the callback function will not be called. The +/// animatable's current animation, if it has one, will be canceled regardless. +/// +/// Returns if the animatable is now animated. +bool animatable_set_target(struct animatable *a, double target, double duration, + const struct curve *curve, + transition_callback_fn cb, void *data); +/// Create a new animatable. +struct animatable animatable_new(double value); + +// ========================== Interpolators ========================== + +const struct curve *curve_new_linear(void); diff --git a/src/types.h b/src/types.h index c8d747b6ee..ffcf3f2a64 100644 --- a/src/types.h +++ b/src/types.h @@ -5,6 +5,7 @@ /// Some common types +#include #include /// Enumeration type to represent switches. @@ -14,6 +15,8 @@ typedef enum { UNSET } switch_t; +enum tristate { TRI_FALSE = -1, TRI_UNKNOWN = 0, TRI_TRUE = 1 }; + /// A structure representing margins around a rectangle. typedef struct { int top; @@ -28,5 +31,52 @@ struct color { typedef uint32_t opacity_t; +typedef struct vec2 { + union { + double x; + double width; + }; + union { + double y; + double height; + }; +} vec2; + +typedef struct ivec2 { + union { + int x; + int width; + }; + union { + int y; + int height; + }; +} ivec2; + +static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { + return (ivec2){ + .x = a.x + b.x, + .y = a.y + b.y, + }; +} + +static inline ivec2 ivec2_sub(ivec2 a, ivec2 b) { + return (ivec2){ + .x = a.x - b.x, + .y = a.y - b.y, + }; +} + +static inline bool ivec2_eq(ivec2 a, ivec2 b) { + return a.x == b.x && a.y == b.y; +} + +static inline ivec2 ivec2_neg(ivec2 a) { + return (ivec2){ + .x = -a.x, + .y = -a.y, + }; +} + #define MARGIN_INIT \ { 0, 0, 0, 0 } diff --git a/src/utils.c b/src/utils.c index 8a27f393fd..b067180787 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,9 +1,12 @@ +#include #include #include #include #include "compiler.h" +#include "rtkit.h" #include "string_utils.h" +#include "test.h" #include "utils.h" /// Report allocation failure without allocating memory @@ -26,18 +29,17 @@ void report_allocation_failure(const char *func, const char *file, unsigned int {.iov_base = (void *)msg2, .iov_len = sizeof(msg2) - 1}, }; - writev(STDERR_FILENO, v, ARR_SIZE(v)); + ssize_t _ attr_unused = writev(STDERR_FILENO, v, ARR_SIZE(v)); abort(); - unreachable; + unreachable(); } /// /// Calculates next closest power of two of 32bit integer n /// ref: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 /// -int next_power_of_two(int n) -{ +int next_power_of_two(int n) { n--; n |= n >> 1; n |= n >> 2; @@ -48,4 +50,269 @@ int next_power_of_two(int n) return n; } +void rolling_window_destroy(struct rolling_window *rw) { + free(rw->elem); + rw->elem = NULL; +} + +void rolling_window_reset(struct rolling_window *rw) { + rw->nelem = 0; + rw->elem_head = 0; +} + +void rolling_window_init(struct rolling_window *rw, int size) { + rw->elem = ccalloc(size, int); + rw->window_size = size; + rolling_window_reset(rw); +} + +int rolling_window_pop_front(struct rolling_window *rw) { + assert(rw->nelem > 0); + auto ret = rw->elem[rw->elem_head]; + rw->elem_head = (rw->elem_head + 1) % rw->window_size; + rw->nelem--; + return ret; +} + +bool rolling_window_push_back(struct rolling_window *rw, int val, int *front) { + bool full = rw->nelem == rw->window_size; + if (full) { + *front = rolling_window_pop_front(rw); + } + rw->elem[(rw->elem_head + rw->nelem) % rw->window_size] = val; + rw->nelem++; + return full; +} + +/// Track the maximum member of a FIFO queue of integers. Integers are pushed to the back +/// and popped from the front, the maximum of the current members in the queue is +/// tracked. +struct rolling_max { + /// A priority queue holding the indices of the maximum element candidates. + /// The head of the queue is the index of the maximum element. + /// The indices in the queue are the "original" indices. + /// + /// There are only `capacity` elements in `elem`, all previous elements are + /// discarded. But the discarded elements' indices are not forgotten, that's why + /// it's called the "original" indices. + int *p; + int p_head, np; + /// The maximum number of in flight elements. + int capacity; +}; + +void rolling_max_destroy(struct rolling_max *rm) { + free(rm->p); + free(rm); +} + +struct rolling_max *rolling_max_new(int capacity) { + auto rm = ccalloc(1, struct rolling_max); + if (!rm) { + return NULL; + } + + rm->p = ccalloc(capacity, int); + if (!rm->p) { + goto err; + } + rm->capacity = capacity; + + return rm; + +err: + rolling_max_destroy(rm); + return NULL; +} + +void rolling_max_reset(struct rolling_max *rm) { + rm->p_head = 0; + rm->np = 0; +} + +#define IDX(n) ((n) % rm->capacity) +/// Remove the oldest element in the window. The caller must maintain the list of elements +/// themselves, i.e. the behavior is undefined if `front` does not 1match the oldest +/// element. +void rolling_max_pop_front(struct rolling_max *rm, int front) { + if (rm->p[rm->p_head] == front) { + // rm->p.pop_front() + rm->p_head = IDX(rm->p_head + 1); + rm->np--; + } +} + +void rolling_max_push_back(struct rolling_max *rm, int val) { + // Update the priority queue. + // Remove all elements smaller than the new element from the queue. Because + // the new element will become the maximum element before them, and since they + // come before the new element, they will have been popped before the new + // element, so they will never become the maximum element. + while (rm->np) { + int p_tail = IDX(rm->p_head + rm->np - 1); + if (rm->p[p_tail] > val) { + break; + } + // rm->p.pop_back() + rm->np--; + } + // Add the new element to the end of the queue. + // rm->p.push_back(rm->start_index + rm->nelem - 1) + assert(rm->np < rm->capacity); + rm->p[IDX(rm->p_head + rm->np)] = val; + rm->np++; +} +#undef IDX + +int rolling_max_get_max(struct rolling_max *rm) { + if (rm->np == 0) { + return INT_MIN; + } + return rm->p[rm->p_head]; +} + +TEST_CASE(rolling_max_test) { +#define NELEM 15 + struct rolling_window queue; + rolling_window_init(&queue, 3); + auto rm = rolling_max_new(3); + const int data[NELEM] = {1, 2, 3, 1, 4, 5, 2, 3, 6, 5, 4, 3, 2, 0, 0}; + const int expected_max[NELEM] = {1, 2, 3, 3, 4, 5, 5, 5, 6, 6, 6, 5, 4, 3, 2}; + int max[NELEM] = {0}; + for (int i = 0; i < NELEM; i++) { + int front; + bool full = rolling_window_push_back(&queue, data[i], &front); + if (full) { + rolling_max_pop_front(rm, front); + } + rolling_max_push_back(rm, data[i]); + max[i] = rolling_max_get_max(rm); + } + rolling_window_destroy(&queue); + rolling_max_destroy(rm); + TEST_TRUE(memcmp(max, expected_max, sizeof(max)) == 0); +#undef NELEM +} + +// Find the k-th smallest element in an array. +int quickselect(int *elems, int nelem, int k) { + int l = 0, r = nelem; // [l, r) is the range of candidates + while (l != r) { + int pivot = elems[l]; + int i = l, j = r; + while (i < j) { + while (i < j && elems[--j] >= pivot) { + } + elems[i] = elems[j]; + while (i < j && elems[++i] <= pivot) { + } + elems[j] = elems[i]; + } + elems[i] = pivot; + + if (i == k) { + break; + } + + if (i < k) { + l = i + 1; + } else { + r = i; + } + } + return elems[k]; +} + +void rolling_quantile_init(struct rolling_quantile *rq, int capacity, int mink, int maxk) { + *rq = (struct rolling_quantile){0}; + rq->tmp_buffer = malloc(sizeof(int) * (size_t)capacity); + rq->capacity = capacity; + rq->min_target_rank = mink; + rq->max_target_rank = maxk; +} + +void rolling_quantile_init_with_tolerance(struct rolling_quantile *rq, int window_size, + double target, double tolerance) { + rolling_quantile_init(rq, window_size, (int)((target - tolerance) * window_size), + (int)((target + tolerance) * window_size)); +} + +void rolling_quantile_reset(struct rolling_quantile *rq) { + rq->current_rank = 0; + rq->estimate = 0; +} + +void rolling_quantile_destroy(struct rolling_quantile *rq) { + free(rq->tmp_buffer); +} + +int rolling_quantile_estimate(struct rolling_quantile *rq, struct rolling_window *elements) { + if (rq->current_rank < rq->min_target_rank || rq->current_rank > rq->max_target_rank) { + if (elements->nelem != elements->window_size) { + return INT_MIN; + } + // Re-estimate the quantile. + assert(elements->nelem <= rq->capacity); + rolling_window_copy_to_array(elements, rq->tmp_buffer); + const int target_rank = + rq->min_target_rank + (rq->max_target_rank - rq->min_target_rank) / 2; + rq->estimate = quickselect(rq->tmp_buffer, elements->nelem, target_rank); + rq->current_rank = target_rank; + } + return rq->estimate; +} + +void rolling_quantile_push_back(struct rolling_quantile *rq, int x) { + if (x <= rq->estimate) { + rq->current_rank++; + } +} + +void rolling_quantile_pop_front(struct rolling_quantile *rq, int x) { + if (x <= rq->estimate) { + rq->current_rank--; + } +} + +/// Switch to real-time scheduling policy (SCHED_RR) if possible +/// +/// Make picom realtime to reduce latency, and make rendering times more predictable to +/// help pacing. +/// +/// This requires the user to set up permissions for the real-time scheduling. e.g. by +/// setting `ulimit -r`, or giving us the CAP_SYS_NICE capability. +void set_rr_scheduling(void) { + static thread_local bool already_tried = false; + if (already_tried) { + return; + } + already_tried = true; + + int priority = sched_get_priority_min(SCHED_RR); + + if (rtkit_make_realtime(0, priority)) { + log_info("Set realtime priority to %d with rtkit.", priority); + return; + } + + // Fallback to use pthread_setschedparam + struct sched_param param; + int old_policy; + int ret = pthread_getschedparam(pthread_self(), &old_policy, ¶m); + if (ret != 0) { + log_info("Couldn't get old scheduling priority."); + return; + } + + param.sched_priority = priority; + + ret = pthread_setschedparam(pthread_self(), SCHED_RR, ¶m); + if (ret != 0) { + log_info("Couldn't set real-time scheduling priority to %d.", priority); + return; + } + + log_info("Set real-time scheduling priority to %d.", priority); +} + // vim: set noet sw=8 ts=8 : diff --git a/src/utils.h b/src/utils.h index 13a77faaf6..60ad46e9b4 100644 --- a/src/utils.h +++ b/src/utils.h @@ -14,7 +14,10 @@ #include +#include + #include "compiler.h" +#include "log.h" #include "types.h" #define ARR_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) @@ -41,6 +44,17 @@ safe_isnan(double a) { assert(false); \ abort(); \ } while (0) +/// Abort the program is `expr` is true. This is similar to assert, but it is not disabled +/// in release builds. +#define BUG_ON(expr) \ + do { \ + bool __bug_on_tmp = (expr); \ + assert(!__bug_on_tmp && "Original expr: " #expr); \ + if (__bug_on_tmp) { \ + fprintf(stderr, "BUG_ON: \"%s\"\n", #expr); \ + abort(); \ + } \ + } while (0) #define CHECK_EXPR(...) ((void)0) /// Same as assert, but evaluates the expression even in release builds #define CHECK(expr) \ @@ -55,11 +69,11 @@ safe_isnan(double a) { /// being always true or false. #define ASSERT_IN_RANGE(var, lower, upper) \ do { \ - auto __tmp attr_unused = (var); \ + auto __assert_in_range_tmp attr_unused = (var); \ _Pragma("GCC diagnostic push"); \ _Pragma("GCC diagnostic ignored \"-Wtype-limits\""); \ - assert(__tmp >= lower); \ - assert(__tmp <= upper); \ + assert(__assert_in_range_tmp >= lower); \ + assert(__assert_in_range_tmp <= upper); \ _Pragma("GCC diagnostic pop"); \ } while (0) @@ -80,40 +94,62 @@ safe_isnan(double a) { #define to_int_checked(val) \ ({ \ - int64_t tmp = (val); \ - ASSERT_IN_RANGE(tmp, INT_MIN, INT_MAX); \ - (int)tmp; \ + int64_t __to_tmp = (val); \ + ASSERT_IN_RANGE(__to_tmp, INT_MIN, INT_MAX); \ + (int)__to_tmp; \ }) #define to_char_checked(val) \ ({ \ - int64_t tmp = (val); \ - ASSERT_IN_RANGE(tmp, CHAR_MIN, CHAR_MAX); \ - (char)tmp; \ + int64_t __to_tmp = (val); \ + ASSERT_IN_RANGE(__to_tmp, CHAR_MIN, CHAR_MAX); \ + (char)__to_tmp; \ }) #define to_u16_checked(val) \ ({ \ - auto tmp = (val); \ - ASSERT_IN_RANGE(tmp, 0, UINT16_MAX); \ - (uint16_t) tmp; \ + auto __to_tmp = (val); \ + ASSERT_IN_RANGE(__to_tmp, 0, UINT16_MAX); \ + (uint16_t) __to_tmp; \ }) #define to_i16_checked(val) \ ({ \ - int64_t tmp = (val); \ - ASSERT_IN_RANGE(tmp, INT16_MIN, INT16_MAX); \ - (int16_t) tmp; \ + int64_t __to_tmp = (val); \ + ASSERT_IN_RANGE(__to_tmp, INT16_MIN, INT16_MAX); \ + (int16_t) __to_tmp; \ }) #define to_u32_checked(val) \ ({ \ - auto tmp = (val); \ - int64_t max attr_unused = UINT32_MAX; /* silence clang tautological \ - comparison warning*/ \ - ASSERT_IN_RANGE(tmp, 0, max); \ - (uint32_t) tmp; \ + auto __to_tmp = (val); \ + int64_t __to_u32_max attr_unused = UINT32_MAX; /* silence clang \ + tautological \ + comparison warning */ \ + ASSERT_IN_RANGE(__to_tmp, 0, __to_u32_max); \ + (uint32_t) __to_tmp; \ + }) + +/* Are two types/vars the same type (ignoring qualifiers)? */ +#define is_same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b)) + +/** + * container_of - cast a member of a structure out to the containing structure + * @ptr: the pointer to the member. + * @type: the type of the container struct this is embedded in. + * @member: the name of the member within the struct. + * + * WARNING: any const qualifier of @ptr is lost. + */ +#define container_of(ptr, type, member) \ + ({ \ + void *__mptr = (void *)(ptr); \ + static_assert(is_same_type(*(ptr), ((type *)0)->member) || \ + is_same_type(*(ptr), void), \ + "pointer type mismatch in container_of()"); \ + ((type *)(__mptr - offsetof(type, member))); \ }) + /** * Normalize an int value to a specific range. * @@ -122,16 +158,25 @@ safe_isnan(double a) { * @param max maximum value * @return normalized value */ -static inline int attr_const normalize_i_range(int i, int min, int max) { - if (i > max) +static inline int attr_const attr_unused normalize_i_range(int i, int min, int max) { + if (i > max) { return max; - if (i < min) + } + if (i < min) { return min; + } return i; } +/// Generic integer abs() +#define iabs(val) \ + ({ \ + __auto_type __tmp = (val); \ + __tmp > 0 ? __tmp : -__tmp; \ + }) #define min2(a, b) ((a) > (b) ? (b) : (a)) #define max2(a, b) ((a) > (b) ? (a) : (b)) +#define min3(a, b, c) min2(a, min2(b, c)) /// clamp `val` into interval [min, max] #define clamp(val, min, max) max2(min2(val, max), min) @@ -145,10 +190,12 @@ static inline int attr_const normalize_i_range(int i, int min, int max) { * @return normalized value */ static inline double attr_const normalize_d_range(double d, double min, double max) { - if (d > max) + if (d > max) { return max; - if (d < min) + } + if (d < min) { return min; + } return d; } @@ -158,7 +205,7 @@ static inline double attr_const normalize_d_range(double d, double min, double m * @param d double value to normalize * @return normalized value */ -static inline double attr_const normalize_d(double d) { +static inline double attr_const attr_unused normalize_d(double d) { return normalize_d_range(d, 0.0, 1.0); } @@ -208,7 +255,7 @@ allocchk_(const char *func_name, const char *file, unsigned int line, void *ptr) ((type *)allocchk(calloc((size_t)tmp, sizeof(type)))); \ }) -/// @brief Wrapper of ealloc(). +/// @brief Wrapper of realloc(). #define crealloc(ptr, nmemb) \ ({ \ auto tmp = (nmemb); \ @@ -270,6 +317,15 @@ allocchk_(const char *func_name, const char *file, unsigned int line, void *ptr) void name##_ref(type *a); \ void name##_unref(type **a); +static inline void free_charpp(char **str) { + if (str) { + free(*str); + *str = NULL; + } +} + +/// An allocated char* that is automatically freed when it goes out of scope. +#define scoped_charp char *cleanup(free_charpp) /// /// Calculates next closest power of two of 32bit integer n @@ -277,5 +333,111 @@ allocchk_(const char *func_name, const char *file, unsigned int line, void *ptr) /// int next_power_of_two(int n); +struct rolling_window { + int *elem; + int elem_head, nelem; + int window_size; +}; + +void rolling_window_destroy(struct rolling_window *rw); +void rolling_window_reset(struct rolling_window *rw); +void rolling_window_init(struct rolling_window *rw, int size); +int rolling_window_pop_front(struct rolling_window *rw); +bool rolling_window_push_back(struct rolling_window *rw, int val, int *front); + +/// Copy the contents of the rolling window to an array. The array is assumed to +/// have enough space to hold the contents of the rolling window. +static inline void attr_unused rolling_window_copy_to_array(struct rolling_window *rw, + int *arr) { + // The length from head to the end of the array + auto head_len = (size_t)(rw->window_size - rw->elem_head); + if (head_len >= (size_t)rw->nelem) { + memcpy(arr, rw->elem + rw->elem_head, sizeof(int) * (size_t)rw->nelem); + } else { + auto tail_len = (size_t)((size_t)rw->nelem - head_len); + memcpy(arr, rw->elem + rw->elem_head, sizeof(int) * head_len); + memcpy(arr + head_len, rw->elem, sizeof(int) * tail_len); + } +} + +struct rolling_max; + +struct rolling_max *rolling_max_new(int capacity); +void rolling_max_destroy(struct rolling_max *rm); +void rolling_max_reset(struct rolling_max *rm); +void rolling_max_pop_front(struct rolling_max *rm, int front); +void rolling_max_push_back(struct rolling_max *rm, int val); +int rolling_max_get_max(struct rolling_max *rm); + +/// Estimate the mean and variance of random variable X using Welford's online +/// algorithm. +struct cumulative_mean_and_var { + double mean; + double m2; + unsigned int n; +}; + +static inline attr_unused void +cumulative_mean_and_var_init(struct cumulative_mean_and_var *cmv) { + *cmv = (struct cumulative_mean_and_var){0}; +} + +static inline attr_unused void +cumulative_mean_and_var_update(struct cumulative_mean_and_var *cmv, double x) { + if (cmv->n == UINT_MAX) { + // We have too many elements, let's keep the mean and variance. + return; + } + cmv->n++; + double delta = x - cmv->mean; + cmv->mean += delta / (double)cmv->n; + cmv->m2 += delta * (x - cmv->mean); +} + +static inline attr_unused double +cumulative_mean_and_var_get_var(struct cumulative_mean_and_var *cmv) { + if (cmv->n < 2) { + return 0; + } + return cmv->m2 / (double)(cmv->n - 1); +} + +// Find the k-th smallest element in an array. +int quickselect(int *elems, int nelem, int k); + +/// A naive quantile estimator. +/// +/// Estimates the N-th percentile of a random variable X in a sliding window. +struct rolling_quantile { + int current_rank; + int min_target_rank, max_target_rank; + int estimate; + int capacity; + int *tmp_buffer; +}; + +void rolling_quantile_init(struct rolling_quantile *rq, int capacity, int mink, int maxk); +void rolling_quantile_init_with_tolerance(struct rolling_quantile *rq, int window_size, + double target, double tolerance); +void rolling_quantile_reset(struct rolling_quantile *rq); +void rolling_quantile_destroy(struct rolling_quantile *rq); +int rolling_quantile_estimate(struct rolling_quantile *rq, struct rolling_window *elements); +void rolling_quantile_push_back(struct rolling_quantile *rq, int x); +void rolling_quantile_pop_front(struct rolling_quantile *rq, int x); +void set_rr_scheduling(void); + +// Some versions of the Android libc do not have timespec_get(), use +// clock_gettime() instead. +#ifdef __ANDROID__ + +#ifndef TIME_UTC +#define TIME_UTC 1 +#endif + +static inline int timespec_get(struct timespec *ts, int base) { + assert(base == TIME_UTC); + return clock_gettime(CLOCK_REALTIME, ts); +} +#endif // vim: set noet sw=8 ts=8 : diff --git a/src/vblank.c b/src/vblank.c new file mode 100644 index 0000000000..093c225126 --- /dev/null +++ b/src/vblank.c @@ -0,0 +1,579 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include "config.h" + +#ifdef CONFIG_OPENGL +// Enable sgi_video_sync_vblank_scheduler +#include +#include +#include +#include +#include +#include + +#endif + +#include "compiler.h" +#include "list.h" // for container_of +#include "log.h" +#include "vblank.h" +#include "x.h" + +struct vblank_closure { + vblank_callback_t fn; + void *user_data; +}; + +#define VBLANK_WIND_DOWN 4 + +struct vblank_scheduler { + struct x_connection *c; + size_t callback_capacity, callback_count; + struct vblank_closure *callbacks; + struct ev_loop *loop; + /// Request extra vblank events even when no callbacks are scheduled. + /// This is because when callbacks are scheduled too close to a vblank, + /// we might send PresentNotifyMsc request too late and miss the vblank event. + /// So we request extra vblank events right after the last vblank event + /// to make sure this doesn't happen. + unsigned int wind_down; + xcb_window_t target_window; + enum vblank_scheduler_type type; + bool vblank_event_requested; + bool use_realtime_scheduling; +}; + +struct present_vblank_scheduler { + struct vblank_scheduler base; + + uint64_t last_msc; + /// The timestamp for the end of last vblank. + uint64_t last_ust; + ev_timer callback_timer; + xcb_present_event_t event_id; + xcb_special_event_t *event; +}; + +struct vblank_scheduler_ops { + size_t size; + bool (*init)(struct vblank_scheduler *self); + void (*deinit)(struct vblank_scheduler *self); + bool (*schedule)(struct vblank_scheduler *self); + bool (*handle_x_events)(struct vblank_scheduler *self); +}; + +static void +vblank_scheduler_invoke_callbacks(struct vblank_scheduler *self, struct vblank_event *event); + +#ifdef CONFIG_OPENGL +struct sgi_video_sync_vblank_scheduler { + struct vblank_scheduler base; + + // Since glXWaitVideoSyncSGI blocks, we need to run it in a separate thread. + // ... and all the thread shenanigans that come with it. + _Atomic unsigned int current_msc; + _Atomic uint64_t current_ust; + ev_async notify; + pthread_t sync_thread; + bool running, error; + unsigned int last_msc; + + /// Protects `running`, and `base.vblank_event_requested` + pthread_mutex_t vblank_requested_mtx; + pthread_cond_t vblank_requested_cnd; +}; + +struct sgi_video_sync_thread_args { + struct sgi_video_sync_vblank_scheduler *self; + int start_status; + bool use_realtime_scheduling; + pthread_mutex_t start_mtx; + pthread_cond_t start_cnd; +}; + +static bool check_sgi_video_sync_extension(Display *dpy, int screen) { + const char *glx_ext = glXQueryExtensionsString(dpy, screen); + const char *needle = "GLX_SGI_video_sync"; + char *found = strstr(glx_ext, needle); + if (!found) { + return false; + } + if (found != glx_ext && found[-1] != ' ') { + return false; + } + if (found[strlen(needle)] != ' ' && found[strlen(needle)] != '\0') { + return false; + } + + return true; +} + +static void *sgi_video_sync_thread(void *data) { + auto args = (struct sgi_video_sync_thread_args *)data; + auto self = args->self; + Display *dpy = XOpenDisplay(NULL); + int error_code = 0; + GLXContext ctx = NULL; + GLXDrawable drawable = None; + Window root = DefaultRootWindow(dpy), dummy = None; + if (!dpy) { + error_code = 1; + goto start_failed; + } + int screen = DefaultScreen(dpy); + int ncfg = 0; + GLXFBConfig *cfg_ = glXChooseFBConfig( + dpy, screen, + (int[]){GLX_RENDER_TYPE, GLX_RGBA_BIT, GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, 0}, + &ncfg); + + if (!cfg_) { + error_code = 2; + goto start_failed; + } + GLXFBConfig cfg = cfg_[0]; + XFree(cfg_); + + XVisualInfo *vi = glXGetVisualFromFBConfig(dpy, cfg); + if (!vi) { + error_code = 3; + goto start_failed; + } + + Visual *visual = vi->visual; + const int depth = vi->depth; + XFree(vi); + + Colormap colormap = XCreateColormap(dpy, root, visual, AllocNone); + XSetWindowAttributes attributes; + attributes.colormap = colormap; + + dummy = XCreateWindow(dpy, root, 0, 0, 1, 1, 0, depth, InputOutput, visual, + CWColormap, &attributes); + XFreeColormap(dpy, colormap); + if (dummy == None) { + error_code = 4; + goto start_failed; + } + + drawable = glXCreateWindow(dpy, cfg, dummy, NULL); + if (drawable == None) { + error_code = 5; + goto start_failed; + } + + ctx = glXCreateNewContext(dpy, cfg, GLX_RGBA_TYPE, 0, true); + if (ctx == NULL) { + error_code = 6; + goto start_failed; + } + + if (!glXMakeContextCurrent(dpy, drawable, drawable, ctx)) { + error_code = 7; + goto start_failed; + } + + if (!check_sgi_video_sync_extension(dpy, screen)) { + error_code = 8; + goto start_failed; + } + + log_init_tls(); + + if (args->use_realtime_scheduling) { + set_rr_scheduling(); + } + + pthread_mutex_lock(&args->start_mtx); + args->start_status = 0; + pthread_cond_signal(&args->start_cnd); + pthread_mutex_unlock(&args->start_mtx); + + pthread_mutex_lock(&self->vblank_requested_mtx); + while (self->running) { + if (!self->base.vblank_event_requested) { + pthread_cond_wait(&self->vblank_requested_cnd, + &self->vblank_requested_mtx); + continue; + } + pthread_mutex_unlock(&self->vblank_requested_mtx); + + unsigned int last_msc; + glXWaitVideoSyncSGI(1, 0, &last_msc); + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + atomic_store(&self->current_msc, last_msc); + atomic_store(&self->current_ust, + (uint64_t)(now.tv_sec * 1000000 + now.tv_nsec / 1000)); + ev_async_send(self->base.loop, &self->notify); + pthread_mutex_lock(&self->vblank_requested_mtx); + } + pthread_mutex_unlock(&self->vblank_requested_mtx); + goto cleanup; + +start_failed: + pthread_mutex_lock(&args->start_mtx); + args->start_status = error_code; + pthread_cond_signal(&args->start_cnd); + pthread_mutex_unlock(&args->start_mtx); + +cleanup: + if (dpy) { + glXMakeCurrent(dpy, None, NULL); + if (ctx) { + glXDestroyContext(dpy, ctx); + } + if (drawable) { + glXDestroyWindow(dpy, drawable); + } + if (dummy) { + XDestroyWindow(dpy, dummy); + } + XCloseDisplay(dpy); + } + return NULL; +} + +static bool sgi_video_sync_scheduler_schedule(struct vblank_scheduler *base) { + auto self = (struct sgi_video_sync_vblank_scheduler *)base; + if (self->error) { + return false; + } + log_verbose("Requesting vblank event for msc %d", self->current_msc + 1); + pthread_mutex_lock(&self->vblank_requested_mtx); + assert(!base->vblank_event_requested); + base->vblank_event_requested = true; + pthread_cond_signal(&self->vblank_requested_cnd); + pthread_mutex_unlock(&self->vblank_requested_mtx); + return true; +} + +static void +sgi_video_sync_scheduler_callback(EV_P attr_unused, ev_async *w, int attr_unused revents); + +static bool sgi_video_sync_scheduler_init(struct vblank_scheduler *base) { + auto self = (struct sgi_video_sync_vblank_scheduler *)base; + auto args = (struct sgi_video_sync_thread_args){ + .self = self, + .start_status = -1, + .use_realtime_scheduling = base->use_realtime_scheduling, + }; + bool succeeded = true; + pthread_mutex_init(&args.start_mtx, NULL); + pthread_cond_init(&args.start_cnd, NULL); + + base->type = VBLANK_SCHEDULER_SGI_VIDEO_SYNC; + ev_async_init(&self->notify, sgi_video_sync_scheduler_callback); + ev_async_start(base->loop, &self->notify); + pthread_mutex_init(&self->vblank_requested_mtx, NULL); + pthread_cond_init(&self->vblank_requested_cnd, NULL); + + self->running = true; + pthread_create(&self->sync_thread, NULL, sgi_video_sync_thread, &args); + + pthread_mutex_lock(&args.start_mtx); + while (args.start_status == -1) { + pthread_cond_wait(&args.start_cnd, &args.start_mtx); + } + if (args.start_status != 0) { + log_fatal("Failed to start sgi_video_sync_thread, error code: %d", + args.start_status); + succeeded = false; + } else { + log_info("Started sgi_video_sync_thread"); + } + self->error = !succeeded; + self->last_msc = 0; + pthread_mutex_destroy(&args.start_mtx); + pthread_cond_destroy(&args.start_cnd); + return succeeded; +} + +static void sgi_video_sync_scheduler_deinit(struct vblank_scheduler *base) { + auto self = (struct sgi_video_sync_vblank_scheduler *)base; + ev_async_stop(base->loop, &self->notify); + pthread_mutex_lock(&self->vblank_requested_mtx); + self->running = false; + pthread_cond_signal(&self->vblank_requested_cnd); + pthread_mutex_unlock(&self->vblank_requested_mtx); + + pthread_join(self->sync_thread, NULL); + + pthread_mutex_destroy(&self->vblank_requested_mtx); + pthread_cond_destroy(&self->vblank_requested_cnd); +} + +static void +sgi_video_sync_scheduler_callback(EV_P attr_unused, ev_async *w, int attr_unused revents) { + auto sched = container_of(w, struct sgi_video_sync_vblank_scheduler, notify); + auto msc = atomic_load(&sched->current_msc); + if (sched->last_msc == msc) { + // NVIDIA spams us with duplicate vblank events after a suspend/resume + // cycle. Recreating the X connection and GLX context seems to fix this. + // Oh NVIDIA. + log_warn("Duplicate vblank event found with msc %d. Possible NVIDIA bug?", msc); + log_warn("Resetting the vblank scheduler"); + sgi_video_sync_scheduler_deinit(&sched->base); + sched->base.vblank_event_requested = false; + if (!sgi_video_sync_scheduler_init(&sched->base)) { + log_error("Failed to reset the vblank scheduler"); + } else { + sgi_video_sync_scheduler_schedule(&sched->base); + } + return; + } + auto event = (struct vblank_event){ + .msc = msc, + .ust = atomic_load(&sched->current_ust), + }; + sched->base.vblank_event_requested = false; + sched->last_msc = msc; + log_verbose("Received vblank event for msc %" PRIu64, event.msc); + vblank_scheduler_invoke_callbacks(&sched->base, &event); +} +#endif + +static bool present_vblank_scheduler_schedule(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + log_verbose("Requesting vblank event for window 0x%08x, msc %" PRIu64, + base->target_window, self->last_msc + 1); + assert(!base->vblank_event_requested); + x_request_vblank_event(base->c, base->target_window, self->last_msc + 1); + base->vblank_event_requested = true; + return true; +} + +static void present_vblank_callback(EV_P attr_unused, ev_timer *w, int attr_unused revents) { + auto sched = container_of(w, struct present_vblank_scheduler, callback_timer); + auto event = (struct vblank_event){ + .msc = sched->last_msc, + .ust = sched->last_ust, + }; + sched->base.vblank_event_requested = false; + vblank_scheduler_invoke_callbacks(&sched->base, &event); +} + +static bool present_vblank_scheduler_init(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + base->type = VBLANK_SCHEDULER_PRESENT; + ev_timer_init(&self->callback_timer, present_vblank_callback, 0, 0); + + self->event_id = x_new_id(base->c); + auto select_input = + xcb_present_select_input(base->c->c, self->event_id, base->target_window, + XCB_PRESENT_EVENT_MASK_COMPLETE_NOTIFY); + set_cant_fail_cookie(base->c, select_input); + self->event = + xcb_register_for_special_xge(base->c->c, &xcb_present_id, self->event_id, NULL); + return true; +} + +static void present_vblank_scheduler_deinit(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + ev_timer_stop(base->loop, &self->callback_timer); + auto select_input = + xcb_present_select_input(base->c->c, self->event_id, base->target_window, 0); + set_cant_fail_cookie(base->c, select_input); + xcb_unregister_for_special_event(base->c->c, self->event); +} + +/// Handle PresentCompleteNotify events +/// +/// Schedule the registered callback to be called when the current vblank ends. +static void handle_present_complete_notify(struct present_vblank_scheduler *self, + xcb_present_complete_notify_event_t *cne) { + assert(self->base.type == VBLANK_SCHEDULER_PRESENT); + + if (cne->kind != XCB_PRESENT_COMPLETE_KIND_NOTIFY_MSC) { + return; + } + + assert(self->base.vblank_event_requested); + + // X sometimes sends duplicate/bogus MSC events, when screen has just been turned + // off. Don't use the msc value in these events. We treat this as not receiving a + // vblank event at all, and try to get a new one. + // + // See: + // https://gitlab.freedesktop.org/xorg/xserver/-/issues/1418 + bool event_is_invalid = cne->msc <= self->last_msc || cne->ust == 0; + if (event_is_invalid) { + log_debug("Invalid PresentCompleteNotify event, %" PRIu64 " %" PRIu64, + cne->msc, cne->ust); + x_request_vblank_event(self->base.c, cne->window, self->last_msc + 1); + return; + } + + self->last_ust = cne->ust; + self->last_msc = cne->msc; + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + auto now_us = (unsigned long)(now.tv_sec * 1000000L + now.tv_nsec / 1000); + double delay_sec = 0.0; + if (now_us < cne->ust) { + log_trace("The end of this vblank is %" PRIu64 " us into the " + "future", + cne->ust - now_us); + delay_sec = (double)(cne->ust - now_us) / 1000000.0; + } + // Wait until the end of the current vblank to invoke callbacks. If we + // call it too early, it can mistakenly think the render missed the + // vblank, and doesn't schedule render for the next vblank, causing frame + // drops. + assert(!ev_is_active(&self->callback_timer)); + ev_timer_set(&self->callback_timer, delay_sec, 0); + ev_timer_start(self->base.loop, &self->callback_timer); +} + +static bool handle_present_events(struct vblank_scheduler *base) { + auto self = (struct present_vblank_scheduler *)base; + xcb_present_generic_event_t *ev; + while ((ev = (void *)xcb_poll_for_special_event(base->c->c, self->event))) { + if (ev->event != self->event_id) { + // This event doesn't have the right event context, it's not meant + // for us. + goto next; + } + + // We only subscribed to the complete notify event. + assert(ev->evtype == XCB_PRESENT_EVENT_COMPLETE_NOTIFY); + handle_present_complete_notify(self, (void *)ev); + next: + free(ev); + } + return true; +} + +static const struct vblank_scheduler_ops vblank_scheduler_ops[LAST_VBLANK_SCHEDULER] = { + [VBLANK_SCHEDULER_PRESENT] = + { + .size = sizeof(struct present_vblank_scheduler), + .init = present_vblank_scheduler_init, + .deinit = present_vblank_scheduler_deinit, + .schedule = present_vblank_scheduler_schedule, + .handle_x_events = handle_present_events, + }, +#ifdef CONFIG_OPENGL + [VBLANK_SCHEDULER_SGI_VIDEO_SYNC] = + { + .size = sizeof(struct sgi_video_sync_vblank_scheduler), + .init = sgi_video_sync_scheduler_init, + .deinit = sgi_video_sync_scheduler_deinit, + .schedule = sgi_video_sync_scheduler_schedule, + .handle_x_events = NULL, + }, +#endif +}; + +static bool vblank_scheduler_schedule_internal(struct vblank_scheduler *self) { + assert(self->type < LAST_VBLANK_SCHEDULER); + auto fn = vblank_scheduler_ops[self->type].schedule; + assert(fn != NULL); + return fn(self); +} + +bool vblank_scheduler_schedule(struct vblank_scheduler *self, + vblank_callback_t vblank_callback, void *user_data) { + if (self->callback_count == 0 && self->wind_down == 0) { + if (!vblank_scheduler_schedule_internal(self)) { + return false; + } + } + if (self->callback_count == self->callback_capacity) { + size_t new_capacity = + self->callback_capacity ? self->callback_capacity * 2 : 1; + void *new_buffer = + realloc(self->callbacks, new_capacity * sizeof(*self->callbacks)); + if (!new_buffer) { + return false; + } + self->callbacks = new_buffer; + self->callback_capacity = new_capacity; + } + self->callbacks[self->callback_count++] = (struct vblank_closure){ + .fn = vblank_callback, + .user_data = user_data, + }; + return true; +} + +static void +vblank_scheduler_invoke_callbacks(struct vblank_scheduler *self, struct vblank_event *event) { + // callbacks might be added during callback invocation, so we need to + // copy the callback_count. + size_t count = self->callback_count, write_head = 0; + if (count == 0) { + self->wind_down--; + } else { + self->wind_down = VBLANK_WIND_DOWN; + } + for (size_t i = 0; i < count; i++) { + auto action = self->callbacks[i].fn(event, self->callbacks[i].user_data); + switch (action) { + case VBLANK_CALLBACK_AGAIN: + if (i != write_head) { + self->callbacks[write_head] = self->callbacks[i]; + } + write_head++; + case VBLANK_CALLBACK_DONE: + default: // nothing to do + break; + } + } + memset(self->callbacks + write_head, 0, + (count - write_head) * sizeof(*self->callbacks)); + assert(count == self->callback_count && "callbacks should not be added when " + "callbacks are being invoked."); + self->callback_count = write_head; + if (self->callback_count || self->wind_down) { + vblank_scheduler_schedule_internal(self); + } +} + +void vblank_scheduler_free(struct vblank_scheduler *self) { + assert(self->type < LAST_VBLANK_SCHEDULER); + auto fn = vblank_scheduler_ops[self->type].deinit; + if (fn != NULL) { + fn(self); + } + free(self->callbacks); + free(self); +} + +struct vblank_scheduler * +vblank_scheduler_new(struct ev_loop *loop, struct x_connection *c, xcb_window_t target_window, + enum vblank_scheduler_type type, bool use_realtime_scheduling) { + size_t object_size = vblank_scheduler_ops[type].size; + auto init_fn = vblank_scheduler_ops[type].init; + if (!object_size || !init_fn) { + log_error("Unsupported or invalid vblank scheduler type: %d", type); + return NULL; + } + + assert(object_size >= sizeof(struct vblank_scheduler)); + struct vblank_scheduler *self = calloc(1, object_size); + self->target_window = target_window; + self->c = c; + self->loop = loop; + self->use_realtime_scheduling = use_realtime_scheduling; + init_fn(self); + return self; +} + +bool vblank_handle_x_events(struct vblank_scheduler *self) { + assert(self->type < LAST_VBLANK_SCHEDULER); + auto fn = vblank_scheduler_ops[self->type].handle_x_events; + if (fn != NULL) { + return fn(self); + } + return true; +} diff --git a/src/vblank.h b/src/vblank.h new file mode 100644 index 0000000000..647a45f3ba --- /dev/null +++ b/src/vblank.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "config.h" +#include "x.h" + +/// An object that schedule vblank events. +struct vblank_scheduler; + +struct vblank_event { + uint64_t msc; + uint64_t ust; +}; + +enum vblank_callback_action { + /// The callback should be called again in the next vblank. + VBLANK_CALLBACK_AGAIN, + /// The callback is done and should not be called again. + VBLANK_CALLBACK_DONE, +}; + +typedef enum vblank_callback_action (*vblank_callback_t)(struct vblank_event *event, + void *user_data); + +/// Schedule a vblank event. +/// +/// Schedule for `cb` to be called when the current vblank ends. If this is called +/// from a callback function for the current vblank, the newly scheduled callback +/// will be called in the next vblank. +/// +/// Returns whether the scheduling is successful. Scheduling can fail if there +/// is not enough memory. +bool vblank_scheduler_schedule(struct vblank_scheduler *self, vblank_callback_t cb, + void *user_data); +struct vblank_scheduler * +vblank_scheduler_new(struct ev_loop *loop, struct x_connection *c, xcb_window_t target_window, + enum vblank_scheduler_type type, bool use_realtime_scheduling); +void vblank_scheduler_free(struct vblank_scheduler *); + +bool vblank_handle_x_events(struct vblank_scheduler *self); diff --git a/src/vsync.c b/src/vsync.c index 5980155837..57fbb595ef 100644 --- a/src/vsync.c +++ b/src/vsync.c @@ -77,31 +77,35 @@ static bool vsync_drm_init(session_t *ps) { * @return true for success, false otherwise */ static bool vsync_opengl_init(session_t *ps) { - if (!ensure_glx_context(ps)) + if (!ensure_glx_context(ps)) { return false; + } return glxext.has_GLX_SGI_video_sync; } static bool vsync_opengl_oml_init(session_t *ps) { - if (!ensure_glx_context(ps)) + if (!ensure_glx_context(ps)) { return false; + } return glxext.has_GLX_OML_sync_control; } static inline bool vsync_opengl_swc_swap_interval(session_t *ps, int interval) { - if (glxext.has_GLX_MESA_swap_control) + if (glxext.has_GLX_MESA_swap_control) { return glXSwapIntervalMESA((uint)interval) == 0; - else if (glxext.has_GLX_SGI_swap_control) + } + if (glxext.has_GLX_SGI_swap_control) { return glXSwapIntervalSGI(interval) == 0; - else if (glxext.has_GLX_EXT_swap_control) { + } + if (glxext.has_GLX_EXT_swap_control) { GLXDrawable d = glXGetCurrentDrawable(); if (d == None) { // We don't have a context?? return false; } - glXSwapIntervalEXT(ps->dpy, glXGetCurrentDrawable(), interval); + glXSwapIntervalEXT(ps->c.dpy, glXGetCurrentDrawable(), interval); return true; } return false; @@ -140,8 +144,8 @@ static int vsync_opengl_wait(session_t *ps attr_unused) { static int vsync_opengl_oml_wait(session_t *ps) { int64_t ust = 0, msc = 0, sbc = 0; - glXGetSyncValuesOML(ps->dpy, ps->reg_win, &ust, &msc, &sbc); - glXWaitForMscOML(ps->dpy, ps->reg_win, 0, 2, (msc + 1) % 2, &ust, &msc, &sbc); + glXGetSyncValuesOML(ps->c.dpy, ps->reg_win, &ust, &msc, &sbc); + glXWaitForMscOML(ps->c.dpy, ps->reg_win, 0, 2, (msc + 1) % 2, &ust, &msc, &sbc); return 0; } #endif diff --git a/src/win.c b/src/win.c index af16273bcc..0ec166bf0e 100644 --- a/src/win.c +++ b/src/win.c @@ -14,7 +14,6 @@ #include #include #include -#include #include "atom.h" #include "backend/backend.h" @@ -22,21 +21,21 @@ #include "common.h" #include "compiler.h" #include "config.h" +#include "dbus.h" #include "list.h" #include "log.h" #include "picom.h" #include "region.h" #include "render.h" #include "string_utils.h" +#include "transition.h" #include "types.h" #include "uthash_extra.h" #include "utils.h" +#include "win_defs.h" +#include "wm.h" #include "x.h" -#ifdef CONFIG_DBUS -#include "dbus.h" -#endif - #ifdef CONFIG_OPENGL // TODO(yshui) Get rid of this include #include "opengl.h" @@ -54,30 +53,52 @@ static const int WIN_GET_LEADER_MAX_RECURSION = 20; static const int ROUNDED_PIXELS = 1; static const double ROUNDED_PERCENT = 0.05; -/** - * Retrieve the WM_CLASS of a window and update its - * win structure. - */ -static bool win_update_class(session_t *ps, struct managed_win *w); -static int win_update_role(session_t *ps, struct managed_win *w); -static void win_update_wintype(session_t *ps, struct managed_win *w); -static int win_update_name(session_t *ps, struct managed_win *w); +// TODO(yshui) +// +// Right now, how window properties/states/information (let's just call them states) +// are calculated is a huge mess. +// +// We can divide a window's states (i.e. fields in struct managed_win) in to two groups: +// one is "raw" window states, those come directly from the X server; the other is +// computed window states, which is calculated based on the raw properties, and user +// configurations like rules etc. +// +// Right now what we do is when some raw states are updated, we set some flags to +// recalculate relevant computed states. This is really hard to get right, because it's +// tedious to figure out the influence a raw window state has. And it is also imprecise, +// just look at our `win_on_factor_changed` - it is so difficult to get the recalculation +// right, so we basically use "factor change" as a catch-all, basically any changes to raw +// states will cause it to be called. And we recalculate everything there, kind of +// destroying the whole point. +// +// A better way is doing this the other way around, we shouldn't need to do anything when +// updating a raw state. Instead, the computed states should declare which raw states they +// depend on, so we can go through the computed states, only recalculate the ones whose +// dependencies have changed. The c2 rules are kind of already calculated this way, we +// should unify the rest of the computed states. This would simplify the code as well. + /** * Reread opacity property of a window. */ -static void win_update_opacity_prop(session_t *ps, struct managed_win *w); +static void win_update_opacity_prop(struct x_connection *c, struct atom *atoms, + struct managed_win *w, bool detect_client_opacity); static void win_update_opacity_target(session_t *ps, struct managed_win *w); +static void win_update_prop_shadow_raw(struct x_connection *c, struct atom *atoms, + struct managed_win *w); +static bool +win_update_prop_shadow(struct x_connection *c, struct atom *atoms, struct managed_win *w); /** - * Retrieve frame extents from a window. + * Update window EWMH fullscreen state. */ -static void -win_update_frame_extents(session_t *ps, struct managed_win *w, xcb_window_t client); -static void win_update_prop_shadow_raw(session_t *ps, struct managed_win *w); -static void win_update_prop_shadow(session_t *ps, struct managed_win *w); +bool win_update_prop_fullscreen(struct x_connection *c, const struct atom *atoms, + struct managed_win *w); /** * Update leader of a window. */ -static void win_update_leader(session_t *ps, struct managed_win *w); +static xcb_window_t +win_get_leader_property(struct x_connection *c, struct atom *atoms, xcb_window_t wid, + bool detect_transient, bool detect_client_leader); +static void win_mark_client(session_t *ps, struct managed_win *w, xcb_window_t client); /// Generate a "no corners" region function, from a function that returns the /// region via a region_t pointer argument. Corners of the window will be removed from @@ -86,7 +107,7 @@ static void win_update_leader(session_t *ps, struct managed_win *w); #define gen_without_corners(fun) \ void fun##_without_corners(const struct managed_win *w, region_t *res) { \ fun(w, res); \ - win_region_remove_corners(w, res); \ + win_region_remove_corners_local(w, res); \ } /// Generate a "return by value" function, from a function that returns the @@ -100,15 +121,6 @@ static void win_update_leader(session_t *ps, struct managed_win *w); return ret; \ } -/** - * Clear leader cache of all windows. - */ -static inline void clear_cache_win_leaders(session_t *ps) { - win_stack_foreach_managed(w, &ps->window_stack) { - w->cache_leader = XCB_NONE; - } -} - static xcb_window_t win_get_leader_raw(session_t *ps, struct managed_win *w, int recursions); /** @@ -120,45 +132,53 @@ static inline xcb_window_t win_get_leader(session_t *ps, struct managed_win *w) return win_get_leader_raw(ps, w, 0); } -/** - * Whether the real content of the window is visible. - * - * A window is not considered "real" visible if it's fading out. Because in that case a - * cached version of the window is displayed. - */ -static inline bool attr_pure win_is_real_visible(const struct managed_win *w) { - return w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING; -} - /** * Update focused state of a window. */ static void win_update_focused(session_t *ps, struct managed_win *w) { - if (UNSET != w->focused_force) { + if (w->focused_force != UNSET) { w->focused = w->focused_force; } else { - w->focused = win_is_focused_raw(ps, w); + bool is_wmwin = win_is_wmwin(w); + w->focused = win_is_focused_raw(w); // Use wintype_focus, and treat WM windows and override-redirected // windows specially if (ps->o.wintype_option[w->window_type].focus || - (ps->o.mark_wmwin_focused && w->wmwin) || - (ps->o.mark_ovredir_focused && w->base.id == w->client_win && !w->wmwin) || + (ps->o.mark_wmwin_focused && is_wmwin) || + (ps->o.mark_ovredir_focused && w->base.id == w->client_win && !is_wmwin) || (w->a.map_state == XCB_MAP_STATE_VIEWABLE && - c2_match(ps, w, ps->o.focus_blacklist, NULL))) { + c2_match(ps->c2_state, w, ps->o.focus_blacklist, NULL))) { w->focused = true; } // If window grouping detection is enabled, mark the window active if // its group is - if (ps->o.track_leader && ps->active_leader && - win_get_leader(ps, w) == ps->active_leader) { + auto active_leader = wm_active_leader(ps->wm); + if (ps->o.track_leader && active_leader && + win_get_leader(ps, w) == active_leader) { w->focused = true; } } } +struct group_callback_data { + struct session *ps; + xcb_window_t leader; +}; + +static inline int group_on_factor_change_callback(struct win *w, void *data_) { + struct group_callback_data *data = data_; + if (!w->managed) { + return 0; + } + auto mw = (struct managed_win *)w; + if (data->leader == win_get_leader(data->ps, mw)) { + win_on_factor_change(data->ps, mw); + } + return 0; +} + /** * Run win_on_factor_change() on all windows with the same leader window. * @@ -169,24 +189,23 @@ static inline void group_on_factor_change(session_t *ps, xcb_window_t leader) { return; } - HASH_ITER2(ps->windows, w) { - assert(!w->destroyed); - if (!w->managed) { - continue; - } - auto mw = (struct managed_win *)w; - if (win_get_leader(ps, mw) == leader) { - win_on_factor_change(ps, mw); - } - } + struct group_callback_data data = { + .ps = ps, + .leader = leader, + }; + wm_foreach(ps->wm, group_on_factor_change_callback, &data); } -static inline const char *win_get_name_if_managed(const struct win *w) { +static inline int group_is_focused_callback(struct win *w, void *data_) { + struct group_callback_data *data = data_; if (!w->managed) { - return "(unmanaged)"; + return 0; } auto mw = (struct managed_win *)w; - return mw->name; + if (data->leader == win_get_leader(data->ps, mw) && win_is_focused_raw(mw)) { + return 1; + } + return 0; } /** @@ -200,18 +219,36 @@ static inline bool group_is_focused(session_t *ps, xcb_window_t leader) { return false; } - HASH_ITER2(ps->windows, w) { - assert(!w->destroyed); - if (!w->managed) { - continue; - } - auto mw = (struct managed_win *)w; - if (win_get_leader(ps, mw) == leader && win_is_focused_raw(ps, mw)) { - return true; - } + struct group_callback_data data = { + .ps = ps, + .leader = leader, + }; + return wm_foreach(ps->wm, group_is_focused_callback, &data); +} + +/** + * Set leader of a window. + */ +static inline void win_set_leader(session_t *ps, struct managed_win *w, xcb_window_t nleader) { + xcb_window_t cache_leader_old = win_get_leader(ps, w); + + w->leader = nleader; + + // Forcefully do this to deal with the case when a child window + // gets mapped before parent, or when the window is a waypoint + win_stack_foreach_managed(i, wm_stack_end(ps->wm)) { + i->cache_leader = XCB_NONE; } - return false; + // Update the old and new window group and active_leader if the + // window could affect their state. + xcb_window_t cache_leader = win_get_leader(ps, w); + if (win_is_focused_raw(w) && cache_leader_old != cache_leader) { + wm_set_active_leader(ps->wm, cache_leader); + + group_on_factor_change(ps, cache_leader_old); + group_on_factor_change(ps, cache_leader); + } } /** @@ -297,20 +334,37 @@ static inline void win_release_pixmap(backend_t *base, struct managed_win *w) { log_debug("Releasing pixmap of window %#010x (%s)", w->base.id, w->name); assert(w->win_image); if (w->win_image) { - base->ops->release_image(base, w->win_image); + xcb_pixmap_t pixmap = XCB_NONE; + pixmap = base->ops->release_image(base, w->win_image); w->win_image = NULL; // Bypassing win_set_flags, because `w` might have been destroyed w->flags |= WIN_FLAGS_PIXMAP_NONE; + if (pixmap != XCB_NONE) { + xcb_free_pixmap(base->c->c, pixmap); + } } } static inline void win_release_shadow(backend_t *base, struct managed_win *w) { log_debug("Releasing shadow of window %#010x (%s)", w->base.id, w->name); - assert(w->shadow_image); if (w->shadow_image) { - base->ops->release_image(base, w->shadow_image); + assert(w->shadow); + xcb_pixmap_t pixmap = XCB_NONE; + pixmap = base->ops->release_image(base, w->shadow_image); w->shadow_image = NULL; - // Bypassing win_set_flags, because `w` might have been destroyed - w->flags |= WIN_FLAGS_SHADOW_NONE; + if (pixmap != XCB_NONE) { + xcb_free_pixmap(base->c->c, pixmap); + } + } +} + +static inline void win_release_mask(backend_t *base, struct managed_win *w) { + if (w->mask_image) { + xcb_pixmap_t pixmap = XCB_NONE; + pixmap = base->ops->release_image(base, w->mask_image); + w->mask_image = NULL; + if (pixmap != XCB_NONE) { + xcb_free_pixmap(base->c->c, pixmap); + } } } @@ -318,7 +372,7 @@ static inline bool win_bind_pixmap(struct backend_base *b, struct managed_win *w assert(!w->win_image); auto pixmap = x_new_id(b->c); auto e = xcb_request_check( - b->c, xcb_composite_name_window_pixmap_checked(b->c, w->base.id, pixmap)); + b->c->c, xcb_composite_name_window_pixmap_checked(b->c->c, w->base.id, pixmap)); if (e) { log_error("Failed to get named pixmap for window %#010x(%s)", w->base.id, w->name); @@ -326,10 +380,10 @@ static inline bool win_bind_pixmap(struct backend_base *b, struct managed_win *w return false; } log_debug("New named pixmap for %#010x (%s) : %#010x", w->base.id, w->name, pixmap); - w->win_image = - b->ops->bind_pixmap(b, pixmap, x_get_visual_info(b->c, w->a.visual), true); + w->win_image = b->ops->bind_pixmap(b, pixmap, x_get_visual_info(b->c, w->a.visual)); if (!w->win_image) { log_error("Failed to bind pixmap"); + xcb_free_pixmap(b->c->c, pixmap); win_set_flags(w, WIN_FLAGS_IMAGE_ERROR); return false; } @@ -338,93 +392,94 @@ static inline bool win_bind_pixmap(struct backend_base *b, struct managed_win *w return true; } -bool win_bind_shadow(struct backend_base *b, struct managed_win *w, struct color c, - struct conv *kernel) { - assert(!w->shadow_image); - assert(w->shadow); - w->shadow_image = b->ops->render_shadow(b, w->widthb, w->heightb, kernel, c.red, - c.green, c.blue, c.alpha); - if (!w->shadow_image) { - log_error("Failed to bind shadow image, shadow will be disabled for " - "%#010x (%s)", - w->base.id, w->name); - win_set_flags(w, WIN_FLAGS_SHADOW_NONE); - w->shadow = false; - return false; - } - - log_debug("New shadow for %#010x (%s)", w->base.id, w->name); - win_clear_flags(w, WIN_FLAGS_SHADOW_NONE); - return true; -} - void win_release_images(struct backend_base *backend, struct managed_win *w) { - // We don't want to decide what we should do if the image we want to release is - // stale (do we clear the stale flags or not?) - // But if we are not releasing any images anyway, we don't care about the stale - // flags. + // We don't want to decide what we should do if the image we want to + // release is stale (do we clear the stale flags or not?) But if we are + // not releasing any images anyway, we don't care about the stale flags. if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { assert(!win_check_flags_all(w, WIN_FLAGS_PIXMAP_STALE)); win_release_pixmap(backend, w); } - if (!win_check_flags_all(w, WIN_FLAGS_SHADOW_NONE)) { - assert(!win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE)); - win_release_shadow(backend, w); - } + win_release_shadow(backend, w); + win_release_mask(backend, w); } -/// Returns true if the `prop` property is stale, as well as clears the stale flag. +/// Returns true if the `prop` property is stale, as well as clears the stale +/// flag. static bool win_fetch_and_unset_property_stale(struct managed_win *w, xcb_atom_t prop); -/// Returns true if any of the properties are stale, as well as clear all the stale flags. +/// Returns true if any of the properties are stale, as well as clear all the +/// stale flags. static void win_clear_all_properties_stale(struct managed_win *w); -/// Fetch new window properties from the X server, and run appropriate updates. Might set -/// WIN_FLAGS_FACTOR_CHANGED +// TODO(yshui) make WIN_FLAGS_FACTOR_CHANGED more fine-grained, or find a better +// alternative +// way to do all this. + +/// Fetch new window properties from the X server, and run appropriate updates. +/// Might set WIN_FLAGS_FACTOR_CHANGED static void win_update_properties(session_t *ps, struct managed_win *w) { + // we cannot receive property change when window has been destroyed + assert(w->state != WSTATE_DESTROYED); + if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_WINDOW_TYPE)) { - win_update_wintype(ps, w); + if (win_update_wintype(&ps->c, ps->atoms, w)) { + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } } if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_WINDOW_OPACITY)) { - win_update_opacity_prop(ps, w); - // we cannot receive OPACITY change when window has been destroyed - assert(w->state != WSTATE_DESTROYING); + win_update_opacity_prop(&ps->c, ps->atoms, w, ps->o.detect_client_opacity); win_update_opacity_target(ps, w); } if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_FRAME_EXTENTS)) { - win_update_frame_extents(ps, w, w->client_win); + win_update_frame_extents(&ps->c, ps->atoms, w, w->client_win, + ps->o.frame_opacity); add_damage_from_win(ps, w); } if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_NAME) || win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_NAME)) { - if (win_update_name(ps, w) == 1) { + if (win_update_name(&ps->c, ps->atoms, w) == 1) { win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); } } if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_CLASS)) { - if (win_update_class(ps, w)) { + if (win_update_class(&ps->c, ps->atoms, w)) { win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); } } if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_WINDOW_ROLE)) { - if (win_update_role(ps, w) == 1) { + if (win_update_role(&ps->c, ps->atoms, w) == 1) { win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); } } if (win_fetch_and_unset_property_stale(w, ps->atoms->a_COMPTON_SHADOW)) { - win_update_prop_shadow(ps, w); + if (win_update_prop_shadow(&ps->c, ps->atoms, w)) { + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } + } + + if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_STATE)) { + if (win_update_prop_fullscreen(&ps->c, ps->atoms, w)) { + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } } if (win_fetch_and_unset_property_stale(w, ps->atoms->aWM_CLIENT_LEADER) || win_fetch_and_unset_property_stale(w, ps->atoms->aWM_TRANSIENT_FOR)) { - win_update_leader(ps, w); + auto new_leader = win_get_leader_property(&ps->c, ps->atoms, w->client_win, + ps->o.detect_transient, + ps->o.detect_client_leader); + if (w->leader != new_leader) { + win_set_leader(ps, w, new_leader); + win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); + } } win_clear_all_properties_stale(w); @@ -432,52 +487,81 @@ static void win_update_properties(session_t *ps, struct managed_win *w) { /// Handle non-image flags. This phase might set IMAGES_STALE flags void win_process_update_flags(session_t *ps, struct managed_win *w) { - // Whether the window was visible before we process the mapped flag. i.e. is the - // window just mapped. - bool was_visible = win_is_real_visible(w); - log_trace("Processing flags for window %#010x (%s), was visible: %d", w->base.id, - w->name, was_visible); + log_trace("Processing flags for window %#010x (%s), was rendered: %d, flags: " + "%#" PRIx64, + w->base.id, w->name, w->to_paint, w->flags); if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { map_win_start(ps, w); win_clear_flags(w, WIN_FLAGS_MAPPED); } - if (!win_is_real_visible(w)) { - // Flags of invisible windows are processed when they are mapped + if (w->state != WSTATE_MAPPED) { + // Window is not mapped, so we ignore all its changes until it's mapped + // again. return; } - // Check client first, because later property updates need accurate client window - // information + // Check client first, because later property updates need accurate client + // window information if (win_check_flags_all(w, WIN_FLAGS_CLIENT_STALE)) { - win_recheck_client(ps, w); + log_debug("Rechecking client window for %#010x (%s)", w->base.id, w->name); + auto client_win = win_get_client_window(&ps->c, ps->wm, ps->atoms, w); + if (w->client_win && w->client_win != client_win) { + win_unmark_client(w); + } + log_debug("New client window for %#010x (%s): %#010x", w->base.id, + w->name, client_win); + win_mark_client(ps, w, client_win); win_clear_flags(w, WIN_FLAGS_CLIENT_STALE); } bool damaged = false; if (win_check_flags_any(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { - if (was_visible) { - // Mark the old extents of this window as damaged. The new extents - // will be marked damaged below, after the window extents are - // updated. - // - // If the window is just mapped, we don't need to mark the old - // extent as damaged. (It's possible that the window was in fading - // and is interrupted by being mapped. In that case, the fading - // window will be added to damage by map_win_start, so we don't - // need to do it here) + // For damage calculation purposes, we don't care if the window + // is mapped in X server, we only care if we rendered it last + // frame. + // + // We do not process window flags for unmapped windows even when + // it was rendered, so an window fading out won't move even if the + // underlying unmapped window is moved. When the window is + // mapped again when it's still fading out, it should have the + // same effect as a mapped window being moved, meaning we have + // to add both the previous and the new window extents to + // damage. + // + // All that is basically me saying what really matters is if the + // window was rendered last frame, not if it's mapped in X server. + if (w->to_paint) { + // Mark the old extents of this window as damaged. The new + // extents will be marked damaged below, after the window + // extents are updated. add_damage_from_win(ps, w); } // Update window geometry w->g = w->pending_g; + // Whether a window is fullscreen changes based on its geometry + win_update_is_fullscreen(ps, w); + if (win_check_flags_all(w, WIN_FLAGS_SIZE_STALE)) { - win_on_win_size_change(ps, w); - win_update_bounding_shape(ps, w); + win_on_win_size_change(w, ps->o.shadow_offset_x, + ps->o.shadow_offset_y, ps->o.shadow_radius); + win_update_bounding_shape(&ps->c, w, ps->shape_exists, + ps->o.detect_rounded_corners); damaged = true; win_clear_flags(w, WIN_FLAGS_SIZE_STALE); + + // Window shape/size changed, invalidate the images we built + // log_trace("free out dated pict"); + win_set_flags(w, WIN_FLAGS_PIXMAP_STALE | WIN_FLAGS_FACTOR_CHANGED); + + win_release_mask(ps->backend_data, w); + win_release_shadow(ps->backend_data, w); + ps->pending_updates = true; + free_paint(ps, &w->paint); + free_paint(ps, &w->shadow_paint); } if (win_check_flags_all(w, WIN_FLAGS_POSITION_STALE)) { @@ -485,7 +569,7 @@ void win_process_update_flags(session_t *ps, struct managed_win *w) { win_clear_flags(w, WIN_FLAGS_POSITION_STALE); } - win_update_screen(ps->xinerama_nscrs, ps->xinerama_scr_regs, w); + win_update_monitor(&ps->monitors, w); } if (win_check_flags_all(w, WIN_FLAGS_PROPERTY_STALE)) { @@ -493,7 +577,8 @@ void win_process_update_flags(session_t *ps, struct managed_win *w) { win_clear_flags(w, WIN_FLAGS_PROPERTY_STALE); } - // Factor change flags could be set by previous stages, so must be handled last + // Factor change flags could be set by previous stages, so must be handled + // last if (win_check_flags_all(w, WIN_FLAGS_FACTOR_CHANGED)) { win_on_factor_change(ps, w); win_clear_flags(w, WIN_FLAGS_FACTOR_CHANGED); @@ -507,16 +592,16 @@ void win_process_update_flags(session_t *ps, struct managed_win *w) { } void win_process_image_flags(session_t *ps, struct managed_win *w) { + // Assert that the MAPPED flag is already handled. assert(!win_check_flags_all(w, WIN_FLAGS_MAPPED)); - if (w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYING || - w->state == WSTATE_UNMAPPING) { + if (w->state != WSTATE_MAPPED) { // Flags of invisible windows are processed when they are mapped return; } // Not a loop - while (win_check_flags_any(w, WIN_FLAGS_IMAGES_STALE) && + while (win_check_flags_any(w, WIN_FLAGS_PIXMAP_STALE) && !win_check_flags_all(w, WIN_FLAGS_IMAGE_ERROR)) { // Image needs to be updated, update it. if (!ps->backend_data) { @@ -525,10 +610,10 @@ void win_process_image_flags(session_t *ps, struct managed_win *w) { } if (win_check_flags_all(w, WIN_FLAGS_PIXMAP_STALE)) { - // Check to make sure the window is still mapped, otherwise we - // won't be able to rebind pixmap after releasing it, yet we might - // still need the pixmap for rendering. - assert(w->state != WSTATE_UNMAPPING && w->state != WSTATE_DESTROYING); + // Check to make sure the window is still mapped, + // otherwise we won't be able to rebind pixmap after + // releasing it, yet we might still need the pixmap for + // rendering. if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { // Must release images first, otherwise breaks // NVIDIA driver @@ -537,27 +622,13 @@ void win_process_image_flags(session_t *ps, struct managed_win *w) { win_bind_pixmap(ps->backend_data, w); } - if (win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE)) { - if (!win_check_flags_all(w, WIN_FLAGS_SHADOW_NONE)) { - win_release_shadow(ps->backend_data, w); - } - if (w->shadow) { - win_bind_shadow(ps->backend_data, w, - (struct color){.red = ps->o.shadow_red, - .green = ps->o.shadow_green, - .blue = ps->o.shadow_blue, - .alpha = ps->o.shadow_opacity}, - ps->gaussian_map); - } - } - // break here, loop always run only once break; } // Clear stale image flags - if (win_check_flags_any(w, WIN_FLAGS_IMAGES_STALE)) { - win_clear_flags(w, WIN_FLAGS_IMAGES_STALE); + if (win_check_flags_any(w, WIN_FLAGS_PIXMAP_STALE)) { + win_clear_flags(w, WIN_FLAGS_PIXMAP_STALE); } } @@ -598,7 +669,7 @@ static bool attr_pure win_has_rounded_corners(const struct managed_win *w) { return false; } -int win_update_name(session_t *ps, struct managed_win *w) { +int win_update_name(struct x_connection *c, struct atom *atoms, struct managed_win *w) { char **strlst = NULL; int nstr = 0; @@ -606,11 +677,13 @@ int win_update_name(session_t *ps, struct managed_win *w) { return 0; } - if (!(wid_get_text_prop(ps, w->client_win, ps->atoms->a_NET_WM_NAME, &strlst, &nstr))) { - log_debug("(%#010x): _NET_WM_NAME unset, falling back to WM_NAME.", + if (!(wid_get_text_prop(c, atoms, w->client_win, atoms->a_NET_WM_NAME, &strlst, &nstr))) { + log_debug("(%#010x): _NET_WM_NAME unset, falling back to " + "WM_NAME.", w->client_win); - if (!wid_get_text_prop(ps, w->client_win, ps->atoms->aWM_NAME, &strlst, &nstr)) { + if (!wid_get_text_prop(c, atoms, w->client_win, atoms->aWM_NAME, &strlst, + &nstr)) { log_debug("Unsetting window name for %#010x", w->client_win); free(w->name); w->name = NULL; @@ -633,11 +706,11 @@ int win_update_name(session_t *ps, struct managed_win *w) { return ret; } -static int win_update_role(session_t *ps, struct managed_win *w) { +int win_update_role(struct x_connection *c, struct atom *atoms, struct managed_win *w) { char **strlst = NULL; int nstr = 0; - if (!wid_get_text_prop(ps, w->client_win, ps->atoms->aWM_WINDOW_ROLE, &strlst, &nstr)) { + if (!wid_get_text_prop(c, atoms, w->client_win, atoms->aWM_WINDOW_ROLE, &strlst, &nstr)) { return -1; } @@ -659,29 +732,25 @@ static int win_update_role(session_t *ps, struct managed_win *w) { /** * Check if a window is bounding-shaped. */ -static inline bool win_bounding_shaped(const session_t *ps, xcb_window_t wid) { - if (ps->shape_exists) { - xcb_shape_query_extents_reply_t *reply; - Bool bounding_shaped; - - reply = xcb_shape_query_extents_reply( - ps->c, xcb_shape_query_extents(ps->c, wid), NULL); - bounding_shaped = reply && reply->bounding_shaped; - free(reply); - - return bounding_shaped; - } +static inline bool win_bounding_shaped(struct x_connection *c, xcb_window_t wid) { + xcb_shape_query_extents_reply_t *reply; + bool bounding_shaped; + reply = xcb_shape_query_extents_reply(c->c, xcb_shape_query_extents(c->c, wid), NULL); + bounding_shaped = reply && reply->bounding_shaped; + free(reply); - return false; + return bounding_shaped; } -static wintype_t wid_get_prop_wintype(session_t *ps, xcb_window_t wid) { +static wintype_t +wid_get_prop_wintype(struct x_connection *c, struct atom *atoms, xcb_window_t wid) { winprop_t prop = - x_get_prop(ps->c, wid, ps->atoms->a_NET_WM_WINDOW_TYPE, 32L, XCB_ATOM_ATOM, 32); + x_get_prop(c, wid, atoms->a_NET_WM_WINDOW_TYPE, 32L, XCB_ATOM_ATOM, 32); for (unsigned i = 0; i < prop.nitems; ++i) { for (wintype_t j = 1; j < NUM_WINTYPES; ++j) { - if (ps->atoms_wintypes[j] == (xcb_atom_t)prop.p32[i]) { + if (get_atom_with_nul(atoms, WINTYPES[j].atom, c->c) == + (xcb_atom_t)prop.p32[i]) { free_winprop(&prop); return j; } @@ -693,13 +762,13 @@ static wintype_t wid_get_prop_wintype(session_t *ps, xcb_window_t wid) { return WINTYPE_UNKNOWN; } -static bool -wid_get_opacity_prop(session_t *ps, xcb_window_t wid, opacity_t def, opacity_t *out) { +static bool wid_get_opacity_prop(struct x_connection *c, struct atom *atoms, + xcb_window_t wid, opacity_t def, opacity_t *out) { bool ret = false; *out = def; - winprop_t prop = x_get_prop(ps->c, wid, ps->atoms->a_NET_WM_WINDOW_OPACITY, 1L, - XCB_ATOM_CARDINAL, 32); + winprop_t prop = + x_get_prop(c, wid, atoms->a_NET_WM_WINDOW_OPACITY, 1L, XCB_ATOM_CARDINAL, 32); if (prop.nitems) { *out = *prop.c32; @@ -722,15 +791,11 @@ bool win_client_has_alpha(const struct managed_win *w) { w->client_pictfmt->direct.alpha_mask; } -winmode_t win_calc_mode(const struct managed_win *w) { - if (w->opacity < 1.0) { - return WMODE_TRANS; - } - +winmode_t win_calc_mode_raw(const struct managed_win *w) { if (win_has_alpha(w)) { if (w->client_win == XCB_NONE) { - // This is a window not managed by the WM, and it has alpha, - // so it's transparent. No need to check WM frame. + // This is a window not managed by the WM, and it has + // alpha, so it's transparent. No need to check WM frame. return WMODE_TRANS; } // The WM window has alpha @@ -740,12 +805,12 @@ winmode_t win_calc_mode(const struct managed_win *w) { return WMODE_TRANS; } if (win_has_frame(w)) { - // The client window doesn't have alpha, but we have a WM frame - // window, which has alpha. + // The client window doesn't have alpha, but we have a WM + // frame window, which has alpha. return WMODE_FRAME_TRANS; } - // Although the WM window has alpha, the frame window has 0 size, so - // consider the window solid + // Although the WM window has alpha, the frame window has 0 size, + // so consider the window solid } if (w->frame_opacity != 1.0 && win_has_frame(w)) { @@ -756,29 +821,34 @@ winmode_t win_calc_mode(const struct managed_win *w) { return WMODE_SOLID; } +winmode_t win_calc_mode(const struct managed_win *w) { + if (animatable_get(&w->opacity) < 1.0) { + return WMODE_TRANS; + } + return win_calc_mode_raw(w); +} + /** * Calculate and return the opacity target of a window. * * The priority of opacity settings are: * - * inactive_opacity_override (if set, and unfocused) > _NET_WM_WINDOW_OPACITY (if set) > - * opacity-rules (if matched) > window type default opacity > active/inactive opacity + * inactive_opacity_override (if set, and unfocused) > _NET_WM_WINDOW_OPACITY (if + * set) > opacity-rules (if matched) > window type default opacity > + * active/inactive opacity * * @param ps current session * @param w struct _win object representing the window * * @return target opacity */ -double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { +static double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { double opacity = 1; - if (w->state == WSTATE_UNMAPPED) { + if (w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYED) { // be consistent return 0; } - if (w->state == WSTATE_UNMAPPING || w->state == WSTATE_DESTROYING) { - return 0; - } // Try obeying opacity property and window type opacity firstly if (w->has_opacity_prop) { opacity = ((double)w->opacity_prop) / OPAQUE; @@ -787,12 +857,14 @@ double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { } else if (!safe_isnan(ps->o.wintype_option[w->window_type].opacity)) { opacity = ps->o.wintype_option[w->window_type].opacity; } else { - // Respect active_opacity only when the window is physically focused - if (win_is_focused_raw(ps, w)) + // Respect active_opacity only when the window is physically + // focused + if (win_is_focused_raw(w)) { opacity = ps->o.active_opacity; - else if (!w->focused) + } else if (!w->focused) { // Respect inactive_opacity in some cases opacity = ps->o.inactive_opacity; + } } // respect inactive override @@ -803,19 +875,62 @@ double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { return opacity; } -/** - * Determine whether a window is to be dimmed. - */ -bool win_should_dim(session_t *ps, const struct managed_win *w) { - // Make sure we do nothing if the window is unmapped / being destroyed - if (w->state == WSTATE_UNMAPPED) { - return false; - } +/// Finish the unmapping of a window (e.g. after fading has finished). +/// Doesn't free `w` +static void unmap_win_finish(session_t *ps, struct managed_win *w) { + w->reg_ignore_valid = false; + w->state = WSTATE_UNMAPPED; - if (ps->o.inactive_dim > 0 && !(w->focused)) { - return true; + // We are in unmap_win, this window definitely was viewable + if (ps->backend_data) { + // Only the pixmap needs to be freed and reacquired when mapping. + // Shadow image can be preserved. + if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { + win_release_pixmap(ps->backend_data, w); + } } else { - return false; + assert(!w->win_image); + assert(!w->shadow_image); + } + + free_paint(ps, &w->paint); + free_paint(ps, &w->shadow_paint); + + // Try again at binding images when the window is mapped next time + win_clear_flags(w, WIN_FLAGS_IMAGE_ERROR); + assert(w->number_of_animations == 0); +} + +struct window_transition_data { + struct managed_win *w; + session_t *ps; + // TODO(yshui) switch to only pass backend_data after the legacy backend removal + // struct backend_base *backend_data; + uint64_t refcount; +}; + +static void win_transition_callback(enum transition_event event, void *data_) { + auto data = (struct window_transition_data *)data_; + auto w = data->w; + w->number_of_animations--; + if (w->number_of_animations == 0 && event != TRANSITION_INTERRUPTED) { + if (w->state == WSTATE_DESTROYED || w->state == WSTATE_UNMAPPED) { + if (animatable_get(&w->opacity) != 0) { + log_warn("Window %#010x (%s) has finished fading out but " + "its opacity is not 0", + w->base.id, w->name); + } + } + if (w->state == WSTATE_UNMAPPED) { + unmap_win_finish(data->ps, data->w); + } + // Destroyed windows are freed in paint_preprocess, this makes managing + // the lifetime of windows easier. + w->in_openclose = false; + } + data->refcount--; + if (data->refcount == 0) { + free(data); } } @@ -823,14 +938,15 @@ bool win_should_dim(session_t *ps, const struct managed_win *w) { * Determine if a window should fade on opacity change. */ bool win_should_fade(session_t *ps, const struct managed_win *w) { - // To prevent it from being overwritten by last-paint value if the window is + // To prevent it from being overwritten by last-paint value if the window + // is if (w->fade_force != UNSET) { return w->fade_force; } if (ps->o.no_fading_openclose && w->in_openclose) { return false; } - if (ps->o.no_fading_destroyed_argb && w->state == WSTATE_DESTROYING && + if (ps->o.no_fading_destroyed_argb && w->state == WSTATE_DESTROYED && win_has_alpha(w) && w->client_win && w->client_win != w->base.id) { // deprecated return false; @@ -841,14 +957,66 @@ bool win_should_fade(session_t *ps, const struct managed_win *w) { return ps->o.wintype_option[w->window_type].fade; } +/// Call `animatable_set_target` on the opacity of a window, with appropriate +/// target opacity and duration. +static inline void +win_start_fade(session_t *ps, struct managed_win *w, double target_blur_opacity) { + double current_opacity = animatable_get(&w->opacity), + target_opacity = win_calc_opacity_target(ps, w); + double step_size = + target_opacity > current_opacity ? ps->o.fade_in_step : ps->o.fade_out_step; + double duration = (fabs(target_opacity - current_opacity) / step_size) * + (double)ps->o.fade_delta / 1000.0; + if (!win_should_fade(ps, w)) { + duration = 0; + } + + auto data = ccalloc(1, struct window_transition_data); + data->ps = ps; + data->w = w; + data->refcount = 0; + if (animatable_set_target(&w->opacity, target_opacity, duration, + curve_new_linear(), win_transition_callback, data)) { + data->refcount++; + w->number_of_animations++; + } + if (animatable_set_target(&w->blur_opacity, target_blur_opacity, duration, + curve_new_linear(), win_transition_callback, data)) { + data->refcount++; + w->number_of_animations++; + } + if (!data->refcount) { + free(data); + } + if (w->number_of_animations == 0 && w->state == WSTATE_UNMAPPED) { + unmap_win_finish(ps, w); + } +} + +/** + * Determine whether a window is to be dimmed. + */ +bool win_should_dim(session_t *ps, const struct managed_win *w) { + // Make sure we do nothing if the window is unmapped / being destroyed + if (w->state == WSTATE_UNMAPPED) { + return false; + } + + if (ps->o.inactive_dim > 0 && !(w->focused)) { + return true; + } + return false; +} + /** * Reread _COMPTON_SHADOW property from a window. * * The property must be set on the outermost window, usually the WM frame. */ -void win_update_prop_shadow_raw(session_t *ps, struct managed_win *w) { - winprop_t prop = x_get_prop(ps->c, w->base.id, ps->atoms->a_COMPTON_SHADOW, 1, - XCB_ATOM_CARDINAL, 32); +void win_update_prop_shadow_raw(struct x_connection *c, struct atom *atoms, + struct managed_win *w) { + winprop_t prop = + x_get_prop(c, w->base.id, atoms->a_COMPTON_SHADOW, 1, XCB_ATOM_CARDINAL, 32); if (!prop.nitems) { w->prop_shadow = -1; @@ -867,9 +1035,9 @@ static void win_set_shadow(session_t *ps, struct managed_win *w, bool shadow_new log_debug("Updating shadow property of window %#010x (%s) to %d", w->base.id, w->name, shadow_new); - // We don't handle property updates of non-visible windows until they are mapped. - assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING); + // We don't handle property updates of non-visible windows until they are + // mapped. + assert(w->state == WSTATE_MAPPED); // Keep a copy of window extent before the shadow change. Will be used for // calculation of damaged region @@ -877,48 +1045,32 @@ static void win_set_shadow(session_t *ps, struct managed_win *w, bool shadow_new pixman_region32_init(&extents); win_extents(w, &extents); - // Apply the shadow change - w->shadow = shadow_new; - if (ps->redirected) { // Add damage for shadow change // Window extents need update on shadow state change // Shadow geometry currently doesn't change on shadow state change // calc_shadow_geometry(ps, w); - - // Note: because the release and creation of the shadow images are - // delayed. When multiple shadow changes happen in a row, without - // rendering phase between them, there could be a stale shadow image - // attached to the window even if w->shadow was previously false. And vice - // versa. So we check the STALE flag before asserting the existence of the - // shadow image. - if (w->shadow) { + if (shadow_new) { // Mark the new extents as damaged if the shadow is added - assert(!w->shadow_image || - win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE) || - !ps->o.experimental_backends); + assert(!w->shadow_image); pixman_region32_clear(&extents); win_extents(w, &extents); add_damage_from_win(ps, w); } else { - // Mark the old extents as damaged if the shadow is removed - assert(w->shadow_image || - win_check_flags_all(w, WIN_FLAGS_SHADOW_STALE) || - !ps->o.experimental_backends); + // Mark the old extents as damaged if the shadow is + // removed add_damage(ps, &extents); + win_release_shadow(ps->backend_data, w); } - // Delayed update of shadow image - // By setting WIN_FLAGS_SHADOW_STALE, we ask win_process_flags to - // re-create or release the shaodw in based on whether w->shadow is set. - win_set_flags(w, WIN_FLAGS_SHADOW_STALE); - - // Only set pending_updates if we are redirected. Otherwise change of a - // shadow won't have influence on whether we should redirect. + // Only set pending_updates if we are redirected. Otherwise change + // of a shadow won't have influence on whether we should redirect. ps->pending_updates = true; } + w->shadow = shadow_new; + pixman_region32_fini(&extents); } @@ -937,7 +1089,7 @@ static void win_determine_shadow(session_t *ps, struct managed_win *w) { if (!ps->o.wintype_option[w->window_type].shadow) { log_debug("Shadow disabled by wintypes"); shadow_new = false; - } else if (c2_match(ps, w, ps->o.shadow_blacklist, NULL)) { + } else if (c2_match(ps->c2_state, w, ps->o.shadow_blacklist, NULL)) { log_debug("Shadow disabled by shadow-exclude"); shadow_new = false; } else if (ps->o.shadow_ignore_shaped && w->bounding_shaped && @@ -957,19 +1109,36 @@ static void win_determine_shadow(session_t *ps, struct managed_win *w) { * Reread _COMPTON_SHADOW property from a window and update related * things. */ -void win_update_prop_shadow(session_t *ps, struct managed_win *w) { - long attr_shadow_old = w->prop_shadow; - - win_update_prop_shadow_raw(ps, w); +static bool +win_update_prop_shadow(struct x_connection *c, struct atom *atoms, struct managed_win *w) { + long long attr_shadow_old = w->prop_shadow; + win_update_prop_shadow_raw(c, atoms, w); + return w->prop_shadow != attr_shadow_old; +} - if (w->prop_shadow != attr_shadow_old) { - win_determine_shadow(ps, w); +/** + * Update window EWMH fullscreen state. + */ +bool win_update_prop_fullscreen(struct x_connection *c, const struct atom *atoms, + struct managed_win *w) { + auto prop = x_get_prop(c, w->client_win, atoms->a_NET_WM_STATE, 12, XCB_ATOM_ATOM, 0); + bool is_fullscreen = false; + for (uint32_t i = 0; i < prop.nitems; i++) { + if (prop.atom[i] == atoms->a_NET_WM_STATE_FULLSCREEN) { + is_fullscreen = true; + break; + } } + free_winprop(&prop); + + bool changed = w->is_ewmh_fullscreen != is_fullscreen; + w->is_ewmh_fullscreen = is_fullscreen; + return changed; } static void win_determine_clip_shadow_above(session_t *ps, struct managed_win *w) { bool should_crop = (ps->o.wintype_option[w->window_type].clip_shadow_above || - c2_match(ps, w, ps->o.shadow_clip_list, NULL)); + c2_match(ps->c2_state, w, ps->o.shadow_clip_list, NULL)); w->clip_shadow_above = should_crop; } @@ -992,7 +1161,7 @@ static void win_determine_invert_color(session_t *ps, struct managed_win *w) { if (UNSET != w->invert_color_force) { invert_color_new = w->invert_color_force; } else if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { - invert_color_new = c2_match(ps, w, ps->o.invert_color_list, NULL); + invert_color_new = c2_match(ps->c2_state, w, ps->o.invert_color_list, NULL); } win_set_invert_color(ps, w, invert_color_new); @@ -1042,13 +1211,28 @@ void win_set_shadow_force(session_t *ps, struct managed_win *w, switch_t val) { static void win_set_blur_background(session_t *ps, struct managed_win *w, bool blur_background_new) { - if (w->blur_background == blur_background_new) + if (w->blur_background == blur_background_new) { return; + } w->blur_background = blur_background_new; - // This damage might not be absolutely necessary (e.g. when the window is opaque), - // but blur_background changes should be rare, so this should be fine. + // This damage might not be absolutely necessary (e.g. when the window is + // opaque), but blur_background changes should be rare, so this should be + // fine. + add_damage_from_win(ps, w); +} + +static void +win_set_fg_shader(session_t *ps, struct managed_win *w, struct shader_info *shader_new) { + if (w->fg_shader == shader_new) { + return; + } + + w->fg_shader = shader_new; + + // A different shader might change how the window is drawn, these changes + // should be rare however, so this should be fine. add_damage_from_win(ps, w); } @@ -1066,8 +1250,9 @@ static void win_determine_blur_background(session_t *ps, struct managed_win *w) if (!ps->o.wintype_option[w->window_type].blur_background) { log_debug("Blur background disabled by wintypes"); blur_background_new = false; - } else if (c2_match(ps, w, ps->o.blur_background_blacklist, NULL)) { - log_debug("Blur background disabled by blur-background-exclude"); + } else if (c2_match(ps->c2_state, w, ps->o.blur_background_blacklist, NULL)) { + log_debug("Blur background disabled by " + "blur-background-exclude"); blur_background_new = false; } } @@ -1079,18 +1264,30 @@ static void win_determine_blur_background(session_t *ps, struct managed_win *w) * Determine if a window should have rounded corners. */ static void win_determine_rounded_corners(session_t *ps, struct managed_win *w) { - if (ps->o.corner_radius == 0) { + void *radius_override = NULL; + if (c2_match(ps->c2_state, w, ps->o.corner_radius_rules, &radius_override)) { + log_debug("Matched corner rule! %d", w->corner_radius); + } + + if (ps->o.corner_radius == 0 && !radius_override) { w->corner_radius = 0; return; } - // Don't round full screen windows & excluded windows - if ((w && win_is_fullscreen(ps, w)) || - c2_match(ps, w, ps->o.rounded_corners_blacklist, NULL)) { + // Don't round full screen windows & excluded windows, + // unless we find a corner override in corner_radius_rules + if (!radius_override && + ((w && w->is_fullscreen) || + c2_match(ps->c2_state, w, ps->o.rounded_corners_blacklist, NULL))) { w->corner_radius = 0; log_debug("Not rounding corners for window %#010x", w->base.id); } else { - w->corner_radius = ps->o.corner_radius; + if (radius_override) { + w->corner_radius = (int)(long)radius_override; + } else { + w->corner_radius = ps->o.corner_radius; + } + log_debug("Rounding corners for window %#010x", w->base.id); // Initialize the border color to an invalid value w->border_col[0] = w->border_col[1] = w->border_col[2] = @@ -1098,6 +1295,28 @@ static void win_determine_rounded_corners(session_t *ps, struct managed_win *w) } } +/** + * Determine custom window shader to use for a window. + */ +static void win_determine_fg_shader(session_t *ps, struct managed_win *w) { + if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { + return; + } + + auto shader_new = ps->o.window_shader_fg; + void *val = NULL; + if (c2_match(ps->c2_state, w, ps->o.window_shader_fg_rules, &val)) { + shader_new = val; + } + + struct shader_info *shader = NULL; + if (shader_new) { + HASH_FIND_STR(ps->shaders, shader_new, shader); + } + + win_set_fg_shader(ps, w, shader); +} + /** * Update window opacity according to opacity rules. */ @@ -1109,7 +1328,7 @@ void win_update_opacity_rule(session_t *ps, struct managed_win *w) { double opacity = 1.0; bool is_set = false; void *val = NULL; - if (c2_match(ps, w, ps->o.opacity_rules, &val)) { + if (c2_match(ps->c2_state, w, ps->o.opacity_rules, &val)) { opacity = ((double)(long)val) / 100.0; is_set = true; } @@ -1125,77 +1344,87 @@ void win_update_opacity_rule(session_t *ps, struct managed_win *w) { */ void win_on_factor_change(session_t *ps, struct managed_win *w) { log_debug("Window %#010x (%s) factor change", w->base.id, w->name); - // Focus needs to be updated first, as other rules might depend on the focused - // state of the window + c2_window_state_update(ps->c2_state, &w->c2_state, ps->c.c, w->client_win, w->base.id); + // Focus and is_fullscreen needs to be updated first, as other rules might depend + // on the focused state of the window win_update_focused(ps, w); + win_update_is_fullscreen(ps, w); win_determine_shadow(ps, w); win_determine_clip_shadow_above(ps, w); win_determine_invert_color(ps, w); win_determine_blur_background(ps, w); win_determine_rounded_corners(ps, w); + win_determine_fg_shader(ps, w); w->mode = win_calc_mode(w); log_debug("Window mode changed to %d", w->mode); win_update_opacity_rule(ps, w); if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { - w->paint_excluded = c2_match(ps, w, ps->o.paint_blacklist, NULL); + w->paint_excluded = c2_match(ps->c2_state, w, ps->o.paint_blacklist, NULL); } if (w->a.map_state == XCB_MAP_STATE_VIEWABLE) { w->unredir_if_possible_excluded = - c2_match(ps, w, ps->o.unredir_if_possible_blacklist, NULL); + c2_match(ps->c2_state, w, ps->o.unredir_if_possible_blacklist, NULL); } - w->fade_excluded = c2_match(ps, w, ps->o.fade_blacklist, NULL); + w->fade_excluded = c2_match(ps->c2_state, w, ps->o.fade_blacklist, NULL); + + w->transparent_clipping = + ps->o.transparent_clipping && + !c2_match(ps->c2_state, w, ps->o.transparent_clipping_blacklist, NULL); win_update_opacity_target(ps, w); w->reg_ignore_valid = false; + if (ps->debug_window != XCB_NONE && + (w->base.id == ps->debug_window || w->client_win == ps->debug_window)) { + w->paint_excluded = true; + } } /** * Update cache data in struct _win that depends on window size. */ -void win_on_win_size_change(session_t *ps, struct managed_win *w) { +void win_on_win_size_change(struct managed_win *w, int shadow_offset_x, + int shadow_offset_y, int shadow_radius) { + log_trace("Window %#010x (%s) size changed, was %dx%d, now %dx%d", w->base.id, + w->name, w->widthb, w->heightb, w->g.width + w->g.border_width * 2, + w->g.height + w->g.border_width * 2); + w->widthb = w->g.width + w->g.border_width * 2; w->heightb = w->g.height + w->g.border_width * 2; - w->shadow_dx = ps->o.shadow_offset_x; - w->shadow_dy = ps->o.shadow_offset_y; - w->shadow_width = w->widthb + ps->o.shadow_radius * 2; - w->shadow_height = w->heightb + ps->o.shadow_radius * 2; - - // We don't handle property updates of non-visible windows until they are mapped. - assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING); - - // Invalidate the shadow we built - win_set_flags(w, WIN_FLAGS_IMAGES_STALE); - ps->pending_updates = true; - free_paint(ps, &w->shadow_paint); + w->shadow_dx = shadow_offset_x; + w->shadow_dy = shadow_offset_y; + w->shadow_width = w->widthb + shadow_radius * 2; + w->shadow_height = w->heightb + shadow_radius * 2; + + // We don't handle property updates of non-visible windows until they are + // mapped. + assert(w->state == WSTATE_MAPPED); } /** * Update window type. */ -void win_update_wintype(session_t *ps, struct managed_win *w) { +bool win_update_wintype(struct x_connection *c, struct atom *atoms, struct managed_win *w) { const wintype_t wtype_old = w->window_type; // Detect window type here - w->window_type = wid_get_prop_wintype(ps, w->client_win); + w->window_type = wid_get_prop_wintype(c, atoms, w->client_win); // Conform to EWMH standard, if _NET_WM_WINDOW_TYPE is not present, take // override-redirect windows or windows without WM_TRANSIENT_FOR as // _NET_WM_WINDOW_TYPE_NORMAL, otherwise as _NET_WM_WINDOW_TYPE_DIALOG. if (WINTYPE_UNKNOWN == w->window_type) { if (w->a.override_redirect || - !wid_has_prop(ps, w->client_win, ps->atoms->aWM_TRANSIENT_FOR)) + !wid_has_prop(c->c, w->client_win, atoms->aWM_TRANSIENT_FOR)) { w->window_type = WINTYPE_NORMAL; - else + } else { w->window_type = WINTYPE_DIALOG; + } } - if (w->window_type != wtype_old) { - win_on_factor_change(ps, w); - } + return w->window_type != wtype_old; } /** @@ -1205,7 +1434,7 @@ void win_update_wintype(session_t *ps, struct managed_win *w) { * @param w struct _win of the parent window * @param client window ID of the client window */ -void win_mark_client(session_t *ps, struct managed_win *w, xcb_window_t client) { +static void win_mark_client(session_t *ps, struct managed_win *w, xcb_window_t client) { w->client_win = client; // If the window isn't mapped yet, stop here, as the function will be @@ -1214,41 +1443,38 @@ void win_mark_client(session_t *ps, struct managed_win *w, xcb_window_t client) return; } - auto e = xcb_request_check( - ps->c, xcb_change_window_attributes( - ps->c, client, XCB_CW_EVENT_MASK, - (const uint32_t[]){determine_evmask(ps, client, WIN_EVMODE_CLIENT)})); - if (e) { - log_error("Failed to change event mask of window %#010x", client); - free(e); - } - - win_update_wintype(ps, w); + win_update_wintype(&ps->c, ps->atoms, w); // Get frame widths. The window is in damaged area already. - win_update_frame_extents(ps, w, client); + win_update_frame_extents(&ps->c, ps->atoms, w, client, ps->o.frame_opacity); // Get window group if (ps->o.track_leader) { - win_update_leader(ps, w); + auto new_leader = win_get_leader_property(&ps->c, ps->atoms, w->client_win, + ps->o.detect_transient, + ps->o.detect_client_leader); + if (w->leader != new_leader) { + win_set_leader(ps, w, new_leader); + } } // Get window name and class if we are tracking them - win_update_name(ps, w); - win_update_class(ps, w); - win_update_role(ps, w); + win_update_name(&ps->c, ps->atoms, w); + win_update_class(&ps->c, ps->atoms, w); + win_update_role(&ps->c, ps->atoms, w); // Update everything related to conditions win_on_factor_change(ps, w); + xcb_generic_error_t *e = NULL; auto r = xcb_get_window_attributes_reply( - ps->c, xcb_get_window_attributes(ps->c, w->client_win), &e); + ps->c.c, xcb_get_window_attributes(ps->c.c, w->client_win), &e); if (!r) { log_error_x_error(e, "Failed to get client window attributes"); return; } - w->client_pictfmt = x_get_pictform_for_visual(ps->c, r->visual); + w->client_pictfmt = x_get_pictform_for_visual(&ps->c, r->visual); free(r); } @@ -1258,84 +1484,72 @@ void win_mark_client(session_t *ps, struct managed_win *w, xcb_window_t client) * @param ps current session * @param w struct _win of the parent window */ -void win_unmark_client(session_t *ps, struct managed_win *w) { +void win_unmark_client(struct managed_win *w) { xcb_window_t client = w->client_win; log_debug("Detaching client window %#010x from frame %#010x (%s)", client, w->base.id, w->name); - w->client_win = XCB_NONE; - - // Recheck event mask - xcb_change_window_attributes( - ps->c, client, XCB_CW_EVENT_MASK, - (const uint32_t[]){determine_evmask(ps, client, WIN_EVMODE_UNKNOWN)}); } /** * Look for the client window of a particular window. */ -static xcb_window_t find_client_win(session_t *ps, xcb_window_t w) { - if (wid_has_prop(ps, w, ps->atoms->aWM_STATE)) { - return w; - } - +static xcb_window_t +find_client_win(struct x_connection *c, struct wm *wm, struct atom *atoms, xcb_window_t w) { xcb_query_tree_reply_t *reply = - xcb_query_tree_reply(ps->c, xcb_query_tree(ps->c, w), NULL); + xcb_query_tree_reply(c->c, xcb_query_tree(c->c, w), NULL); if (!reply) { - return 0; + return XCB_NONE; } xcb_window_t *children = xcb_query_tree_children(reply); int nchildren = xcb_query_tree_children_length(reply); - int i; - xcb_window_t ret = 0; - - for (i = 0; i < nchildren; ++i) { - if ((ret = find_client_win(ps, children[i]))) { + xcb_window_t ret = XCB_NONE; + + for (int i = 0; i < nchildren; ++i) { + auto subwin = wm ? wm_subwin_find(wm, children[i]) : NULL; + bool has_wm_state; + assert(subwin != NULL || wm == NULL); + if (!subwin || subwin->has_wm_state == TRI_UNKNOWN) { + has_wm_state = wid_has_prop(c->c, children[i], atoms->aWM_STATE); + if (subwin) { + subwin->has_wm_state = has_wm_state ? TRI_TRUE : TRI_FALSE; + } + } else { + has_wm_state = subwin->has_wm_state == TRI_TRUE; + } + if (has_wm_state) { + ret = children[i]; break; } } free(reply); - return ret; } /** - * Recheck client window of a window. + * Get client window of a window. * * @param ps current session * @param w struct _win of the parent window */ -void win_recheck_client(session_t *ps, struct managed_win *w) { - assert(ps->server_grabbed); - // Initialize wmwin to false - w->wmwin = false; - - // Look for the client window - +xcb_window_t win_get_client_window(struct x_connection *c, struct wm *wm, + struct atom *atoms, const struct managed_win *w) { // Always recursively look for a window with WM_STATE, as Fluxbox // sets override-redirect flags on all frame windows. - xcb_window_t cw = find_client_win(ps, w->base.id); + xcb_window_t cw = find_client_win(c, wm, atoms, w->base.id); if (cw) { log_debug("(%#010x): client %#010x", w->base.id, cw); - } - // Set a window's client window to itself if we couldn't find a - // client window - if (!cw) { + } else { + // Set a window's client window to itself if we couldn't find a + // client window cw = w->base.id; - w->wmwin = !w->a.override_redirect; log_debug("(%#010x): client self (%s)", w->base.id, - (w->wmwin ? "wmwin" : "override-redirected")); + (w->a.override_redirect ? "override-redirected" : "wmwin")); } - // Unmark the old one - if (w->client_win && w->client_win != cw) { - win_unmark_client(ps, w); - } - - // Mark the new one - win_mark_client(ps, w, cw); + return cw; } /** @@ -1352,9 +1566,10 @@ void free_win_res(session_t *ps, struct managed_win *w) { // Above should be done during unmapping // Except when we are called by session_destroy + pixman_region32_fini(&w->damaged); pixman_region32_fini(&w->bounding_shape); // BadDamage may be thrown if the window is destroyed - set_ignore_cookie(ps, xcb_damage_destroy(ps->c, w->damage)); + set_ignore_cookie(&ps->c, xcb_damage_destroy(ps->c.c, w->damage)); rc_region_unref(&w->reg_ignore); free(w->name); free(w->class_instance); @@ -1364,62 +1579,17 @@ void free_win_res(session_t *ps, struct managed_win *w) { free(w->stale_props); w->stale_props = NULL; w->stale_props_capacity = 0; + c2_window_state_destroy(ps->c2_state, &w->c2_state); } -/// Insert a new window after list_node `prev` -/// New window will be in unmapped state -static struct win *add_win(session_t *ps, xcb_window_t id, struct list_node *prev) { - log_debug("Adding window %#010x", id); - struct win *old_w = NULL; - HASH_FIND_INT(ps->windows, &id, old_w); - assert(old_w == NULL); - - auto new_w = cmalloc(struct win); - list_insert_after(prev, &new_w->stack_neighbour); - new_w->id = id; - new_w->managed = false; - new_w->is_new = true; - new_w->destroyed = false; - - HASH_ADD_INT(ps->windows, id, new_w); - ps->pending_updates = true; - return new_w; -} - -/// Insert a new win entry at the top of the stack -struct win *add_win_top(session_t *ps, xcb_window_t id) { - return add_win(ps, id, &ps->window_stack); -} - -/// Insert a new window above window with id `below`, if there is no window, add to top -/// New window will be in unmapped state -struct win *add_win_above(session_t *ps, xcb_window_t id, xcb_window_t below) { - struct win *w = NULL; - HASH_FIND_INT(ps->windows, &below, w); - if (!w) { - if (!list_is_empty(&ps->window_stack)) { - // `below` window is not found even if the window stack is not - // empty - return NULL; - } - return add_win_top(ps, id); - } else { - // we found something from the hash table, so if the stack is empty, - // we are in an inconsistent state. - assert(!list_is_empty(&ps->window_stack)); - return add_win(ps, id, w->stack_neighbour.prev); - } -} - -/// Query the Xorg for information about window `win` -/// `win` pointer might become invalid after this function returns -/// Returns the pointer to the window, might be different from `w` -struct win *fill_win(session_t *ps, struct win *w) { +/// Query the Xorg for information about window `win`, and return that +/// information in a new managed_win object. However, if the window does +/// not need to be managed, the original `win` object is returned. +struct win *attr_ret_nonnull maybe_allocate_managed_win(session_t *ps, struct win *w) { static const struct managed_win win_def = { // No need to initialize. (or, you can think that // they are initialized right here). // The following ones are updated during paint or paint preprocess - .shadow_opacity = 0.0, .to_paint = false, .frame_opacity = 1.0, .dim = false, @@ -1432,7 +1602,8 @@ struct win *fill_win(session_t *ps, struct win *w) { .in_openclose = true, // set to false after first map is done, // true here because window is just created .reg_ignore_valid = false, // set to true when damaged - .flags = WIN_FLAGS_IMAGES_NONE, // updated by property/attributes/etc + .flags = WIN_FLAGS_PIXMAP_NONE, // updated by + // property/attributes/etc // change .stale_props = NULL, .stale_props_capacity = 0, @@ -1460,20 +1631,19 @@ struct win *fill_win(session_t *ps, struct win *w) { // is mapped .win_image = NULL, .shadow_image = NULL, + .mask_image = NULL, .prev_trans = NULL, .shadow = false, .clip_shadow_above = false, - .xinerama_scr = -1, + .fg_shader = NULL, + .randr_monitor = -1, .mode = WMODE_TRANS, .ever_damaged = false, .client_win = XCB_NONE, .leader = XCB_NONE, .cache_leader = XCB_NONE, .window_type = WINTYPE_UNKNOWN, - .wmwin = false, .focused = false, - .opacity = 0, - .opacity_target = 0, .has_opacity_prop = false, .opacity_prop = OPAQUE, .opacity_is_set = false, @@ -1484,6 +1654,7 @@ struct win *fill_win(session_t *ps, struct win *w) { .rounded_corners = false, .paint_excluded = false, .fade_excluded = false, + .transparent_clipping = false, .unredir_if_possible_excluded = false, .prop_shadow = -1, // following 4 are set in win_mark_client @@ -1504,34 +1675,39 @@ struct win *fill_win(session_t *ps, struct win *w) { w->is_new = false; - // Reject overlay window and already added windows + // Reject overlay window if (w->id == ps->overlay) { + // Would anyone reparent windows to the overlay window? Doing this + // just in case. return w; } - auto duplicated_win = find_managed_win(ps, w->id); + auto duplicated_win = wm_find_managed(ps->wm, w->id); if (duplicated_win) { - log_debug("Window %#010x (recorded name: %s) added multiple times", w->id, - duplicated_win->name); + log_debug("Window %#010x (recorded name: %s) added multiple " + "times", + w->id, duplicated_win->name); return &duplicated_win->base; } log_debug("Managing window %#010x", w->id); - xcb_get_window_attributes_cookie_t acookie = xcb_get_window_attributes(ps->c, w->id); + xcb_get_window_attributes_cookie_t acookie = + xcb_get_window_attributes(ps->c.c, w->id); xcb_get_window_attributes_reply_t *a = - xcb_get_window_attributes_reply(ps->c, acookie, NULL); + xcb_get_window_attributes_reply(ps->c.c, acookie, NULL); if (!a || a->map_state == XCB_MAP_STATE_UNVIEWABLE) { // Failed to get window attributes or geometry probably means // the window is gone already. Unviewable means the window is // already reparented elsewhere. - // BTW, we don't care about Input Only windows, except for stacking - // proposes, so we need to keep track of them still. + // BTW, we don't care about Input Only windows, except for + // stacking proposes, so we need to keep track of them still. free(a); return w; } if (a->_class == XCB_WINDOW_CLASS_INPUT_ONLY) { - // No need to manage this window, but we still keep it on the window stack + // No need to manage this window, but we still keep it on the + // window stack w->managed = false; free(a); return w; @@ -1548,12 +1724,15 @@ struct win *fill_win(session_t *ps, struct win *w) { new->base = *w; new->base.managed = true; new->a = *a; + new->opacity = animatable_new(0); + new->blur_opacity = animatable_new(0); + new->shadow_opacity = ps->o.shadow_opacity; pixman_region32_init(&new->bounding_shape); free(a); xcb_generic_error_t *e; - auto g = xcb_get_geometry_reply(ps->c, xcb_get_geometry(ps->c, w->id), &e); + auto g = xcb_get_geometry_reply(ps->c.c, xcb_get_geometry(ps->c.c, w->id), &e); if (!g) { log_error_x_error(e, "Failed to get geometry of window %#010x", w->id); free(e); @@ -1571,10 +1750,10 @@ struct win *fill_win(session_t *ps, struct win *w) { free(g); // Create Damage for window (if not Input Only) - new->damage = x_new_id(ps->c); + new->damage = x_new_id(&ps->c); e = xcb_request_check( - ps->c, xcb_damage_create_checked(ps->c, new->damage, w->id, - XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY)); + ps->c.c, xcb_damage_create_checked(ps->c.c, new->damage, w->id, + XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY)); if (e) { log_error_x_error(e, "Failed to create damage"); free(e); @@ -1583,26 +1762,35 @@ struct win *fill_win(session_t *ps, struct win *w) { } // Set window event mask - xcb_change_window_attributes( - ps->c, new->base.id, XCB_CW_EVENT_MASK, - (const uint32_t[]){determine_evmask(ps, new->base.id, WIN_EVMODE_FRAME)}); + uint32_t frame_event_mask = + XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY; + if (!ps->o.use_ewmh_active_win) { + frame_event_mask |= XCB_EVENT_MASK_FOCUS_CHANGE; + } + xcb_change_window_attributes(ps->c.c, new->base.id, XCB_CW_EVENT_MASK, + (const uint32_t[]){frame_event_mask}); + + // Add existing subwins of this window + auto tree_reply = + xcb_query_tree_reply(ps->c.c, xcb_query_tree(ps->c.c, new->base.id), NULL); + if (tree_reply) { + auto children = xcb_query_tree_children(tree_reply); + for (int i = 0; i < xcb_query_tree_children_length(tree_reply); i++) { + wm_subwin_add_and_subscribe(ps->wm, &ps->c, children[i], new->base.id); + } + free(tree_reply); + } // Get notification when the shape of a window changes if (ps->shape_exists) { - xcb_shape_select_input(ps->c, new->base.id, 1); + xcb_shape_select_input(ps->c.c, new->base.id, 1); } - new->pictfmt = x_get_pictform_for_visual(ps->c, new->a.visual); + new->pictfmt = x_get_pictform_for_visual(&ps->c, new->a.visual); new->client_pictfmt = NULL; - list_replace(&w->stack_neighbour, &new->base.stack_neighbour); - struct win *replaced = NULL; - HASH_REPLACE_INT(ps->windows, id, &new->base, replaced); - assert(replaced == w); - free(w); - - // Set all the stale flags on this new window, so it's properties will get updated - // when it's mapped + // Set all the stale flags on this new window, so it's properties will get + // updated when it's mapped win_set_flags(new, WIN_FLAGS_CLIENT_STALE | WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE | WIN_FLAGS_PROPERTY_STALE | WIN_FLAGS_FACTOR_CHANGED); @@ -1612,68 +1800,34 @@ struct win *fill_win(session_t *ps, struct win *w) { ps->atoms->a_NET_WM_NAME, ps->atoms->aWM_CLASS, ps->atoms->aWM_WINDOW_ROLE, ps->atoms->a_COMPTON_SHADOW, ps->atoms->aWM_CLIENT_LEADER, ps->atoms->aWM_TRANSIENT_FOR, + ps->atoms->a_NET_WM_STATE, }; win_set_properties_stale(new, init_stale_props, ARR_SIZE(init_stale_props)); + c2_window_state_init(ps->c2_state, &new->c2_state); + pixman_region32_init(&new->damaged); -#ifdef CONFIG_DBUS - // Send D-Bus signal - if (ps->o.dbus) { - cdbus_ev_win_added(ps, &new->base); - } -#endif return &new->base; } -/** - * Set leader of a window. - */ -static inline void win_set_leader(session_t *ps, struct managed_win *w, xcb_window_t nleader) { - // If the leader changes - if (w->leader != nleader) { - xcb_window_t cache_leader_old = win_get_leader(ps, w); - - w->leader = nleader; - - // Forcefully do this to deal with the case when a child window - // gets mapped before parent, or when the window is a waypoint - clear_cache_win_leaders(ps); - - // Update the old and new window group and active_leader if the window - // could affect their state. - xcb_window_t cache_leader = win_get_leader(ps, w); - if (win_is_focused_raw(ps, w) && cache_leader_old != cache_leader) { - ps->active_leader = cache_leader; - - group_on_factor_change(ps, cache_leader_old); - group_on_factor_change(ps, cache_leader); - } - - // Update everything related to conditions - win_on_factor_change(ps, w); - } -} - /** * Update leader of a window. */ -void win_update_leader(session_t *ps, struct managed_win *w) { +static xcb_window_t +win_get_leader_property(struct x_connection *c, struct atom *atoms, xcb_window_t wid, + bool detect_transient, bool detect_client_leader) { xcb_window_t leader = XCB_NONE; // Read the leader properties - if (ps->o.detect_transient && !leader) { - leader = - wid_get_prop_window(ps->c, w->client_win, ps->atoms->aWM_TRANSIENT_FOR); + if (detect_transient) { + leader = wid_get_prop_window(c, wid, atoms->aWM_TRANSIENT_FOR); } - if (ps->o.detect_client_leader && !leader) { - leader = - wid_get_prop_window(ps->c, w->client_win, ps->atoms->aWM_CLIENT_LEADER); + if (detect_client_leader && leader == XCB_NONE) { + leader = wid_get_prop_window(c, wid, atoms->aWM_CLIENT_LEADER); } - win_set_leader(ps, w, leader); - - log_trace("(%#010x): client %#010x, leader %#010x, cache %#010x", w->base.id, - w->client_win, w->leader, win_get_leader(ps, w)); + log_trace("window %#010x: leader %#010x", wid, leader); + return leader; } /** @@ -1683,16 +1837,19 @@ static xcb_window_t win_get_leader_raw(session_t *ps, struct managed_win *w, int // Rebuild the cache if needed if (!w->cache_leader && (w->client_win || w->leader)) { // Leader defaults to client window - if (!(w->cache_leader = w->leader)) + if (!(w->cache_leader = w->leader)) { w->cache_leader = w->client_win; + } - // If the leader of this window isn't itself, look for its ancestors + // If the leader of this window isn't itself, look for its + // ancestors if (w->cache_leader && w->cache_leader != w->client_win) { - auto wp = find_toplevel(ps, w->cache_leader); + auto wp = wm_find_by_client(ps->wm, w->cache_leader); if (wp) { // Dead loop? - if (recursions > WIN_GET_LEADER_MAX_RECURSION) + if (recursions > WIN_GET_LEADER_MAX_RECURSION) { return XCB_NONE; + } w->cache_leader = win_get_leader_raw(ps, wp, recursions + 1); } @@ -1706,13 +1863,14 @@ static xcb_window_t win_get_leader_raw(session_t *ps, struct managed_win *w, int * Retrieve the WM_CLASS of a window and update its * win structure. */ -bool win_update_class(session_t *ps, struct managed_win *w) { +bool win_update_class(struct x_connection *c, struct atom *atoms, struct managed_win *w) { char **strlst = NULL; int nstr = 0; // Can't do anything if there's no client window - if (!w->client_win) + if (!w->client_win) { return false; + } // Free and reset old strings free(w->class_instance); @@ -1721,7 +1879,7 @@ bool win_update_class(session_t *ps, struct managed_win *w) { w->class_general = NULL; // Retrieve the property string list - if (!wid_get_text_prop(ps, w->client_win, ps->atoms->aWM_CLASS, &strlst, &nstr)) { + if (!wid_get_text_prop(c, atoms, w->client_win, atoms->aWM_CLASS, &strlst, &nstr)) { return false; } @@ -1750,18 +1908,19 @@ static void win_on_focus_change(session_t *ps, struct managed_win *w) { xcb_window_t leader = win_get_leader(ps, w); // If the window gets focused, replace the old active_leader - if (win_is_focused_raw(ps, w) && leader != ps->active_leader) { - xcb_window_t active_leader_old = ps->active_leader; + auto active_leader = wm_active_leader(ps->wm); + if (win_is_focused_raw(w) && leader != active_leader) { + xcb_window_t active_leader_old = active_leader; - ps->active_leader = leader; + wm_set_active_leader(ps->wm, leader); group_on_factor_change(ps, active_leader_old); group_on_factor_change(ps, leader); } // If the group get unfocused, remove it from active_leader - else if (!win_is_focused_raw(ps, w) && leader && - leader == ps->active_leader && !group_is_focused(ps, leader)) { - ps->active_leader = XCB_NONE; + else if (!win_is_focused_raw(w) && leader && leader == active_leader && + !group_is_focused(ps, leader)) { + wm_set_active_leader(ps->wm, XCB_NONE); group_on_factor_change(ps, leader); } } @@ -1769,16 +1928,14 @@ static void win_on_focus_change(session_t *ps, struct managed_win *w) { // Update everything related to conditions win_on_factor_change(ps, w); -#ifdef CONFIG_DBUS // Send D-Bus signal if (ps->o.dbus) { - if (win_is_focused_raw(ps, w)) { - cdbus_ev_win_focusin(ps, &w->base); + if (win_is_focused_raw(w)) { + cdbus_ev_win_focusin(session_get_cdbus(ps), &w->base); } else { - cdbus_ev_win_focusout(ps, &w->base); + cdbus_ev_win_focusout(session_get_cdbus(ps), &w->base); } } -#endif } /** @@ -1790,15 +1947,18 @@ void win_set_focused(session_t *ps, struct managed_win *w) { return; } - if (win_is_focused_raw(ps, w)) { + auto old_active_win = wm_active_win(ps->wm); + if (w->is_ewmh_focused) { + assert(old_active_win == w); return; } - auto old_active_win = ps->active_win; - ps->active_win = w; - assert(win_is_focused_raw(ps, w)); + wm_set_active_win(ps->wm, w); + w->is_ewmh_focused = true; if (old_active_win) { + assert(old_active_win->is_ewmh_focused); + old_active_win->is_ewmh_focused = false; win_on_focus_change(ps, old_active_win); } win_on_focus_change(ps, w); @@ -1829,19 +1989,20 @@ gen_by_val(win_extents); * * Mark the window shape as updated */ -void win_update_bounding_shape(session_t *ps, struct managed_win *w) { - if (ps->shape_exists) { - w->bounding_shaped = win_bounding_shaped(ps, w->base.id); - } - - // We don't handle property updates of non-visible windows until they are mapped. - assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING); +void win_update_bounding_shape(struct x_connection *c, struct managed_win *w, + bool shape_exists, bool detect_rounded_corners) { + // We don't handle property updates of non-visible windows until they are + // mapped. + assert(w->state == WSTATE_MAPPED); pixman_region32_clear(&w->bounding_shape); // Start with the window rectangular region win_get_region_local(w, &w->bounding_shape); + if (shape_exists) { + w->bounding_shaped = win_bounding_shaped(c, w->base.id); + } + // Only request for a bounding region if the window is shaped // (while loop is used to avoid goto, not an actual loop) while (w->bounding_shaped) { @@ -1851,8 +2012,8 @@ void win_update_bounding_shape(session_t *ps, struct managed_win *w) { */ xcb_shape_get_rectangles_reply_t *r = xcb_shape_get_rectangles_reply( - ps->c, - xcb_shape_get_rectangles(ps->c, w->base.id, XCB_SHAPE_SK_BOUNDING), NULL); + c->c, xcb_shape_get_rectangles(c->c, w->base.id, XCB_SHAPE_SK_BOUNDING), + NULL); if (!r) { break; @@ -1869,70 +2030,65 @@ void win_update_bounding_shape(session_t *ps, struct managed_win *w) { // Add border width because we are using a different origin. // X thinks the top left of the inner window is the origin - // (for the bounding shape, althought xcb_get_geometry thinks + // (for the bounding shape, although xcb_get_geometry thinks // the outer top left (outer means outside of the window // border) is the origin), // We think the top left of the border is the origin pixman_region32_translate(&br, w->g.border_width, w->g.border_width); - // Intersect the bounding region we got with the window rectangle, to - // make sure the bounding region is not bigger than the window + // Intersect the bounding region we got with the window rectangle, + // to make sure the bounding region is not bigger than the window // rectangle pixman_region32_intersect(&w->bounding_shape, &w->bounding_shape, &br); pixman_region32_fini(&br); break; } - if (w->bounding_shaped && ps->o.detect_rounded_corners) { + if (w->bounding_shaped && detect_rounded_corners) { w->rounded_corners = win_has_rounded_corners(w); } - - // Window shape changed, we should free old wpaint and shadow pict - // log_trace("free out dated pict"); - win_set_flags(w, WIN_FLAGS_IMAGES_STALE); - ps->pending_updates = true; - - free_paint(ps, &w->paint); - free_paint(ps, &w->shadow_paint); - - win_on_factor_change(ps, w); } /** * Reread opacity property of a window. */ -void win_update_opacity_prop(session_t *ps, struct managed_win *w) { +void win_update_opacity_prop(struct x_connection *c, struct atom *atoms, + struct managed_win *w, bool detect_client_opacity) { // get frame opacity first - w->has_opacity_prop = wid_get_opacity_prop(ps, w->base.id, OPAQUE, &w->opacity_prop); + w->has_opacity_prop = + wid_get_opacity_prop(c, atoms, w->base.id, OPAQUE, &w->opacity_prop); if (w->has_opacity_prop) { // opacity found return; } - if (ps->o.detect_client_opacity && w->client_win && w->base.id == w->client_win) { + if (detect_client_opacity && w->client_win && w->base.id == w->client_win) { // checking client opacity not allowed return; } // get client opacity w->has_opacity_prop = - wid_get_opacity_prop(ps, w->client_win, OPAQUE, &w->opacity_prop); + wid_get_opacity_prop(c, atoms, w->client_win, OPAQUE, &w->opacity_prop); } /** * Retrieve frame extents from a window. */ -void win_update_frame_extents(session_t *ps, struct managed_win *w, xcb_window_t client) { - winprop_t prop = x_get_prop(ps->c, client, ps->atoms->a_NET_FRAME_EXTENTS, 4L, - XCB_ATOM_CARDINAL, 32); +void win_update_frame_extents(struct x_connection *c, struct atom *atoms, + struct managed_win *w, xcb_window_t client, + double frame_opacity) { + winprop_t prop = + x_get_prop(c, client, atoms->a_NET_FRAME_EXTENTS, 4L, XCB_ATOM_CARDINAL, 32); if (prop.nitems == 4) { int extents[4]; for (int i = 0; i < 4; i++) { if (prop.c32[i] > (uint32_t)INT_MAX) { log_warn("Your window manager sets a absurd " - "_NET_FRAME_EXTENTS value (%u), ignoring it.", + "_NET_FRAME_EXTENTS value (%u), " + "ignoring it.", prop.c32[i]); memset(extents, 0, sizeof(extents)); break; @@ -1951,7 +2107,7 @@ void win_update_frame_extents(session_t *ps, struct managed_win *w, xcb_window_t // If frame_opacity != 1, then frame of this window // is not included in reg_ignore of underneath windows - if (ps->o.frame_opacity == 1 && changed) { + if (frame_opacity == 1 && changed) { w->reg_ignore_valid = false; } } @@ -1963,7 +2119,7 @@ void win_update_frame_extents(session_t *ps, struct managed_win *w, xcb_window_t } bool win_is_region_ignore_valid(session_t *ps, const struct managed_win *w) { - win_stack_foreach_managed(i, &ps->window_stack) { + win_stack_foreach_managed(i, wm_stack_end(ps->wm)) { if (i == w) { break; } @@ -1974,77 +2130,21 @@ bool win_is_region_ignore_valid(session_t *ps, const struct managed_win *w) { return true; } -/** - * Stop listening for events on a particular window. - */ -void win_ev_stop(session_t *ps, const struct win *w) { - xcb_change_window_attributes(ps->c, w->id, XCB_CW_EVENT_MASK, (const uint32_t[]){0}); - - if (!w->managed) { - return; - } - - auto mw = (struct managed_win *)w; - if (mw->client_win) { - xcb_change_window_attributes(ps->c, mw->client_win, XCB_CW_EVENT_MASK, - (const uint32_t[]){0}); - } - - if (ps->shape_exists) { - xcb_shape_select_input(ps->c, w->id, 0); - } -} - -/// Finish the unmapping of a window (e.g. after fading has finished). -/// Doesn't free `w` -static void unmap_win_finish(session_t *ps, struct managed_win *w) { - w->reg_ignore_valid = false; - w->state = WSTATE_UNMAPPED; - - // We are in unmap_win, this window definitely was viewable - if (ps->backend_data) { - // Only the pixmap needs to be freed and reacquired when mapping. - // Shadow image can be preserved. - if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { - win_release_pixmap(ps->backend_data, w); - } - } else { - assert(!w->win_image); - assert(!w->shadow_image); - } - - free_paint(ps, &w->paint); - free_paint(ps, &w->shadow_paint); - - // Try again at binding images when the window is mapped next time - win_clear_flags(w, WIN_FLAGS_IMAGE_ERROR); -} - /// Finish the destruction of a window (e.g. after fading has finished). /// Frees `w` -static void destroy_win_finish(session_t *ps, struct win *w) { - log_trace("Trying to finish destroying (%#010x)", w->id); +void destroy_win_finish(session_t *ps, struct win *w) { + log_verbose("Trying to finish destroying (%#010x)", w->id); - auto next_w = win_stack_find_next_managed(ps, &w->stack_neighbour); + auto next_w = wm_stack_next_managed(ps->wm, &w->stack_neighbour); list_remove(&w->stack_neighbour); if (w->managed) { auto mw = (struct managed_win *)w; + unmap_win_finish(ps, mw); - if (mw->state != WSTATE_UNMAPPED) { - // Only UNMAPPED state has window resources freed, otherwise - // we need to call unmap_win_finish to free them. - // XXX actually we unmap_win_finish only frees the rendering - // resources, we still need to call free_win_res. will fix - // later. - unmap_win_finish(ps, mw); - } - - // Unmapping preserves the shadow image, so free it here - if (!win_check_flags_all(mw, WIN_FLAGS_SHADOW_NONE)) { - assert(mw->shadow_image != NULL); - win_release_shadow(ps->backend_data, mw); - } + // Unmapping might preserve the shadow image, so free it here + win_release_shadow(ps->backend_data, mw); + win_release_mask(ps->backend_data, mw); // Invalidate reg_ignore of windows below this one // TODO(yshui) what if next_w is not mapped?? @@ -2058,14 +2158,15 @@ static void destroy_win_finish(session_t *ps, struct win *w) { next_w->reg_ignore_valid = false; } - if (mw == ps->active_win) { - // Usually, the window cannot be the focused at destruction. - // FocusOut should be generated before the window is destroyed. We - // do this check just to be completely sure we don't have dangling - // references. - log_debug("window %#010x (%s) is destroyed while being focused", + if (mw == wm_active_win(ps->wm)) { + // Usually, the window cannot be the focused at + // destruction. FocusOut should be generated before the + // window is destroyed. We do this check just to be + // completely sure we don't have dangling references. + log_debug("window %#010x (%s) is destroyed while being " + "focused", w->id, mw->name); - ps->active_win = NULL; + wm_set_active_win(ps->wm, NULL); } free_win_res(ps, mw); @@ -2073,7 +2174,7 @@ static void destroy_win_finish(session_t *ps, struct win *w) { // Drop w from all prev_trans to avoid accessing freed memory in // repair_win() // TODO(yshui) there can only be one prev_trans pointing to w - win_stack_foreach_managed(w2, &ps->window_stack) { + win_stack_foreach_managed(w2, wm_stack_end(ps->wm)) { if (mw == w2->prev_trans) { w2->prev_trans = NULL; } @@ -2083,162 +2184,90 @@ static void destroy_win_finish(session_t *ps, struct win *w) { free(w); } -static void map_win_finish(struct managed_win *w) { - w->in_openclose = false; - w->state = WSTATE_MAPPED; -} - -/// Move window `w` so it's before `next` in the list -static inline void restack_win(session_t *ps, struct win *w, struct list_node *next) { - struct managed_win *mw = NULL; - if (w->managed) { - mw = (struct managed_win *)w; - } - - if (mw) { - // This invalidates all reg_ignore below the new stack position of `w` - mw->reg_ignore_valid = false; - rc_region_unref(&mw->reg_ignore); - - // This invalidates all reg_ignore below the old stack position of `w` - auto next_w = win_stack_find_next_managed(ps, &w->stack_neighbour); - if (next_w) { - next_w->reg_ignore_valid = false; - rc_region_unref(&next_w->reg_ignore); - } - } - - list_move_before(&w->stack_neighbour, next); - - // add damage for this window - if (mw) { - add_damage_from_win(ps, mw); - } - -#ifdef DEBUG_RESTACK - log_trace("Window stack modified. Current stack:"); - for (auto c = ps->list; c; c = c->next) { - const char *desc = ""; - if (c->state == WSTATE_DESTROYING) { - desc = "(D) "; - } - log_trace("%#010x \"%s\" %s", c->id, c->name, desc); - } -#endif -} - -/// Move window `w` so it's right above `below` -void restack_above(session_t *ps, struct win *w, xcb_window_t below) { - xcb_window_t old_below; - - if (!list_node_is_last(&ps->window_stack, &w->stack_neighbour)) { - old_below = list_next_entry(w, stack_neighbour)->id; - } else { - old_below = XCB_NONE; - } - log_debug("Restack %#010x (%s), old_below: %#010x, new_below: %#010x", w->id, - win_get_name_if_managed(w), old_below, below); - - if (old_below != below) { - struct list_node *new_next; - if (!below) { - new_next = &ps->window_stack; - } else { - struct win *tmp_w = NULL; - HASH_FIND_INT(ps->windows, &below, tmp_w); - - if (!tmp_w) { - log_error("Failed to found new below window %#010x.", below); - return; - } - - new_next = &tmp_w->stack_neighbour; - } - restack_win(ps, w, new_next); - } -} - -void restack_bottom(session_t *ps, struct win *w) { - restack_above(ps, w, 0); -} - -void restack_top(session_t *ps, struct win *w) { - log_debug("Restack %#010x (%s) to top", w->id, win_get_name_if_managed(w)); - if (&w->stack_neighbour == ps->window_stack.next) { - // already at top - return; - } - restack_win(ps, w, ps->window_stack.next); -} - /// Start destroying a window. Windows cannot always be destroyed immediately /// because of fading and such. -/// -/// @return whether the window has finished destroying and is freed -bool destroy_win_start(session_t *ps, struct win *w) { - auto mw = (struct managed_win *)w; +void destroy_win_start(session_t *ps, struct win *w) { assert(w); + // A toplevel window is destroyed, all of its children lose their + // subwin status. + wm_subwin_remove_and_unsubscribe_for_toplevel(ps->wm, &ps->c, w->id); + + auto mw = (struct managed_win *)w; log_debug("Destroying %#010x \"%s\", managed = %d", w->id, (w->managed ? mw->name : NULL), w->managed); - // Delete destroyed window from the hash table, even though the window might still - // be rendered for a while. We need to make sure future window with the same - // window id won't confuse us. Keep the window in the window stack if it's managed - // and mapped, since we might still need to render it (e.g. fading out). Window - // will be removed from the stack when it finishes destroying. - HASH_DEL(ps->windows, w); - - if (!w->managed || mw->state == WSTATE_UNMAPPED) { - // Window is already unmapped, or is an unmanged window, just destroy it - destroy_win_finish(ps, w); - return true; - } + // Delete destroyed window from the hash table, even though the window + // might still be rendered for a while. We need to make sure future window + // with the same window id won't confuse us. Keep the window in the window + // stack if it's managed and mapped, since we might still need to render + // it (e.g. fading out). Window will be removed from the stack when it + // finishes destroying. + wm_remove(ps->wm, w); if (w->managed) { + if (mw->state != WSTATE_UNMAPPED) { + // Only UNMAPPED state has window resources freed, + // otherwise we need to call unmap_win_finish to free + // them. + log_warn("Did X server not unmap window %#010x before destroying " + "it?", + w->id); + } // Clear IMAGES_STALE flags since the window is destroyed: Clear - // PIXMAP_STALE as there is no pixmap available anymore, so STALE doesn't - // make sense. - // XXX Clear SHADOW_STALE as setting/clearing flags on a destroyed window - // doesn't work leading to an inconsistent state where the shadow is - // refreshed but the flags are stuck in STALE. - // Do this before changing the window state to destroying - win_clear_flags(mw, WIN_FLAGS_IMAGES_STALE); - - // If size/shape/position information is stale, win_process_update_flags - // will update them and add the new window extents to damage. Since the - // window has been destroyed, we cannot get the complete information at - // this point, so we just add what we currently have to the damage. + // PIXMAP_STALE as there is no pixmap available anymore, so STALE + // doesn't make sense. + // XXX Clear SHADOW_STALE as setting/clearing flags on a destroyed + // window doesn't work leading to an inconsistent state where the + // shadow is refreshed but the flags are stuck in STALE. Do this + // before changing the window state to destroying + win_clear_flags(mw, WIN_FLAGS_PIXMAP_STALE); + + // If size/shape/position information is stale, + // win_process_update_flags will update them and add the new + // window extents to damage. Since the window has been destroyed, + // we cannot get the complete information at this point, so we + // just add what we currently have to the damage. if (win_check_flags_any(mw, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { add_damage_from_win(ps, mw); } - // Clear some flags about stale window information. Because now the window - // is destroyed, we can't update them anyway. + if (win_check_flags_all(mw, WIN_FLAGS_CLIENT_STALE)) { + mw->client_win = mw->base.id; + log_debug("(%#010x): client self (%s)", mw->base.id, + (mw->a.override_redirect ? "override-redirected" : "wmwin")); + } + + // Clear some flags about stale window information. Because now + // the window is destroyed, we can't update them anyway. win_clear_flags(mw, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE | WIN_FLAGS_PROPERTY_STALE | WIN_FLAGS_FACTOR_CHANGED | WIN_FLAGS_CLIENT_STALE); // Update state flags of a managed window - mw->state = WSTATE_DESTROYING; + mw->state = WSTATE_DESTROYED; mw->a.map_state = XCB_MAP_STATE_UNMAPPED; mw->in_openclose = true; + + // We don't initiate animation here, because it should already have been + // started by unmap_win_start, because X automatically unmaps windows + // before destroying them. But we do need to stop animation if + // no_fading_destroyed_windows, or no_fading_openclose is enabled. + if (!win_should_fade(ps, mw)) { + win_skip_fading(mw); + } } // don't need win_ev_stop because the window is gone anyway -#ifdef CONFIG_DBUS // Send D-Bus signal if (ps->o.dbus) { - cdbus_ev_win_destroyed(ps, w); + cdbus_ev_win_destroyed(session_get_cdbus(ps), w); } -#endif - if (!ps->redirected) { + if (!ps->redirected && w->managed) { // Skip transition if we are not rendering - return win_skip_fading(ps, mw); + win_skip_fading(mw); } - - return false; } void unmap_win_start(session_t *ps, struct managed_win *w) { @@ -2248,24 +2277,13 @@ void unmap_win_start(session_t *ps, struct managed_win *w) { log_debug("Unmapping %#010x \"%s\"", w->base.id, w->name); - if (unlikely(w->state == WSTATE_DESTROYING)) { - log_warn("Trying to undestroy a window?"); - assert(false); - } - - bool was_damaged = w->ever_damaged; - w->ever_damaged = false; + assert(w->state != WSTATE_DESTROYED); - if (unlikely(w->state == WSTATE_UNMAPPING || w->state == WSTATE_UNMAPPED)) { - if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { - // Clear the pending map as this window is now unmapped - win_clear_flags(w, WIN_FLAGS_MAPPED); - } else { - log_warn("Trying to unmapping an already unmapped window %#010x " - "\"%s\"", - w->base.id, w->name); - assert(false); - } + if (unlikely(w->state == WSTATE_UNMAPPED)) { + assert(win_check_flags_all(w, WIN_FLAGS_MAPPED)); + // Window is mapped, but we hadn't had a chance to handle the MAPPED flag. + // Clear the pending map as this window is now unmapped + win_clear_flags(w, WIN_FLAGS_MAPPED); return; } @@ -2273,89 +2291,52 @@ void unmap_win_start(session_t *ps, struct managed_win *w) { // triggered by subsequence Focus{In, Out} event, or by recheck_focus w->a.map_state = XCB_MAP_STATE_UNMAPPED; - w->state = WSTATE_UNMAPPING; - w->opacity_target_old = fmax(w->opacity_target, w->opacity_target_old); - w->opacity_target = win_calc_opacity_target(ps, w); + w->state = WSTATE_UNMAPPED; + win_start_fade(ps, w, 0); -#ifdef CONFIG_DBUS // Send D-Bus signal if (ps->o.dbus) { - cdbus_ev_win_unmapped(ps, &w->base); + cdbus_ev_win_unmapped(session_get_cdbus(ps), &w->base); } -#endif - if (!ps->redirected || !was_damaged) { - // If we are not redirected, we skip fading because we aren't rendering - // anything anyway. - // If the window wasn't ever damaged, it shouldn't be painted either. But - // a fading out window is always painted, so we have to skip fading here. - CHECK(!win_skip_fading(ps, w)); + if (!ps->redirected || !w->ever_damaged) { + // If we are not redirected, we skip fading because we aren't + // rendering anything anyway. If the window wasn't ever damaged, + // it shouldn't be painted either. But a fading out window is + // always painted, so we have to skip fading here. + win_skip_fading(w); } } -/** - * Execute fade callback of a window if fading finished. - * - * @return whether the window is destroyed and freed - */ -bool win_check_fade_finished(session_t *ps, struct managed_win *w) { - if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { - // No fading in progress - assert(w->opacity_target == w->opacity); - return false; - } - if (w->opacity == w->opacity_target) { - switch (w->state) { - case WSTATE_UNMAPPING: unmap_win_finish(ps, w); return false; - case WSTATE_DESTROYING: destroy_win_finish(ps, &w->base); return true; - case WSTATE_MAPPING: map_win_finish(w); return false; - case WSTATE_FADING: w->state = WSTATE_MAPPED; break; - default: unreachable; - } - } - - return false; -} - /// Skip the current in progress fading of window, /// transition the window straight to its end state -/// -/// @return whether the window is destroyed and freed -bool win_skip_fading(session_t *ps, struct managed_win *w) { - if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { - assert(w->opacity_target == w->opacity); - return false; +void win_skip_fading(struct managed_win *w) { + if (w->number_of_animations == 0) { + return; } log_debug("Skipping fading process of window %#010x (%s)", w->base.id, w->name); - w->opacity = w->opacity_target; - return win_check_fade_finished(ps, w); -} - -/** - * Get the Xinerama screen a window is on. - * - * Return an index >= 0, or -1 if not found. - * - * TODO(yshui) move to x.c - * TODO(yshui) use xrandr - */ -void win_update_screen(int nscreens, region_t *screens, struct managed_win *w) { - w->xinerama_scr = -1; - - for (int i = 0; i < nscreens; i++) { - auto e = pixman_region32_extents(&screens[i]); - if (e->x1 <= w->g.x && e->y1 <= w->g.y && e->x2 >= w->g.x + w->widthb && - e->y2 >= w->g.y + w->heightb) { - w->xinerama_scr = i; - log_debug("Window %#010x (%s), %dx%d+%dx%d, is on screen %d " - "(%dx%d+%dx%d)", - w->base.id, w->name, w->g.x, w->g.y, w->widthb, w->heightb, - i, e->x1, e->y1, e->x2 - e->x1, e->y2 - e->y1); + animatable_skip(&w->opacity); + animatable_skip(&w->blur_opacity); +} + +// TODO(absolutelynothelix): rename to x_update_win_(randr_?)monitor and move to +// the x.c. +void win_update_monitor(struct x_monitors *monitors, struct managed_win *mw) { + mw->randr_monitor = -1; + for (int i = 0; i < monitors->count; i++) { + auto e = pixman_region32_extents(&monitors->regions[i]); + if (e->x1 <= mw->g.x && e->y1 <= mw->g.y && + e->x2 >= mw->g.x + mw->widthb && e->y2 >= mw->g.y + mw->heightb) { + mw->randr_monitor = i; + log_debug("Window %#010x (%s), %dx%d+%dx%d, is entirely on the " + "monitor %d (%dx%d+%dx%d)", + mw->base.id, mw->name, mw->g.x, mw->g.y, mw->widthb, + mw->heightb, i, e->x1, e->y1, e->x2 - e->x1, e->y2 - e->y1); return; } } - log_debug("Window %#010x (%s), %dx%d+%dx%d, is not contained by any screen", - w->base.id, w->name, w->g.x, w->g.y, w->g.width, w->g.height); + log_debug("Window %#010x (%s), %dx%d+%dx%d, is not entirely on any monitor", + mw->base.id, mw->name, mw->g.x, mw->g.y, mw->widthb, mw->heightb); } /// Map an already registered window @@ -2371,31 +2352,19 @@ void map_win_start(session_t *ps, struct managed_win *w) { log_debug("Mapping (%#010x \"%s\")", w->base.id, w->name); - assert(w->state != WSTATE_DESTROYING); - if (w->state != WSTATE_UNMAPPED && w->state != WSTATE_UNMAPPING) { - log_warn("Mapping an already mapped window"); + assert(w->state != WSTATE_DESTROYED); + if (w->state == WSTATE_MAPPED) { + log_error("Mapping an already mapped window"); + assert(false); return; } - if (w->state == WSTATE_UNMAPPING) { - CHECK(!win_skip_fading(ps, w)); - // We skipped the unmapping process, the window was rendered, now it is - // not anymore. So we need to mark the then unmapping window as damaged. - // - // Solves problem when, for example, a window is unmapped then mapped in a - // different location - add_damage_from_win(ps, w); - assert(w); - } - - assert(w->state == WSTATE_UNMAPPED); - - // Rant: window size could change after we queried its geometry here and before - // we get its pixmap. Later, when we get back to the event processing loop, we - // will get the notification about size change from Xserver and try to refresh the - // pixmap, while the pixmap is actually already up-to-date (i.e. the notification - // is stale). There is basically no real way to prevent this, aside from grabbing - // the server. + // Rant: window size could change after we queried its geometry here and + // before we get its pixmap. Later, when we get back to the event + // processing loop, we will get the notification about size change from + // Xserver and try to refresh the pixmap, while the pixmap is actually + // already up-to-date (i.e. the notification is stale). There is basically + // no real way to prevent this, aside from grabbing the server. // XXX Can we assume map_state is always viewable? w->a.map_state = XCB_MAP_STATE_VIEWABLE; @@ -2403,36 +2372,22 @@ void map_win_start(session_t *ps, struct managed_win *w) { // Update window mode here to check for ARGB windows w->mode = win_calc_mode(w); - log_debug("Window (%#010x) has type %s", w->base.id, WINTYPES[w->window_type]); + log_debug("Window (%#010x) has type %s", w->base.id, WINTYPES[w->window_type].name); - // XXX We need to make sure that win_data is available - // iff `state` is MAPPED - w->state = WSTATE_MAPPING; - w->opacity_target_old = 0; - w->opacity_target = win_calc_opacity_target(ps, w); + w->state = WSTATE_MAPPED; + win_start_fade(ps, w, 1); + win_set_flags(w, WIN_FLAGS_PIXMAP_STALE); log_debug("Window %#010x has opacity %f, opacity target is %f", w->base.id, - w->opacity, w->opacity_target); - - // Cannot set w->ever_damaged = false here, since window mapping could be - // delayed, so a damage event might have already arrived before this function - // is called. But this should be unnecessary in the first place, since - // ever_damaged is set to false in unmap_win_finish anyway. - - // Sets the WIN_FLAGS_IMAGES_STALE flag so later in the critical section - // the window's image will be bound + animatable_get(&w->opacity), w->opacity.target); - win_set_flags(w, WIN_FLAGS_PIXMAP_STALE); - -#ifdef CONFIG_DBUS // Send D-Bus signal if (ps->o.dbus) { - cdbus_ev_win_mapped(ps, &w->base); + cdbus_ev_win_mapped(session_get_cdbus(ps), &w->base); } -#endif if (!ps->redirected) { - CHECK(!win_skip_fading(ps, w)); + win_skip_fading(w); } } @@ -2440,185 +2395,25 @@ void map_win_start(session_t *ps, struct managed_win *w) { * Update target window opacity depending on the current state. */ void win_update_opacity_target(session_t *ps, struct managed_win *w) { - auto opacity_target_old = w->opacity_target; - w->opacity_target = win_calc_opacity_target(ps, w); + win_start_fade(ps, w, w->blur_opacity.target); // We don't want to change + // blur_opacity target - if (opacity_target_old == w->opacity_target) { + if (w->number_of_animations == 0) { return; } - if (w->state == WSTATE_MAPPED) { - // Opacity target changed while MAPPED. Transition to FADING. - assert(w->opacity == opacity_target_old); - w->opacity_target_old = opacity_target_old; - w->state = WSTATE_FADING; - log_debug("Window %#010x (%s) opacity %f, opacity target %f, set " - "old target %f", - w->base.id, w->name, w->opacity, w->opacity_target, - w->opacity_target_old); - } else if (w->state == WSTATE_MAPPING) { - // Opacity target changed while fading in. - if (w->opacity >= w->opacity_target) { - // Already reached new target opacity. Transition to - // FADING. - map_win_finish(w); - w->opacity_target_old = fmax(opacity_target_old, w->opacity); - w->state = WSTATE_FADING; - log_debug("Window %#010x (%s) opacity %f already reached " - "new opacity target %f while mapping, set old " - "target %f", - w->base.id, w->name, w->opacity, w->opacity_target, - w->opacity_target_old); - } - } else if (w->state == WSTATE_FADING) { - // Opacity target changed while FADING. - if ((w->opacity < opacity_target_old && w->opacity > w->opacity_target) || - (w->opacity > opacity_target_old && w->opacity < w->opacity_target)) { - // Changed while fading in and will fade out or while - // fading out and will fade in. - w->opacity_target_old = opacity_target_old; - log_debug("Window %#010x (%s) opacity %f already reached " - "new opacity target %f while fading, set " - "old target %f", - w->base.id, w->name, w->opacity, w->opacity_target, - w->opacity_target_old); - } - } + log_debug("Window %#010x (%s) opacity %f, opacity target %f, start %f", w->base.id, + w->name, animatable_get(&w->opacity), w->opacity.target, w->opacity.start); if (!ps->redirected) { - CHECK(!win_skip_fading(ps, w)); - } -} - -/** - * Find a managed window from window id in window linked list of the session. - */ -struct win *find_win(session_t *ps, xcb_window_t id) { - if (!id) { - return NULL; - } - - struct win *w = NULL; - HASH_FIND_INT(ps->windows, &id, w); - assert(w == NULL || !w->destroyed); - return w; -} - -/** - * Find a managed window from window id in window linked list of the session. - */ -struct managed_win *find_managed_win(session_t *ps, xcb_window_t id) { - struct win *w = find_win(ps, id); - if (!w || !w->managed) { - return NULL; - } - - auto mw = (struct managed_win *)w; - assert(mw->state != WSTATE_DESTROYING); - return mw; -} - -/** - * Find out the WM frame of a client window using existing data. - * - * @param id window ID - * @return struct win object of the found window, NULL if not found - */ -struct managed_win *find_toplevel(session_t *ps, xcb_window_t id) { - if (!id) { - return NULL; - } - - HASH_ITER2(ps->windows, w) { - assert(!w->destroyed); - if (!w->managed) { - continue; - } - - auto mw = (struct managed_win *)w; - if (mw->client_win == id) { - return mw; - } + win_skip_fading(w); } - - return NULL; -} - -/** - * Find a managed window that is, or is a parent of `wid`. - * - * @param ps current session - * @param wid window ID - * @return struct _win object of the found window, NULL if not found - */ -struct managed_win *find_managed_window_or_parent(session_t *ps, xcb_window_t wid) { - // TODO(yshui) this should probably be an "update tree", then find_toplevel. - // current approach is a bit more "racy", as the server state might be ahead of - // our state - struct win *w = NULL; - - // We traverse through its ancestors to find out the frame - // Using find_win here because if we found a unmanaged window we know about, we - // can stop early. - while (wid && wid != ps->root && !(w = find_win(ps, wid))) { - // xcb_query_tree probably fails if you run picom when X is somehow - // initializing (like add it in .xinitrc). In this case - // just leave it alone. - auto reply = xcb_query_tree_reply(ps->c, xcb_query_tree(ps->c, wid), NULL); - if (reply == NULL) { - break; - } - - wid = reply->parent; - free(reply); - } - - if (w == NULL || !w->managed) { - return NULL; - } - - return (struct managed_win *)w; -} - -/** - * Check if a rectangle includes the whole screen. - */ -static inline bool rect_is_fullscreen(const session_t *ps, int x, int y, int wid, int hei) { - return (x <= 0 && y <= 0 && (x + wid) >= ps->root_width && (y + hei) >= ps->root_height); -} - -/** - * Check if a window is fulscreen using EWMH - * - * TODO(yshui) cache this property - */ -static inline bool -win_is_fullscreen_xcb(xcb_connection_t *c, const struct atom *a, const xcb_window_t w) { - xcb_get_property_cookie_t prop = - xcb_get_property(c, 0, w, a->a_NET_WM_STATE, XCB_ATOM_ATOM, 0, 12); - xcb_get_property_reply_t *reply = xcb_get_property_reply(c, prop, NULL); - if (!reply) { - return false; - } - - if (reply->length) { - xcb_atom_t *val = xcb_get_property_value(reply); - for (uint32_t i = 0; i < reply->length; i++) { - if (val[i] != a->a_NET_WM_STATE_FULLSCREEN) { - continue; - } - free(reply); - return true; - } - } - free(reply); - return false; } /// Set flags on a window. Some sanity checks are performed void win_set_flags(struct managed_win *w, uint64_t flags) { log_debug("Set flags %" PRIu64 " to window %#010x (%s)", flags, w->base.id, w->name); - if (unlikely(w->state == WSTATE_DESTROYING)) { + if (unlikely(w->state == WSTATE_DESTROYED)) { log_error("Flags set on a destroyed window %#010x (%s)", w->base.id, w->name); return; } @@ -2630,7 +2425,7 @@ void win_set_flags(struct managed_win *w, uint64_t flags) { void win_clear_flags(struct managed_win *w, uint64_t flags) { log_debug("Clear flags %" PRIu64 " from window %#010x (%s)", flags, w->base.id, w->name); - if (unlikely(w->state == WSTATE_DESTROYING)) { + if (unlikely(w->state == WSTATE_DESTROYED)) { log_warn("Flags cleared on a destroyed window %#010x (%s)", w->base.id, w->name); return; @@ -2640,7 +2435,7 @@ void win_clear_flags(struct managed_win *w, uint64_t flags) { } void win_set_properties_stale(struct managed_win *w, const xcb_atom_t *props, int nprops) { - const auto bits_per_element = sizeof(*w->stale_props) * 8; + auto const bits_per_element = sizeof(*w->stale_props) * 8; size_t new_capacity = w->stale_props_capacity; // Calculate the new capacity of the properties array @@ -2675,12 +2470,12 @@ static void win_clear_all_properties_stale(struct managed_win *w) { } static bool win_fetch_and_unset_property_stale(struct managed_win *w, xcb_atom_t prop) { - const auto bits_per_element = sizeof(*w->stale_props) * 8; + auto const bits_per_element = sizeof(*w->stale_props) * 8; if (prop >= w->stale_props_capacity * bits_per_element) { return false; } - const auto mask = 1UL << (prop % bits_per_element); + auto const mask = 1UL << (prop % bits_per_element); bool ret = w->stale_props[prop / bits_per_element] & mask; w->stale_props[prop / bits_per_element] &= ~mask; return ret; @@ -2699,13 +2494,15 @@ bool win_check_flags_all(struct managed_win *w, uint64_t flags) { * * It's not using w->border_size for performance measures. */ -bool win_is_fullscreen(const session_t *ps, const struct managed_win *w) { - if (!ps->o.no_ewmh_fullscreen && - win_is_fullscreen_xcb(ps->c, ps->atoms, w->client_win)) { - return true; +void win_update_is_fullscreen(const session_t *ps, struct managed_win *w) { + if (!ps->o.no_ewmh_fullscreen && w->is_ewmh_fullscreen) { + w->is_fullscreen = true; + return; } - return rect_is_fullscreen(ps, w->g.x, w->g.y, w->widthb, w->heightb) && - (!w->bounding_shaped || w->rounded_corners); + w->is_fullscreen = w->g.x <= 0 && w->g.y <= 0 && + (w->g.x + w->widthb) >= ps->root_width && + (w->g.y + w->heightb) >= ps->root_height && + (!w->bounding_shaped || w->rounded_corners); } /** @@ -2716,7 +2513,7 @@ bool win_is_fullscreen(const session_t *ps, const struct managed_win *w) { bool win_is_bypassing_compositor(const session_t *ps, const struct managed_win *w) { bool ret = false; - auto prop = x_get_prop(ps->c, w->client_win, ps->atoms->a_NET_WM_BYPASS_COMPOSITOR, + auto prop = x_get_prop(&ps->c, w->client_win, ps->atoms->a_NET_WM_BYPASS_COMPOSITOR, 1L, XCB_ATOM_CARDINAL, 32); if (prop.nitems && *prop.c32 == 1) { @@ -2728,27 +2525,9 @@ bool win_is_bypassing_compositor(const session_t *ps, const struct managed_win * } /** - * Check if a window is focused, without using any focus rules or forced focus settings + * Check if a window is focused, without using any focus rules or forced focus + * settings */ -bool win_is_focused_raw(const session_t *ps, const struct managed_win *w) { - return w->a.map_state == XCB_MAP_STATE_VIEWABLE && ps->active_win == w; -} - -// Find the managed window immediately below `i` in the window stack -struct managed_win * -win_stack_find_next_managed(const session_t *ps, const struct list_node *i) { - while (!list_node_is_last(&ps->window_stack, i)) { - auto next = list_entry(i->next, struct win, stack_neighbour); - if (next->managed) { - return (struct managed_win *)next; - } - i = &next->stack_neighbour; - } - return NULL; -} - -/// Return whether this window is mapped on the X server side -bool win_is_mapped_in_x(const struct managed_win *w) { - return w->state == WSTATE_MAPPING || w->state == WSTATE_FADING || - w->state == WSTATE_MAPPED || (w->flags & WIN_FLAGS_MAPPED); +bool win_is_focused_raw(const struct managed_win *w) { + return w->a.map_state == XCB_MAP_STATE_VIEWABLE && w->is_ewmh_focused; } diff --git a/src/win.h b/src/win.h index aae8d5f8b4..58c094d675 100644 --- a/src/win.h +++ b/src/win.h @@ -7,33 +7,33 @@ #include #include -#include "uthash_extra.h" +#include -// FIXME shouldn't need this -#ifdef CONFIG_OPENGL -#include -#endif +#include "uthash_extra.h" #include "c2.h" #include "compiler.h" #include "list.h" #include "region.h" #include "render.h" +#include "transition.h" #include "types.h" #include "utils.h" #include "win_defs.h" #include "x.h" +#include "xcb/xproto.h" struct backend_base; typedef struct session session_t; typedef struct _glx_texture glx_texture_t; #define win_stack_foreach_managed(w, win_stack) \ - list_foreach(struct managed_win, w, win_stack, base.stack_neighbour) if (w->base.managed) + list_foreach(struct managed_win, w, win_stack, \ + base.stack_neighbour) if ((w)->base.managed) #define win_stack_foreach_managed_safe(w, win_stack) \ list_foreach_safe(struct managed_win, w, win_stack, \ - base.stack_neighbour) if (w->base.managed) + base.stack_neighbour) if ((w)->base.managed) #ifdef CONFIG_OPENGL // FIXME this type should be in opengl.h @@ -49,7 +49,7 @@ typedef struct { int height; } glx_blur_cache_t; #endif - +struct wm; /// An entry in the window stack. May or may not correspond to a window we know about. struct window_stack_entry { struct list_node stack_neighbour; @@ -74,12 +74,14 @@ struct window_stack_entry { */ /// Structure representing a top-level managed window. -typedef struct win win; struct win { UT_hash_handle hh; struct list_node stack_neighbour; /// ID of the top-level frame window. xcb_window_t id; + /// Generation of the window. + /// (see `struct wm::generation` for explanation of what a generation is) + uint64_t generation; /// Whether the window is destroyed from Xorg's perspective bool destroyed : 1; /// True if we just received CreateNotify, and haven't queried X for any info @@ -102,8 +104,9 @@ struct managed_win { struct win base; /// backend data attached to this window. Only available when /// `state` is not UNMAPPED - void *win_image; - void *shadow_image; + image_handle win_image; + image_handle shadow_image; + image_handle mask_image; /// Pointer to the next higher window to paint. struct managed_win *prev_trans; /// Number of windows above this window @@ -120,15 +123,18 @@ struct managed_win { struct win_geometry g; /// Updated geometry received in events struct win_geometry pending_g; - /// Xinerama screen this window is on. - int xinerama_scr; + /// X RandR monitor this window is on. + int randr_monitor; /// Window visual pict format const xcb_render_pictforminfo_t *pictfmt; /// Client window visual pict format const xcb_render_pictforminfo_t *client_pictfmt; /// Window painting mode. winmode_t mode; - /// Whether the window has been damaged at least once. + /// Whether the window has been damaged at least once since it + /// was mapped. Unmapped windows that were previously mapped + /// retain their `ever_damaged` state. Mapping a window resets + /// this. bool ever_damaged; /// Whether the window was damaged after last paint. bool pixmap_damaged; @@ -139,7 +145,7 @@ struct managed_win { /// bitmap for properties which needs to be updated uint64_t *stale_props; /// number of uint64_ts that has been allocated for stale_props - uint64_t stale_props_capacity; + size_t stale_props_capacity; /// Bounding shape of the window. In local coordinates. /// See above about coordinate systems. @@ -176,10 +182,6 @@ struct managed_win { xcb_window_t client_win; /// Type of the window. wintype_t window_type; - /// Whether it looks like a WM window. We consider a window WM window if - /// it does not have a decedent with WM_STATE and it is not override- - /// redirected itself. - bool wmwin; /// Leader window ID of the window. xcb_window_t leader; /// Cached topmost window ID of the window. @@ -200,16 +202,25 @@ struct managed_win { char *class_general; /// WM_WINDOW_ROLE value of the window. char *role; + /// Whether the window sets the EWMH fullscreen property. + bool is_ewmh_fullscreen; + /// Whether the window should be considered fullscreen. Based on + /// `is_ewmh_fullscreen`, or the windows spatial relation with the + /// root window. Which one is used is determined by user configuration. + bool is_fullscreen; + /// Whether the window is the EWMH active window. + bool is_ewmh_focused; // Opacity-related members - /// Current window opacity. - double opacity; - /// Target window opacity. - double opacity_target; - /// Previous window opacity. - double opacity_target_old; + /// Window opacity + struct animatable opacity; + /// Opacity of the window's background blur + /// Used to gracefully fade in/out the window, otherwise the blur + /// would be at full/zero intensity immediately which will be jarring. + struct animatable blur_opacity; /// true if window (or client window, for broken window managers - /// not transferring client window's _NET_WM_OPACITY value) has opacity prop + /// not transferring client window's _NET_WM_WINDOW_OPACITY value) has opacity + /// prop bool has_opacity_prop; /// Cached value of opacity window attribute. opacity_t opacity_prop; @@ -228,6 +239,9 @@ struct managed_win { /// Whether fading is excluded by the rules. Calculated. bool fade_excluded; + /// Whether transparent clipping is excluded by the rules. + bool transparent_clipping; + // Frame-opacity-related members /// Current window frame opacity. Affected by window opacity. double frame_opacity; @@ -239,21 +253,22 @@ struct managed_win { bool shadow; /// Override value of window shadow state. Set by D-Bus method calls. switch_t shadow_force; - /// Opacity of the shadow. Affected by window opacity and frame opacity. + /// Window specific shadow factor. The final shadow opacity is a combination of + /// this, the window opacity, and the window frame opacity. double shadow_opacity; - /// X offset of shadow. Affected by commandline argument. + /// X offset of shadow. Affected by command line argument. int shadow_dx; - /// Y offset of shadow. Affected by commandline argument. + /// Y offset of shadow. Affected by command line argument. int shadow_dy; - /// Width of shadow. Affected by window size and commandline argument. + /// Width of shadow. Affected by window size and command line argument. int shadow_width; - /// Height of shadow. Affected by window size and commandline argument. + /// Height of shadow. Affected by window size and command line argument. int shadow_height; /// Picture to render shadow. Affected by window size. paint_t shadow_paint; /// The value of _COMPTON_SHADOW attribute of the window. Below 0 for /// none. - long prop_shadow; + long long prop_shadow; /// Do not paint shadow over this window. bool clip_shadow_above; @@ -270,21 +285,30 @@ struct managed_win { /// Whether to blur window background. bool blur_background; + /// The custom window shader to use when rendering. + struct shader_info *fg_shader; + + struct c2_window_state c2_state; + + // Animation related + /// Number of animations currently in progress + unsigned int number_of_animations; + #ifdef CONFIG_OPENGL /// Textures and FBO background blur use. glx_blur_cache_t glx_blur_cache; /// Background texture of the window glx_texture_t *glx_texture_bg; #endif + + /// The damaged region of the window, in window local coordinates. + region_t damaged; }; /// Process pending updates/images flags on a window. Has to be called in X critical /// section void win_process_update_flags(session_t *ps, struct managed_win *w); void win_process_image_flags(session_t *ps, struct managed_win *w); -/// Bind a shadow to the window, with color `c` and shadow kernel `kernel` -bool win_bind_shadow(struct backend_base *b, struct managed_win *w, struct color c, - struct conv *kernel); /// Start the unmap of a window. We cannot unmap immediately since we might need to fade /// the window out. @@ -296,11 +320,13 @@ void map_win_start(struct session *, struct managed_win *); /// Start the destroying of a window. Windows cannot always be destroyed immediately /// because of fading and such. -bool must_use destroy_win_start(session_t *ps, struct win *w); +void destroy_win_start(session_t *ps, struct win *w); /// Release images bound with a window, set the *_NONE flags on the window. Only to be /// used when de-initializing the backend outside of win.c void win_release_images(struct backend_base *base, struct managed_win *w); +winmode_t attr_pure win_calc_mode_raw(const struct managed_win *w); +// TODO(yshui) `win_calc_mode` is only used by legacy backends winmode_t attr_pure win_calc_mode(const struct managed_win *w); void win_set_shadow_force(session_t *ps, struct managed_win *w, switch_t val); void win_set_fade_force(struct managed_win *w, switch_t val); @@ -312,34 +338,14 @@ void win_set_invert_color_force(session_t *ps, struct managed_win *w, switch_t v void win_set_focused(session_t *ps, struct managed_win *w); bool attr_pure win_should_fade(session_t *ps, const struct managed_win *w); void win_on_factor_change(session_t *ps, struct managed_win *w); -/** - * Update cache data in struct _win that depends on window size. - */ -void win_on_win_size_change(session_t *ps, struct managed_win *w); -void win_unmark_client(session_t *ps, struct managed_win *w); -void win_recheck_client(session_t *ps, struct managed_win *w); +void win_unmark_client(struct managed_win *w); -/** - * Calculate and return the opacity target of a window. - * - * The priority of opacity settings are: - * - * inactive_opacity_override (if set, and unfocused) > _NET_WM_WINDOW_OPACITY (if set) > - * opacity-rules (if matched) > window type default opacity > active/inactive opacity - * - * @param ps current session - * @param w struct _win object representing the window - * - * @return target opacity - */ -double attr_pure win_calc_opacity_target(session_t *ps, const struct managed_win *w); bool attr_pure win_should_dim(session_t *ps, const struct managed_win *w); -void win_update_screen(int nscreens, region_t *screens, struct managed_win *w); -/** - * Retrieve the bounding shape of a window. - */ -// XXX was win_border_size -void win_update_bounding_shape(session_t *ps, struct managed_win *w); + +void win_update_monitor(struct x_monitors *monitors, struct managed_win *mw); + +/// Recheck if a window is fullscreen +void win_update_is_fullscreen(const session_t *ps, struct managed_win *w); /** * Check if a window has BYPASS_COMPOSITOR property set */ @@ -372,73 +378,52 @@ void win_get_region_noframe_local_without_corners(const struct managed_win *w, r void win_get_region_frame_local(const struct managed_win *w, region_t *res); /// Get the region for the frame of the window, by value region_t win_get_region_frame_local_by_val(const struct managed_win *w); -/// Insert a new window above window with id `below`, if there is no window, add to top -/// New window will be in unmapped state -struct win *add_win_above(session_t *ps, xcb_window_t id, xcb_window_t below); -/// Insert a new win entry at the top of the stack -struct win *add_win_top(session_t *ps, xcb_window_t id); /// Query the Xorg for information about window `win` /// `win` pointer might become invalid after this function returns -struct win *fill_win(session_t *ps, struct win *win); -/// Move window `w` to be right above `below` -void restack_above(session_t *ps, struct win *w, xcb_window_t below); -/// Move window `w` to the bottom of the stack -void restack_bottom(session_t *ps, struct win *w); -/// Move window `w` to the top of the stack -void restack_top(session_t *ps, struct win *w); +struct win *attr_ret_nonnull maybe_allocate_managed_win(session_t *ps, struct win *win); /** - * Execute fade callback of a window if fading finished. + * Release a destroyed window that is no longer needed. */ -bool must_use win_check_fade_finished(session_t *ps, struct managed_win *w); - -// Stop receiving events (except ConfigureNotify, XXX why?) from a window -void win_ev_stop(session_t *ps, const struct win *w); +void destroy_win_finish(session_t *ps, struct win *w); /// Skip the current in progress fading of window, /// transition the window straight to its end state -/// -/// @return whether the window is destroyed and freed -bool must_use win_skip_fading(session_t *ps, struct managed_win *w); -/** - * Find a managed window from window id in window linked list of the session. - */ -struct managed_win *find_managed_win(session_t *ps, xcb_window_t id); -struct win *find_win(session_t *ps, xcb_window_t id); -struct managed_win *find_toplevel(session_t *ps, xcb_window_t id); -/** - * Find a managed window that is, or is a parent of `wid`. - * - * @param ps current session - * @param wid window ID - * @return struct _win object of the found window, NULL if not found - */ -struct managed_win *find_managed_window_or_parent(session_t *ps, xcb_window_t wid); - -/** - * Check if a window is a fullscreen window. - * - * It's not using w->border_size for performance measures. - */ -bool attr_pure win_is_fullscreen(const session_t *ps, const struct managed_win *w); +void win_skip_fading(struct managed_win *w); /** * Check if a window is focused, without using any focus rules or forced focus settings */ -bool attr_pure win_is_focused_raw(const session_t *ps, const struct managed_win *w); +bool attr_pure win_is_focused_raw(const struct managed_win *w); /// check if window has ARGB visual bool attr_pure win_has_alpha(const struct managed_win *w); +/// Whether it looks like a WM window. We consider a window WM window if +/// it does not have a decedent with WM_STATE and it is not override- +/// redirected itself. +static inline bool attr_pure win_is_wmwin(const struct managed_win *w) { + return w->base.id == w->client_win && !w->a.override_redirect; +} + +static inline struct managed_win *win_as_managed(struct win *w) { + BUG_ON(!w->managed); + return (struct managed_win *)w; +} + +static inline const char *win_get_name_if_managed(const struct win *w) { + if (!w->managed) { + return "(unmanaged)"; + } + auto mw = (struct managed_win *)w; + return mw->name; +} + /// check if reg_ignore_valid is true for all windows above us bool attr_pure win_is_region_ignore_valid(session_t *ps, const struct managed_win *w); /// Whether a given window is mapped on the X server side bool win_is_mapped_in_x(const struct managed_win *w); - -// Find the managed window immediately below `w` in the window stack -struct managed_win *attr_pure win_stack_find_next_managed(const session_t *ps, - const struct list_node *w); /// Set flags on a window. Some sanity checks are performed void win_set_flags(struct managed_win *w, uint64_t flags); /// Clear flags on a window. Some sanity checks are performed @@ -450,6 +435,29 @@ bool win_check_flags_all(struct managed_win *w, uint64_t flags); /// Mark properties as stale for a window void win_set_properties_stale(struct managed_win *w, const xcb_atom_t *prop, int nprops); +xcb_window_t win_get_client_window(struct x_connection *c, struct wm *wm, + struct atom *atoms, const struct managed_win *w); +bool win_update_wintype(struct x_connection *c, struct atom *atoms, struct managed_win *w); +/** + * Retrieve frame extents from a window. + */ +void win_update_frame_extents(struct x_connection *c, struct atom *atoms, + struct managed_win *w, xcb_window_t client, + double frame_opacity); +/** + * Retrieve the WM_CLASS of a window and update its + * win structure. + */ +bool win_update_class(struct x_connection *c, struct atom *atoms, struct managed_win *w); +int win_update_role(struct x_connection *c, struct atom *atoms, struct managed_win *w); +int win_update_name(struct x_connection *c, struct atom *atoms, struct managed_win *w); +void win_on_win_size_change(struct managed_win *w, int shadow_offset_x, + int shadow_offset_y, int shadow_radius); +void win_update_bounding_shape(struct x_connection *c, struct managed_win *w, + bool shape_exists, bool detect_rounded_corners); +bool win_update_prop_fullscreen(struct x_connection *c, const struct atom *atoms, + struct managed_win *w); + static inline attr_unused void win_set_property_stale(struct managed_win *w, xcb_atom_t prop) { return win_set_properties_stale(w, (xcb_atom_t[]){prop}, 1); } @@ -457,24 +465,37 @@ static inline attr_unused void win_set_property_stale(struct managed_win *w, xcb /// Free all resources in a struct win void free_win_res(session_t *ps, struct managed_win *w); -static inline void win_region_remove_corners(const struct managed_win *w, region_t *res) { +/// Remove the corners of window `w` from region `res`. `origin` is the top-left corner of +/// `w` in `res`'s coordinate system. +static inline void +win_region_remove_corners(const struct managed_win *w, ivec2 origin, region_t *res) { + static const int corner_index[][2] = { + {0, 0}, + {0, 1}, + {1, 0}, + {1, 1}, + }; + rect_t rectangles[4]; + for (size_t i = 0; i < ARR_SIZE(corner_index); i++) { + rectangles[i] = (rect_t){ + .x1 = origin.x + corner_index[i][0] * (w->widthb - w->corner_radius), + .y1 = origin.y + corner_index[i][1] * (w->heightb - w->corner_radius), + }; + rectangles[i].x2 = rectangles[i].x1 + w->corner_radius; + rectangles[i].y2 = rectangles[i].y1 + w->corner_radius; + } region_t corners; - pixman_region32_init_rects( - &corners, - (rect_t[]){ - {.x1 = 0, .y1 = 0, .x2 = w->corner_radius, .y2 = w->corner_radius}, - {.x1 = 0, .y1 = w->heightb - w->corner_radius, .x2 = w->corner_radius, .y2 = w->heightb}, - {.x1 = w->widthb - w->corner_radius, .y1 = 0, .x2 = w->widthb, .y2 = w->corner_radius}, - {.x1 = w->widthb - w->corner_radius, - .y1 = w->heightb - w->corner_radius, - .x2 = w->widthb, - .y2 = w->heightb}, - }, - 4); + pixman_region32_init_rects(&corners, rectangles, 4); pixman_region32_subtract(res, res, &corners); pixman_region32_fini(&corners); } +/// Like `win_region_remove_corners`, but `origin` is (0, 0). +static inline void +win_region_remove_corners_local(const struct managed_win *w, region_t *res) { + win_region_remove_corners(w, (ivec2){0, 0}, res); +} + static inline region_t attr_unused win_get_bounding_shape_global_by_val(struct managed_win *w) { region_t ret; pixman_region32_init(&ret); @@ -488,7 +509,7 @@ win_get_bounding_shape_global_without_corners_by_val(struct managed_win *w) { region_t ret; pixman_region32_init(&ret); pixman_region32_copy(&ret, &w->bounding_shape); - win_region_remove_corners(w, &ret); + win_region_remove_corners_local(w, &ret); pixman_region32_translate(&ret, w->g.x, w->g.y); return ret; } diff --git a/src/win_defs.h b/src/win_defs.h index e032bc747c..a9d3b5fd4d 100644 --- a/src/win_defs.h +++ b/src/win_defs.h @@ -2,7 +2,7 @@ #include typedef enum { - WINTYPE_UNKNOWN, + WINTYPE_UNKNOWN = 0, WINTYPE_DESKTOP, WINTYPE_DOCK, WINTYPE_TOOLBAR, @@ -27,48 +27,22 @@ typedef enum { WMODE_SOLID, // The window is opaque including the frame } winmode_t; -/// Transition table: -/// (DESTROYED is when the win struct is destroyed and freed) -/// ('o' means in all other cases) -/// (Window is created in the UNMAPPED state) -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | |UNMAPPING|DESTROYING|MAPPING|FADING |UNMAPPED| MAPPED |DESTROYED| -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | UNMAPPING | o | Window |Window | - | Fading | - | - | -/// | | |destroyed |mapped | |finished| | | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | DESTROYING | - | o | - | - | - | - | Fading | -/// | | | | | | | |finished | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | MAPPING | Window | Window | o |Opacity| - | Fading | - | -/// | |unmapped |destroyed | |change | |finished| | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | FADING | Window | Window | - | o | - | Fading | - | -/// | |unmapped |destroyed | | | |finished| | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | UNMAPPED | - | - |Window | - | o | - | Window | -/// | | | |mapped | | | |destroyed| -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | MAPPED | Window | Window | - |Opacity| - | o | - | -/// | |unmapped |destroyed | |change | | | | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// The state of a window from Xserver's perspective typedef enum { - // The window is being faded out because it's unmapped. - WSTATE_UNMAPPING, - // The window is being faded out because it's destroyed, - WSTATE_DESTROYING, - // The window is being faded in - WSTATE_MAPPING, - // Window opacity is not at the target level - WSTATE_FADING, - // The window is mapped, no fading is in progress. - WSTATE_MAPPED, - // The window is unmapped, no fading is in progress. + /// The window is unmapped. Equivalent to map-state == XCB_MAP_STATE_UNMAPPED WSTATE_UNMAPPED, + /// The window no longer exists on the X server. + WSTATE_DESTROYED, + /// The window is mapped and viewable. Equivalent to map-state == + /// XCB_MAP_STATE_VIEWABLE + WSTATE_MAPPED, + + // XCB_MAP_STATE_UNVIEWABLE is not represented here because it should not be + // possible for top-level windows. } winstate_t; enum win_flags { - // Note: *_NONE flags are mostly redudant and meant for detecting logical errors + // Note: *_NONE flags are mostly redundant and meant for detecting logical errors // in the code /// pixmap is out of date, will be update in win_process_flags @@ -77,10 +51,6 @@ enum win_flags { WIN_FLAGS_PIXMAP_NONE = 2, /// there was an error trying to bind the images WIN_FLAGS_IMAGE_ERROR = 4, - /// shadow is out of date, will be updated in win_process_flags - WIN_FLAGS_SHADOW_STALE = 8, - /// shadow has not been generated - WIN_FLAGS_SHADOW_NONE = 16, /// the client window needs to be updated WIN_FLAGS_CLIENT_STALE = 32, /// the window is mapped by X, we need to call map_win_start for it @@ -95,8 +65,3 @@ enum win_flags { /// need better name for this, is set when some aspects of the window changed WIN_FLAGS_FACTOR_CHANGED = 1024, }; - -static const uint64_t WIN_FLAGS_IMAGES_STALE = - WIN_FLAGS_PIXMAP_STALE | WIN_FLAGS_SHADOW_STALE; - -#define WIN_FLAGS_IMAGES_NONE (WIN_FLAGS_PIXMAP_NONE | WIN_FLAGS_SHADOW_NONE) diff --git a/src/wm.c b/src/wm.c new file mode 100644 index 0000000000..2d680cd639 --- /dev/null +++ b/src/wm.c @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#include +#include + +#include "list.h" +#include "log.h" +#include "uthash_extra.h" +#include "win.h" +#include "wm.h" +#include "x.h" + +struct wm { + /// Current window generation, start from 1. 0 is reserved for using as + /// an invalid generation number. + /// + /// Because X server recycles window IDs, `id` along + /// is not enough to uniquely identify a window. This generation number is + /// incremented every time a window is destroyed, so that if a window ID is + /// reused, its generation number will be different from before. + /// Unless, of course, if the generation number overflows, but since we are + /// using a uint64_t here, that won't happen for a very long time. Still, + /// it is recommended that you restart the compositor at least once before + /// the Universe collapse back on itself. + uint64_t generation; + /// A hash table of all windows. + struct win *windows; + /// Windows in their stacking order + struct list_node window_stack; + /// Pointer to win of current active window. Used by + /// EWMH _NET_ACTIVE_WINDOW focus detection. In theory, + /// it's more reliable to store the window ID directly here, just in + /// case the WM does something extraordinary, but caching the pointer + /// means another layer of complexity. + struct managed_win *active_win; + /// Window ID of leader window of currently active window. Used for + /// subsidiary window detection. + xcb_window_t active_leader; + struct subwin *subwins; +}; + +unsigned int wm_get_window_count(struct wm *wm) { + unsigned int count = 0; + HASH_ITER2(wm->windows, w) { + assert(!w->destroyed); + ++count; + } + return count; +} + +struct managed_win *wm_active_win(struct wm *wm) { + return wm->active_win; +} + +void wm_set_active_win(struct wm *wm, struct managed_win *w) { + wm->active_win = w; +} + +xcb_window_t wm_active_leader(struct wm *wm) { + return wm->active_leader; +} + +void wm_set_active_leader(struct wm *wm, xcb_window_t leader) { + wm->active_leader = leader; +} + +struct win *wm_stack_next(struct wm *wm, struct list_node *cursor) { + if (!list_node_is_last(&wm->window_stack, cursor)) { + return list_entry(cursor->next, struct win, stack_neighbour); + } + return NULL; +} + +// Find the managed window immediately below `i` in the window stack +struct managed_win * +wm_stack_next_managed(const struct wm *wm, const struct list_node *cursor) { + while (!list_node_is_last(&wm->window_stack, cursor)) { + auto next = list_entry(cursor->next, struct win, stack_neighbour); + if (next->managed) { + return (struct managed_win *)next; + } + cursor = &next->stack_neighbour; + } + return NULL; +} + +/// Find a managed window from window id in window linked list of the session. +struct win *wm_find(struct wm *wm, xcb_window_t id) { + if (!id) { + return NULL; + } + + struct win *w = NULL; + HASH_FIND_INT(wm->windows, &id, w); + assert(w == NULL || !w->destroyed); + return w; +} + +void wm_remove(struct wm *wm, struct win *w) { + wm->generation++; + HASH_DEL(wm->windows, w); +} + +int wm_foreach(struct wm *wm, int (*func)(struct win *, void *), void *data) { + HASH_ITER2(wm->windows, w) { + assert(!w->destroyed); + int ret = func(w, data); + if (ret) { + return ret; + } + } + return 0; +} + +void wm_stack_replace(struct wm *wm, struct win *old, struct win *new_) { + list_replace(&old->stack_neighbour, &new_->stack_neighbour); + struct win *replaced = NULL; + HASH_REPLACE_INT(wm->windows, id, new_, replaced); + assert(replaced == old); + free(old); +} + +/// Insert a new window after list_node `prev` +/// New window will be in unmapped state +static struct win * +wm_stack_insert_after(struct wm *wm, xcb_window_t id, struct list_node *prev) { + log_debug("Adding window %#010x", id); + struct win *old_w = NULL; + HASH_FIND_INT(wm->windows, &id, old_w); + assert(old_w == NULL); + + auto new_w = cmalloc(struct win); + list_insert_after(prev, &new_w->stack_neighbour); + new_w->id = id; + new_w->managed = false; + new_w->is_new = true; + new_w->destroyed = false; + new_w->generation = wm->generation; + + HASH_ADD_INT(wm->windows, id, new_w); + return new_w; +} + +struct win *wm_stack_add_top(struct wm *wm, xcb_window_t id) { + return wm_stack_insert_after(wm, id, &wm->window_stack); +} + +struct win *wm_stack_add_above(struct wm *wm, xcb_window_t id, xcb_window_t below) { + struct win *w = NULL; + HASH_FIND_INT(wm->windows, &below, w); + if (!w) { + if (!list_is_empty(&wm->window_stack)) { + // `below` window is not found even if the window stack is + // not empty + return NULL; + } + return wm_stack_add_top(wm, id); + } + // we found something from the hash table, so if the stack is + // empty, we are in an inconsistent state. + assert(!list_is_empty(&wm->window_stack)); + return wm_stack_insert_after(wm, id, w->stack_neighbour.prev); +} + +/// Move window `w` so it's before `next` in the list +static inline void wm_stack_move_before(struct wm *wm, struct win *w, struct list_node *next) { + struct managed_win *mw = NULL; + if (w->managed) { + mw = (struct managed_win *)w; + } + + if (mw) { + // This invalidates all reg_ignore below the new stack position of + // `w` + mw->reg_ignore_valid = false; + rc_region_unref(&mw->reg_ignore); + + // This invalidates all reg_ignore below the old stack position of + // `w` + auto next_w = wm_stack_next_managed(wm, &w->stack_neighbour); + if (next_w) { + next_w->reg_ignore_valid = false; + rc_region_unref(&next_w->reg_ignore); + } + } + + list_move_before(&w->stack_neighbour, next); + +#ifdef DEBUG_RESTACK + log_trace("Window stack modified. Current stack:"); + for (auto c = &wm->window_stack; c; c = c->next) { + const char *desc = ""; + if (c->state == WSTATE_DESTROYING) { + desc = "(D) "; + } + log_trace("%#010x \"%s\" %s", c->id, c->name, desc); + } +#endif +} + +struct list_node *wm_stack_end(struct wm *wm) { + return &wm->window_stack; +} + +/// Move window `w` so it's right above `below`, if `below` is 0, `w` is moved +/// to the bottom of the stack +void wm_stack_move_above(struct wm *wm, struct win *w, xcb_window_t below) { + xcb_window_t old_below; + + if (!list_node_is_last(&wm->window_stack, &w->stack_neighbour)) { + old_below = list_next_entry(w, stack_neighbour)->id; + } else { + old_below = XCB_NONE; + } + log_debug("Restack %#010x (%s), old_below: %#010x, new_below: %#010x", w->id, + win_get_name_if_managed(w), old_below, below); + + if (old_below != below) { + struct list_node *new_next; + if (!below) { + new_next = &wm->window_stack; + } else { + struct win *tmp_w = NULL; + HASH_FIND_INT(wm->windows, &below, tmp_w); + + if (!tmp_w) { + log_error("Failed to found new below window %#010x.", below); + return; + } + + new_next = &tmp_w->stack_neighbour; + } + wm_stack_move_before(wm, w, new_next); + } +} + +void wm_stack_move_to_top(struct wm *wm, struct win *w) { + if (&w->stack_neighbour == wm->window_stack.next) { + // already at top + return; + } + wm_stack_move_before(wm, w, wm->window_stack.next); +} + +unsigned wm_stack_num_managed_windows(const struct wm *wm) { + unsigned count = 0; + list_foreach(struct win, w, &wm->window_stack, stack_neighbour) { + if (w->managed) { + count += 1; + } + } + return count; +} + +struct managed_win *wm_find_by_client(struct wm *wm, xcb_window_t client) { + if (!client) { + return NULL; + } + + HASH_ITER2(wm->windows, w) { + assert(!w->destroyed); + if (!w->managed) { + continue; + } + + auto mw = (struct managed_win *)w; + if (mw->client_win == client) { + return mw; + } + } + + return NULL; +} + +struct managed_win *wm_find_managed(struct wm *wm, xcb_window_t id) { + struct win *w = wm_find(wm, id); + if (!w || !w->managed) { + return NULL; + } + + auto mw = (struct managed_win *)w; + assert(mw->state != WSTATE_DESTROYED); + return mw; +} + +unsigned wm_num_windows(const struct wm *wm) { + return HASH_COUNT(wm->windows); +} + +struct subwin *wm_subwin_add_and_subscribe(struct wm *wm, struct x_connection *c, + xcb_window_t id, xcb_window_t parent) { + struct subwin *subwin = NULL; + HASH_FIND_INT(wm->subwins, &id, subwin); + BUG_ON(subwin != NULL); + + subwin = ccalloc(1, struct subwin); + subwin->id = id; + subwin->toplevel = parent; + HASH_ADD_INT(wm->subwins, id, subwin); + + log_debug("Allocated subwin %p for window %#010x, toplevel %#010x, total: %d", + subwin, id, parent, HASH_COUNT(wm->subwins)); + XCB_AWAIT_VOID(xcb_change_window_attributes, c->c, id, XCB_CW_EVENT_MASK, + (const uint32_t[]){XCB_EVENT_MASK_PROPERTY_CHANGE}); + return subwin; +} + +struct subwin *wm_subwin_find(struct wm *wm, xcb_window_t id) { + struct subwin *subwin = NULL; + HASH_FIND_INT(wm->subwins, &id, subwin); + return subwin; +} + +void wm_subwin_remove(struct wm *wm, struct subwin *subwin) { + log_debug("Freeing subwin %p for window %#010x, toplevel %#010x", subwin, + subwin->id, subwin->toplevel); + HASH_DEL(wm->subwins, subwin); + free(subwin); +} + +void wm_subwin_remove_and_unsubscribe(struct wm *wm, struct x_connection *c, + struct subwin *subwin) { + log_debug("Freeing subwin %p for window %#010x", subwin, subwin->id); + XCB_AWAIT_VOID(xcb_change_window_attributes, c->c, subwin->id, XCB_CW_EVENT_MASK, + (const uint32_t[]){0}); + wm_subwin_remove(wm, subwin); +} +void wm_subwin_remove_and_unsubscribe_for_toplevel(struct wm *wm, struct x_connection *c, + xcb_window_t toplevel) { + struct subwin *subwin, *next_subwin; + HASH_ITER(hh, wm->subwins, subwin, next_subwin) { + if (subwin->toplevel == toplevel) { + wm_subwin_remove_and_unsubscribe(wm, c, subwin); + } + } +} + +struct wm *wm_new(void) { + auto wm = ccalloc(1, struct wm); + list_init_head(&wm->window_stack); + wm->generation = 1; + return wm; +} + +void wm_free(struct wm *wm, struct x_connection *c) { + list_foreach_safe(struct win, w, &wm->window_stack, stack_neighbour) { + if (w->managed) { + XCB_AWAIT_VOID(xcb_change_window_attributes, c->c, w->id, + XCB_CW_EVENT_MASK, (const uint32_t[]){0}); + } + if (!w->destroyed) { + HASH_DEL(wm->windows, w); + } + free(w); + } + list_init_head(&wm->window_stack); + + struct subwin *subwin, *next_subwin; + HASH_ITER(hh, wm->subwins, subwin, next_subwin) { + wm_subwin_remove_and_unsubscribe(wm, c, subwin); + } + + free(wm); +} diff --git a/src/wm.h b/src/wm.h new file mode 100644 index 0000000000..b9778c191b --- /dev/null +++ b/src/wm.h @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +/// Yeah... We have our own window manager inside the compositor. As a compositor, we do +/// need to do a little bit of what a window manager does, to correctly render windows. +/// But our window manager is a lot less sophisticated than a average window manager. We +/// only keep track of a list of top-level windows, and the order they are stacked. +/// But OTOH doing window managing here is also somewhat more challenging. As we are not +/// a window manager, we don't actually know what window is an application window, what +/// is not. We have to rely on the real window manager playing nice and following the +/// ICCCM and EWMH standards. + +#pragma once + +#include +#include + +#include "compiler.h" +#include "utils.h" + +struct wm; +struct managed_win; +struct list_node; +struct x_connection; + +/// Direct children of a toplevel. +struct subwin { + xcb_window_t id; + xcb_window_t toplevel; + enum tristate has_wm_state; + UT_hash_handle hh; +}; + +struct wm *wm_new(void); +void wm_free(struct wm *wm, struct x_connection *c); + +struct managed_win *wm_active_win(struct wm *wm); +void wm_set_active_win(struct wm *wm, struct managed_win *w); +xcb_window_t wm_active_leader(struct wm *wm); +void wm_set_active_leader(struct wm *wm, xcb_window_t leader); + +// Note: `wm` keeps track of 2 lists of windows. One is the window stack, which includes +// all windows that might need to be rendered, which means it would include destroyed +// windows in case they need to be faded out. This list is accessed by `wm_stack_*` series +// of functions. The other is a hash table of windows, which does not include destroyed +// windows. This list is accessed by `wm_find_*`, `wm_foreach`, and `wm_num_windows`. +// Adding a window to the window stack also automatically adds it to the hash table. + +/// Find a window in the hash table from window id. +struct win *wm_find(struct wm *wm, xcb_window_t id); +/// Remove a window from the hash table. +void wm_remove(struct wm *wm, struct win *w); +/// Find a managed window from window id in window linked list of the session. +struct managed_win *wm_find_managed(struct wm *wm, xcb_window_t id); +// Find the WM frame of a client window. `id` is the client window id. +struct managed_win *wm_find_by_client(struct wm *wm, xcb_window_t client); +/// Call `func` on each toplevel window. `func` should return 0 if the iteration +/// should continue. If it returns anything else, the iteration will stop and the +/// return value will be returned from `wm_foreach`. If the iteration finishes +/// naturally, 0 will be returned. +int wm_foreach(struct wm *wm, int (*func)(struct win *, void *), void *data); +/// Returns the number of windows in the hash table. +unsigned attr_const wm_num_windows(const struct wm *wm); + +/// Returns the cursor past the last window in the stack (the `end`). The window stack is +/// a cyclic linked list, so the next element after `end` is the first element. The `end` +/// itself does not point to a valid window. The address of `end` is stable as long as +/// the `struct wm` itself is not freed. +struct list_node *attr_const wm_stack_end(struct wm *wm); +/// Insert a new win entry at the top of the stack +struct win *wm_stack_add_top(struct wm *wm, xcb_window_t id); +/// Insert a new window above window with id `below`, if there is no window, add +/// to top New window will be in unmapped state +struct win *wm_stack_add_above(struct wm *wm, xcb_window_t id, xcb_window_t below); +// Find the managed window immediately below `i` in the window stack +struct managed_win *attr_pure wm_stack_next_managed(const struct wm *wm, + const struct list_node *cursor); +/// Move window `w` so it's right above `below`, if `below` is 0, `w` is moved +/// to the bottom of the stack +void wm_stack_move_above(struct wm *wm, struct win *w, xcb_window_t below); +/// Move window `w` to the bottom of the stack. +static inline void wm_stack_move_to_bottom(struct wm *wm, struct win *w) { + wm_stack_move_above(wm, w, 0); +} +/// Move window `w` to the top of the stack. +void wm_stack_move_to_top(struct wm *wm, struct win *w); +/// Replace window `old` with `new_` in the stack, also replace the window in the hash +/// table. `old` will be freed. +void wm_stack_replace(struct wm *wm, struct win *old, struct win *new_); +unsigned attr_const wm_stack_num_managed_windows(const struct wm *wm); + +struct subwin *wm_subwin_add_and_subscribe(struct wm *wm, struct x_connection *c, + xcb_window_t id, xcb_window_t parent); +struct subwin *wm_subwin_find(struct wm *wm, xcb_window_t id); +void wm_subwin_remove(struct wm *wm, struct subwin *subwin); +void wm_subwin_remove_and_unsubscribe(struct wm *wm, struct x_connection *c, + struct subwin *subwin); +/// Remove all subwins associated with a toplevel window +void wm_subwin_remove_and_unsubscribe_for_toplevel(struct wm *wm, struct x_connection *c, + xcb_window_t toplevel); diff --git a/src/x.c b/src/x.c index c146f48e33..78d9c68b57 100644 --- a/src/x.c +++ b/src/x.c @@ -4,14 +4,19 @@ #include #include +#include #include #include #include #include +#include #include +#include +#include #include #include #include +#include #include #include @@ -27,6 +32,72 @@ #include "utils.h" #include "x.h" +// === Error handling === + +/** + * Xlib error handler function. + */ +static int xerror(Display attr_unused *dpy, XErrorEvent *ev) { + if (!ps_g) { + // Do not ignore errors until the session has been initialized + return 0; + } + + // Fake a xcb error, fill in just enough information + xcb_generic_error_t xcb_err; + xcb_err.full_sequence = (uint32_t)ev->serial; + xcb_err.major_code = ev->request_code; + xcb_err.minor_code = ev->minor_code; + xcb_err.error_code = ev->error_code; + x_handle_error(&ps_g->c, &xcb_err); + return 0; +} + +void x_discard_pending(struct x_connection *c, uint32_t sequence) { + while (c->pending_reply_head && sequence > c->pending_reply_head->sequence) { + auto next = c->pending_reply_head->next; + free(c->pending_reply_head); + c->pending_reply_head = next; + } + if (!c->pending_reply_head) { + c->pending_reply_tail = &c->pending_reply_head; + } +} + +void x_handle_error(struct x_connection *c, xcb_generic_error_t *ev) { + x_discard_pending(c, ev->full_sequence); + if (c->pending_reply_head && c->pending_reply_head->sequence == ev->full_sequence) { + if (c->pending_reply_head->action != PENDING_REPLY_ACTION_IGNORE) { + x_log_error(LOG_LEVEL_ERROR, ev->full_sequence, ev->major_code, + ev->minor_code, ev->error_code); + } + switch (c->pending_reply_head->action) { + case PENDING_REPLY_ACTION_ABORT: + log_fatal("An unrecoverable X error occurred, aborting..."); + abort(); + case PENDING_REPLY_ACTION_DEBUG_ABORT: assert(false); break; + case PENDING_REPLY_ACTION_IGNORE: break; + } + return; + } + x_log_error(LOG_LEVEL_WARN, ev->full_sequence, ev->major_code, ev->minor_code, + ev->error_code); +} + +/// Initialize x_connection struct from an Xlib Display. +/// +/// Note this function doesn't take ownership of the Display, the caller is still +/// responsible for closing it after `free_x_connection` is called. +void x_connection_init(struct x_connection *c, Display *dpy) { + c->dpy = dpy; + c->c = XGetXCBConnection(dpy); + c->pending_reply_tail = &c->pending_reply_head; + c->previous_xerror_handler = XSetErrorHandler(xerror); + + c->screen = DefaultScreen(dpy); + c->screen_info = xcb_aux_get_screen(c->c, c->screen); +} + /** * Get a specific attribute of a window. * @@ -42,11 +113,11 @@ * @return a winprop_t structure containing the attribute * and number of items. A blank one on failure. */ -winprop_t x_get_prop_with_offset(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom, +winprop_t x_get_prop_with_offset(const struct x_connection *c, xcb_window_t w, xcb_atom_t atom, int offset, int length, xcb_atom_t rtype, int rformat) { xcb_get_property_reply_t *r = xcb_get_property_reply( - c, - xcb_get_property(c, 0, w, atom, rtype, to_u32_checked(offset), + c->c, + xcb_get_property(c->c, 0, w, atom, rtype, to_u32_checked(offset), to_u32_checked(length)), NULL); @@ -70,10 +141,10 @@ winprop_t x_get_prop_with_offset(xcb_connection_t *c, xcb_window_t w, xcb_atom_t } /// Get the type, format and size in bytes of a window's specific attribute. -winprop_info_t x_get_prop_info(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom) { +winprop_info_t x_get_prop_info(const struct x_connection *c, xcb_window_t w, xcb_atom_t atom) { xcb_generic_error_t *e = NULL; auto r = xcb_get_property_reply( - c, xcb_get_property(c, 0, w, atom, XCB_ATOM_ANY, 0, 0), &e); + c->c, xcb_get_property(c->c, 0, w, atom, XCB_ATOM_ANY, 0, 0), &e); if (!r) { log_debug_x_error(e, "Failed to get property info for window %#010x", w); free(e); @@ -93,7 +164,7 @@ winprop_info_t x_get_prop_info(xcb_connection_t *c, xcb_window_t w, xcb_atom_t a * * @return the value if successful, 0 otherwise */ -xcb_window_t wid_get_prop_window(xcb_connection_t *c, xcb_window_t wid, xcb_atom_t aprop) { +xcb_window_t wid_get_prop_window(struct x_connection *c, xcb_window_t wid, xcb_atom_t aprop) { // Get the attribute xcb_window_t p = XCB_NONE; winprop_t prop = x_get_prop(c, wid, aprop, 1L, XCB_ATOM_WINDOW, 32); @@ -111,10 +182,9 @@ xcb_window_t wid_get_prop_window(xcb_connection_t *c, xcb_window_t wid, xcb_atom /** * Get the value of a text property of a window. */ -bool wid_get_text_prop(session_t *ps, xcb_window_t wid, xcb_atom_t prop, char ***pstrlst, - int *pnstr) { - assert(ps->server_grabbed); - auto prop_info = x_get_prop_info(ps->c, wid, prop); +bool wid_get_text_prop(struct x_connection *c, struct atom *atoms, xcb_window_t wid, + xcb_atom_t prop, char ***pstrlst, int *pnstr) { + auto prop_info = x_get_prop_info(c, wid, prop); auto type = prop_info.type; auto format = prop_info.format; auto length = prop_info.length; @@ -123,8 +193,7 @@ bool wid_get_text_prop(session_t *ps, xcb_window_t wid, xcb_atom_t prop, char ** return false; } - if (type != XCB_ATOM_STRING && type != ps->atoms->aUTF8_STRING && - type != ps->atoms->aC_STRING) { + if (!x_is_type_string(atoms, type)) { log_warn("Text property %d of window %#010x has unsupported type: %d", prop, wid, type); return false; @@ -139,7 +208,7 @@ bool wid_get_text_prop(session_t *ps, xcb_window_t wid, xcb_atom_t prop, char ** xcb_generic_error_t *e = NULL; auto word_count = (length + 4 - 1) / 4; auto r = xcb_get_property_reply( - ps->c, xcb_get_property(ps->c, 0, wid, prop, type, 0, word_count), &e); + c->c, xcb_get_property(c->c, 0, wid, prop, type, 0, word_count), &e); if (!r) { log_debug_x_error(e, "Failed to get window property for %#010x", wid); free(e); @@ -197,14 +266,14 @@ bool wid_get_text_prop(session_t *ps, xcb_window_t wid, xcb_atom_t prop, char ** // of this program static thread_local xcb_render_query_pict_formats_reply_t *g_pictfmts = NULL; -static inline void x_get_server_pictfmts(xcb_connection_t *c) { +static inline void x_get_server_pictfmts(struct x_connection *c) { if (g_pictfmts) { return; } xcb_generic_error_t *e = NULL; // Get window picture format - g_pictfmts = - xcb_render_query_pict_formats_reply(c, xcb_render_query_pict_formats(c), &e); + g_pictfmts = xcb_render_query_pict_formats_reply( + c->c, xcb_render_query_pict_formats(c->c), &e); if (e || !g_pictfmts) { log_fatal("failed to get pict formats\n"); abort(); @@ -212,7 +281,7 @@ static inline void x_get_server_pictfmts(xcb_connection_t *c) { } const xcb_render_pictforminfo_t * -x_get_pictform_for_visual(xcb_connection_t *c, xcb_visualid_t visual) { +x_get_pictform_for_visual(struct x_connection *c, xcb_visualid_t visual) { x_get_server_pictfmts(c); xcb_render_pictvisual_t *pv = xcb_render_util_find_visual_format(g_pictfmts, visual); @@ -243,7 +312,7 @@ static xcb_visualid_t attr_pure x_get_visual_for_pictfmt(xcb_render_query_pict_f return XCB_NONE; } -xcb_visualid_t x_get_visual_for_standard(xcb_connection_t *c, xcb_pict_standard_t std) { +xcb_visualid_t x_get_visual_for_standard(struct x_connection *c, xcb_pict_standard_t std) { x_get_server_pictfmts(c); auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, std); @@ -251,8 +320,19 @@ xcb_visualid_t x_get_visual_for_standard(xcb_connection_t *c, xcb_pict_standard_ return x_get_visual_for_pictfmt(g_pictfmts, pictfmt->id); } +xcb_visualid_t x_get_visual_for_depth(xcb_screen_t *screen, uint8_t depth) { + xcb_depth_iterator_t depth_it = xcb_screen_allowed_depths_iterator(screen); + for (; depth_it.rem; xcb_depth_next(&depth_it)) { + if (depth_it.data->depth == depth) { + return xcb_depth_visuals_iterator(depth_it.data).data->visual_id; + } + } + + return XCB_NONE; +} + xcb_render_pictformat_t -x_get_pictfmt_for_standard(xcb_connection_t *c, xcb_pict_standard_t std) { +x_get_pictfmt_for_standard(struct x_connection *c, xcb_pict_standard_t std) { x_get_server_pictfmts(c); auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, std); @@ -260,27 +340,8 @@ x_get_pictfmt_for_standard(xcb_connection_t *c, xcb_pict_standard_t std) { return pictfmt->id; } -int x_get_visual_depth(xcb_connection_t *c, xcb_visualid_t visual) { - auto setup = xcb_get_setup(c); - for (auto screen = xcb_setup_roots_iterator(setup); screen.rem; - xcb_screen_next(&screen)) { - for (auto depth = xcb_screen_allowed_depths_iterator(screen.data); - depth.rem; xcb_depth_next(&depth)) { - const int len = xcb_depth_visuals_length(depth.data); - const xcb_visualtype_t *visuals = xcb_depth_visuals(depth.data); - for (int i = 0; i < len; i++) { - if (visual == visuals[i].visual_id) { - return depth.data->depth; - } - } - } - } - return -1; -} - xcb_render_picture_t -x_create_picture_with_pictfmt_and_pixmap(xcb_connection_t *c, - const xcb_render_pictforminfo_t *pictfmt, +x_create_picture_with_pictfmt_and_pixmap(struct x_connection *c, xcb_render_pictformat_t pictfmt, xcb_pixmap_t pixmap, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) { void *buf = NULL; @@ -294,56 +355,58 @@ x_create_picture_with_pictfmt_and_pixmap(xcb_connection_t *c, xcb_render_picture_t tmp_picture = x_new_id(c); xcb_generic_error_t *e = - xcb_request_check(c, xcb_render_create_picture_checked( - c, tmp_picture, pixmap, pictfmt->id, valuemask, buf)); + xcb_request_check(c->c, xcb_render_create_picture_checked( + c->c, tmp_picture, pixmap, pictfmt, valuemask, buf)); free(buf); if (e) { log_error_x_error(e, "failed to create picture"); + free(e); + abort(); return XCB_NONE; } return tmp_picture; } xcb_render_picture_t -x_create_picture_with_visual_and_pixmap(xcb_connection_t *c, xcb_visualid_t visual, +x_create_picture_with_visual_and_pixmap(struct x_connection *c, xcb_visualid_t visual, xcb_pixmap_t pixmap, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) { const xcb_render_pictforminfo_t *pictfmt = x_get_pictform_for_visual(c, visual); - return x_create_picture_with_pictfmt_and_pixmap(c, pictfmt, pixmap, valuemask, attr); + return x_create_picture_with_pictfmt_and_pixmap(c, pictfmt->id, pixmap, valuemask, attr); } xcb_render_picture_t -x_create_picture_with_standard_and_pixmap(xcb_connection_t *c, xcb_pict_standard_t standard, +x_create_picture_with_standard_and_pixmap(struct x_connection *c, xcb_pict_standard_t standard, xcb_pixmap_t pixmap, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) { x_get_server_pictfmts(c); auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, standard); assert(pictfmt); - return x_create_picture_with_pictfmt_and_pixmap(c, pictfmt, pixmap, valuemask, attr); + return x_create_picture_with_pictfmt_and_pixmap(c, pictfmt->id, pixmap, valuemask, attr); } xcb_render_picture_t -x_create_picture_with_standard(xcb_connection_t *c, xcb_drawable_t d, int w, int h, +x_create_picture_with_standard(struct x_connection *c, int w, int h, xcb_pict_standard_t standard, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) { x_get_server_pictfmts(c); auto pictfmt = xcb_render_util_find_standard_format(g_pictfmts, standard); assert(pictfmt); - return x_create_picture_with_pictfmt(c, d, w, h, pictfmt, valuemask, attr); + return x_create_picture_with_pictfmt(c, w, h, pictfmt->id, pictfmt->depth, + valuemask, attr); } /** * Create an picture. */ xcb_render_picture_t -x_create_picture_with_pictfmt(xcb_connection_t *c, xcb_drawable_t d, int w, int h, - const xcb_render_pictforminfo_t *pictfmt, uint32_t valuemask, +x_create_picture_with_pictfmt(struct x_connection *c, int w, int h, + xcb_render_pictformat_t pictfmt, uint8_t depth, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) { - uint8_t depth = pictfmt->depth; - xcb_pixmap_t tmp_pixmap = x_create_pixmap(c, depth, d, w, h); + xcb_pixmap_t tmp_pixmap = x_create_pixmap(c, depth, w, h); if (!tmp_pixmap) { return XCB_NONE; } @@ -351,23 +414,24 @@ x_create_picture_with_pictfmt(xcb_connection_t *c, xcb_drawable_t d, int w, int xcb_render_picture_t picture = x_create_picture_with_pictfmt_and_pixmap( c, pictfmt, tmp_pixmap, valuemask, attr); - xcb_free_pixmap(c, tmp_pixmap); + set_cant_fail_cookie(c, xcb_free_pixmap(c->c, tmp_pixmap)); return picture; } xcb_render_picture_t -x_create_picture_with_visual(xcb_connection_t *c, xcb_drawable_t d, int w, int h, - xcb_visualid_t visual, uint32_t valuemask, +x_create_picture_with_visual(struct x_connection *c, int w, int h, xcb_visualid_t visual, + uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) { auto pictfmt = x_get_pictform_for_visual(c, visual); - return x_create_picture_with_pictfmt(c, d, w, h, pictfmt, valuemask, attr); + return x_create_picture_with_pictfmt(c, w, h, pictfmt->id, pictfmt->depth, + valuemask, attr); } -bool x_fetch_region(xcb_connection_t *c, xcb_xfixes_region_t r, pixman_region32_t *res) { +bool x_fetch_region(struct x_connection *c, xcb_xfixes_region_t r, pixman_region32_t *res) { xcb_generic_error_t *e = NULL; xcb_xfixes_fetch_region_reply_t *xr = - xcb_xfixes_fetch_region_reply(c, xcb_xfixes_fetch_region(c, r), &e); + xcb_xfixes_fetch_region_reply(c->c, xcb_xfixes_fetch_region(c->c, r), &e); if (!xr) { log_error_x_error(e, "Failed to fetch rectangles"); return false; @@ -388,7 +452,70 @@ bool x_fetch_region(xcb_connection_t *c, xcb_xfixes_region_t r, pixman_region32_ return ret; } -void x_set_picture_clip_region(xcb_connection_t *c, xcb_render_picture_t pict, +bool x_set_region(struct x_connection *c, xcb_xfixes_region_t dst, const region_t *src) { + if (!src || dst == XCB_NONE) { + return false; + } + + int32_t nrects = 0; + const rect_t *rects = pixman_region32_rectangles((region_t *)src, &nrects); + if (!rects || nrects < 1) { + return false; + } + + xcb_rectangle_t *xrects = ccalloc(nrects, xcb_rectangle_t); + for (int32_t i = 0; i < nrects; i++) { + xrects[i] = + (xcb_rectangle_t){.x = to_i16_checked(rects[i].x1), + .y = to_i16_checked(rects[i].y1), + .width = to_u16_checked(rects[i].x2 - rects[i].x1), + .height = to_u16_checked(rects[i].y2 - rects[i].y1)}; + } + + bool success = + XCB_AWAIT_VOID(xcb_xfixes_set_region, c->c, dst, to_u32_checked(nrects), xrects); + + free(xrects); + + return success; +} + +uint32_t x_create_region(struct x_connection *c, const region_t *reg) { + if (!reg) { + return XCB_NONE; + } + + int nrects; + // In older pixman versions, pixman_region32_rectangles doesn't take const + // region_t, instead of dealing with this version difference, just suppress the + // warning. + const pixman_box32_t *rects = pixman_region32_rectangles((region_t *)reg, &nrects); + auto xrects = ccalloc(nrects, xcb_rectangle_t); + for (int i = 0; i < nrects; i++) { + xrects[i] = + (xcb_rectangle_t){.x = to_i16_checked(rects[i].x1), + .y = to_i16_checked(rects[i].y1), + .width = to_u16_checked(rects[i].x2 - rects[i].x1), + .height = to_u16_checked(rects[i].y2 - rects[i].y1)}; + } + + xcb_xfixes_region_t ret = x_new_id(c); + bool success = XCB_AWAIT_VOID(xcb_xfixes_create_region, c->c, ret, + to_u32_checked(nrects), xrects); + free(xrects); + if (!success) { + return XCB_NONE; + } + return ret; +} + +void x_destroy_region(struct x_connection *c, xcb_xfixes_region_t r) { + if (r != XCB_NONE) { + set_debug_cant_fail_cookie(c, xcb_xfixes_destroy_region(c->c, r)); + } +} + +void x_set_picture_clip_region(struct x_connection *c, xcb_render_picture_t pict, int16_t clip_x_origin, int16_t clip_y_origin, const region_t *reg) { int nrects; @@ -403,9 +530,10 @@ void x_set_picture_clip_region(xcb_connection_t *c, xcb_render_picture_t pict, }; } - xcb_generic_error_t *e = xcb_request_check( - c, xcb_render_set_picture_clip_rectangles_checked( - c, pict, clip_x_origin, clip_y_origin, to_u32_checked(nrects), xrects)); + xcb_generic_error_t *e = + xcb_request_check(c->c, xcb_render_set_picture_clip_rectangles_checked( + c->c, pict, clip_x_origin, clip_y_origin, + to_u32_checked(nrects), xrects)); if (e) { log_error_x_error(e, "Failed to set clip region"); free(e); @@ -413,16 +541,28 @@ void x_set_picture_clip_region(xcb_connection_t *c, xcb_render_picture_t pict, free(xrects); } -void x_clear_picture_clip_region(xcb_connection_t *c, xcb_render_picture_t pict) { +void x_clear_picture_clip_region(struct x_connection *c, xcb_render_picture_t pict) { + assert(pict != XCB_NONE); xcb_render_change_picture_value_list_t v = {.clipmask = XCB_NONE}; xcb_generic_error_t *e = xcb_request_check( - c, xcb_render_change_picture(c, pict, XCB_RENDER_CP_CLIP_MASK, &v)); + c->c, xcb_render_change_picture_checked(c->c, pict, XCB_RENDER_CP_CLIP_MASK, &v)); if (e) { log_error_x_error(e, "failed to clear clip region"); free(e); } } +/** + * Destroy a Picture. + * + * Picture must be valid. + */ +void x_free_picture(struct x_connection *c, xcb_render_picture_t p) { + assert(p != XCB_NONE); + auto cookie = xcb_render_free_picture(c->c, p); + set_debug_cant_fail_cookie(c, cookie); +} + enum { XSyncBadCounter = 0, XSyncBadAlarm = 1, @@ -443,9 +583,7 @@ _x_strerror(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_c const char *name = "Unknown"; #define CASESTRRET(s) \ - case s: \ - name = #s; \ - break + case s: name = #s; break #define CASESTRRET2(s) \ case XCB_##s: name = #s; break @@ -527,8 +665,16 @@ _x_strerror(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_c /** * Log a X11 error */ +void x_log_error(enum log_level level, unsigned long serial, uint8_t major, + uint16_t minor, uint8_t error_code) { + if (unlikely(level >= log_get_level_tls())) { + log_printf(tls_logger, level, __func__, "%s", + _x_strerror(serial, major, minor, error_code)); + } +} + void x_print_error(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code) { - log_debug("%s", _x_strerror(serial, major, minor, error_code)); + x_log_error(LOG_LEVEL_DEBUG, serial, major, minor, error_code); } /* @@ -547,12 +693,12 @@ const char *x_strerror(xcb_generic_error_t *e) { /** * Create a pixmap and check that creation succeeded. */ -xcb_pixmap_t x_create_pixmap(xcb_connection_t *c, uint8_t depth, xcb_drawable_t drawable, - int width, int height) { +xcb_pixmap_t x_create_pixmap(struct x_connection *c, uint8_t depth, int width, int height) { xcb_pixmap_t pix = x_new_id(c); - xcb_void_cookie_t cookie = xcb_create_pixmap_checked( - c, depth, pix, drawable, to_u16_checked(width), to_u16_checked(height)); - xcb_generic_error_t *err = xcb_request_check(c, cookie); + xcb_void_cookie_t cookie = + xcb_create_pixmap_checked(c->c, depth, pix, c->screen_info->root, + to_u16_checked(width), to_u16_checked(height)); + xcb_generic_error_t *err = xcb_request_check(c->c, cookie); if (err == NULL) { return pix; } @@ -562,42 +708,26 @@ xcb_pixmap_t x_create_pixmap(xcb_connection_t *c, uint8_t depth, xcb_drawable_t return XCB_NONE; } -/** - * Validate a pixmap. - * - * Detect whether the pixmap is valid with XGetGeometry. Well, maybe there - * are better ways. - */ -bool x_validate_pixmap(xcb_connection_t *c, xcb_pixmap_t pixmap) { - if (pixmap == XCB_NONE) { - return false; - } - - auto r = xcb_get_geometry_reply(c, xcb_get_geometry(c, pixmap), NULL); - if (!r) { - return false; - } - - bool ret = r->width && r->height; - free(r); - return ret; -} -/// Names of root window properties that could point to a pixmap of -/// background. -static const char *background_props_str[] = { - "_XROOTPMAP_ID", - "_XSETROOT_ID", - 0, -}; - -xcb_pixmap_t -x_get_root_back_pixmap(xcb_connection_t *c, xcb_window_t root, struct atom *atoms) { +/// We don't use the _XSETROOT_ID root window property as a source of the background +/// pixmap because it most likely points to a dummy pixmap used to keep the colormap +/// associated with the background pixmap alive but we listen for it's changes and update +/// the background pixmap accordingly. +/// +/// For details on the _XSETROOT_ID root window property and it's usage see: +/// https://metacpan.org/pod/X11::Protocol::XSetRoot#_XSETROOT_ID +/// https://gitlab.freedesktop.org/xorg/app/xsetroot/-/blob/435d35409768de7cbc2c47a6322192dd4b480545/xsetroot.c#L318-352 +/// https://github.com/ImageMagick/ImageMagick/blob/d04a47227637dbb3af9231b0107ccf9677bf985e/MagickCore/xwindow.c#L9203-L9260 +/// https://github.com/ImageMagick/ImageMagick/blob/d04a47227637dbb3af9231b0107ccf9677bf985e/MagickCore/xwindow.c#L1853-L1922 +/// https://www.fvwm.org/Archive/Manpages/fvwm-root.html + +xcb_pixmap_t x_get_root_back_pixmap(struct x_connection *c, struct atom *atoms) { xcb_pixmap_t pixmap = XCB_NONE; - // Get the values of background attributes - for (int p = 0; background_props_str[p]; p++) { - xcb_atom_t prop_atom = get_atom(atoms, background_props_str[p]); - winprop_t prop = x_get_prop(c, root, prop_atom, 1, XCB_ATOM_PIXMAP, 32); + xcb_atom_t root_back_pixmap_atoms[] = {atoms->a_XROOTPMAP_ID, atoms->aESETROOT_PMAP_ID}; + for (size_t i = 0; i < ARR_SIZE(root_back_pixmap_atoms); i++) { + winprop_t prop = + x_get_prop(c, c->screen_info->root, root_back_pixmap_atoms[i], 1, + XCB_ATOM_PIXMAP, 32); if (prop.nitems) { pixmap = (xcb_pixmap_t)*prop.p32; free_winprop(&prop); @@ -610,37 +740,32 @@ x_get_root_back_pixmap(xcb_connection_t *c, xcb_window_t root, struct atom *atom } bool x_is_root_back_pixmap_atom(struct atom *atoms, xcb_atom_t atom) { - for (int p = 0; background_props_str[p]; p++) { - xcb_atom_t prop_atom = get_atom(atoms, background_props_str[p]); - if (prop_atom == atom) { - return true; - } - } - return false; + return atom == atoms->a_XROOTPMAP_ID || atom == atoms->aESETROOT_PMAP_ID || + atom == atoms->a_XSETROOT_ID; } /** * Synchronizes a X Render drawable to ensure all pending painting requests * are completed. */ -bool x_fence_sync(xcb_connection_t *c, xcb_sync_fence_t f) { +bool x_fence_sync(struct x_connection *c, xcb_sync_fence_t f) { // TODO(richardgv): If everybody just follows the rules stated in X Sync // prototype, we need only one fence per screen, but let's stay a bit // cautious right now - auto e = xcb_request_check(c, xcb_sync_trigger_fence_checked(c, f)); + auto e = xcb_request_check(c->c, xcb_sync_trigger_fence_checked(c->c, f)); if (e) { log_error_x_error(e, "Failed to trigger the fence"); goto err; } - e = xcb_request_check(c, xcb_sync_await_fence_checked(c, 1, &f)); + e = xcb_request_check(c->c, xcb_sync_await_fence_checked(c->c, 1, &f)); if (e) { log_error_x_error(e, "Failed to await on a fence"); goto err; } - e = xcb_request_check(c, xcb_sync_reset_fence_checked(c, f)); + e = xcb_request_check(c->c, xcb_sync_reset_fence_checked(c->c, f)); if (e) { log_error_x_error(e, "Failed to reset the fence"); goto err; @@ -652,9 +777,30 @@ bool x_fence_sync(xcb_connection_t *c, xcb_sync_fence_t f) { return false; } -// xcb-render specific macros -#define XFIXED_TO_DOUBLE(value) (((double)(value)) / 65536) -#define DOUBLE_TO_XFIXED(value) ((xcb_render_fixed_t)(((double)(value)) * 65536)) +void x_request_vblank_event(struct x_connection *c, xcb_window_t window, uint64_t msc) { + auto cookie = xcb_present_notify_msc(c->c, window, 0, msc, 1, 0); + set_cant_fail_cookie(c, cookie); +} + +static inline bool dpms_screen_is_off(xcb_dpms_info_reply_t *info) { + // state is a bool indicating whether dpms is enabled + return info->state && (info->power_level != XCB_DPMS_DPMS_MODE_ON); +} + +bool x_check_dpms_status(struct x_connection *c, bool *screen_is_off) { + auto r = xcb_dpms_info_reply(c->c, xcb_dpms_info(c->c), NULL); + if (!r) { + log_error("Failed to query DPMS status."); + return false; + } + auto now_screen_is_off = dpms_screen_is_off(r); + if (*screen_is_off != now_screen_is_off) { + log_debug("Screen is now %s", now_screen_is_off ? "off" : "on"); + *screen_is_off = now_screen_is_off; + } + free(r); + return true; +} /** * Convert a struct conv to a X picture convolution filter, normalizing the kernel @@ -706,10 +852,10 @@ void x_create_convolution_kernel(const conv *kernel, double center, /// Generate a search criteria for fbconfig from a X visual. /// Returns {-1, -1, -1, -1, -1, 0} on failure -struct xvisual_info x_get_visual_info(xcb_connection_t *c, xcb_visualid_t visual) { +struct xvisual_info x_get_visual_info(struct x_connection *c, xcb_visualid_t visual) { auto pictfmt = x_get_pictform_for_visual(c, visual); - auto depth = x_get_visual_depth(c, visual); - if (!pictfmt || depth == -1) { + auto depth = xcb_aux_get_depth_of_visual(c->screen_info, visual); + if (!pictfmt || depth == 0) { log_error("Invalid visual %#03x", visual); return (struct xvisual_info){-1, -1, -1, -1, -1, 0}; } @@ -734,15 +880,34 @@ struct xvisual_info x_get_visual_info(xcb_connection_t *c, xcb_visualid_t visual }; } -xcb_screen_t *x_screen_of_display(xcb_connection_t *c, int screen) { - xcb_screen_iterator_t iter; +void x_update_monitors(struct x_connection *c, struct x_monitors *m) { + x_free_monitor_info(m); - iter = xcb_setup_roots_iterator(xcb_get_setup(c)); - for (; iter.rem; --screen, xcb_screen_next(&iter)) { - if (screen == 0) { - return iter.data; - } + xcb_randr_get_monitors_reply_t *r = xcb_randr_get_monitors_reply( + c->c, xcb_randr_get_monitors(c->c, c->screen_info->root, true), NULL); + if (!r) { + return; } - return NULL; + m->count = xcb_randr_get_monitors_monitors_length(r); + m->regions = ccalloc(m->count, region_t); + xcb_randr_monitor_info_iterator_t monitor_info_it = + xcb_randr_get_monitors_monitors_iterator(r); + for (int i = 0; monitor_info_it.rem; xcb_randr_monitor_info_next(&monitor_info_it)) { + xcb_randr_monitor_info_t *mi = monitor_info_it.data; + pixman_region32_init_rect(&m->regions[i++], mi->x, mi->y, mi->width, mi->height); + } + + free(r); +} + +void x_free_monitor_info(struct x_monitors *m) { + if (m->regions) { + for (int i = 0; i < m->count; i++) { + pixman_region32_fini(&m->regions[i]); + } + free(m->regions); + m->regions = NULL; + } + m->count = 0; } diff --git a/src/x.h b/src/x.h index e01aa0ae7d..3a7b0fd9b4 100644 --- a/src/x.h +++ b/src/x.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #pragma once +#include #include #include #include @@ -10,6 +11,7 @@ #include #include +#include "atom.h" #include "compiler.h" #include "kernel.h" #include "log.h" @@ -26,6 +28,7 @@ typedef struct winprop { int16_t *p16; int32_t *p32; uint32_t *c32; // 32bit cardinal + xcb_atom_t *atom; }; unsigned long nitems; xcb_atom_t type; @@ -55,6 +58,42 @@ struct xvisual_info { xcb_visualid_t visual; }; +enum pending_reply_action { + PENDING_REPLY_ACTION_IGNORE, + PENDING_REPLY_ACTION_ABORT, + PENDING_REPLY_ACTION_DEBUG_ABORT, +}; + +typedef struct pending_reply { + struct pending_reply *next; + unsigned long sequence; + enum pending_reply_action action; +} pending_reply_t; + +struct x_connection { + /// XCB connection. + xcb_connection_t *c; + /// Display in use. + Display *dpy; + /// Head pointer of the error ignore linked list. + pending_reply_t *pending_reply_head; + /// Pointer to the next member of tail element of the error + /// ignore linked list. + pending_reply_t **pending_reply_tail; + /// Previous handler of X errors + XErrorHandler previous_xerror_handler; + /// Default screen + int screen; + /// Information about the default screen + xcb_screen_t *screen_info; +}; + +/// Monitor info +struct x_monitors { + int count; + region_t *regions; +}; + #define XCB_AWAIT_VOID(func, c, ...) \ ({ \ bool __success = true; \ @@ -87,9 +126,13 @@ struct xvisual_info { #define log_fatal_x_error(e, fmt, ...) \ LOG(FATAL, fmt " (%s)", ##__VA_ARGS__, x_strerror(e)) +// xcb-render specific macros +#define XFIXED_TO_DOUBLE(value) (((double)(value)) / 65536) +#define DOUBLE_TO_XFIXED(value) ((xcb_render_fixed_t)(((double)(value)) * 65536)) + /// Wraps x_new_id. abort the program if x_new_id returns error -static inline uint32_t x_new_id(xcb_connection_t *c) { - auto ret = xcb_generate_id(c); +static inline uint32_t x_new_id(struct x_connection *c) { + auto ret = xcb_generate_id(c->c); if (ret == (uint32_t)-1) { log_fatal("We seems to have run of XIDs. This is either a bug in the X " "server, or a resource leakage in the compositor. Please open " @@ -99,17 +142,73 @@ static inline uint32_t x_new_id(xcb_connection_t *c) { return ret; } +static void set_reply_action(struct x_connection *c, uint32_t sequence, + enum pending_reply_action action) { + auto i = cmalloc(pending_reply_t); + + i->sequence = sequence; + i->next = 0; + i->action = action; + *c->pending_reply_tail = i; + c->pending_reply_tail = &i->next; +} + /** - * Send a request to X server and get the reply to make sure all previous - * requests are processed, and their replies received - * - * xcb_get_input_focus is used here because it is the same request used by - * libX11 + * Ignore X errors caused by given X request. */ -static inline void x_sync(xcb_connection_t *c) { - free(xcb_get_input_focus_reply(c, xcb_get_input_focus(c), NULL)); +static inline void attr_unused set_ignore_cookie(struct x_connection *c, + xcb_void_cookie_t cookie) { + set_reply_action(c, cookie.sequence, PENDING_REPLY_ACTION_IGNORE); +} + +static inline void attr_unused set_cant_fail_cookie(struct x_connection *c, + xcb_void_cookie_t cookie) { + set_reply_action(c, cookie.sequence, PENDING_REPLY_ACTION_ABORT); +} + +static inline void attr_unused set_debug_cant_fail_cookie(struct x_connection *c, + xcb_void_cookie_t cookie) { +#ifndef NDEBUG + set_reply_action(c, cookie.sequence, PENDING_REPLY_ACTION_DEBUG_ABORT); +#else + (void)c; + (void)cookie; +#endif +} + +static inline void attr_unused free_x_connection(struct x_connection *c) { + pending_reply_t *next = NULL; + for (auto ign = c->pending_reply_head; ign; ign = next) { + next = ign->next; + + free(ign); + } + + // Reset head and tail + c->pending_reply_head = NULL; + c->pending_reply_tail = &c->pending_reply_head; + + XSetErrorHandler(c->previous_xerror_handler); } +/// Initialize x_connection struct from an Xlib Display. +/// +/// Note this function doesn't take ownership of the Display, the caller is still +/// responsible for closing it after `free_x_connection` is called. +void x_connection_init(struct x_connection *c, Display *dpy); + +/// Discard queued pending replies. +/// +/// We have received reply with sequence number `sequence`, which means all pending +/// replies with sequence number less than `sequence` will never be received. So discard +/// them. +void x_discard_pending(struct x_connection *c, uint32_t sequence); + +/// Handle X errors. +/// +/// This function logs X errors, or aborts the program based on severity of the error. +void x_handle_error(struct x_connection *c, xcb_generic_error_t *ev); + /** * Get a specific attribute of a window. * @@ -125,25 +224,26 @@ static inline void x_sync(xcb_connection_t *c) { * @return a winprop_t structure containing the attribute * and number of items. A blank one on failure. */ -winprop_t x_get_prop_with_offset(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom, +winprop_t x_get_prop_with_offset(const struct x_connection *c, xcb_window_t w, xcb_atom_t atom, int offset, int length, xcb_atom_t rtype, int rformat); /** * Wrapper of wid_get_prop_adv(). */ -static inline winprop_t x_get_prop(xcb_connection_t *c, xcb_window_t wid, xcb_atom_t atom, - int length, xcb_atom_t rtype, int rformat) { +static inline winprop_t +x_get_prop(const struct x_connection *c, xcb_window_t wid, xcb_atom_t atom, int length, + xcb_atom_t rtype, int rformat) { return x_get_prop_with_offset(c, wid, atom, 0L, length, rtype, rformat); } /// Get the type, format and size in bytes of a window's specific attribute. -winprop_info_t x_get_prop_info(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom); +winprop_info_t x_get_prop_info(const struct x_connection *c, xcb_window_t w, xcb_atom_t atom); /// Discard all X events in queue or in flight. Should only be used when the server is /// grabbed -static inline void x_discard_events(xcb_connection_t *c) { +static inline void x_discard_events(struct x_connection *c) { xcb_generic_event_t *e; - while ((e = xcb_poll_for_event(c))) { + while ((e = xcb_poll_for_event(c->c))) { free(e); } } @@ -153,7 +253,7 @@ static inline void x_discard_events(xcb_connection_t *c) { * * @return the value if successful, 0 otherwise */ -xcb_window_t wid_get_prop_window(xcb_connection_t *c, xcb_window_t wid, xcb_atom_t aprop); +xcb_window_t wid_get_prop_window(struct x_connection *c, xcb_window_t wid, xcb_atom_t aprop); /** * Get the value of a text property of a window. @@ -162,34 +262,37 @@ xcb_window_t wid_get_prop_window(xcb_connection_t *c, xcb_window_t wid, xcb_atom * array * @param[out] pnstr Number of strings in the array */ -bool wid_get_text_prop(session_t *ps, xcb_window_t wid, xcb_atom_t prop, char ***pstrlst, - int *pnstr); +bool wid_get_text_prop(struct x_connection *c, struct atom *atoms, xcb_window_t wid, + xcb_atom_t prop, char ***pstrlst, int *pnstr); + +static inline bool x_is_type_string(struct atom *atoms, xcb_atom_t type) { + return type == XCB_ATOM_STRING || type == atoms->aUTF8_STRING || + type == atoms->aC_STRING; +} const xcb_render_pictforminfo_t * -x_get_pictform_for_visual(xcb_connection_t *, xcb_visualid_t); -int x_get_visual_depth(xcb_connection_t *, xcb_visualid_t); +x_get_pictform_for_visual(struct x_connection *, xcb_visualid_t); xcb_render_picture_t -x_create_picture_with_pictfmt_and_pixmap(xcb_connection_t *, - const xcb_render_pictforminfo_t *pictfmt, +x_create_picture_with_pictfmt_and_pixmap(struct x_connection *, xcb_render_pictformat_t pictfmt, xcb_pixmap_t pixmap, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) - attr_nonnull(1, 2); + attr_nonnull(1); xcb_render_picture_t -x_create_picture_with_visual_and_pixmap(xcb_connection_t *, xcb_visualid_t visual, +x_create_picture_with_visual_and_pixmap(struct x_connection *, xcb_visualid_t visual, xcb_pixmap_t pixmap, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) attr_nonnull(1); xcb_render_picture_t -x_create_picture_with_standard_and_pixmap(xcb_connection_t *, xcb_pict_standard_t standard, +x_create_picture_with_standard_and_pixmap(struct x_connection *, xcb_pict_standard_t standard, xcb_pixmap_t pixmap, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) attr_nonnull(1); xcb_render_picture_t -x_create_picture_with_standard(xcb_connection_t *c, xcb_drawable_t d, int w, int h, +x_create_picture_with_standard(struct x_connection *c, int w, int h, xcb_pict_standard_t standard, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) attr_nonnull(1); @@ -198,29 +301,47 @@ x_create_picture_with_standard(xcb_connection_t *c, xcb_drawable_t d, int w, int * Create an picture. */ xcb_render_picture_t -x_create_picture_with_pictfmt(xcb_connection_t *, xcb_drawable_t, int w, int h, - const xcb_render_pictforminfo_t *pictfmt, uint32_t valuemask, +x_create_picture_with_pictfmt(struct x_connection *, int w, int h, + xcb_render_pictformat_t pictfmt, uint8_t depth, uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) - attr_nonnull(1, 5); + attr_nonnull(1); xcb_render_picture_t -x_create_picture_with_visual(xcb_connection_t *, xcb_drawable_t, int w, int h, - xcb_visualid_t visual, uint32_t valuemask, +x_create_picture_with_visual(struct x_connection *, int w, int h, xcb_visualid_t visual, + uint32_t valuemask, const xcb_render_create_picture_value_list_t *attr) attr_nonnull(1); /// Fetch a X region and store it in a pixman region -bool x_fetch_region(xcb_connection_t *, xcb_xfixes_region_t r, region_t *res); +bool x_fetch_region(struct x_connection *, xcb_xfixes_region_t r, region_t *res); + +/// Set an X region to a pixman region +bool x_set_region(struct x_connection *c, xcb_xfixes_region_t dst, const region_t *src); + +/// Create a X region from a pixman region +uint32_t x_create_region(struct x_connection *c, const region_t *reg); -void x_set_picture_clip_region(xcb_connection_t *, xcb_render_picture_t, int16_t clip_x_origin, - int16_t clip_y_origin, const region_t *); +/// Destroy a X region +void x_destroy_region(struct x_connection *c, uint32_t region); -void x_clear_picture_clip_region(xcb_connection_t *, xcb_render_picture_t pict); +void x_set_picture_clip_region(struct x_connection *, xcb_render_picture_t, + int16_t clip_x_origin, int16_t clip_y_origin, const region_t *); + +void x_clear_picture_clip_region(struct x_connection *, xcb_render_picture_t pict); + +/** + * Destroy a Picture. + * + * Picture must be valid. + */ +void x_free_picture(struct x_connection *c, xcb_render_picture_t p); /** * Log a X11 error */ void x_print_error(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code); +void x_log_error(enum log_level level, unsigned long serial, uint8_t major, + uint16_t minor, uint8_t error_code); /* * Convert a xcb_generic_error_t to a string that describes the error @@ -230,10 +351,7 @@ void x_print_error(unsigned long serial, uint8_t major, uint16_t minor, uint8_t */ const char *x_strerror(xcb_generic_error_t *e); -xcb_pixmap_t x_create_pixmap(xcb_connection_t *, uint8_t depth, xcb_drawable_t drawable, - int width, int height); - -bool x_validate_pixmap(xcb_connection_t *, xcb_pixmap_t pxmap); +xcb_pixmap_t x_create_pixmap(struct x_connection *, uint8_t depth, int width, int height); /** * Free a winprop_t. @@ -242,22 +360,22 @@ bool x_validate_pixmap(xcb_connection_t *, xcb_pixmap_t pxmap); */ static inline void free_winprop(winprop_t *pprop) { // Empty the whole structure to avoid possible issues - if (pprop->r) + if (pprop->r) { free(pprop->r); + } pprop->ptr = NULL; pprop->r = NULL; pprop->nitems = 0; } /// Get the back pixmap of the root window -xcb_pixmap_t -x_get_root_back_pixmap(xcb_connection_t *c, xcb_window_t root, struct atom *atoms); +xcb_pixmap_t x_get_root_back_pixmap(struct x_connection *c, struct atom *atoms); /// Return true if the atom refers to a property name that is used for the /// root window background pixmap bool x_is_root_back_pixmap_atom(struct atom *atoms, xcb_atom_t atom); -bool x_fence_sync(xcb_connection_t *, xcb_sync_fence_t); +bool x_fence_sync(struct x_connection *, xcb_sync_fence_t); struct x_convolution_kernel { int size; @@ -281,13 +399,26 @@ void attr_nonnull(1, 3) x_create_convolution_kernel(const conv *kernel, double c /// Generate a search criteria for fbconfig from a X visual. /// Returns {-1, -1, -1, -1, -1, -1} on failure -struct xvisual_info x_get_visual_info(xcb_connection_t *c, xcb_visualid_t visual); +struct xvisual_info x_get_visual_info(struct x_connection *c, xcb_visualid_t visual); + +xcb_visualid_t x_get_visual_for_standard(struct x_connection *c, xcb_pict_standard_t std); -xcb_visualid_t x_get_visual_for_standard(xcb_connection_t *c, xcb_pict_standard_t std); +xcb_visualid_t x_get_visual_for_depth(xcb_screen_t *screen, uint8_t depth); xcb_render_pictformat_t -x_get_pictfmt_for_standard(xcb_connection_t *c, xcb_pict_standard_t std); +x_get_pictfmt_for_standard(struct x_connection *c, xcb_pict_standard_t std); -xcb_screen_t *x_screen_of_display(xcb_connection_t *c, int screen); +/// Populates a `struct x_monitors` with the current monitor configuration. +void x_update_monitors(struct x_connection *, struct x_monitors *); +/// Free memory allocated for a `struct x_monitors`. +void x_free_monitor_info(struct x_monitors *); uint32_t attr_deprecated xcb_generate_id(xcb_connection_t *c); + +/// Ask X server to send us a notification for the next end of vblank. +void x_request_vblank_event(struct x_connection *c, xcb_window_t window, uint64_t msc); + +/// Update screen_is_off to reflect the current DPMS state. +/// +/// Returns true if the DPMS state was successfully queried, false otherwise. +bool x_check_dpms_status(struct x_connection *c, bool *screen_is_off); diff --git a/subprojects/test.h/test.h b/subprojects/test.h/test.h index fd8dc22293..3aa722022e 100644 --- a/subprojects/test.h/test.h +++ b/subprojects/test.h/test.h @@ -25,6 +25,7 @@ struct test_failure { const char *message; const char *file; int line; + bool owned; }; struct test_case_metadata { @@ -43,15 +44,24 @@ struct test_file_metadata { struct test_file_metadata __attribute__((weak)) * test_file_head; -#define SET_FAILURE(_message) \ - metadata->failure = (struct test_failure) { \ - .message = _message, .file = __FILE__, .line = __LINE__, .present = true, \ +#define SET_FAILURE(_message, _owned) \ + metadata->failure = (struct test_failure) { \ + .message = _message, .file = __FILE__, .line = __LINE__, \ + .present = true, .owned = _owned, \ } #define TEST_EQUAL(a, b) \ do { \ if ((a) != (b)) { \ - SET_FAILURE(#a " != " #b); \ + SET_FAILURE(#a " != " #b, false); \ + return; \ + } \ + } while (0) + +#define TEST_NOTEQUAL(a, b) \ + do { \ + if ((a) == (b)) { \ + SET_FAILURE(#a " == " #b, false); \ return; \ } \ } while (0) @@ -59,7 +69,7 @@ struct test_file_metadata __attribute__((weak)) * test_file_head; #define TEST_TRUE(a) \ do { \ if (!(a)) { \ - SET_FAILURE(#a " is not true"); \ + SET_FAILURE(#a " is not true", false); \ return; \ } \ } while (0) @@ -67,7 +77,41 @@ struct test_file_metadata __attribute__((weak)) * test_file_head; #define TEST_STREQUAL(a, b) \ do { \ if (strcmp(a, b) != 0) { \ - SET_FAILURE(#a " != " #b); \ + const char *test_strequal__part2 = " != " #b; \ + size_t test_strequal__len = \ + strlen(a) + strlen(test_strequal__part2) + 3; \ + char *test_strequal__buf = malloc(test_strequal__len); \ + snprintf(test_strequal__buf, test_strequal__len, "\"%s\"%s", a, \ + test_strequal__part2); \ + SET_FAILURE(test_strequal__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_STRNEQUAL(a, b, len) \ + do { \ + if (strncmp(a, b, len) != 0) { \ + const char *test_strnequal__part2 = " != " #b; \ + size_t test_strnequal__len2 = \ + len + strlen(test_strnequal__part2) + 3; \ + char *test_strnequal__buf = malloc(test_strnequal__len2); \ + snprintf(test_strnequal__buf, test_strnequal__len2, \ + "\"%.*s\"%s", (int)len, a, test_strnequal__part2); \ + SET_FAILURE(test_strnequal__buf, true); \ + return; \ + } \ + } while (0) + +#define TEST_STREQUAL3(str, expected, len) \ + do { \ + if (len != strlen(expected) || strncmp(str, expected, len) != 0) { \ + const char *test_strequal3__part2 = " != " #expected; \ + size_t test_strequal3__len2 = \ + len + strlen(test_strequal3__part2) + 3; \ + char *test_strequal3__buf = malloc(test_strequal3__len2); \ + snprintf(test_strequal3__buf, test_strequal3__len2, \ + "\"%.*s\"%s", (int)len, str, test_strequal3__part2); \ + SET_FAILURE(test_strequal3__buf, true); \ return; \ } \ } while (0) @@ -152,6 +196,10 @@ static inline void __attribute__((constructor(102))) run_tests(void) { if (j->failure.present) { fprintf(stderr, "failed (%s at %s:%d)\n", j->failure.message, j->failure.file, j->failure.line); + if (j->failure.owned) { + free((char *)j->failure.message); + j->failure.message = NULL; + } failed++; } else { fprintf(stderr, "passed\n"); @@ -177,9 +225,19 @@ static inline void __attribute__((constructor(102))) run_tests(void) { #define TEST_EQUAL(a, b) \ (void)(a); \ (void)(b) +#define TEST_NOTEQUAL(a, b) \ + (void)(a); \ + (void)(b) #define TEST_TRUE(a) (void)(a) #define TEST_STREQUAL(a, b) \ (void)(a); \ (void)(b) - +#define TEST_STRNEQUAL(a, b, len) \ + (void)(a); \ + (void)(b); \ + (void)(len) +#define TEST_STREQUAL3(str, expected, len) \ + (void)(str); \ + (void)(expected); \ + (void)(len) #endif diff --git a/tests/configs/parsing_test.conf b/tests/configs/parsing_test.conf new file mode 100644 index 0000000000..a30886800a --- /dev/null +++ b/tests/configs/parsing_test.conf @@ -0,0 +1,425 @@ +################################# +# Shadows # +################################# + + +# Enabled client-side shadows on windows. Note desktop windows +# (windows with '_NET_WM_WINDOW_TYPE_DESKTOP') never get shadow, +# unless explicitly requested using the wintypes option. +# +# shadow = false +shadow = true; + +# The blur radius for shadows, in pixels. (defaults to 12) +# shadow-radius = 12 +shadow-radius = 7; + +# The opacity of shadows. (0.0 - 1.0, defaults to 0.75) +# shadow-opacity = .75 + +# The left offset for shadows, in pixels. (defaults to -15) +# shadow-offset-x = -15 +shadow-offset-x = -7; + +# The top offset for shadows, in pixels. (defaults to -15) +# shadow-offset-y = -15 +shadow-offset-y = -7; + +# Red color value of shadow (0.0 - 1.0, defaults to 0). +# shadow-red = 0 + +# Green color value of shadow (0.0 - 1.0, defaults to 0). +# shadow-green = 0 + +# Blue color value of shadow (0.0 - 1.0, defaults to 0). +# shadow-blue = 0 + +# Hex string color value of shadow (#000000 - #FFFFFF, defaults to #000000). This option will override options set shadow-(red/green/blue) +# shadow-color = "#000000" + +# Specify a list of conditions of windows that should have no shadow. +# +# examples: +# shadow-exclude = "n:e:Notification"; +# +# shadow-exclude = [] +shadow-exclude = [ + "name = 'Notification'", + "class_g = 'Conky'", + "class_g ?= 'Notify-osd'", + "class_g *= 'Cairo-clock'", + "class_g %= 'a'", + "class_g ~= 'a'", + "_GTK_FRAME_EXTENTS@:c", + "class_g = '\x64\\'\"\a\b\f\n\r\t\v\o11'" +]; + +# Specify a list of conditions of windows that should have no shadow painted over, such as a dock window. +# clip-shadow-above = [] + +# Crop shadow of a window fully on a particular monitor to that monitor. This is +# currently implemented using the X RandR extension. +# crop-shadow-to-monitor = false + + +################################# +# Fading # +################################# + + +# Fade windows in/out when opening/closing and when opacity changes, +# unless no-fading-openclose is used. +# fading = false +fading = true; + +# Opacity change between steps while fading in. (0.01 - 1.0, defaults to 0.028) +# fade-in-step = 0.028 +fade-in-step = 0.03; + +# Opacity change between steps while fading out. (0.01 - 1.0, defaults to 0.03) +# fade-out-step = 0.03 +fade-out-step = 0.03; + +# The time between steps in fade step, in milliseconds. (> 0, defaults to 10) +# fade-delta = 10 + +# Specify a list of conditions of windows that should not be faded. +# fade-exclude = [] + +# Do not fade on window open/close. +# no-fading-openclose = false + +# Do not fade destroyed ARGB windows with WM frame. Workaround of bugs in Openbox, Fluxbox, etc. +# no-fading-destroyed-argb = false + + +################################# +# Transparency / Opacity # +################################# + + +# Opacity of inactive windows. (0.1 - 1.0, defaults to 1.0) +# inactive-opacity = 1 +inactive-opacity = 0.8; + +# Opacity of window titlebars and borders. (0.1 - 1.0, disabled by default) +# frame-opacity = 1.0 +frame-opacity = 0.7; + +# Let inactive opacity set by -i override the '_NET_WM_WINDOW_OPACITY' values of windows. +# inactive-opacity-override = true +inactive-opacity-override = false; + +# Default opacity for active windows. (0.0 - 1.0, defaults to 1.0) +# active-opacity = 1.0 + +# Dim inactive windows. (0.0 - 1.0, defaults to 0.0) +# inactive-dim = 0.0 + +# Specify a list of conditions of windows that should never be considered focused. +# focus-exclude = [] +focus-exclude = [ "class_g = 'Cairo-clock'" ]; + +# Use fixed inactive dim value, instead of adjusting according to window opacity. +# inactive-dim-fixed = 1.0 + +# Specify a list of opacity rules, in the format `PERCENT:PATTERN`, +# like `50:name *= "Firefox"`. picom-trans is recommended over this. +# Note we don't make any guarantee about possible conflicts with other +# programs that set '_NET_WM_WINDOW_OPACITY' on frame or client windows. +# example: +# opacity-rule = [ "80:class_g = 'URxvt'" ]; +# +# opacity-rule = [] + + +################################# +# Corners # +################################# + +# Sets the radius of rounded window corners. When > 0, the compositor will +# round the corners of windows. Does not interact well with +# `transparent-clipping`. +corner-radius = 0 + +# Exclude conditions for rounded corners. +rounded-corners-exclude = [ + "window_type = 'dock'", + "window_type = 'desktop'" +]; + +corner-radius-rules = [ + "10:window_type = 'dock'", + "0x32:window_type = 'desktop'" +]; + + +################################# +# Background-Blurring # +################################# + + +# Parameters for background blurring, see the *BLUR* section for more information. +# blur-method = +# blur-size = 12 +# +# blur-deviation = false +# +# blur-strength = 5 + +# Blur background of semi-transparent / ARGB windows. +# Bad in performance, with driver-dependent behavior. +# The name of the switch may change without prior notifications. +# +# blur-background = false + +# Blur background of windows when the window frame is not opaque. +# Implies: +# blur-background +# Bad in performance, with driver-dependent behavior. The name may change. +# +# blur-background-frame = false + + +# Use fixed blur strength rather than adjusting according to window opacity. +# blur-background-fixed = false + + +# Specify the blur convolution kernel, with the following format: +# example: +# blur-kern = "5,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1"; +# +# blur-kern = "" +blur-kern = "3x3box"; + + +# Exclude conditions for background blur. +# blur-background-exclude = [] +blur-background-exclude = [ + "window_type = 'dock'", + "window_type = 'desktop'", + "_GTK_FRAME_EXTENTS@:c" +]; + +################################# +# General Settings # +################################# + +# Enable remote control via D-Bus. See the man page for more details. +# dbus = true + +# Daemonize process. Fork to background after initialization. Causes issues with certain (badly-written) drivers. +# daemon = false + +# Specify the backend to use: `xrender`, `glx`, or `xr_glx_hybrid`. +# `xrender` is the default one. +# +# backend = "glx" +backend = "xrender"; + +# Enable/disable VSync. +# vsync = false +vsync = true; + +# Enable remote control via D-Bus. See the *D-BUS API* section below for more details. +# dbus = false + +# Try to detect WM windows (a non-override-redirect window with no +# child that has 'WM_STATE') and mark them as active. +# +# mark-wmwin-focused = false +mark-wmwin-focused = true; + +# Mark override-redirect windows that doesn't have a child window with 'WM_STATE' focused. +# mark-ovredir-focused = false +mark-ovredir-focused = true; + +# Try to detect windows with rounded corners and don't consider them +# shaped windows. The accuracy is not very high, unfortunately. +# +# detect-rounded-corners = false +detect-rounded-corners = true; + +# Detect '_NET_WM_WINDOW_OPACITY' on client windows, useful for window managers +# not passing '_NET_WM_WINDOW_OPACITY' of client windows to frame windows. +# +# detect-client-opacity = false +detect-client-opacity = true; + +# Use EWMH '_NET_ACTIVE_WINDOW' to determine currently focused window, +# rather than listening to 'FocusIn'/'FocusOut' event. Might have more accuracy, +# provided that the WM supports it. +# +# use-ewmh-active-win = false + +# Unredirect all windows if a full-screen opaque window is detected, +# to maximize performance for full-screen windows. Known to cause flickering +# when redirecting/unredirecting windows. +# +# unredir-if-possible = false + +# Delay before unredirecting the window, in milliseconds. Defaults to 0. +# unredir-if-possible-delay = 0 + +# Conditions of windows that shouldn't be considered full-screen for unredirecting screen. +# unredir-if-possible-exclude = [] + +# Use 'WM_TRANSIENT_FOR' to group windows, and consider windows +# in the same group focused at the same time. +# +# detect-transient = false +detect-transient = true; + +# Use 'WM_CLIENT_LEADER' to group windows, and consider windows in the same +# group focused at the same time. This usually means windows from the same application +# will be considered focused or unfocused at the same time. +# 'WM_TRANSIENT_FOR' has higher priority if detect-transient is enabled, too. +# +# detect-client-leader = false + +# Resize damaged region by a specific number of pixels. +# A positive value enlarges it while a negative one shrinks it. +# If the value is positive, those additional pixels will not be actually painted +# to screen, only used in blur calculation, and such. (Due to technical limitations, +# with use-damage, those pixels will still be incorrectly painted to screen.) +# Primarily used to fix the line corruption issues of blur, +# in which case you should use the blur radius value here +# (e.g. with a 3x3 kernel, you should use `--resize-damage 1`, +# with a 5x5 one you use `--resize-damage 2`, and so on). +# May or may not work with *--glx-no-stencil*. Shrinking doesn't function correctly. +# +# resize-damage = 1 + +# Specify a list of conditions of windows that should be painted with inverted color. +# Resource-hogging, and is not well tested. +# +# invert-color-include = [] + +# GLX backend: Avoid using stencil buffer, useful if you don't have a stencil buffer. +# Might cause incorrect opacity when rendering transparent content (but never +# practically happened) and may not work with blur-background. +# My tests show a 15% performance boost. Recommended. +# +# glx-no-stencil = false + +# GLX backend: Avoid rebinding pixmap on window damage. +# Probably could improve performance on rapid window content changes, +# but is known to break things on some drivers (LLVMpipe, xf86-video-intel, etc.). +# Recommended if it works. +# +# glx-no-rebind-pixmap = false + +# Disable the use of damage information. +# This cause the whole screen to be redrawn every time, instead of the part of the screen +# has actually changed. Potentially degrades the performance, but might fix some artifacts. +# The opposing option is use-damage +# +# no-use-damage = false +use-damage = true; + +# Use X Sync fence to sync clients' draw calls, to make sure all draw +# calls are finished before picom starts drawing. Needed on nvidia-drivers +# with GLX backend for some users. +# +# xrender-sync-fence = false + +# GLX backend: Use specified GLSL fragment shader for rendering window contents. +# See `compton-default-fshader-win.glsl` and `compton-fake-transparency-fshader-win.glsl` +# in the source tree for examples. +# +# glx-fshader-win = "" + +# Force all windows to be painted with blending. Useful if you +# have a glx-fshader-win that could turn opaque pixels transparent. +# +# force-win-blend = false + +# Do not use EWMH to detect fullscreen windows. +# Reverts to checking if a window is fullscreen based only on its size and coordinates. +# +# no-ewmh-fullscreen = false + +# Dimming bright windows so their brightness doesn't exceed this set value. +# Brightness of a window is estimated by averaging all pixels in the window, +# so this could comes with a performance hit. +# Setting this to 1.0 disables this behaviour. Requires --use-damage to be disabled. (default: 1.0) +# +# max-brightness = 1.0 + +# Make transparent windows clip other windows like non-transparent windows do, +# instead of blending on top of them. +# +# transparent-clipping = false + +# Set the log level. Possible values are: +# "trace", "debug", "info", "warn", "error" +# in increasing level of importance. Case doesn't matter. +# If using the "TRACE" log level, it's better to log into a file +# using *--log-file*, since it can generate a huge stream of logs. +# +# log-level = "debug" +log-level = "warn"; + +# Set the log file. +# If *--log-file* is never specified, logs will be written to stderr. +# Otherwise, logs will to written to the given file, though some of the early +# logs might still be written to the stderr. +# When setting this option from the config file, it is recommended to use an absolute path. +# +# log-file = "/path/to/your/log/file" + +# Show all X errors (for debugging) +# show-all-xerrors = false + +# Write process ID to a file. +# write-pid-path = "/path/to/your/log/file" + +# Window type settings +# +# 'WINDOW_TYPE' is one of the 15 window types defined in EWMH standard: +# "unknown", "desktop", "dock", "toolbar", "menu", "utility", +# "splash", "dialog", "normal", "dropdown_menu", "popup_menu", +# "tooltip", "notification", "combo", and "dnd". +# +# Following per window-type options are available: :: +# +# fade, shadow::: +# Controls window-type-specific shadow and fade settings. +# +# opacity::: +# Controls default opacity of the window type. +# +# focus::: +# Controls whether the window of this type is to be always considered focused. +# (By default, all window types except "normal" and "dialog" has this on.) +# +# full-shadow::: +# Controls whether shadow is drawn under the parts of the window that you +# normally won't be able to see. Useful when the window has parts of it +# transparent, and you want shadows in those areas. +# +# clip-shadow-above::: +# Controls whether shadows that would have been drawn above the window should +# be clipped. Useful for dock windows that should have no shadow painted on top. +# +# redir-ignore::: +# Controls whether this type of windows should cause screen to become +# redirected again after been unredirected. If you have unredir-if-possible +# set, and doesn't want certain window to cause unnecessary screen redirection, +# you can set this to `true`. +# +wintypes: +{ + tooltip = { fade = true; shadow = true; opacity = 0.75; focus = true; full-shadow = false; }; + dock = { shadow = false; clip-shadow-above = true; } + dnd = { shadow = false; } + popup_menu = { opacity = 0.8; } + dropdown_menu = { opacity = 0.8; } +}; + +window-shader-fg-rule = +[ + "shader.frag:name = 'test'", + " shader.frag :name = 'a'", + "default:name = 'b'" +] diff --git a/tests/configs/pull1091.conf b/tests/configs/pull1091.conf new file mode 100644 index 0000000000..37141aba22 --- /dev/null +++ b/tests/configs/pull1091.conf @@ -0,0 +1 @@ +unredir-if-possible = true; diff --git a/tests/configs/shader.frag b/tests/configs/shader.frag new file mode 100644 index 0000000000..a25e29459c --- /dev/null +++ b/tests/configs/shader.frag @@ -0,0 +1 @@ +vec4 window_shader() {} diff --git a/tests/run_one_test.sh b/tests/run_one_test.sh index 4619359589..2b745710c9 100755 --- a/tests/run_one_test.sh +++ b/tests/run_one_test.sh @@ -7,7 +7,7 @@ fi echo "Running test $2" # TODO keep the log file, and parse it to see if test is successful -($1 --dbus --experimental-backends --backend dummy --log-level=debug --log-file=$PWD/log --config=$2) & +($1 --dbus --backend dummy --log-level=debug --log-file=$PWD/log --config=$2) & main_pid=$! $3 diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 6a5a6b3ffc..e74fcdd4cd 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -20,3 +20,4 @@ eval `dbus-launch --sh-syntax` ./run_one_test.sh $exe configs/clear_shadow_unredirected.conf testcases/redirect_when_unmapped_window_has_shadow.py ./run_one_test.sh $exe configs/issue394.conf testcases/issue394.py ./run_one_test.sh $exe configs/issue239.conf testcases/issue525.py +./run_one_test.sh $exe configs/pull1091.conf testcases/pull1091.py diff --git a/tests/testcases/basic.py b/tests/testcases/basic.py index d0ecaf10e9..78a6136d8b 100755 --- a/tests/testcases/basic.py +++ b/tests/testcases/basic.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/clear_shadow_unredirected.py b/tests/testcases/clear_shadow_unredirected.py index 4a9f5313ea..360a522379 100755 --- a/tests/testcases/clear_shadow_unredirected.py +++ b/tests/testcases/clear_shadow_unredirected.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/common.py b/tests/testcases/common.py index 8214aff081..f4d5076a1f 100644 --- a/tests/testcases/common.py +++ b/tests/testcases/common.py @@ -33,6 +33,10 @@ def set_window_size_async(conn, wid, width, height): value_list = [width, height] return conn.core.ConfigureWindowChecked(wid, value_mask, value_list) +def set_window_bypass_compositor(conn, wid, value = 1): + prop_name = to_atom(conn, "_NET_WM_BYPASS_COMPOSITOR") + return conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, prop_name, xproto.Atom.CARDINAL, 32, 1, [value]) + def find_picom_window(conn): prop_name = to_atom(conn, "WM_NAME") setup = conn.get_setup() @@ -45,13 +49,13 @@ def find_picom_window(conn): if name.value.buf() == b"picom": return w -def prepare_root_configure(conn): +def prepare_root_configure(conn, size = 1000): setup = conn.get_setup() root = setup.roots[0].root # Xorg sends root ConfigureNotify when we add a new mode to an output rr = conn(randr.key) name = ''.join([random.choice(string.ascii_letters + string.digits) for n in range(0, 32)]) - mode_info = randr.ModeInfo.synthetic(id = 0, width = 1000, height = 1000, dot_clock = 0, + mode_info = randr.ModeInfo.synthetic(id = 0, width = size, height = size, dot_clock = 0, hsync_start = 0, hsync_end = 0, htotal = 0, hskew = 0, vsync_start = 0, vsync_end = 0, vtotal = 0, name_len = len(name), mode_flags = 0) diff --git a/tests/testcases/issue239.py b/tests/testcases/issue239.py index fe3f636494..3b5b058bdc 100755 --- a/tests/testcases/issue239.py +++ b/tests/testcases/issue239.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue239_2.py b/tests/testcases/issue239_2.py index 54f5412c5b..6ce7cf7899 100755 --- a/tests/testcases/issue239_2.py +++ b/tests/testcases/issue239_2.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue239_3.py b/tests/testcases/issue239_3.py index 3bc966113a..41d58c453b 100755 --- a/tests/testcases/issue239_3.py +++ b/tests/testcases/issue239_3.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue239_3_norefresh.py b/tests/testcases/issue239_3_norefresh.py index edcf1c2300..f87e2dac99 100755 --- a/tests/testcases/issue239_3_norefresh.py +++ b/tests/testcases/issue239_3_norefresh.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue299.py b/tests/testcases/issue299.py index d8be70c7ee..83a88cb96a 100755 --- a/tests/testcases/issue299.py +++ b/tests/testcases/issue299.py @@ -21,7 +21,7 @@ async def get_client_win_async(wid): message = await bus.call(Message(destination='com.github.chjj.compton.'+display, - path='/', + path='/com/github/chjj/compton', interface='com.github.chjj.compton', member='win_get', signature='us', diff --git a/tests/testcases/issue314.py b/tests/testcases/issue314.py index ee29fc49d1..acc9142dda 100755 --- a/tests/testcases/issue314.py +++ b/tests/testcases/issue314.py @@ -1,9 +1,9 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time -from common import set_window_name, trigger_root_configure +from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() diff --git a/tests/testcases/issue314_2.py b/tests/testcases/issue314_2.py index c19d6cd5fe..3fd95796ee 100755 --- a/tests/testcases/issue314_2.py +++ b/tests/testcases/issue314_2.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue314_3.py b/tests/testcases/issue314_3.py index 9d4ee3a277..0fb18809f6 100755 --- a/tests/testcases/issue314_3.py +++ b/tests/testcases/issue314_3.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue357.py b/tests/testcases/issue357.py index 9f95a4dcee..cd74c8ca12 100755 --- a/tests/testcases/issue357.py +++ b/tests/testcases/issue357.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue394.py b/tests/testcases/issue394.py index 3bc4877ab2..e0918ef96e 100755 --- a/tests/testcases/issue394.py +++ b/tests/testcases/issue394.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue465.py b/tests/testcases/issue465.py index 588be91fd5..fc33a6de47 100755 --- a/tests/testcases/issue465.py +++ b/tests/testcases/issue465.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/issue525.py b/tests/testcases/issue525.py index 317e30882d..50dd32c69d 100755 --- a/tests/testcases/issue525.py +++ b/tests/testcases/issue525.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib diff --git a/tests/testcases/pull1091.py b/tests/testcases/pull1091.py new file mode 100755 index 0000000000..880c72508f --- /dev/null +++ b/tests/testcases/pull1091.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import xcffib.xproto as xproto +import xcffib +from common import * + +conn = xcffib.connect() +setup = conn.get_setup() +root = setup.roots[0].root +visual = setup.roots[0].root_visual +depth = setup.roots[0].root_depth + +# assertion failure mentioned in 1091 happens when a root change happens right after we +# redirected the screen, before we have even rendered a single frame +wid = conn.generate_id() +print("Window id is ", hex(wid)) + +# Create a window +conn.core.CreateWindowChecked(depth, wid, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() + +# Map the window +print("mapping") +conn.core.MapWindowChecked(wid).check() + +time.sleep(0.5) + +for i in range(0, 8): + modes = [] + for s in range(0, 10): + reply, mode, output = prepare_root_configure(conn, i * 100 + 100 + s) + modes.append((reply, mode, output)) + + set_window_bypass_compositor(conn, wid).check() + time.sleep(0.1) + + set_window_bypass_compositor(conn, wid, 0) + conn.flush() + for reply, mode, output in modes: + trigger_root_configure(conn, reply, mode, output).reply() + + time.sleep(0.1) diff --git a/tests/testcases/redirect_when_unmapped_window_has_shadow.py b/tests/testcases/redirect_when_unmapped_window_has_shadow.py index aeceb33c4a..748834077a 100755 --- a/tests/testcases/redirect_when_unmapped_window_has_shadow.py +++ b/tests/testcases/redirect_when_unmapped_window_has_shadow.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib