pax_global_header00006660000000000000000000000064147150457060014523gustar00rootroot0000000000000052 comment=a456d438d0a681365d90a8adbf0941cede41aaa9 picom-12.5/000077500000000000000000000000001471504570600125615ustar00rootroot00000000000000picom-12.5/.circleci/000077500000000000000000000000001471504570600144145ustar00rootroot00000000000000picom-12.5/.circleci/config.yml000066400000000000000000000077301471504570600164130ustar00rootroot00000000000000executors: e: docker: - image: yshui/comptonci working_directory: "/tmp/workspace" environment: UBSAN_OPTIONS: "halt_on_error=1" version: 2.1 commands: build: parameters: build-config: type: string default: "" cc: type: string default: cc steps: - restore_cache: keys: - source-v1-{{ .Branch }}-{{ .Revision }} - source-v1-{{ .Branch }}- - source-v1- - checkout - save_cache: key: source-v1-{{ .Branch }}-{{ .Revision }} paths: - ".git" - run: name: config command: CC=<< parameters.cc >> meson setup << parameters.build-config >> -Dunittest=true --werror . build - run: name: build command: ninja -vC build jobs: basic: executor: e steps: - build: build-config: -Dwith_docs=true -Db_coverage=true - run: name: build animgen command: ninja -vC build tools/animgen - persist_to_workspace: root: . paths: - . test: executor: e steps: - attach_workspace: at: /tmp/workspace - run: name: Tests command: | ulimit -c unlimited printf "\n::: test animgen :::\n" build/tools/animgen data/animation_presets.conf >/dev/null 2>error.log [ -s error.log ] && cat error.log && exit 1 printf "\n::: Unit tests :::\n" ninja -vC build test printf "\n::: test config file parsing :::\n" xvfb-run -a -s "-screen 0 640x480x24" build/src/picom --config tests/configs/parsing_test.conf --no-vsync --diagnostics printf "\n::: test config file parsing in a different locale :::\n" LC_NUMERIC=de_DE.UTF-8 xvfb-run -a -s "-screen 0 640x480x24" build/src/picom --config tests/configs/parsing_test.conf --no-vsync --diagnostics printf "\n::: run testsuite :::\n" tests/run_tests.sh build/src/picom - run: name: generate coverage reports command: cd build; find -name '*.gcno' -exec gcov -pb {} + - run: name: download codecov scripts command: curl -s https://codecov.io/bash > codecov.sh - run: name: upload coverage reports command: bash ./codecov.sh -X gcov - run: name: collect coredumps when: on_fail command: | . $HOME/.cargo/env mkdir /tmp/artifacts for c in tests/core.*; do coredump-copy $c /tmp/coredumps/`basename $c` done tar Jcf /tmp/artifacts/coredumps.tar.xz /tmp/coredumps - store_artifacts: path: /tmp/artifacts minimal: executor: e steps: - build: build-config: -Dopengl=false -Ddbus=false -Dregex=false release: executor: e steps: - build: build-config: --buildtype=release release-clang: executor: e steps: - build: cc: clang build-config: --buildtype=release nogl: executor: e steps: - build: build-config: -Dopengl=false noregex: executor: e steps: - build: build-config: -Dregex=false clang_basic: executor: e steps: - build: cc: clang clang_minimal: executor: e steps: - build: cc: clang build-config: -Dopengl=false -Ddbus=false -Dregex=false clang_nogl: executor: e steps: - build: cc: clang build-config: -Dopengl=false clang_noregex: executor: e steps: - build: cc: clang build-config: -Dregex=false workflows: all_builds: jobs: - basic - clang_basic - minimal - clang_minimal - nogl - clang_nogl - release - release-clang - test: requires: - basic # vim: set sw=2 ts=8 et: picom-12.5/.clang-format000066400000000000000000000021261471504570600151350ustar00rootroot00000000000000BasedOnStyle: LLVM TabWidth: 8 UseTab: ForIndentation BreakBeforeBraces: Attach #BreakStringLiterals: true IndentWidth: 8 AlignAfterOpenBracket: Align ColumnLimit: 90 #ExperimentalAutoDetectBinPacking: true BinPackArguments: true BinPackParameters: true #ReflowComments: true AlignTrailingComments: true SpacesBeforeTrailingComments: 8 SpaceBeforeAssignmentOperators: true SpaceBeforeParens: ControlStatements AllowShortIfStatementsOnASingleLine: false AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: false IndentCaseLabels: false IndentPPDirectives: None PenaltyReturnTypeOnItsOwnLine: 0 PenaltyBreakAssignment: 0 PenaltyBreakBeforeFirstCallParameter: 1 PenaltyBreakComment: 1 PenaltyBreakString: 36 PenaltyExcessCharacter: 3 PenaltyBreakFirstLessLess: 0 PenaltyBreakTemplateDeclaration: 0 BreakBeforeBinaryOperators: None IncludeCategories: - Regex: '<.*\.h>' Priority: 1 - Regex: '".*\.h"' Priority: 2 SortIncludes: true #ForEachMacros: [ list_for_each_entry, list_for_each_entry_safe, HASH_ITER ] #AlignConsecutiveAssignments: true picom-12.5/.clang-tidy000066400000000000000000000017421471504570600146210ustar00rootroot00000000000000Checks: > readability-*, performance-*, modernize-*, google-readability-todo, cert-err34-c, cert-flp30-c, bugprone-*, misc-misplaced-const, misc-redundant-expression, misc-static-assert, -clang-analyzer-*, -readability-isolate-declaration, -readability-magic-numbers, -readability-identifier-length, -bugprone-easily-swappable-parameters FormatStyle: file CheckOptions: - key: readability-magic-numbers.IgnoredIntegerValues 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 - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison value: true picom-12.5/.editorconfig000066400000000000000000000001731471504570600152370ustar00rootroot00000000000000root = true [*.{c,h}] indent_style = tab indent_size = 8 max_line_length = 90 [*.nix] indent_style = space indent_size = 2 picom-12.5/.github/000077500000000000000000000000001471504570600141215ustar00rootroot00000000000000picom-12.5/.github/FUNDING.yml000066400000000000000000000002231471504570600157330ustar00rootroot00000000000000github: yshui patreon: open_collective: ko_fi: tidelift: community_bridge: liberapay: issuehunt: lfx_crowdfunding: polar: buy_me_a_coffee: custom: picom-12.5/.github/issue_template.md000066400000000000000000000041011471504570600174620ustar00rootroot00000000000000 ### Platform ### GPU, drivers, and screen setup ### Environment ### picom version
Diagnostics
### Configuration:
Configuration file ``` // Paste your configuration here ```
### Steps of reproduction 1. 2. ### Expected behavior ### Current Behavior ### Stack trace ### OpenGL trace ### Other details picom-12.5/.github/pull_request_template.md000066400000000000000000000001421471504570600210570ustar00rootroot00000000000000 picom-12.5/.github/workflows/000077500000000000000000000000001471504570600161565ustar00rootroot00000000000000picom-12.5/.github/workflows/codeql-analysis.yml000066400000000000000000000025671471504570600220030ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [next] pull_request: branches: [next] jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: language: ['cpp', 'python'] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install libconfig 1.7 run: | wget https://hyperrealm.github.io/libconfig/dist/libconfig-1.7.3.tar.gz tar -xvf libconfig-1.7.3.tar.gz cd libconfig-1.7.3 ./configure --prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu/ sudo make install # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # Install dependencies - run: sudo apt update && sudo apt install 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-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 meson ninja-build uthash-dev if: ${{ matrix.language == 'cpp' }} # Autobuild - name: Autobuild uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 picom-12.5/.github/workflows/coding-style-pr.yml000066400000000000000000000004751471504570600217270ustar00rootroot00000000000000name: coding-style on: pull_request jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }} - uses: yshui/git-clang-format-lint@v1.16 with: base: ${{ github.event.pull_request.base.sha }} picom-12.5/.github/workflows/coding-style.yml000066400000000000000000000003701471504570600213020ustar00rootroot00000000000000name: coding-style on: push jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: yshui/git-clang-format-lint@v1.16 with: base: ${{ github.event.ref }}~1 picom-12.5/.github/workflows/freebsd.yml000066400000000000000000000011601471504570600203110ustar00rootroot00000000000000name: freebsd on: push jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - uses: cross-platform-actions/action@6acac3ca1b632aa762721d537dea32398ba0f2b1 with: operating_system: freebsd version: '14.1' shell: bash run: | sudo pkg install -y libev libxcb meson pkgconf cmake xcb-util-renderutil xcb-util-image pixman uthash libconfig libglvnd libepoxy dbus pcre2 CPPFLAGS="-I/usr/local/include" meson setup -Dunittest=true --werror build ninja -C build ninja -C build test picom-12.5/.github/workflows/openbsd.yml000066400000000000000000000010431471504570600203310ustar00rootroot00000000000000name: 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 picom-12.5/.github/workflows/page.yml000066400000000000000000000024651471504570600176240ustar00rootroot00000000000000 name: GitHub Pages on: push: pull_request: permissions: contents: read pages: write id-token: write jobs: build: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: submodules: false fetch-depth: 0 - name: Setup Pages id: pages uses: actions/configure-pages@v5 - name: Install asciidoctor run: sudo apt install -y asciidoctor - name: Build run: | asciidoctor -a env-web -a doctype=article -a stylesheet=../assets/next.css -b html man/picom.1.adoc -D _site asciidoctor -a doctype=article -a stylesheet=../assets/next.css -b html man/picom-inspect.1.adoc -D _site asciidoctor -a doctype=article -a stylesheet=../assets/next.css -b html man/picom-trans.1.adoc -D _site cp -r assets _site/ cp _site/picom.1.html _site/index.html - name: Upload uses: actions/upload-pages-artifact@v3 deploy: concurrency: group: "pages" cancel-in-progress: true if: github.ref == 'refs/heads/next' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 picom-12.5/.gitignore000066400000000000000000000016221471504570600145520ustar00rootroot00000000000000# Build files .deps .direnv aclocal.m4 autom4te.cache config.log config.status configure depcomp install-sh missing stamp-h1 compton *.o *.d build/ compile_commands.json build.ninja # language servers .ccls-cache .clangd .cache # CMake files compton-*.deb compton-*.rpm compton-*.tar.bz2 release/ _CPack_Packages/ CMakeCache.txt CMakeFiles/ 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 .vscode *.conf !/tests/configs/*.conf !/data/*.conf perf.data perf.data.old core.* vgcore.* .gdb_history oprofile_data/ compton.plist callgrind.out.* man/*.html man/*.1 doxygen/ .clang_complete .ycm_extra_conf.py .ycm_extra_conf.pyc /src/backtrace-symbols.[ch] /compton*.trace *.orig /tests/log /tests/testcases/__pycache__/ *.profraw # Subproject files subprojects/libconfig picom-12.5/CHANGELOG.md000066400000000000000000000221601471504570600143730ustar00rootroot00000000000000# 12.5 (2024-Nov-13) ## Bug fixes * Fix assertion failure when running with some window managers (e.g. qtile) and no window is focused (#1384) # 12.4 (2024-Nov-09) ## Improvements * Better workaround for a NVIDIA qurik, fix high CPU usage when screen is off (#1265) * Avoid using xrender convolution in all cases, should improve shadow performance for most users. (#1349) # Bug fixes * Fix leak of saved window images. ## Build fixes * Fix build on arm32 (#1355) # 12.3 (2024-Oct-14) ## Improvements * Extend workaround for missing hardware accelerated convolution to more drivers (#1349) ## Bug fixes * Fix memory corruption that can happen when handling window property changes (#1350) * Fix `force-win-blend` having no effect (#1354) * Fix shadow being rendered incorrectly in xrender backend (#1352) # 12.2 (2024-Oct-10) ## Improvements * fly-out/slide-out animation presets no longer cause jumps in window opacity ## Bug fixes * Random delays before screen is updated (#1345 #1330) * Fix building on 32-bit systems (#1346) * Fix blank screen on 32-bit systems * Fix fly-in/fly-out animation presets so they work with directions other than up and left # v12.1 (2024-Sep-29) ## Bug fixes * picom stops rendering correctly after monitor configuration changes (#1338, thanks to @Suyooo) # v12 (2024-Sep-27) ## Bug fixes * Fix crash related to window leader updates (#1337 + extra). * Remove an invalid assertion. # v12-rc4 (2024-Sep-08) ## Bug fixes * Windows become completely black when `rules` and `inactive-dim` are set at the same time * Fix segmentation fault during unredirection if the geometry change animation is used (#1333, thanks to @monsterovich) * Fix many rare race conditions in the window management code (#1334) # v12-rc3 (2024-Aug-30) ## Bug fixes * `transparent-clipping` has no effect (#1317) * `unredir` in window rules not being parsed correctly * Changing window opacity with `picom-trans` does not take effect immediately (#1315) ## Documentation * Document behavior change around rounded corners and fullscreen windows (#1323) # v12-rc2 (2024-Aug-17) ## Bug fixes * Setting corner-radius to 0 cause all windows to not render (#1311) * Setting corner-radius causes windows to have a 1-pixel transparent border * Window shaders no longer work (#1312) # v12-rc1 (2024-Aug-12) ## New features * Animations! Yes, now picom officially supports animations. For examples, and information on how to configure it, please go to our [documentation site](https://picom.app/#_animations). There are some video clips in #1253 as well. (#1220 #1253 #1303 #1305 #1308 #1310) * Universal window rules (#1284). One option to rule them all! Added new configuration option `rules` to replace all existing rule options, and to provide more flexibility on top of that. See [picom(1)](https://picom.app/#_window_rules) for more details. This can be used to configure per-window animations. * `@include` directives in config file now also search in `$XDG_CONFIG_HOME/picom/include` and `$XDG_CONFIG_DIRS/picom/include`, in addition to relative to the config file's parent directory. * 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. `man picom-inspect(1)` for more details. 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 ``` * picom now has a rudimentary plugin system. At the moment, the only thing you can do with it is loading custom backends. ## 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. * `backend` is now a required option. picom will not start if one is not specified explicitly. * New predefined target for conditions: `group_focused`. This target indicate whether the focused window is in the same window group as the window being matched. * Meaning of `window_type` in conditions changed slightly, now it supports windows with multiple types. (However the behavior of `wintypes` remains unchanged.) ## 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) * Fix ugly looking rounded corners (#1261) ## Build 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, with a minimal supported version of 1.7. * `xcb-dpms` is not needed anymore. * `libXext` is not needed anymore. * man pages are now built with asciidoctor, instead of asciidoc. ## Behind the scene changes * The X critical section is removed, picom no longer grabs the server to fetch updates. Hopefully, if everything works, this change is unnoticeable. Minor responsiveness improvements could be possible, but I won't bet on it. The main point of this change is this makes debugging much less painful. Previously if you breaks inside the X critical section, the whole X server will lock up, and you would have to connect to the computer remotely to recover. Now there is no longer such worries. This also avoids a bug inside Xorg that makes server grabbing unreliable. # 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. picom-12.5/CONTRIBUTORS000066400000000000000000000067631471504570600144550ustar00rootroot00000000000000Sorted 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 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 Evgeniy Baskov Greg Flynn 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 Lenivaya 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 ಠ_ಠ picom-12.5/COPYING000066400000000000000000000012731471504570600136170ustar00rootroot00000000000000picom - a compositor for X11 Based on xcompmgr, originally written by Keith Packard, with modifications from several contributors (according to the xcompmgr man page): Matthew Allum, Eric Anholt, Dan Doel, Thomas Luebking, Matthew Hawn, Ely Levy, Phil Blundell, and Carl Worth. Menu transparency was implemented by Dana Jansens. Numerous contributions to picom from Richard Grenville. See the CONTRIBUTORS file for a complete list of contributors This source code is provided under: SPDX-License-Identifier: MPL-2.0 AND MIT And the preferred license for new source files in this project is: SPDX-License-Identifier: MPL-2.0 You can find the text of the licenses in the LICENSES directory. picom-12.5/Doxyfile000066400000000000000000002266651471504570600143100ustar00rootroot00000000000000# Doxyfile 1.8.2 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. # # All text after a hash (#) is considered a comment and will be ignored. # The format is: # TAG = value [value, ...] # For lists items can also be appended using: # TAG += value [value, ...] # Values that contain spaces should be placed between quotes (" "). #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- # This tag specifies the encoding used for all characters in the config file # that follow. The default is UTF-8 which is also the encoding used for all # text before the first occurrence of this tag. Doxygen uses libiconv (or the # iconv built into libc) for the transcoding. See # http://www.gnu.org/software/libiconv for the list of possible encodings. DOXYFILE_ENCODING = UTF-8 # The PROJECT_NAME tag is a single word (or sequence of words) that should # identify the project. Note that if you do not use Doxywizard you need # to put quotes around the project name if it contains spaces. PROJECT_NAME = "compton" # The PROJECT_NUMBER tag can be used to enter a project or revision number. # This could be handy for archiving the generated documentation or # if some version control system is used. PROJECT_NUMBER = # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer # a quick idea about the purpose of the project. Keep the description short. PROJECT_BRIEF = # With the PROJECT_LOGO tag one can specify an logo or icon that is # included in the documentation. The maximum height of the logo should not # exceed 55 pixels and the maximum width should not exceed 200 pixels. # Doxygen will copy the logo to the output directory. PROJECT_LOGO = # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # base path where the generated documentation will be put. # If a relative path is entered, it will be relative to the location # where doxygen was started. If left blank the current directory will be used. OUTPUT_DIRECTORY = doxygen # If the CREATE_SUBDIRS tag is set to YES, then doxygen will create # 4096 sub-directories (in 2 levels) under the output directory of each output # format and will distribute the generated files over these directories. # Enabling this option can be useful when feeding doxygen a huge amount of # source files, where putting all generated files in the same directory would # otherwise cause performance problems for the file system. CREATE_SUBDIRS = NO # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. # The default language is English, other supported languages are: # Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, # Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, # Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English # messages), Korean, Korean-en, Lithuanian, Norwegian, Macedonian, Persian, # Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, # Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. OUTPUT_LANGUAGE = English # If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will # include brief member descriptions after the members that are listed in # the file and class documentation (similar to JavaDoc). # Set to NO to disable this. BRIEF_MEMBER_DESC = YES # If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend # the brief description of a member or function before the detailed description. # Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the # brief descriptions will be completely suppressed. REPEAT_BRIEF = YES # This tag implements a quasi-intelligent brief description abbreviator # that is used to form the text in various listings. Each string # in this list, if found as the leading text of the brief description, will be # stripped from the text and the result after processing the whole list, is # used as the annotated text. Otherwise, the brief description is used as-is. # If left blank, the following values are used ("$name" is automatically # replaced with the name of the entity): "The $name class" "The $name widget" # "The $name file" "is" "provides" "specifies" "contains" # "represents" "a" "an" "the" ABBREVIATE_BRIEF = # If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then # Doxygen will generate a detailed section even if there is only a brief # description. ALWAYS_DETAILED_SEC = NO # If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all # inherited members of a class in the documentation of that class as if those # members were ordinary class members. Constructors, destructors and assignment # operators of the base classes will not be shown. INLINE_INHERITED_MEMB = NO # If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full # path before files name in the file list and in the header files. If set # to NO the shortest path that makes the file name unique will be used. FULL_PATH_NAMES = YES # If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag # can be used to strip a user-defined part of the path. Stripping is # only done if one of the specified strings matches the left-hand part of # the path. The tag can be used to show relative paths in the file list. # If left blank the directory from which doxygen is run is used as the # path to strip. Note that you specify absolute paths here, but also # relative paths, which will be relative from the directory where doxygen is # started. STRIP_FROM_PATH = # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of # the path mentioned in the documentation of a class, which tells # the reader which header file to include in order to use a class. # If left blank only the name of the header file containing the class # definition is used. Otherwise one should specify the include paths that # are normally passed to the compiler using the -I flag. STRIP_FROM_INC_PATH = # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter # (but less readable) file names. This can be useful if your file system # doesn't support long names like on DOS, Mac, or CD-ROM. SHORT_NAMES = NO # If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen # will interpret the first line (until the first dot) of a JavaDoc-style # comment as the brief description. If set to NO, the JavaDoc # comments will behave just like regular Qt-style comments # (thus requiring an explicit @brief command for a brief description.) JAVADOC_AUTOBRIEF = YES # If the QT_AUTOBRIEF tag is set to YES then Doxygen will # interpret the first line (until the first dot) of a Qt-style # comment as the brief description. If set to NO, the comments # will behave just like regular Qt-style comments (thus requiring # an explicit \brief command for a brief description.) QT_AUTOBRIEF = NO # The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen # treat a multi-line C++ special comment block (i.e. a block of //! or /// # comments) as a brief description. This used to be the default behaviour. # The new default is to treat a multi-line C++ comment block as a detailed # description. Set this tag to YES if you prefer the old behaviour instead. MULTILINE_CPP_IS_BRIEF = NO # If the INHERIT_DOCS tag is set to YES (the default) then an undocumented # member inherits the documentation from any documented member that it # re-implements. INHERIT_DOCS = YES # If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce # a new page for each member. If set to NO, the documentation of a member will # be part of the file/class/namespace that contains it. SEPARATE_MEMBER_PAGES = NO # The TAB_SIZE tag can be used to set the number of spaces in a tab. # Doxygen uses this value to replace tabs by spaces in code fragments. TAB_SIZE = 4 # This tag can be used to specify a number of aliases that acts # as commands in the documentation. An alias has the form "name=value". # For example adding "sideeffect=\par Side Effects:\n" will allow you to # put the command \sideeffect (or @sideeffect) in the documentation, which # will result in a user-defined paragraph with heading "Side Effects:". # You can put \n's in the value part of an alias to insert newlines. ALIASES = # This tag can be used to specify a number of word-keyword mappings (TCL only). # A mapping has the form "name=value". For example adding # "class=itcl::class" will allow you to use the command class in the # itcl::class meaning. TCL_SUBST = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C # sources only. Doxygen will then generate output that is more tailored for C. # For instance, some of the names that are used will be different. The list # of all members will be omitted, etc. OPTIMIZE_OUTPUT_FOR_C = YES # Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java # sources only. Doxygen will then generate output that is more tailored for # Java. For instance, namespaces will be presented as packages, qualified # scopes will look different, etc. OPTIMIZE_OUTPUT_JAVA = NO # Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran # sources only. Doxygen will then generate output that is more tailored for # Fortran. OPTIMIZE_FOR_FORTRAN = NO # Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL # sources. Doxygen will then generate output that is tailored for # VHDL. OPTIMIZE_OUTPUT_VHDL = NO # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, # and language is one of the parsers supported by doxygen: IDL, Java, # Javascript, CSharp, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, # C++. For instance to make doxygen treat .inc files as Fortran files (default # is PHP), and .f files as C (default is Fortran), use: inc=Fortran f=C. Note # that for custom extensions you also need to set FILE_PATTERNS otherwise the # files are not read by doxygen. EXTENSION_MAPPING = # If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all # comments according to the Markdown format, which allows for more readable # documentation. See http://daringfireball.net/projects/markdown/ for details. # The output of markdown processing is further processed by doxygen, so you # can mix doxygen, HTML, and XML commands with Markdown formatting. # Disable only in case of backward compatibilities issues. MARKDOWN_SUPPORT = YES # When enabled doxygen tries to link words that correspond to documented classes, # or namespaces to their corresponding documentation. Such a link can be # prevented in individual cases by by putting a % sign in front of the word or # globally by setting AUTOLINK_SUPPORT to NO. AUTOLINK_SUPPORT = YES # If you use STL classes (i.e. std::string, std::vector, etc.) but do not want # to include (a tag file for) the STL sources as input, then you should # set this tag to YES in order to let doxygen match functions declarations and # definitions whose arguments contain STL classes (e.g. func(std::string); v.s. # func(std::string) {}). This also makes the inheritance and collaboration # diagrams that involve STL classes more complete and accurate. BUILTIN_STL_SUPPORT = NO # If you use Microsoft's C++/CLI language, you should set this option to YES to # enable parsing support. CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. # Doxygen will parse them like normal C++ but will assume all classes use public # instead of private inheritance when no explicit protection keyword is present. SIP_SUPPORT = NO # For Microsoft's IDL there are propget and propput attributes to indicate getter and setter methods for a property. Setting this option to YES (the default) will make doxygen replace the get and set methods by a property in the documentation. This will only work if the methods are indeed getting or setting a simple type. If this is not the case, or you want to show the methods anyway, you should set this option to NO. IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC # tag is set to YES, then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. DISTRIBUTE_GROUP_DOC = NO # Set the SUBGROUPING tag to YES (the default) to allow class member groups of # the same type (for instance a group of public functions) to be put as a # subgroup of that type (e.g. under the Public Functions section). Set it to # NO to prevent subgrouping. Alternatively, this can be done per class using # the \nosubgrouping command. SUBGROUPING = YES # When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and # unions are shown inside the group in which they are included (e.g. using # @ingroup) instead of on a separate page (for HTML and Man pages) or # section (for LaTeX and RTF). INLINE_GROUPED_CLASSES = NO # When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and # unions with only public data fields will be shown inline in the documentation # of the scope in which they are defined (i.e. file, namespace, or group # documentation), provided this scope is documented. If set to NO (the default), # structs, classes, and unions are shown on a separate page (for HTML and Man # pages) or section (for LaTeX and RTF). INLINE_SIMPLE_STRUCTS = NO # When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum # is documented as struct, union, or enum with the name of the typedef. So # typedef struct TypeS {} TypeT, will appear in the documentation as a struct # with name TypeT. When disabled the typedef will appear as a member of a file, # namespace, or class. And the struct will be named TypeS. This can typically # be useful for C code in case the coding convention dictates that all compound # types are typedef'ed and only the typedef is referenced, never the tag name. TYPEDEF_HIDES_STRUCT = NO # The SYMBOL_CACHE_SIZE determines the size of the internal cache use to # determine which symbols to keep in memory and which to flush to disk. # When the cache is full, less often used symbols will be written to disk. # For small to medium size projects (<1000 input files) the default value is # probably good enough. For larger projects a too small cache size can cause # doxygen to be busy swapping symbols to and from disk most of the time # causing a significant performance penalty. # If the system has enough physical memory increasing the cache will improve the # performance by keeping more symbols in memory. Note that the value works on # a logarithmic scale so increasing the size by one will roughly double the # memory usage. The cache size is given by this formula: # 2^(16+SYMBOL_CACHE_SIZE). The valid range is 0..9, the default is 0, # corresponding to a cache size of 2^16 = 65536 symbols. SYMBOL_CACHE_SIZE = 0 # Similar to the SYMBOL_CACHE_SIZE the size of the symbol lookup cache can be # set using LOOKUP_CACHE_SIZE. This cache is used to resolve symbols given # their name and scope. Since this can be an expensive process and often the # same symbol appear multiple times in the code, doxygen keeps a cache of # pre-resolved symbols. If the cache is too small doxygen will become slower. # If the cache is too large, memory is wasted. The cache size is given by this # formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range is 0..9, the default is 0, # corresponding to a cache size of 2^16 = 65536 symbols. LOOKUP_CACHE_SIZE = 0 #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- # If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in # documentation are documented, even if no documentation was available. # Private class members and static file members will be hidden unless # the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES EXTRACT_ALL = YES # If the EXTRACT_PRIVATE tag is set to YES all private members of a class # will be included in the documentation. EXTRACT_PRIVATE = NO # If the EXTRACT_PACKAGE tag is set to YES all members with package or internal # scope will be included in the documentation. EXTRACT_PACKAGE = NO # If the EXTRACT_STATIC tag is set to YES all static members of a file # will be included in the documentation. EXTRACT_STATIC = YES # If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) # defined locally in source files will be included in the documentation. # If set to NO only classes defined in header files are included. EXTRACT_LOCAL_CLASSES = YES # This flag is only useful for Objective-C code. When set to YES local # methods, which are defined in the implementation section but not in # the interface are included in the documentation. # If set to NO (the default) only methods in the interface are included. EXTRACT_LOCAL_METHODS = NO # If this flag is set to YES, the members of anonymous namespaces will be # extracted and appear in the documentation as a namespace called # 'anonymous_namespace{file}', where file will be replaced with the base # name of the file that contains the anonymous namespace. By default # anonymous namespaces are hidden. EXTRACT_ANON_NSPACES = NO # If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all # undocumented members of documented classes, files or namespaces. # If set to NO (the default) these members will be included in the # various overviews, but no documentation section is generated. # This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. # If set to NO (the default) these classes will be included in the various # overviews. This option has no effect if EXTRACT_ALL is enabled. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all # friend (class|struct|union) declarations. # If set to NO (the default) these declarations will be included in the # documentation. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any # documentation blocks found inside the body of a function. # If set to NO (the default) these blocks will be appended to the # function's detailed documentation block. HIDE_IN_BODY_DOCS = NO # The INTERNAL_DOCS tag determines if documentation # that is typed after a \internal command is included. If the tag is set # to NO (the default) then the documentation will be excluded. # Set it to YES to include the internal documentation. INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate # file names in lower-case letters. If set to YES upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows # and Mac users are advised to set this option to NO. CASE_SENSE_NAMES = YES # If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen # will show members with their full class and namespace scopes in the # documentation. If set to YES the scope will be hidden. HIDE_SCOPE_NAMES = NO # If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen # will put a list of the files that are included by a file in the documentation # of that file. SHOW_INCLUDE_FILES = YES # If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen # will list include files with double quotes in the documentation # rather than with sharp brackets. FORCE_LOCAL_INCLUDES = NO # If the INLINE_INFO tag is set to YES (the default) then a tag [inline] # is inserted in the documentation for inline members. INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen # will sort the (detailed) documentation of file and class members # alphabetically by member name. If set to NO the members will appear in # declaration order. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the # brief documentation of file, namespace and class members alphabetically # by member name. If set to NO (the default) the members will appear in # declaration order. SORT_BRIEF_DOCS = NO # If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen # will sort the (brief and detailed) documentation of class members so that # constructors and destructors are listed first. If set to NO (the default) # the constructors will appear in the respective orders defined by # SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. # This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO # and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. SORT_MEMBERS_CTORS_1ST = NO # If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the # hierarchy of group names into alphabetical order. If set to NO (the default) # the group names will appear in their defined order. SORT_GROUP_NAMES = NO # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be # sorted by fully-qualified names, including namespaces. If set to # NO (the default), the class list will be sorted only by class name, # not including the namespace part. # Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. # Note: This option applies only to the class list, not to the # alphabetical list. SORT_BY_SCOPE_NAME = NO # If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to # do proper type resolution of all parameters of a function it will reject a # match between the prototype and the implementation of a member function even # if there is only one candidate or it is obvious which candidate to choose # by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen # will still accept a match between prototype and implementation in such cases. STRICT_PROTO_MATCHING = NO # The GENERATE_TODOLIST tag can be used to enable (YES) or # disable (NO) the todo list. This list is created by putting \todo # commands in the documentation. GENERATE_TODOLIST = YES # The GENERATE_TESTLIST tag can be used to enable (YES) or # disable (NO) the test list. This list is created by putting \test # commands in the documentation. GENERATE_TESTLIST = YES # The GENERATE_BUGLIST tag can be used to enable (YES) or # disable (NO) the bug list. This list is created by putting \bug # commands in the documentation. GENERATE_BUGLIST = YES # The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or # disable (NO) the deprecated list. This list is created by putting # \deprecated commands in the documentation. GENERATE_DEPRECATEDLIST= YES # The ENABLED_SECTIONS tag can be used to enable conditional # documentation sections, marked by \if sectionname ... \endif. ENABLED_SECTIONS = # The MAX_INITIALIZER_LINES tag determines the maximum number of lines # the initial value of a variable or macro consists of for it to appear in # the documentation. If the initializer consists of more lines than specified # here it will be hidden. Use a value of 0 to hide initializers completely. # The appearance of the initializer of individual variables and macros in the # documentation can be controlled using \showinitializer or \hideinitializer # command in the documentation regardless of this setting. MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated # at the bottom of the documentation of classes and structs. If set to YES the # list will mention the files that were used to generate the documentation. SHOW_USED_FILES = YES # Set the SHOW_FILES tag to NO to disable the generation of the Files page. # This will remove the Files entry from the Quick Index and from the # Folder Tree View (if specified). The default is YES. SHOW_FILES = YES # Set the SHOW_NAMESPACES tag to NO to disable the generation of the # Namespaces page. # This will remove the Namespaces entry from the Quick Index # and from the Folder Tree View (if specified). The default is YES. SHOW_NAMESPACES = YES # The FILE_VERSION_FILTER tag can be used to specify a program or script that # doxygen should invoke to get the current version for each file (typically from # the version control system). Doxygen will invoke the program by executing (via # popen()) the command , where is the value of # the FILE_VERSION_FILTER tag, and is the name of an input file # provided by doxygen. Whatever the program writes to standard output # is used as the file version. See the manual for examples. FILE_VERSION_FILTER = # The LAYOUT_FILE tag can be used to specify a layout file which will be parsed # by doxygen. The layout file controls the global structure of the generated # output files in an output format independent way. To create the layout file # that represents doxygen's defaults, run doxygen with the -l option. # You can optionally specify a file name after the option, if omitted # DoxygenLayout.xml will be used as the name of the layout file. LAYOUT_FILE = # The CITE_BIB_FILES tag can be used to specify one or more bib files # containing the references data. This must be a list of .bib files. The # .bib extension is automatically appended if omitted. Using this command # requires the bibtex tool to be installed. See also # http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style # of the bibliography can be controlled using LATEX_BIB_STYLE. To use this # feature you need bibtex and perl available in the search path. CITE_BIB_FILES = #--------------------------------------------------------------------------- # configuration options related to warning and progress messages #--------------------------------------------------------------------------- # The QUIET tag can be used to turn on/off the messages that are generated # by doxygen. Possible values are YES and NO. If left blank NO is used. QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are # generated by doxygen. Possible values are YES and NO. If left blank # NO is used. WARNINGS = YES # If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings # for undocumented members. If EXTRACT_ALL is set to YES then this flag will # automatically be disabled. WARN_IF_UNDOCUMENTED = YES # If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for # potential errors in the documentation, such as not documenting some # parameters in a documented function, or documenting parameters that # don't exist or using markup commands wrongly. WARN_IF_DOC_ERROR = YES # The WARN_NO_PARAMDOC option can be enabled to get warnings for # functions that are documented, but have no documentation for their parameters # or return value. If set to NO (the default) doxygen will only warn about # wrong or incomplete parameter documentation, but not about the absence of # documentation. WARN_NO_PARAMDOC = NO # The WARN_FORMAT tag determines the format of the warning messages that # doxygen can produce. The string should contain the $file, $line, and $text # tags, which will be replaced by the file and line number from which the # warning originated and the warning text. Optionally the format may contain # $version, which will be replaced by the version of the file (if it could # be obtained via FILE_VERSION_FILTER) WARN_FORMAT = "$file:$line: $text" # The WARN_LOGFILE tag can be used to specify a file to which warning # and error messages should be written. If left blank the output is written # to stderr. WARN_LOGFILE = #--------------------------------------------------------------------------- # configuration options related to the input files #--------------------------------------------------------------------------- # The INPUT tag can be used to specify the files and/or directories that contain # documented source files. You may enter file names like "myfile.cpp" or # directories like "/usr/src/myproject". Separate the files or directories # with spaces. INPUT = src # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is # also the default input encoding. Doxygen uses libiconv (or the iconv built # into libc) for the transcoding. See http://www.gnu.org/software/libiconv for # the list of possible encodings. INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank the following patterns are tested: # *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh # *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py # *.f90 *.f *.for *.vhd *.vhdl FILE_PATTERNS = *.c *.h # The RECURSIVE tag can be used to turn specify whether or not subdirectories # should be searched for input files as well. Possible values are YES and NO. # If left blank NO is used. RECURSIVE = NO # The EXCLUDE tag can be used to specify files and/or directories that should be # excluded from the INPUT source files. This way you can easily exclude a # subdirectory from a directory tree whose root is specified with the INPUT tag. # Note that relative paths are relative to the directory from which doxygen is # run. EXCLUDE = # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded # from the input. EXCLUDE_SYMLINKS = NO # If the value of the INPUT tag contains directories, you can use the # EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude # certain files from those directories. Note that the wildcards are matched # against the file with absolute path, so to exclude all test directories # for example use the pattern */test/* EXCLUDE_PATTERNS = # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # AClass::ANamespace, ANamespace::*Test EXCLUDE_SYMBOLS = # The EXAMPLE_PATH tag can be used to specify one or more files or # directories that contain example code fragments that are included (see # the \include command). EXAMPLE_PATH = # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp # and *.h) to filter out the source-files in the directories. If left # blank all files are included. EXAMPLE_PATTERNS = # If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be # searched for input files to be used with the \include or \dontinclude # commands irrespective of the value of the RECURSIVE tag. # Possible values are YES and NO. If left blank NO is used. EXAMPLE_RECURSIVE = NO # The IMAGE_PATH tag can be used to specify one or more files or # directories that contain image that are included in the documentation (see # the \image command). IMAGE_PATH = # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program # by executing (via popen()) the command , where # is the value of the INPUT_FILTER tag, and is the name of an # input file. Doxygen will then use the output that the filter program writes # to standard output. # If FILTER_PATTERNS is specified, this tag will be # ignored. INPUT_FILTER = # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. # Doxygen will compare the file name with each pattern and apply the # filter if there is a match. # The filters are a list of the form: # pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further # info on how filters are used. If FILTER_PATTERNS is empty or if # non of the patterns match the file name, INPUT_FILTER is applied. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using # INPUT_FILTER) will be used to filter the input files when producing source # files to browse (i.e. when SOURCE_BROWSER is set to YES). FILTER_SOURCE_FILES = NO # The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file # pattern. A pattern will override the setting for FILTER_PATTERN (if any) # and it is also possible to disable source filtering for a specific pattern # using *.ext= (so without naming a filter). This option only has effect when # FILTER_SOURCE_FILES is enabled. FILTER_SOURCE_PATTERNS = #--------------------------------------------------------------------------- # configuration options related to source browsing #--------------------------------------------------------------------------- # If the SOURCE_BROWSER tag is set to YES then a list of source files will # be generated. Documented entities will be cross-referenced with these sources. # Note: To get rid of all source code in the generated output, make sure also # VERBATIM_HEADERS is set to NO. SOURCE_BROWSER = NO # Setting the INLINE_SOURCES tag to YES will include the body # of functions and classes directly in the documentation. INLINE_SOURCES = NO # Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct # doxygen to hide any special comment blocks from generated source code # fragments. Normal C, C++ and Fortran comments will always remain visible. STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES # then for each documented function all documented # functions referencing it will be listed. REFERENCED_BY_RELATION = NO # If the REFERENCES_RELATION tag is set to YES # then for each documented function all documented entities # called/used by that function will be listed. REFERENCES_RELATION = NO # If the REFERENCES_LINK_SOURCE tag is set to YES (the default) # and SOURCE_BROWSER tag is set to YES, then the hyperlinks from # functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will # link to the source code. # Otherwise they will link to the documentation. REFERENCES_LINK_SOURCE = YES # If the USE_HTAGS tag is set to YES then the references to source code # will point to the HTML generated by the htags(1) tool instead of doxygen # built-in source browser. The htags tool is part of GNU's global source # tagging system (see http://www.gnu.org/software/global/global.html). You # will need version 4.8.6 or higher. USE_HTAGS = NO # If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen # will generate a verbatim copy of the header file for each class for # which an include is specified. Set to NO to disable this. VERBATIM_HEADERS = YES #--------------------------------------------------------------------------- # configuration options related to the alphabetical class index #--------------------------------------------------------------------------- # If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index # of all compounds will be generated. Enable this if the project # contains a lot of classes, structs, unions or interfaces. ALPHABETICAL_INDEX = YES # If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then # the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns # in which this list will be split (can be a number in the range [1..20]) COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all # classes will be put under the same header in the alphabetical index. # The IGNORE_PREFIX tag can be used to specify one or more prefixes that # should be ignored while generating the index headers. IGNORE_PREFIX = #--------------------------------------------------------------------------- # configuration options related to the HTML output #--------------------------------------------------------------------------- # If the GENERATE_HTML tag is set to YES (the default) Doxygen will # generate HTML output. GENERATE_HTML = YES # The HTML_OUTPUT tag is used to specify where the HTML docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `html' will be used as the default path. HTML_OUTPUT = html # The HTML_FILE_EXTENSION tag can be used to specify the file extension for # each generated HTML page (for example: .htm,.php,.asp). If it is left blank # doxygen will generate files with .html extension. HTML_FILE_EXTENSION = .html # The HTML_HEADER tag can be used to specify a personal HTML header for # each generated HTML page. If it is left blank doxygen will generate a # standard header. Note that when using a custom header you are responsible # for the proper inclusion of any scripts and style sheets that doxygen # needs, which is dependent on the configuration options used. # It is advised to generate a default header using "doxygen -w html # header.html footer.html stylesheet.css YourConfigFile" and then modify # that header. Note that the header is subject to change so you typically # have to redo this when upgrading to a newer version of doxygen or when # changing the value of configuration settings such as GENERATE_TREEVIEW! HTML_HEADER = # The HTML_FOOTER tag can be used to specify a personal HTML footer for # each generated HTML page. If it is left blank doxygen will generate a # standard footer. HTML_FOOTER = # The HTML_STYLESHEET tag can be used to specify a user-defined cascading # style sheet that is used by each HTML page. It can be used to # fine-tune the look of the HTML output. If left blank doxygen will # generate a default style sheet. Note that it is recommended to use # HTML_EXTRA_STYLESHEET instead of this one, as it is more robust and this # tag will in the future become obsolete. HTML_STYLESHEET = # The HTML_EXTRA_STYLESHEET tag can be used to specify an additional # user-defined cascading style sheet that is included after the standard # style sheets created by doxygen. Using this option one can overrule # certain style aspects. This is preferred over using HTML_STYLESHEET # since it does not replace the standard style sheet and is therefor more # robust against future updates. Doxygen will copy the style sheet file to # the output directory. HTML_EXTRA_STYLESHEET = # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note # that these files will be copied to the base HTML output directory. Use the # $relpath$ marker in the HTML_HEADER and/or HTML_FOOTER files to load these # files. In the HTML_STYLESHEET file, use the file name only. Also note that # the files will be copied as-is; there are no commands or markers available. HTML_EXTRA_FILES = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. # Doxygen will adjust the colors in the style sheet and background images # according to this color. Hue is specified as an angle on a colorwheel, # see http://en.wikipedia.org/wiki/Hue for more information. # For instance the value 0 represents red, 60 is yellow, 120 is green, # 180 is cyan, 240 is blue, 300 purple, and 360 is red again. # The allowed range is 0 to 359. HTML_COLORSTYLE_HUE = 220 # The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of # the colors in the HTML output. For a value of 0 the output will use # grayscales only. A value of 255 will produce the most vivid colors. HTML_COLORSTYLE_SAT = 100 # The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to # the luminance component of the colors in the HTML output. Values below # 100 gradually make the output lighter, whereas values above 100 make # the output darker. The value divided by 100 is the actual gamma applied, # so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, # and 100 does not change the gamma. HTML_COLORSTYLE_GAMMA = 80 # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting # this to NO can help when comparing the output of multiple runs. HTML_TIMESTAMP = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the # page has loaded. HTML_DYNAMIC_SECTIONS = NO # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of # entries shown in the various tree structured indices initially; the user # can expand and collapse entries dynamically later on. Doxygen will expand # the tree to such a level that at most the specified number of entries are # visible (unless a fully collapsed tree already exceeds this amount). # So setting the number of entries 1 will produce a full collapsed tree by # default. 0 is a special value representing an infinite number of entries # and will result in a full expanded tree by default. HTML_INDEX_NUM_ENTRIES = 100 # If the GENERATE_DOCSET tag is set to YES, additional index files # will be generated that can be used as input for Apple's Xcode 3 # integrated development environment, introduced with OSX 10.5 (Leopard). # To create a documentation set, doxygen will generate a Makefile in the # HTML output directory. Running make will produce the docset in that # directory and running "make install" will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find # it at startup. # See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html # for more information. GENERATE_DOCSET = NO # When GENERATE_DOCSET tag is set to YES, this tag determines the name of the # feed. A documentation feed provides an umbrella under which multiple # documentation sets from a single provider (such as a company or product suite) # can be grouped. DOCSET_FEEDNAME = "Doxygen generated docs" # When GENERATE_DOCSET tag is set to YES, this tag specifies a string that # should uniquely identify the documentation set bundle. This should be a # reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen # will append .docset to the name. DOCSET_BUNDLE_ID = org.doxygen.Project # When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely # identify the documentation publisher. This should be a reverse domain-name # style string, e.g. com.mycompany.MyDocSet.documentation. DOCSET_PUBLISHER_ID = org.doxygen.Publisher # The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES, additional index files # will be generated that can be used as input for tools like the # Microsoft HTML help workshop to generate a compiled HTML help file (.chm) # of the generated HTML documentation. GENERATE_HTMLHELP = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can # be used to specify the file name of the resulting .chm file. You # can add a path in front of the file if the result should not be # written to the html output directory. CHM_FILE = # If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can # be used to specify the location (absolute path including file name) of # the HTML help compiler (hhc.exe). If non-empty doxygen will try to run # the HTML help compiler on the generated index.hhp. HHC_LOCATION = # If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag # controls if a separate .chi index file is generated (YES) or that # it should be included in the master .chm file (NO). GENERATE_CHI = NO # If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING # is used to encode HtmlHelp index (hhk), content (hhc) and project file # content. CHM_INDEX_ENCODING = # If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag # controls whether a binary table of contents is generated (YES) or a # normal table of contents (NO) in the .chm file. BINARY_TOC = NO # The TOC_EXPAND flag can be set to YES to add extra items for group members # to the contents of the HTML help documentation and to the tree view. TOC_EXPAND = NO # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated # that can be used as input for Qt's qhelpgenerator to generate a # Qt Compressed Help (.qch) of the generated HTML documentation. GENERATE_QHP = NO # If the QHG_LOCATION tag is specified, the QCH_FILE tag can # be used to specify the file name of the resulting .qch file. # The path specified is relative to the HTML output folder. QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#namespace QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating # Qt Help Project output. For more information please see # http://doc.trolltech.com/qthelpproject.html#virtual-folders QHP_VIRTUAL_FOLDER = doc # If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to # add. For more information please see # http://doc.trolltech.com/qthelpproject.html#custom-filters QHP_CUST_FILTER_NAME = # The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see # # Qt Help Project / Custom Filters. QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this # project's # filter section matches. # # Qt Help Project / Filter Attributes. QHP_SECT_FILTER_ATTRS = # If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can # be used to specify the location of Qt's qhelpgenerator. # If non-empty doxygen will try to run qhelpgenerator on the generated # .qhp file. QHG_LOCATION = # If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files # will be generated, which together with the HTML files, form an Eclipse help # plugin. To install this plugin and make it available under the help contents # menu in Eclipse, the contents of the directory containing the HTML and XML # files needs to be copied into the plugins directory of eclipse. The name of # the directory within the plugins directory should be the same as # the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before # the help appears. GENERATE_ECLIPSEHELP = NO # A unique identifier for the eclipse help plugin. When installing the plugin # the directory name containing the HTML and XML files should also have # this name. ECLIPSE_DOC_ID = org.doxygen.Project # The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) # at top of each HTML page. The value NO (the default) enables the index and # the value YES disables it. Since the tabs have the same information as the # navigation tree you can set this option to NO if you already set # GENERATE_TREEVIEW to YES. DISABLE_INDEX = NO # The GENERATE_TREEVIEW tag is used to specify whether a tree-like index # structure should be generated to display hierarchical information. # If the tag value is set to YES, a side panel will be generated # containing a tree-like index structure (just like the one that # is generated for HTML Help). For this to work a browser that supports # JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). # Windows users are probably better off using the HTML help feature. # Since the tree basically has the same information as the tab index you # could consider to set DISABLE_INDEX to NO when enabling this option. GENERATE_TREEVIEW = NO # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values # (range [0,1..20]) that doxygen will group on one line in the generated HTML # documentation. Note that a value of 0 will completely suppress the enum # values from appearing in the overview section. ENUM_VALUES_PER_LINE = 4 # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be # used to set the initial width (in pixels) of the frame in which the tree # is shown. TREEVIEW_WIDTH = 250 # When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open # links to external symbols imported via tag files in a separate window. EXT_LINKS_IN_WINDOW = NO # Use this tag to change the font size of Latex formulas included # as images in the HTML documentation. The default is 10. Note that # when you change the font size after a successful doxygen run you need # to manually remove any form_*.png images from the HTML output directory # to force them to be regenerated. FORMULA_FONTSIZE = 10 # Use the FORMULA_TRANPARENT tag to determine whether or not the images # generated for formulas are transparent PNGs. Transparent PNGs are # not supported properly for IE 6.0, but are supported on all modern browsers. # Note that when changing this option you need to delete any form_*.png files # in the HTML output before the changes have effect. FORMULA_TRANSPARENT = YES # Enable the USE_MATHJAX option to render LaTeX formulas using MathJax # (see http://www.mathjax.org) which uses client side Javascript for the # rendering instead of using prerendered bitmaps. Use this if you do not # have LaTeX installed or if you want to formulas look prettier in the HTML # output. When enabled you may also need to install MathJax separately and # configure the path to it using the MATHJAX_RELPATH option. USE_MATHJAX = NO # When MathJax is enabled you need to specify the location relative to the # HTML output directory using the MATHJAX_RELPATH option. The destination # directory should contain the MathJax.js script. For instance, if the mathjax # directory is located at the same level as the HTML output directory, then # MATHJAX_RELPATH should be ../mathjax. The default value points to # the MathJax Content Delivery Network so you can quickly see the result without # installing MathJax. # However, it is strongly recommended to install a local # copy of MathJax from http://www.mathjax.org before deployment. MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest # The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension # names that should be enabled during MathJax rendering. MATHJAX_EXTENSIONS = # When the SEARCHENGINE tag is enabled doxygen will generate a search box # for the HTML output. The underlying search engine uses javascript # and DHTML and should work on any modern browser. Note that when using # HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets # (GENERATE_DOCSET) there is already a search function so this one should # typically be disabled. For large projects the javascript based search engine # can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be # implemented using a PHP enabled web server instead of at the web client # using Javascript. Doxygen will generate the search PHP script and index # file to put on the web server. The advantage of the server # based approach is that it scales better to large projects and allows # full text search. The disadvantages are that it is more difficult to setup # and does not have live searching capabilities. SERVER_BASED_SEARCH = NO #--------------------------------------------------------------------------- # configuration options related to the LaTeX output #--------------------------------------------------------------------------- # If the GENERATE_LATEX tag is set to YES (the default) Doxygen will # generate Latex output. GENERATE_LATEX = NO # The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `latex' will be used as the default path. LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. If left blank `latex' will be used as the default command name. # Note that when enabling USE_PDFLATEX this option is only used for # generating bitmaps for formulas in the HTML output, but not in the # Makefile that is written to the output directory. LATEX_CMD_NAME = latex # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to # generate index for LaTeX. If left blank `makeindex' will be used as the # default command name. MAKEINDEX_CMD_NAME = makeindex # If the COMPACT_LATEX tag is set to YES Doxygen generates more compact # LaTeX documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_LATEX = NO # The PAPER_TYPE tag can be used to set the paper type that is used # by the printer. Possible values are: a4, letter, legal and # executive. If left blank a4wide will be used. PAPER_TYPE = a4 # The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX # packages that should be included in the LaTeX output. EXTRA_PACKAGES = # The LATEX_HEADER tag can be used to specify a personal LaTeX header for # the generated latex document. The header should contain everything until # the first chapter. If it is left blank doxygen will generate a # standard header. Notice: only use this tag if you know what you are doing! LATEX_HEADER = # The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for # the generated latex document. The footer should contain everything after # the last chapter. If it is left blank doxygen will generate a # standard footer. Notice: only use this tag if you know what you are doing! LATEX_FOOTER = # If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated # is prepared for conversion to pdf (using ps2pdf). The pdf file will # contain links (just like the HTML output) instead of page references # This makes the output suitable for online browsing using a pdf viewer. PDF_HYPERLINKS = YES # If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of # plain latex in the generated Makefile. Set this option to YES to get a # higher quality PDF documentation. USE_PDFLATEX = YES # If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. # command to the generated LaTeX files. This will instruct LaTeX to keep # running if errors occur, instead of asking the user for help. # This option is also used when generating formulas in HTML. LATEX_BATCHMODE = NO # If LATEX_HIDE_INDICES is set to YES then doxygen will not # include the index chapters (such as File Index, Compound Index, etc.) # in the output. LATEX_HIDE_INDICES = NO # If LATEX_SOURCE_CODE is set to YES then doxygen will include # source code with syntax highlighting in the LaTeX output. # Note that which sources are shown also depends on other settings # such as SOURCE_BROWSER. LATEX_SOURCE_CODE = NO # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See # http://en.wikipedia.org/wiki/BibTeX for more info. LATEX_BIB_STYLE = plain #--------------------------------------------------------------------------- # configuration options related to the RTF output #--------------------------------------------------------------------------- # If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output # The RTF output is optimized for Word 97 and may not look very pretty with # other RTF readers or editors. GENERATE_RTF = NO # The RTF_OUTPUT tag is used to specify where the RTF docs will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `rtf' will be used as the default path. RTF_OUTPUT = rtf # If the COMPACT_RTF tag is set to YES Doxygen generates more compact # RTF documents. This may be useful for small projects and may help to # save some trees in general. COMPACT_RTF = NO # If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated # will contain hyperlink fields. The RTF file will # contain links (just like the HTML output) instead of page references. # This makes the output suitable for online browsing using WORD or other # programs which support those fields. # Note: wordpad (write) and others do not support links. RTF_HYPERLINKS = NO # Load style sheet definitions from file. Syntax is similar to doxygen's # config file, i.e. a series of assignments. You only have to provide # replacements, missing definitions are set to their default value. RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an rtf document. # Syntax is similar to doxygen's config file. RTF_EXTENSIONS_FILE = #--------------------------------------------------------------------------- # configuration options related to the man page output #--------------------------------------------------------------------------- # If the GENERATE_MAN tag is set to YES (the default) Doxygen will # generate man pages GENERATE_MAN = NO # The MAN_OUTPUT tag is used to specify where the man pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `man' will be used as the default path. MAN_OUTPUT = man # The MAN_EXTENSION tag determines the extension that is added to # the generated man pages (default is the subroutine's section .3) MAN_EXTENSION = .3 # If the MAN_LINKS tag is set to YES and Doxygen generates man output, # then it will generate one additional man file for each entity # documented in the real man page(s). These additional files # only source the real man page, but without them the man command # would be unable to find the correct page. The default is NO. MAN_LINKS = NO #--------------------------------------------------------------------------- # configuration options related to the XML output #--------------------------------------------------------------------------- # If the GENERATE_XML tag is set to YES Doxygen will # generate an XML file that captures the structure of # the code including all documentation. GENERATE_XML = NO # The XML_OUTPUT tag is used to specify where the XML pages will be put. # If a relative path is entered the value of OUTPUT_DIRECTORY will be # put in front of it. If left blank `xml' will be used as the default path. XML_OUTPUT = xml # The XML_SCHEMA tag can be used to specify an XML schema, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_SCHEMA = # The XML_DTD tag can be used to specify an XML DTD, # which can be used by a validating XML parser to check the # syntax of the XML files. XML_DTD = # If the XML_PROGRAMLISTING tag is set to YES Doxygen will # dump the program listings (including syntax highlighting # and cross-referencing information) to the XML output. Note that # enabling this will significantly increase the size of the XML output. XML_PROGRAMLISTING = YES #--------------------------------------------------------------------------- # configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will # generate an AutoGen Definitions (see autogen.sf.net) file # that captures the structure of the code including all # documentation. Note that this feature is still experimental # and incomplete at the moment. GENERATE_AUTOGEN_DEF = NO #--------------------------------------------------------------------------- # configuration options related to the Perl module output #--------------------------------------------------------------------------- # If the GENERATE_PERLMOD tag is set to YES Doxygen will # generate a Perl module file that captures the structure of # the code including all documentation. Note that this # feature is still experimental and incomplete at the # moment. GENERATE_PERLMOD = NO # If the PERLMOD_LATEX tag is set to YES Doxygen will generate # the necessary Makefile rules, Perl scripts and LaTeX code to be able # to generate PDF and DVI output from the Perl module output. PERLMOD_LATEX = NO # If the PERLMOD_PRETTY tag is set to YES the Perl module output will be # nicely formatted so it can be parsed by a human reader. # This is useful # if you want to understand what is going on. # On the other hand, if this # tag is set to NO the size of the Perl module output will be much smaller # and Perl will parse it just the same. PERLMOD_PRETTY = YES # The names of the make variables in the generated doxyrules.make file # are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. # This is useful so different doxyrules.make files included by the same # Makefile don't overwrite each other's variables. PERLMOD_MAKEVAR_PREFIX = #--------------------------------------------------------------------------- # Configuration options related to the preprocessor #--------------------------------------------------------------------------- # If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will # evaluate all C-preprocessor directives found in the sources and include # files. ENABLE_PREPROCESSING = YES # If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro # names in the source code. If set to NO (the default) only conditional # compilation will be performed. Macro expansion can be done in a controlled # way by setting EXPAND_ONLY_PREDEF to YES. MACRO_EXPANSION = YES # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES # then the macro expansion is limited to the macros specified with the # PREDEFINED and EXPAND_AS_DEFINED tags. EXPAND_ONLY_PREDEF = YES # If the SEARCH_INCLUDES tag is set to YES (the default) the includes files # pointed to by INCLUDE_PATH will be searched when a #include is found. SEARCH_INCLUDES = YES # The INCLUDE_PATH tag can be used to specify one or more directories that # contain include files that are not input files but should be processed by # the preprocessor. INCLUDE_PATH = # You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard # patterns (like *.h and *.hpp) to filter out the header-files in the # directories. If left blank, the patterns specified with FILE_PATTERNS will # be used. INCLUDE_FILE_PATTERNS = # The PREDEFINED tag can be used to specify one or more macro names that # are defined before the preprocessor is started (similar to the -D option of # gcc). The argument of the tag is a list of macros of the form: name # or name=definition (no spaces). If the definition and the = are # omitted =1 is assumed. To prevent a macro definition from being # undefined via #undef or recursively expanded use the := operator # instead of the = operator. PREDEFINED = __attribute__(x)= # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then # this tag can be used to specify a list of macro names that should be expanded. # The macro definition that is found in the sources will be used. # Use the PREDEFINED tag if you want to use a different macro definition that # overrules the definition found in the source code. EXPAND_AS_DEFINED = # If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then # doxygen's preprocessor will remove all references to function-like macros # that are alone on a line, have an all uppercase name, and do not end with a # semicolon, because these will confuse the parser if not removed. SKIP_FUNCTION_MACROS = YES #--------------------------------------------------------------------------- # Configuration::additions related to external references #--------------------------------------------------------------------------- # The TAGFILES option can be used to specify one or more tagfiles. For each # tag file the location of the external documentation should be added. The # format of a tag file without this location is as follows: # # TAGFILES = file1 file2 ... # Adding location for the tag files is done as follows: # # TAGFILES = file1=loc1 "file2 = loc2" ... # where "loc1" and "loc2" can be relative or absolute paths # or URLs. Note that each tag file must have a unique name (where the name does # NOT include the path). If a tag file is not located in the directory in which # doxygen is run, you must also specify the path to the tagfile here. TAGFILES = # When a file name is specified after GENERATE_TAGFILE, doxygen will create # a tag file that is based on the input files it reads. GENERATE_TAGFILE = # If the ALLEXTERNALS tag is set to YES all external classes will be listed # in the class index. If set to NO only the inherited external classes # will be listed. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed # in the modules index. If set to NO, only the current project's groups will # be listed. EXTERNAL_GROUPS = YES # The PERL_PATH should be the absolute path and name of the perl script # interpreter (i.e. the result of `which perl'). PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- # If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will # generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base # or super classes. Setting the tag to NO turns the diagrams off. Note that # this option also works with HAVE_DOT disabled, but it is recommended to # install and use dot, since it yields more powerful graphs. CLASS_DIAGRAMS = YES # You can define message sequence charts within doxygen comments using the \msc # command. Doxygen will then run the mscgen tool (see # http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the # documentation. The MSCGEN_PATH tag allows you to specify the directory where # the mscgen tool resides. If left empty the tool is assumed to be found in the # default search path. MSCGEN_PATH = # If set to YES, the inheritance and collaboration graphs will hide # inheritance and usage relations if the target is undocumented # or is not a class. HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz, a graph visualization # toolkit from AT&T and Lucent Bell Labs. The other options in this section # have no effect if this option is set to NO (the default) HAVE_DOT = NO # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is # allowed to run in parallel. When set to 0 (the default) doxygen will # base this on the number of processors available in the system. You can set it # explicitly to a value larger than 0 to get control over the balance # between CPU load and processing speed. DOT_NUM_THREADS = 0 # By default doxygen will use the Helvetica font for all dot files that # doxygen generates. When you want a differently looking font you can specify # the font name using DOT_FONTNAME. You need to make sure dot is able to find # the font, which can be done by putting it in a standard location or by setting # the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the # directory containing the font. DOT_FONTNAME = Helvetica # The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. # The default size is 10pt. DOT_FONTSIZE = 10 # By default doxygen will tell dot to use the Helvetica font. # If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to # set the path where dot can find it. DOT_FONTPATH = # If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect inheritance relations. Setting this tag to YES will force the # CLASS_DIAGRAMS tag to NO. CLASS_GRAPH = YES # If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen # will generate a graph for each documented class showing the direct and # indirect implementation dependencies (inheritance, containment, and # class references variables) of the class with other documented classes. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen # will generate a graph for groups, showing the direct groups dependencies GROUP_GRAPHS = YES # If the UML_LOOK tag is set to YES doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. UML_LOOK = NO # If the UML_LOOK tag is enabled, the fields and methods are shown inside # the class node. If there are many fields or methods and many nodes the # graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS # threshold limits the number of items for each type to make the size more # manageable. Set this to 0 for no limit. Note that the threshold may be # exceeded by 50% before the limit is enforced. UML_LIMIT_NUM_FIELDS = 10 # If set to YES, the inheritance and collaboration graphs will show the # relations between templates and their instances. TEMPLATE_RELATIONS = NO # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT # tags are set to YES then doxygen will generate a graph for each documented # file showing the direct and indirect include dependencies of the file with # other documented files. INCLUDE_GRAPH = YES # If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and # HAVE_DOT tags are set to YES then doxygen will generate a graph for each # documented header file showing the documented files that directly or # indirectly include this file. INCLUDED_BY_GRAPH = YES # If the CALL_GRAPH and HAVE_DOT options are set to YES then # doxygen will generate a call dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable call graphs # for selected functions only using the \callgraph command. CALL_GRAPH = NO # If the CALLER_GRAPH and HAVE_DOT tags are set to YES then # doxygen will generate a caller dependency graph for every global function # or class method. Note that enabling this option will significantly increase # the time of a run. So in most cases it will be better to enable caller # graphs for selected functions only using the \callergraph command. CALLER_GRAPH = NO # If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen # will generate a graphical hierarchy of all classes instead of a textual one. GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES # then doxygen will show the dependencies a directory has on other directories # in a graphical way. The dependency relations are determined by the #include # relations between the files in the directories. DIRECTORY_GRAPH = YES # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. Possible values are svg, png, jpg, or gif. # If left blank png will be used. If you choose svg you need to set # HTML_FILE_EXTENSION to xhtml in order to make the SVG files # visible in IE 9+ (other browsers do not have this requirement). DOT_IMAGE_FORMAT = png # If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to # enable generation of interactive SVG images that allow zooming and panning. # Note that this requires a modern browser other than Internet Explorer. # Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you # need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files # visible. Older versions of IE do not have SVG support. INTERACTIVE_SVG = NO # The tag DOT_PATH can be used to specify the path where the dot tool can be # found. If left blank, it is assumed the dot tool can be found in the path. DOT_PATH = # The DOTFILE_DIRS tag can be used to specify one or more directories that # contain dot files that are included in the documentation (see the # \dotfile command). DOTFILE_DIRS = # The MSCFILE_DIRS tag can be used to specify one or more directories that # contain msc files that are included in the documentation (see the # \mscfile command). MSCFILE_DIRS = # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of # nodes that will be shown in the graph. If the number of nodes in a graph # becomes larger than this value, doxygen will truncate the graph, which is # visualized by representing a node as a red box. Note that doxygen if the # number of direct children of the root node in a graph is already larger than # DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note # that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. DOT_GRAPH_MAX_NODES = 50 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the # graphs generated by dot. A depth value of 3 means that only nodes reachable # from the root by following a path via at most 3 edges will be shown. Nodes # that lay further from the root node will be omitted. Note that setting this # option to 1 or 2 may greatly reduce the computation time needed for large # code bases. Also note that the size of a graph can be further restricted by # DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. MAX_DOT_GRAPH_DEPTH = 0 # Set the DOT_TRANSPARENT tag to YES to generate images with a transparent # background. This is disabled by default, because dot on Windows does not # seem to support this out of the box. Warning: Depending on the platform used, # enabling this option may lead to badly anti-aliased labels on the edges of # a graph (i.e. they become hard to read). DOT_TRANSPARENT = NO # Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) # support this, this feature is disabled by default. DOT_MULTI_TARGETS = NO # If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will # generate a legend page explaining the meaning of the various boxes and # arrows in the dot generated graphs. GENERATE_LEGEND = YES # If the DOT_CLEANUP tag is set to YES (the default) Doxygen will # remove the intermediate dot files that are used to generate # the various graphs. DOT_CLEANUP = YES picom-12.5/History.md000066400000000000000000000151241471504570600145470ustar00rootroot00000000000000# 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__. I was frustrated by the low amount of standalone lightweight compositors. Compton was forked from Dana Jansens' fork of xcompmgr and refactored. I fixed whatever bug I found, and added features I wanted. Things seem stable, but don't quote me on it. I will most likely be actively working on this until I get the features I want. This is also a learning experience for me. That is, I'm partially doing this out of a desire to learn Xlib. ## Changes from xcompmgr: * OpenGL backend (`--backend glx`), in addition to the old X Render backend. * Inactive window transparency (`-i`) / dimming (`--inactive-dim`). * Titlebar/frame transparency (`-e`). * Menu transparency (`-m`, thanks to Dana). * shadows are now enabled for argb windows, e.g. terminals with transparency * removed serverside shadows (and simple compositing) to clean the code, the only option that remains is clientside shadows * configuration files (see the man page for more details) * colored shadows (`--shadow-[red/green/blue]`) * a new fade system * VSync support (not always working) * Blur of background of transparent windows, window color inversion (bad in performance) * Some more options... ## Fixes from the original xcompmgr: * fixed a segfault when opening certain window types * fixed a memory leak caused by not freeing up shadows (from the freedesktop repo) * fixed the conflict with chromium and similar windows * [many more](https://github.com/chjj/compton/issues) ## Building ### Dependencies: __B__ for build-time __R__ for runtime * libx11 (B,R) * libxcomposite (B,R) * libxdamage (B,R) * libxfixes (B,R) * libXext (B,R) * libxrender (B,R) * libXrandr (B,R) * libXinerama (B,R) (Can be disabled with `NO_XINERAMA` at compile time) * pkg-config (B) * make (B) * xproto / x11proto (B) * sh (R) * xprop,xwininfo / x11-utils (R) * libpcre (B,R) (Can be disabled with `NO_REGEX_PCRE` at compile time) * libconfig (B,R) (Can be disabled with `NO_LIBCONFIG` at compile time) * libdrm (B) (Can be disabled with `NO_VSYNC_DRM` at compile time) * libGL (B,R) (Can be disabled with `NO_VSYNC_OPENGL` at compile time) * libdbus (B,R) (Can be disabled with `NO_DBUS` at compile time) * asciidoc (B) (and docbook-xml-dtd-4.5, libxml-utils, libxslt, xsltproc, xmlto, etc. if your distro doesn't pull them in) ### How to build To build, make sure you have the dependencies above: ```bash # Make the main program $ make # Make the man pages $ make docs # Install $ make install ``` (Compton does include a `_CMakeLists.txt` in the tree, but we haven't decided whether we should switch to CMake yet. The `Makefile` is fully usable right now.) ## Known issues * Our [FAQ](https://github.com/chjj/compton/wiki/faq) covers some known issues. * VSync does not work too well. You may check the [VSync Guide](https://github.com/chjj/compton/wiki/vsync-guide) for how to get (possibly) better effects. * If `--unredir-if-possible` is enabled, when compton redirects/unredirects windows, the screen may flicker. Using `--paint-on-overlay` minimizes the problem from my observation, yet I do not know if there's a cure. * compton may not track focus correctly in all situations. The focus tracking code is experimental. `--use-ewmh-active-win` might be helpful. * The performance of blur under X Render backend might be pretty bad. OpenGL backend could be faster. * With `--blur-background` you may sometimes see weird lines around damaged area. `--resize-damage YOUR_BLUR_RADIUS` might be helpful in the case. ## Usage Please refer to the Asciidoc man pages (`man/compton.1.asciidoc` & `man/compton-trans.1.asciidoc`) for more details and examples. Note a sample configuration file `compton.sample.conf` is included in the repository. ## Support * Bug reports and feature requests should go to the "Issues" section above. * Our (semi?) official IRC channel is #compton on FreeNode. * Some information is available on the wiki, including [FAQ](https://github.com/chjj/compton/wiki/faq), [VSync Guide](https://github.com/chjj/compton/wiki/vsync-guide), and [Performance Guide](https://github.com/chjj/compton/wiki/perf-guide). ## License Although compton has kind of taken on a life of its own, it was originally an xcompmgr fork. xcompmgr has gotten around. As far as I can tell, the lineage for this particular tree is something like: * Keith Packard (original author) * Matthew Hawn * ... * Dana Jansens * chjj and richardgv Not counting the tens of people who forked it in between. Compton is distributed under MIT license, as far as I (richardgv) know. See LICENSE for more info. picom-12.5/LICENSE.spdx000066400000000000000000000001461471504570600145440ustar00rootroot00000000000000SPDXVersion: SPDX-2.1 DataLicense: CC0-1.0 PackageName: picom PackageLicenseDeclared: MPL-2.0 AND MIT picom-12.5/LICENSES/000077500000000000000000000000001471504570600137665ustar00rootroot00000000000000picom-12.5/LICENSES/MIT000066400000000000000000000017771471504570600143560ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. picom-12.5/LICENSES/MPL-2.0000066400000000000000000000405261471504570600146450ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. picom-12.5/README.md000066400000000000000000000101731471504570600140420ustar00rootroot00000000000000picom ===== [![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) __picom__ is a compositor for X, and a [fork of Compton](History.md). **This is a development branch, bugs to be expected** 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 See [Releases](https://github.com/yshui/picom/releases) ## Build ### Dependencies Assuming you already have all the usual building tools installed (e.g. gcc, python, meson, ninja, etc.), you still need: * libx11 * libx11-xcb * xproto * xcb * xcb-util * xcb-damage * xcb-xfixes * xcb-shape * xcb-renderutil * xcb-render * xcb-randr * xcb-composite * xcb-image * xcb-present * xcb-glx * pixman * libconfig * libdbus (optional, disable with the `-Ddbus=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 ``` 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-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 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 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 `asciidoctor` ### To build ```bash $ meson setup --buildtype=release build $ ninja -C build ``` Built binary can be found in `build/src` If you have libraries and/or headers installed at non-default location (e.g. under `/usr/local/`), you might need to tell meson about them, since meson doesn't look for dependencies there by default. 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 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 setup --buildtype=release build $ ninja -C build ``` ### To install ``` bash $ ninja -C build install ``` Default install prefix is `/usr/local`, you can change it with `meson configure -Dprefix= build` ## How to Contribute 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. 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. ### Become a Collaborator 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. 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. picom-12.5/assets/000077500000000000000000000000001471504570600140635ustar00rootroot00000000000000picom-12.5/assets/appear.mp4000066400000000000000000001442221471504570600157620ustar00rootroot00000000000000 ftypisomisomiso2avc1mp41freemdatEH, #x264 - core 164 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=34 lookahead_threads=5 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00 e#oY3k/ٛnybZ}应T1   !@@bW@l{@ J)iBL kAMJ x܍ܾ6~,phef}D~yqw ZqjG~=ywf4W ǥ5"zG`q~-*@fJ  /ꔺLZoeLw@6Bh*MH!%6965ci&[P$(fsQip62_?nҘk+ڇv]ӰU 9K$M ga0UAс*ÎLǚ6ZEA"Fh9͟L*FJ''=Vq"1iy+]VmN YW|X$a (;*ƅ➀A%p݅]w`N]%5N#|t'[ixtw|l$Jq㬚|AINyTmIp (<-]H`K<JK[|1B4xأHP KJ(oz-yYh\|V.o.S$XCBW/~­52{iVIwlP}QRbiɦ&'@m7P.Fz1dfϤ@/{VJc!͟+)ޡ<5TUn3Rvy,yX"T:at4ĀZv]GL{VS?2@5>56hͽ|)6ALYT#+pRTs.Hk!4nBƍ ,!4C]\!5sV v6j›4CXX#9  Wڪ8cR@#:  !Bcp}?F͡←v,G/u^0@L=Zm⅌diG1k>ٰHw/MПC| bՒY$ۿaq؈τl ?ŸVə>?7v( x8/4R{7 Ev!(W:~Lf[^2uU:Y(n`9H Hņ BSB߾ =;71`[JC^Yg ~PUL1 g?^vЁlG-`2BpEn nuY E04ѤBߔbB F qt #S'qsC=4=.gJI+oHl1xԃUa(d 9*+ jf+!IT&4:qî*7OeZ.4HnX`c9ŜfDY挈 ל#v/JX[7i&[yp&C;~aGZ^DV#Q?.D00 ﲛ e@KV.]b l|_Z*H|k:VO~Vdq#elH" m|ʇr]CZM AV1L{&WR9%e3~/+5LVh?Ɗ#ُ>pU'S+| HyvHZ2oFUoV(0 ZN$|a JjI2ݣeR}~@@Ec&|R(Nyꗵ5`w. 0s];8^ju|oNDa+0:5 5#@H@+x7(^oA$lG5ܾSp^<vZ'ELK)%z\o|@[-UKAqA?zʛyeZ> $ fծ&jYC(}k"iS:1 `]RE2 ?n3[qq6ȍ Q<W*^zӵmS,*pABx 'A^[B-Z_S?jLTP^ m1-V2KatDw=j7mږWP hhcjDd#ۦʃ>d G |WB V P-a-a8 AfIAhL; ZTï"rsRp.^ʤ:Η!4/!q"ZM-*/~D<1iB~<&0ĩ=QwRAVjD XmlSD-# j PAI Re0# s;֞g yӓqW^Y0badrP. e[R뷺t(LqAE4L_ 'lC$FvI/" }5er,7R;7\PtD*]uGOsXI/4] +cjD*ZHc.qU:E2N,٦s}ݸ,bX;̎Cϣu'tT9jA E4L_ 'f^]6@8/N9 vV󆸲K1cʁ!0j2ziBv8mS_$sq\V눗~F0@0$!AE,_ '巃-Ѣ)WaՈ Ҭ/=h^}KkcG Qn-Xs|`HH)wg$B%) ctD9h|U 'U]V8d.N$AyԾjDz"OT 2P.rV:HpQUe (JtIS+DIH|2kzٺT0AIAlLG@̉;6N ^#DGejEpfP~W߆sQ'j`V`~w9=b䲩PLLłx)˔wGVB""5Xv+8y#1|;5=i`H=qtTӜꙉ Үvc@AE,_ '!"I/1KAX?!KL  UD_gLvVlyYN`uŎ-Z'SϬ#.F&9ѸЬC:D›ߡqWQ~䭦2Z.dNT47\@I/I]Ĕ5^;ţуTo&p @|jD|YbBn}GhAX`0ۀ 7A#IAlLG :<.#f>,?TtETO5m.(  ?J`@tM5) };%75Evv)K\w>b^OIʣ%-k$_sF$+i[cP0ãH PV$i@}5_B6w~$ en0ILdM/pu@c"P 1!(+úHEe^9\d_Z,dy‰;4ҧT x)gNS=fܣrib@ Yyȁ/v5%dɴ1RH:ef㰟Te9ZD3kCB;ajm`P!q-b$&gDC}# 02?ENJ2U&-=*̛\Է 2P='X$J4dG~*5wMR&((̱A~"2o{)~wo_O3XTXKbya~@x7!y%.o ~%'c̪ZX:d&$d߱9sA!{Aly:±B/RY҈ r@ơS^su?”0܁q)Yl9OHM<#DDGx=B*v/?fY4 )jiM2-U~Ln_rB{0ex*eF\]U/9vy{ʎD <+ icxK=? yQ_CE?Ԛ[iGY`1 *Tw L7c`(zD< etC$jx<`r옞pGNfNh䮚 {ZІjwp1&&&zGDys|O[(mMo@ZOP7a|(A<;L0]̚te#{rtKjqN^!ydw׾VUf'1󅟤O$lZƖ>5@ȰcKsc[rQ7qN J/9M"q|[jJ[:`Ĵi!vyj3Z*M^hA/U%%^aF)_9RP'X ݍ6dǥiCt "}_ o r [&ctl*L] eT 1Su͟޾oeum? Mou6%D1;d3L>];),k%wQtS5Y c˥ZƁYӁXP;E(Z!vmԦpRlFUdZ+}],X+u͠M=pK(nH"-^ Id&Za*9-?X0Jp,>(B:Š1e&̥cthc_*dQ!'qLeҘ!nW,@Ket-`~ #G.\]0%d.pk MI`~mSavvόI+ '4k41o(*)$uv ]Wp 7=4C6R8k8JCk>y}6/95D]eH{'z9;,`$jƲhX@F@ڦ4E~ZxTb3C(oq4UWbG[wvHV/!K^̶`a9I✕$':]RgfQ=-^®aj}3`5>Mc;K9?oG%CHqlOd:=Fwr"aG G2%QB !B\_>v֗x9'eSrAH}AޘBw @?2'^Q9Q.̹뒘b |\;c0my8x;++SmLbC4ILAV)ؐe◠ 2J܅]aΒ}=`35Rp{)L]I0 3:yД6sAVS7o')JKF-P3B~8|UXƬ@pf⿨K1?yBZ48;䚼(^㘒zfwS{Qx:TKQmYȞх:ĝMc0RCNfwE'&OU ]X2(/kyFjCXƮM'N-"baWL'sCo`gCJF3#HZqjTU*kPrYK7W~.A~ D>; ? ]f/ )83wЮBqޭ< f ʼk{ _io̊0ug7a5erوˀS]K\>1p=?OL(0} ׽0v!am(RbUy&@0mrXs~$!+i@ ,>9y4y: Gf1R8,~"eGDfB9M^WFK:X~Msd]Ɨj{pk_*na.亼VБY8Xl*]78zR@&ܺTsk.vbF8t55wo5ՙb >LsgKasҾOoM< l/@RoB8HhBQp3]XB?♆3\ 1ȕTwXHNI̷wJxg>hO@).4.$ e8JsaǜdP,z?մk{3~ʳIk ek̸SAE,_Zf͜Jگ8'1 $zH.Kf9! \k$;i1 Xz \u+Ɓ_kPKMټFlZZ-˱<>NT pA.4oE:=xG&-\Dk&3_Ko0w- `ҩM^\Zߕ/\LyXtgS3uєʇf xy0-Uv$+_$Iwif-8|iG3*橪ug<4Xcǐ-: @ԓeeb$xLxNP]̑8>]2ˇd֤}m募LyX;L;PQL?-IDJ? Rб6:V6xJ}ȁ,tvL^yˡ0NLH]VAޜ(+=ԅKic`e.?ZtDd;*F hv}K (N*^PQ2ԭ|#4^;S;3ƉO]c+^)X"gjL'H16]XDL>קּYz޶-ftU K 8_8͔ջ\˝ x"yIG@l@Y,6>-u]DdݷFhOgo ;4[^{AߤE N;3eD_/keFP39у{Q ].7 E `K"Q(bٞͬҫ 'G#9dp2`^wP>XhDq4z0H2L`:|~q>pz˹#Vb0J3gV>a7 *bG#M>IFOw| ⡑֍c A$~n9w/4~(W H_%M:j 6SZnTe,bp2ʒe.9A$}5rm7 [xփG*e{v5CFܨ'@w1}(n<55euunv`dDC3T"FyK䣭N "Fz+=w9$}%P@'_X#f4%zO lQ%'y$Yvb훵*lIgR&ߪ:(Bt~dhm!7ډ*Y|q7*>wfJ[:FAT2aRtwd[oN,sce0Xv=Zy<ąĚ#2<`R  ' —9ugU7-\6/2/'C8ϙ:[X k2d.ˡ%ג>Q]x)X$ҹ#dsC3 LtĨEVDR9 x@`ݬ,+`y7rf|[kǎB!_NՑLӻ>",O ^,ടys=NST'%]ˋ9'" /Z7n\| \%os16$?~1zeT^yxWҺ9ĮE[ ռ$Óa_0P Z8IDO{4Uٷ0nE_ˣb0TAZB%v_o]L'S^`}20ZwGJX,>?݌Ɂb%lhyg N_>/>Q́7$slP5KWh(Gct#%Jq'o|l( . KX-nE-3! ₅L ;_&pxՎ.ŻZzG`P jW"ۯ.w3hGMm+{0?#1(OC!{%Эm.J7We+~ S&^Q\}SΑ2=ʞ_4~kc M:<ܓ A`;'dE B>}k0VXz-y~Sj\$A{PYe d3w &FA "-AVMLVeK'0d f}%-M \4X\7tT?Oȹ =0"%~=[@Q*p-PK+7~{GD Ig;D&a jaC;*bk ]&?G-pBrjH #1j$"..-")$b2#ga\ []! "pP=,J9!*O$k p;PH<\֫84B7}fHExb2ߟ%AøB)=sMq(i$DJ @* F"Џ{2d,pr3q-]9]1bk EJ7"žVA5ߝa#o7@u[ímI1:Jja .gS/U*r`}$χc#ϵP✁LMVWW +<|`iVP{^:T^[}A-yJrfTkʣlIaN 0᬴zR*Yg _JУ,3eTF=kOX@ )sp !:$3~VjB USSTkXg1_roo,2SdJTPH_䄩 ʇ즕!?'#/ _\({r[=؇lX3"LJü-qq"tE#L"0+Xf"9qC~ot:\$i)  F2/Y0} \8녰Ϙ,AE,_U誺3hD3U1%BN'!'@"ZsTޞDgT As 1 {D1>܏4'J C˩4ظ'EBWgT;z1#ӏ5t:\@ݕTh,`~]y93A_bݖęjKW JlO (l&~eaߠfS܍Z_+&rs[z9t& n)RրO/Mmgr0@z[ Ac+Y0Otrpe}:0BUlKB7ˊK~ U3R~aRւ<_S`N[gm$ܹ[*:SEkslv{lG'Ao%$dD ^$l)] &!`aJuoVV_C8c:FP539֮+LxPf O4^c (l<]#QVjDe&?hG_?z0jK=tyW ~1%)S5:߇MEC8kt':^Ή8oj_}ϒJ$|9y<_=t?6vn]}\$EZ?߆%}=] uY|Ŋd eg5?UZ" 隽xO׳kyc]=V|biFҵ`={? !&ࡴ1µi 6 -ށxںiZT;nEs>V)ǁ{&'yhtA&t&p=`|VɝPmIGCI$S*~CE5H|a+d>.pSAKB[!))G!htmd>8o.J?%~c/tixZX_۫4 l$zH_7|Jo0iwє(P[&} @b c ::Eq>(u"L ǡ4a%Ӊ:AYGbD>7e'h9D^.;Qȸ]\v @tfn~HW]^h\qD( ;yw5v&u/V HV;Fnx>Hi0I ;aEL5E0C2H%Db :c9[5jY'z=O;T%0Ev\·{Cvr3}drFR9;ȉ~ o_jy +ծ2(Y'Z y1x?k`O0)ܰ/j]0?ו+DAswbn̪BgnOkhgԄ2R!>KE\9RQfĶ ?Kh]DoFmoH#…k9S#xAzͻw+}D.f= ڿɥ`[\\询߫Y jIø.w`XxEk@zYWKX.H9Ɉ+WZ`*jGDhS=h/%y˃%c}Ρs_@E x `}cZŸXǯ U+eJ-(xNWXm-U W 5_[nwQ42w.=7Tw|y}|jŽ6`u`5Gᵀ:EZ 7K.{Qc6"x1 Hhۯäb(zN܅@hO%0LJۏե3S1Ob'Yn\ȣ]E09"䮐rPr+ȊuZq/-`8sqx>u]8}N[E_EBnbkr,6UocTJ8f{oY觀9OC!`Zu)懖C{UAJ 0[?zQnı{ 'PΞ'8bsS7am}P {K] ]''uC?JG}X3v6$7f5geB8/Ca{_x*E$P#Fl~m[U ?ˈ}x7s|;JÆ}<|ѐVQz.bXǸl[L#Sv!4~o4t , pc&Ē4:^h>o:qbZ}&uӟږ4@ 帢\U!ވÜ"|gQs.;A۾]xDԕFUr~y`K"R-CQ]޸وѓʏ)encԔM.>iAZe"Bakb_)!zԂB }ĿԊs2 dvC5a(#tC .1V 8վ,ȯ0w?p(AE4:_ 6LOoL %A"KCBV>Γ[9&,d2ޘ>8O0EDDg4,_R 1 &E@Cȟ>.9kӓR̯ո%}S*G+Nu&`su88GCd#Jf(k{`O<YT a&?S&Rz&A؏Zc īpr)T)c$!&{!1mCR~oJ3ݥĄc| Y`.b}uV@[5 G$PQX3 ĽvXiC>hPSIx>QөOPvL:aa Pk˜z٧DH F+^)a*Y|b|K^NTE{lkB PMZ@9nVpc CLu}Jdtm;! sr:]FXb%ҁ]̾b~eBIr\C4Fl$b&$x@|h[>җQLb8}☰6¢{ )IK鈨v5*veǛI&2؞-J"81HxmHWs~( צ1F#]4#nk; N=3nקoU=i4_1҆ \nՅ:V,$BξXr"潡OS(_/۪X߃2ЉlAZD UPnUCBҔ#2[d.܎$  Sv-=v~Yvz$ypxK_ cNՂ(؅ 3R@_2 njp6¯+_2ZHoNX)5pK0Q 76#ͱ"ɕ>O14ն s\`))NsjVu34:RjYqd:WN3`odR Ĵ6%8 0'@D{*OǾ7Yj>zH-:QFy`*f}%$tɟ%]~KTp.$8FfU5^m^SkU^ߌ[.lut@@=\,T[G޴d|K` ?WC^S0dfYaRyX"{!y;(GŬ.6p hSm ͤLR0eX d$3\i`i؂ ! 7}冼U0 й ]CD޿Nؕ1F7d˅˔Vߤ0B%^/q<ᙻXZXQͦ!1G =LK:`:`EKwU{TߛEk7" `(@b)n9^:>),rlUo[k?6oغT"g@Ly韧vtg~zU \ qc9fŷmѵ}׻^ހ4io$27,ѭV @u+ΩT")Zm:.:(-H$5tVgXq'M{v8C'2*{Pk 5x'B9[/{Ǽ {^&6M멏Vs+z~:Is2'X_]Jo2Itx"xfDX2;X/:=v-Wwb`hc%jIZ0IeS Ert= ZӜrY|[(n!skn%npdN1r#%LK(4b;:ǶAtH`q|U:e{ŜƍgLpMQ@7$ (ٜ:m]S60$JY~wt*AW^M:L# 0u-/~ꕏQ[UgPPAxA61yC%;2'2K-v*d~3{Q=Yƀ:pl9 |s ljpfO1\T]Bd&6͉ ?a*k6ӠR]LSM@)sy(TJO<8NIY;@+`9B Bj:%}w4nȫpi$ A0V |A E,O K*:CzsBR_(f['?fjp<[D܋n,qq.&t dK%J*(r!/T獥qo$Z9 }P;{G&RWI)@3}W)bRKBjt8u֢ȃin=njtxV4QFԗ*d-=O$#eVw}\;MtڲY'b3usWW֖#|1g/)rZa n!^KMK}H Yh 4N9)Zfo{>@ 1'baΎhSW/+I}({ѥ"7ZgAPE,_ '܈2>Ͱ܎߶WⲘ@ ,Ŷ7&ۧw2Nd?9q>N՛rrcRKG)cH|P_SeotDv% (l&S]>ޏ`rMj|ۘH)mN~ }XqjDh+2 f5m:dӀ@3.6bZ* AvIAlLG?>uqT+T6,{I6ySHJz.`#{<I ~4 LPo[1Y@wAE,_ '俓:Q&Z-V:M!z#N<]X[#9I/`UtD*dUX%OEe m)y:{OjD( ւy 3ˊ!+AIAlLGI?kLV6 ö`b# =U&0 U!հkOh6W&BcK0 8;Qde!9spSf^ypj3ƬzJTe-|ߊ N9 wY,_<#ߤȗ0+S%UlqoځAE,_ '俸}XZŕ @PǪJC4HA F <:++nt>p 'S+2I{f0}f;^tDCe 7DZѝ 4MjhaTFxx2jDRkDzxP! =;'@q8#["k+l^UxbkWlmTЗ9XХ$T"4W<H8,,pz`Ճ黥.-$C먮i1 !G"?KUcNI3o.GMAIAlLG'յ8ÀV8 lm79cp0"0uRLl`n\guizfd3ouL Dx0=􊊽Ӗo,c6) h@UiM5Dh@([OmH6{!$ɰJQu0#0AE,_ '俸}XZ۴XuK$FCȼo75Nb-HL| ִed! C{Ji?c;tDCe 7DZѝ 9Tƥ bw8.[NjsTi=jDRkDzx%.9"ŏ‚[0d8JiT? A"IAlLG-?Qh l;#cL톤@`eRxL1(;BV{aPC7cET,:tw'(ma|b,/Qע!c# qJ?0‰Ō1`A@E,_ '俸}XZ۴XuMHl(X@ ր{f(>UB\/D&6K*xGWkO7+!$?ӭv2+;״T[,me:ҫ@Eѽr&S}Z*%Bu@(5 lI`89SLxI<5L+/ŀށZtDCe 7DZѝ  -Z'qi)^JZŀ\ajDRkDzuAxk1+pXe9`A@ 9AfIAlLd_xJs7Do!B_LY'Fh}W|$[́-M1w-èf*]?,\T[nF;p2v4^"|7-KK`9?/RзT{?]נĸba[@TAE,_ '俸}XZ۴XuMHl(X%/G!Bxb6}+Sʖ‡1{K Jjҳ+kLnP$vu| v@tDCe 7DZѝ * fl3 Ѡmķ3֙ vK.voPfqKk$7蜛p:#v-JCȵDHA]R]ā3i|yFq6ou6\jDRkDzuTW9se2Rj H/vAIAlLG @״# OzI⮽^ϖr pX ZFek ?޳]1AI Re0!\:F9>ٷꡚ/)r66u Jo37$ҋ)qe>lQj9#ga}@UNRayP UXbv`oj_@O:֛ݑ1X_;WVP#ZX񑘪.AE4L_ '俸}TyR;qk<⯥*t+ԏ(̲bыS Qc[ԀKE3vBʻ{!әzA2󉐝}tDCe 7DZѝ Č18IW7rk$;nӝiϴ͐mBti6ZCi` jDRkDzu fN;\5 "{]oKIw'{Ѫ+UXxYHjn{6Nmgor@::`AIAhL:9` 7E nmJꢾxYUc:OVӫ^&*{Sp_Kky&?DG|7V/ e1GRv.o(nxZ],; 8?FPf]=i H6}-7'~~9$Ċ_xy*vwϧe!۲KMV%qRś0BJ"-u}` |C7c~me{ ?'R(ȹY`A,E,O KsrWa-^{$Ye¹zm:vh0M%*kINXr!7\8X664=v2yZ:ԡu5CGd-I܁L\*]L*iE:Żb}MjDR\4s%~ ) uS<5*v\-xk=e9F݉#MpI:uاf_AOIAlLG2(!Gh5J;D#[I/}A3yHĪlFL/[_nC׿( N\aJ I`I%#0)A=P fIעm Y{:oUUUӌAsI Re0!7n,|}FAD2B}k-v.+[f%W̴aS*bHhwͨ~nj2}bKȠG+Û[ה P̱Zn* *vٚZ;T{ڹ>w`2U`(= h]yNggR0otA;Fp n0J> bOҼRE$M=BR"[>@lD IU"1-֒I`HLϐѪI}jYj4fSr+ҮQ !e+B#=z,= 9c:-qԼF=5DRg{i|&gyAE4L_ '俸}Ty8O,3xK$~#,fY-< QCev;_Nr+2H۴1VtDCYʍ<iVry(**CO,OjDR\f\i? )pu_AIAlLG$kaY2\d.t(ʾcAE,_ '膍MKȤ3XM_}3҂"7 ,שǕ pJ8tDCYʍ<iV)ŁI:jDR\f\i? tPA?IAlLGkGO:@{aA]E,_ '俸}XZ`F>Cev;^Ȍ<*̀"*E[p0J|tDCYʍ<iV(^*DI~jDR\f\i? Y= QAcIAlLG Hr2 Ջmp`AE,_ '俸}XZ`F>Cev;^Ȍ<*̀"*8BHtDCYʍ<iVMHjDR\f\i? UAIAlLGKi2To~fMi}`AE,_ '俸}XZ`F|gTZ;Nz >8 k\vjPmJtDCYʍ<7J2[AHjDR\f\i? AKB[ pq#5.77ųWHBRjK!UH!is%35`8#Yѡ潿8e1wt;ϱIrYJ9jtAm{檱6KL) G&aI*:}֮8L-ʠ7P#cA֤]zVO|wǤ<]k2?nkWrU/$?*>KVA*Tc੄9X/Ve~_ߪ6TIQ zN5h8bY X." p9jͮٛbInWH^;acfs g/\p {ϲKV/-8 {6^KoS6UkLѭJT^!խF2͚0HH[+kΥٓ%hSQ-Ӧ'a^:='h (,'f($c2C>}‘V sIZ*uԶ ^03i^g6gЛmB?*Fm@bz.v'(95t?aTfp_C}<=:މ&o#^oY}0,ǬxL @sN>3OSc\6JWv?d+Tڜ_}r uN*CM&C6c9"@͎W*hA!Np>4+unC*tdNos:@O AlC.r1Ř՛EAD`4H<ƕ4GfrOb038 գY w2k$^尜Io&(|IJ_w0G}A E,_ '܋#Rȸ!Ȫ6XeLx`@5禞&¹4isrJSv5cA.5LK(tDOuqNf*jDîà%jUj2yz3<;V\T%0A-KB[ ID73w$55N:0>6)(Rb!P <=)4y w= WmJ<qv ϥF O~Pg30:5^s8F2 $foՙlB*E|Gc%h;^1 W(zv1%2ɌeI!KqtbnM),6!v̎˚_N}}t/b3&Ğ7$ &7 ɕퟵwɐOvUUMKI! c7*QӉwRqSbt)elɜƂPL_CW>gk# 'c̶ C_.}gUZ6KlJ]jO 7{y6x-), e^ \ fI5D>5cm-z M7VtR(KRYrCI~5 WA O݇Q.H̙%%x^/? G<)1Hb-|F٣Lj%*KLʼnۯb}g('0<[lKn^3R-,Svo?G5[vIJwb晀ȮN@=/R'tc`Ҙ .G;0<;`/&@4nڥ~Y+y.`< 05.qXjbG m(:.i܊s eRӃ҄ aLjD Xѫm<vf]pSAb\0HT0If0ީp^%jwx )+>"yCYd} *{TQ钺4S*Gz.,ze ]_&lBL8JeAUp ƘP{]+fAJkˉ{w`}".+[9[h7k`]Ysw~\G5g9a%ji(j ^_E6Xr]7$wmwǴ yC_슳־E\NX9W~`&g N*wf{Ҟc`(%&'J2}P U 0WXe%ANKB0#70x? ` +:6& WO}RGm\K'+o+ #U&ưggNWim m'q(`jO ˶.,~rLd2ЪF5s5ϑ{W - G`*-HꛚhFO}G'kr.]L2\,TrX ˥*l::Vz(LE7OW9˜Y~y|g-xIpř6MZȏ*fCSsJLwZSp.ʒf t([óN>4/H<>ѩ.^0: VzX6EC P38ndV+$R`#R9܈QuYm|bZmY>M>tyZo, s鐭JuF>oȂ3ku1rY Y> Xu0oiܞ sVw:MP%T . ՆʙP- j{,O{ۯ.TK{ Үf%f_NhbC0b@q}ApKCeA@0!htmdb@1juWclj2e8(&BGJ(*֪tϽՉ#Q'--"is%+[Y 9Q63 F_[2BP,\Nqn}L4Oͅ{ΘhK'MnMkV@p۵T76KB)A4 Mxt>\Q7=`@ܬ/jUf12Sa|AF8B&Rf1(JQJkآ ݃AN `z\nQN/ чfP}0l%ߘB2}9?[`Hö>me!{PLAxŅv,q" 50|l9ZY3|g^I:H0%| zCwjt$8A# 3͘eI˂;n`6$cjWƈMÜ42VI\fy5ͱ(_C,E~[Kx1@6܎4W[.="Oii-j\lXHj I{ҲN3 Ԁ' Gme["!=TGEO , #VKt];b⬣@Zv*olsqjJ HK4THjRإqO| '\j?磲z[:Ce*C,e/;)ritK)'tFy~mL5Mfx;B-K8,5 {Nno㮴no_*hŌjZa#f)ڽCV@]sL1|:L'Yj)&b~Zc-eO=#]3%FA2^٥L_MabwVڥipMA#ΑH>xcŋ?ahLtѸH?hqC/Pl`9P?N,I7 PO_Y^EKw[pM^quC]S&S)6Й!9T 4J& 4(kEc'S]'צ+I@ÓGrV-VaIaOڎb!Ulh弬c÷ttr..@kCGϹu┹b(xA⨫5g8(X"s_knKh)>Z{riTt("n7˵dHQ]¹>j4ӫKp%4-Q|SGAύqOoceM-y~!^9% ~UT6|qw2W6hQjDguu:cP@zzagv7XO 2 i⃒/nsJJ {LHz܍϶^T듮gJj–"VBYx~xF@+ 'f#1Aa4' t*IlXy-LMTXhN&$zZWV$< }C BH$Tœ?j[9hPI\~|5BuбkUs)e~ҽǜutgK  \T>ח/MA͇z.w/s]/rwZl&7'ʶ^#۩a2nNf`:S@ i2xQC_T†eTAKCS?!htmd9d `8ork[x*Oz_K &4^$ki3 &@oF>"J5˗0]RGaAn pubz yM=lI7FX(d_@۷{d~JҘ>¸,BoFo(/idݕ{.xvھ6TşP043t >1!Nn9TV5L{ #+Dfaڒؾy#S3_EhS!; iH@ fiәc  #L&Z'Xq !NhIO\US 9}+pS`#اǍݛ yԄ5Y-3ll+Zgm|<(yVj]M. `F^[_ևϩzP^S B>3VWϱokD;4#rJT' $@ԬI~H{es =5Ԯsiک +﫡: ܞh&be~B{,3M[{sx"fy0Qupj\8(Ͼy%z0ʥΔ5BxAr 6| FȥlԞiFB$x)^'|IJ; @'󝚈ʩW4?x>'㧾/k|3lod&_O/^ꪤ©iB ^mZ|* XBIqoJeG#2=<^c-%;Y_/kI 򶖢QY81Jw>0\sڽ coU!k\#RpqQgjpN-"jJ1{B%{"h!(!#=Ԉ2Ѷx_C,>#Hh̠jDdނRMS1 *GbƭLf#5anWѤaZ$kgg٘BN8ʂ}qHri|vU62 iשM0uȽxkmSs-akc4=,Cwr8coB^uIh*j7W)_-_c#`ǘJ<_UtãzdoU"wrAI&SWzA\I {ooFφ3_ 6-J_<9"g".cQ-tõ&SbvΤ\WtDUg:On[5&lٳfͧ(g"*~jI wzf~<줤qј i%LxK㤯sTAE<_]N:s,/(^)xܣvp݅K:ڇv}DPi6Şvu''JENkܸ$f G'XԾ¶tס{أ%k%Mش7p' Vs)q];ruĈme뾹gY?uy9)_Y>Sks}Y,3]9Ŷ %껤hQN+ѹR&nl ,DiMnJ־B Y*.2SF[h`nyQ^ vu &w5V%z'6BB-IMw q(ښ_1޷<'7T8H It0# a2E B12:W>w+Z>+nL@ |s: 083>@F ķ<a$$fYXuNAn%zM 's55v(`lBZԙ|p(2(0ROܰx&W37{UV\ȹsۦkA6GPo@tDc$U#*-yB (78ðe~+qٷX8&]K'h`9:m`AnDĖGS:^"vVCˇKk^ *؊,vVqQ?ۺ4Ch? )x`P`G9yD5|tp -.3_|tLtJlq~a@k%^5§mߢ}+g,{=<>MpY|v4}6Uc\<5R[ ""ڤ @}N*ϒضx7R>ݦ9d#݁q8ɵhDg"O|x?ʟ3g|eLϝ=Ci>p9 k3)d p!F IRHhW5ZrIQ +x,*_/9 vmWd7PV@BWkg OL!A *EE̶1-#V4@psRU`۳ ^ahquh?/ (SQj(S;w:g9IJ_(<c)B[YxF"t2ۗ XjdG<|< Ѡ`PCxCU=`K$0֜'J3-Gٷ Ĵ!!ZwI'U.Q(+{> फlbvdDxa\ha108dBpFNH=tPQ܁ %-n * `u$ 1io1s\'H@AIAhLGM#YJ'UK0hkdRWکvT]f֔Ā;Qvkk/1ƇT!F wJ~&Μ*yVz$_QK_P-Ť8ڝxy|4v"ڎ l s2 6%<}yLAE,_qWn6`8`P҇a>فD7tDv>fwـD9jD53d8`ʖA>IAlLG-Y-!H};2Y?ay0bװ|(qXU{uz O˳b6ad$W}<́D3;^yOs#v OA\E,_/k ItҨ S0[@zE{tD5 Eh8E}jD53d뙒hUMMLAbIAlLG5cHʀCAE,_/k It@@tD5 Eh^@@jD53/!HAIAlLGCAE,_/k ItC|9@tD5 Eh^A@jD53/!rAIAlLG-& $h5@Х;Mڒ϶sEPh.cm0t)HAE,O5=Sm7PW(jD53eJ#(v<HPQ 0:A-IAlLG,| Vf!Ns}:_u]y`.Bӓ񈬕19 ZSs[Ņ0Ǡ zPعNlFmaC7'瓦.p _q ѣb9QJqvHJ˪Qa%ϵL*% yD3N:b4hB5A$/gG^M-h J@uqԬq ỉ?P' AE,_/k J2@cEZ8Z, 1o}M3/B07^?psHKyfoY%1QucQ 5FC|6&M#An520$Ղ LUȊpʿ=a u.J,zXفRtD5 Ei36I~Vo#{ҖzjD53Qx8c?/6ɀgR{dibiͳwyhl0A 9ҫ|||Z! !7g^KyK#p/B`G/1دUGnhg6v!NklLjPvS vbS7P_-AIAlLGơX5)([Y\e_5=\/a]ng= >Gvv򭜮vn'2ǻ4̔۟`!utsήzMZC*-'GmN~ڣPa!9~K-Cvk1q;HbI\0_Y,a2,hϖϨڄb%K|1_A@guAE,_/k Ipl,q LgFaV__]1C}?rr`Q2q 68F׽G.Ξ s Go,)yo,XHjD53;$ns "^jAJAIAlLG2EAE,_/k I2vu@6tD5 Eh@ށ@8jD53b ^@HA=IAlLGEA[E,_/k I2gz{H@ztD5 Eh@ށ@|jD53b ^AAaIAlLG '` m Ue۞W`nU;r8OigEW&K?ÿPڋNm@ Ν0[Ґ E^ٝ61:Zn`jyy&+`wBͶf8_t';jrwKAE,_/k I'GY׊;U4x HDtD5 Eh &+@ jD53/q@F@7${D)$Рn@-1}V)5@9 dOM.?ՙ@RAIAlLGwDO3vI ̀AE,_/k I*HtvJ`r#J6ݞJ؆Jkr?&SqɥLZ!k"ZNRhAnC}dgZH/I@C&wHdͩ˰㼷a- ֱK14 XtD5 Eh AO' |-!{pyh'Qh%vuғ /,U) 켹r|$qvtylW On& J75@i rDĴ&!"=Gw<jD53 fJp@5)6 -_ȟ¾Hd6@+c;UE2o`rE%i&Ow5ԉƧftPrnf 97L݁M0XRi;/BbT~Jg"kAIAlLG6r,p|T\W![V qDAE,_ *<۹$ Gl@YA@&tD M[<,\A(jD53WGA-IAlLqCAKE,_/k I2g AjtD5 Eh AπAljD53WKAqIAlLX//ƁCAE,_/k I2g AtD5 Eh AπAjD53WHAIAlLLO oA@jD5=@ moovlmvhd @ trak\tkhd @8$edtselst  Mmdia mdhd<U-hdlrvideVideoHandler minfvmhd$dinfdref url  stblstsdavc18HHLavc60.31.102 libx264;avcCd*gd*@x'Z 2h"colrnclxpaspbtrt==sttsstsscttsstscstsz YtOlZuTgnac\{{lf[gf;dt ^~ !W4i\{YSbgm^``z}MmZScgNMTeNMUdLLYdNL OjvXPHHSIIPGDDLGDDvL[v_kV1yLNIDDLIDDOHVoHDEKGEEOGEELDstco0budtaZskippicom-12.5/assets/fly.mp4000066400000000000000000002317561471504570600153150ustar00rootroot00000000000000 ftypisomisomiso2avc1mp41free(mdatEH, #x264 - core 164 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=34 lookahead_threads=5 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00 ]e#oY3k/ٛnybZ}应T1   !@@bWxc@N646}8/%bb5u NJ p3751͹,ƗI츢7֛oǒWZ_\[,o1Z+ ہBITj r|p\0a\Nf]uu{/ QwCG7lϘ/ެȪ e %tXi.{% B.`׏)R؟c}8Ļq ;uӳ}Q< ,R~ĺ+i H`1P;aP7,b!g@ф.w6~~ĨuzX;֊$oρ^XBJEOO:DHv9{1Qw{i(ҧe\ŽD$"7  >G39Ѥdâ%0>Uy4.BءF!;˳w`$nEV,OռvĬZ8MI|ۆ[YD;bJ# e: *һ`,OvLG=oslBV4G;@{Y+-Mi@z=~KwTeys8@yXqk~Bھh5؅}8n_b)!_]pJnr^5ig8Y'Ѳ BtC)vi{-[{ % U7 ޅ_g#̮jK6d˖{ xox9Mi(G0kdVz&]]XPuŔRxb[zeܲ7{PVJ=L*9U؆O'4W#=%%JbhuqͶqM: xХ8͞뵛֎=KTDXIVwr 0\4OߴԲWxR)p^/>Nԃؤž /cS;TC+'uP)cT06W4qww|{̢!a4&Jqz|̔n0]븰u۩ɵ!>ʼn*w_j3 eV9u m"sʰB1Q ?ZW%m}HuzjU;8j>prS⌹OY@s:"YU#hna; K)96+w'c o`[L~KQՙjP"X:ld@@Y˝V=Ҝ@E}.?(lLYqO= ūq=Hr.!Һ㲫U| j5Sĝw4uYk5W)#԰6c#hdˇ^V YeqAnodt| 2gs/UUCq&1HNj5Yb̎:uc /J;'m-= 15q[<-T晭z B&wD}m,e>c[ոe3/ޓቌYnͫߕ%/JJ5=8.ly~G_X~ĒZ^;$d@;@⠏ I"aYjI*SC< Vv{k2EO@rvg~FViVѳ̿o9T!=YY4`/~9$>c] gkgP 8pQa֘4P>؄ȹs;eo8zKU9 rABx *m ?7Pd-R?S>gZ A",N[ !tQiOatD M[WR` HDd  Gl$(n xccjDegjQk| }@XoV >-^1VTAhIAhLGE]<YvPAE,_YPGJ{iWmh@j =!DtDe=~?BjDeg`AIAlLG;YuǠmFGHqE), ՜@XAE,_YPGJvZ\AJ @ c.#>b>فYtDe"7h9&w,rR@K`jDeg0N~7@\hC T׾?W)F*`AIAlLG}1\`*;Pӊcz7ե$-ېkӑz1Fd4)Nk|o'`zH9cmm-v'Q$a"Ҍ?RF+>辎_ViPAE,_YPGJ{$Dl¯z%/] pE-tDeviyՁB/jDeg%P@IA4IAlLGLdd<MARE,_YPGJ{$Dl¯z?BqtDe/ BsjDegHAxIAlLGMAE,_YPGJ{$Dl¯z>BtDe/!BjDegHAIAlLGMAE,_YPGJ{$Dl¯z?BtDe/ BjDegHAIAlLGMAE,_YPGJ{$Dl¯z>B=tDe/ B?jDegHA$IAlLGMABE,_YPGJ{$Dl¯z?BatDe/ BcjDegHAhIAlLGMAE,_YPGJ{$Dl¯z?BtDe/!BjDegNAIAlL5"9$ XMAE,_YPGJ{$Dl¯z?BtDe/ BjDegFAIAlLXS'h@-+5~__I-:W%h#E]jry}ΚZ1,Wu 9-w Z<>T"`Dx(&*b0k챙G[;&w*_e{z]T$)I/Ag :Uet)v6P=uד ǃmjO;^Py^!;tC\J+ϱwZԀ8f>(zDHr#O 0 hh~(jdmQMb'Fc$s-h}8$놼!#uDdwP*tf42K0j8Vl07@/=*CNAE,_.{1eb(V?HeQ?f`!$GB-tDe/!C/jDVgo+` A3IAlL  &{CI78@HVF:$jV907x k.op: L]63uN :NY2{լ%fn=y.Z)̽ĔӴE٬hº::Jz d[PSb]ײgByU[. Y]Ny^(igZ d"i|QORz8!QN7 (HLA{\^G5T0ʪO݉Is-}aHu\J<,FҜJ'uHh|KȞΙthy@g;K 7XLj QHao#:h>dn?UZ(B *~m~8pUט,5yߕʿl" 0Rc5HP/b$v$L59u_xH$IO ܙX}NbqqORl&$z:gqmN %̅R«d1<0}yovF$a͵i 'i)\geK_9pae+kcJ< &Zii'*z.36FuҶŇR= ,87C͂l:L@`NdTџdп 0HKxt oO˿yr,#*ۧ7#C9<թ| eQ*+fXNLLC2D"ԉ1-=?(%IH\:d D@omqRhMl./C^?0 LW2ŠAv#ШEe8_ٟ![7gvwreJ}svs|[Du$TZ[5R1f>Oljͤk sMb#&E8G>\WkԦ1:4 K?Vsr%gܛ/!+ψAB!6 7>kTtM yO OLr/P4"r$*pFRʘud=`lWI}q%`huZEn3V~a!rdϥoTn_C5ki5}jiÁżǒ2eظUX aSp^[lK6(;V멺TWǽWMr}4ԇP~xŸNv/[A ^;_1ܬN9w91#ZzFim9Ῑ23$7?3Of?$̎W D1 W\IQx#3;1IOS4wG@<9Ehe9Jq⧘%:g2WY!Elswm?Ń4;(d2Nr^N?oM|*>U ;c%F3Uܺ Yբ_u?^8}. )DC[.YmZ/{%^*F@aX\u: ?ҌvAHjFT(җn$978حZUHVRi @Q}YS\1-eZ\u:+6Ֆp&aD8e(BwQ NW/+ϱdcn}0 1,6/1ݛ,>$אQh}P?ڸ&JXR ҧȍ "ti2VS)VS&FD!Q[^,)L` .6SwgOUd1Jk=Ce\ EnHM E/i%MY°9Y: XzPŢ9e2Mث l>A<& 6ٴ ǞbsFH$y9ehOt'-:Ub|J2LvBi62xy5kq = mVln7W1_;xj}fvqTП\EG5,GpLG0l*H{Tڊaf'(M =1d?D1Tublvuw85_7[a|Է(πNo cszK>co"zɝTŘx6jj 0a5)#pouFIpLWP&8=A<Q4o;2P&֢)>m7PPx7M Eϰxz d)gyg0qŻ~1Y%.fGza=RK{ V7q U_.UVbD)9\s4]Ts>iDvɬLQ[NM,J KDhR?>M 3 q*X(lnb4$GB _S}]XޙYus/Y'|l'e%uC4@妌5>u-LDm9`OߨI o;OIt:u/i/j Z@`@fy#Y:`qqQZc:z2AS mu8Yz9.G !(.3 rjDzځu pa#7tko]٠NrBrnܣAXfRlIT]A/p{nb3]Eb ^:lEC(,ؾ_ƒA2A k++m,QnIlS( 4>MôֲWZA!n:/Ӄ3O(aT&v~4$XY o 69pl!C߄0flB=l̄g6 $32iJlcq^Ct#` E"f'pp{}rPk4:st& ήkN-]/X26U8lUk~؀3\Q:竱ɑTl)KYFUsJr - x!l^]61V&q]:i+8 #;7Nc8mq;7M%5.օ+'{ܕ.m,tz, \S)9#UHd}C9֩eU}@Qc hw:wR+: "hQ\|:(l-h&d)> j$PE<"e jV7P<بҐy@Nѿ1><,83R@4J~o+X^HC6ro~nZ4[ VW^BΩ!w<PCۥ3ČZ:Kxg[IZj>*AnbJսdK-]º &}gJtgm)i%\ږzrbIKٸ :;:"U%e >34 qI%uSk*nIoLer9䃄EUZp5Ӟ=J/{bw?Y?2Ǻ,@L,0XP]Y砫Ld4eԘ=Aˮ\*|FfJ2+ taIm9{=l鞤m_NOBf넒ŭĻE0ucwh?4Z|Mj1b2g FY I.q9O:L@hQx9XG)K:`&= ]{RœajqFG@U73p+cunvݑ9zpјܻlO%Q:rOAuqFpabBzƼjN_O^UmNsr%ÁHB>#Eiϟit,ឯ55|##to`&t=<m'Sfy0:9*m2};"Z+ ~(+`bDGۖ]]Ɛ79R.S$ΪI[)%lt#.$;3 ij8-b$".]QcX"1MH0B "c&&~hRt4,M8ڃbyBПajaqBBgV`A mOHLa b  AuIAlLLO D%QT H&݀0ѿsуkcV.-d5Uu %* V(}PK{U9|?46 VeL;$'gz6G(le2C+: ໡q 8d?6Q,>b.BFxHR':\]ᶠIsPh@_%t}d_ CmP5%r~|^ ˠ<&IeqL*(*K -ަ >zW.m[TsOVBOxQgDphDށisҍ kжLύr&|q#Mp2T-`I h+HhNfP"bg󦙲^C2 $E]cI)*r_$)m?H:ȕ}xnw?漕XWr_}˛ş'vxT_Zu?e2LP?M+sh3KBkD).)r{XW=,5;8<n&FRm% ,ݨ+_tFue 4$LvY00P#UbD2ĉR .f} s.83$hSNg΢;LΊsh>gv\DA78!ʤ}0u*cE ̤%CX( BKjʅ>zdY_kS5R;ɅvwBƒ;PSN'#@v5ޔ mા"e AB2/1fD.vr:LO3)WA9'm0^|0H79N!HG~}`akU}=KĒblzV Ŗa|9Drz8bb(#4NdU sh583F<;s#%a_ޙ_d3WKG=$Mrz2Xp95WP/ S_rꜵ-"$ԹxUȞ#oA4'3g5od}bS6po\dB~!S7~p*tڷy1)ӒrnJʫt,2G_;;9:\A:z3,.>L gF$JYXO97$7Z C qbԦEO2|kԫR sd_ 1c62 Q#>pć&[aE;]3V;@,b,ިPJ%_%˾WG 'W_ݟPIO|L\prZހfDL靘 d;6ѴxӃֶ #1"9݈xoww-w̪fg$}?+wA }@S i|M5ӹ|73-̝h fʈ~{Ku,!J{f{H ccϦ)J?kaJ|x z)ժ>d7C;#'GM"&Kdd{%6;GP!`[_Sm~nU8R#Law5ZKbL %Q\Vp ΢qqhp˅œDV/&qnXO˖`cb#ЖsՃFlNN$]e6RoqS2c9$ "q0:o@i1}u:~w"DD*UJ>Y:ԉzVq^0}fh4.P[(\? XfZZܔ ,sB1;x!jg!G'k׎ełs!`J@ok}ښnM&h32I ]i~@ybH}*}(i~̇Lݚ͠ "VLq2gNtZt^P&t? pJaPt:̀t1A^ Ii@6` whQ؎+ pD菴ku`#4,\ "1 \5ﵬ@yt墶=7iެ,5-$5]$&M>M`^p XjDhxs9mS,$^$9|02&{;Q="yK;eAw"zd"Sk`o-AYp5+lk+ӌȕZt<̊0˲|&# C KGkKZ9&Γ=xe]W*Dtjec+ޯρgu5B?tiJcȼ;zElTP@]9*~M qCz Atњхbc54"#Zn;jrdt.@".#<7[A0Blw`6ެD/#hYK5 ")-ڋTd\)?Fg~:u}[m T3:8"]#H;-,@'0xYh.a߂7${]V/RJmbӉ9^j|ώf]PJDgKNN?hFdVa/4! bTrg$.;e۵D6:!xJA]':ŖH?CSf^E rwQL_mʦxWW)@T'9]zֈqy++ A3!6!a:L03^ 浑i8<` y; /IJVAxoUؚ{ZU9k|k$1GXO|fH,WЪ;8ƷV$zm+zQUhM/H{Ѕ1Pn2ĿϋI7 }s(ħ%C{XVc?۲3vU2#HA/YA:?xP 6. lD]dobBH|V <2 Xc n&0j'ȭ)sXdomFz(pkDR\-^.sDE};xLⵘf+Oa:-JatHЋZ`:deJ+>O>ڡ`R3x+P+}n^5n#7(0bO?lsa7+cTFQ7,\Ç~ЀSFgl%ű;=T8GB&e#8eݮm}[ImɝKVRD[Q&a=8/._0g7VQ/@Yݯ P2Nn*B+1oymZZr(fp$y>J Aq,1\iԬUHwn\?U+' ` W· oAtwHp[ĻXPQKV4AD>oz.^Z&`,$Jk"L "iFݒp7:PPAI Re0" oX*S*`- X {fm<9(/svb ~zaR更p[I>fsh)ⓜٔ0 &&&>vWi=]> ~%PB$۟@U#:eC[Dͻ S'bá;p 8`RпOWqR6yK#rVDȈ {5/4#Ӽ|_mF:A5鏆@|T- B,1Yf$]|YH*~z6oq3bx-| ,pಶŃy2VD᭒_NBO6RNH@azo,mc SI7]ǧ u2@_K_?*6@k2([ԃ(h/Ά]F!ZY 7+{uW^D\3E1 ;c[2bdGӅJk}4!k̂(6y˱C#xk7i^,m}똯NQvU 5}$֗}CDWH3:U"̪yэI dޢHt4qLG- iPxw,Lӯj:>]=CEOv 95H_[nK|@{ .tO*o}#˘6&5VĤ+7L "IL#= ].-kvTI*r,SVYi>0mە n^1[Kp.ɖPD/`0z~Xn=vhB?dDzZ `.|w,RviXn@e)+F@$PxP*#`h* xl5sD-KY.;iAfvȦ? ^3RlPbqtn/$tepwyM$8EjLRrY/>)os+Pj!^B]3~%+ި]n3q>Ws#uF *Lj ~?$ B[* 搧k=xIY=oA#m8 x%2-L|N]L Uo*g *,bDy1{kx]XumtѾ2B"cYP%yma!5 DAI Di'?逆 p+u!]8E{ѝ*dŬC{jd>գ8B̥ `! ,r ^i7]qt,QsHt>l"dWrcV9MrqF55 8RmwLMVvpYز(6S )n9X;a"eu:8aPe8#&Y  KARc搼+mG\մ`'|Y#b}ҘNVLW0|h! oP.Cw;E+e>Ո&g(,fWM<3]aNh5O85oƠey.%xT~3, ,C*Y#qэ[S'VQ3G(vC3vr{{?G/cl̠~ԁ K_d5a;H3`g;[p#'jafS,[j^. ׳ɴC_ sV+p$A(Tth{ Ob J/iEqSLGH0@Y27=lbH6 HX+LwH%ȿ˞۠AدFcVr3.b1Q( KHWRL K&7)l^-bWjUkq,j= %abTfe %KAAI&S7XB㘚ћg7@0y!L q砻aOK+:au^|K[EaDC(R0"ʮ}Uyq5)W q Ȃ^#jxYnZlG_EpgmLRT51r^dkDJ!/郏(\LpȪ*%m0c/ѷF:ؼMYCSh'`*b*هLvQZλpiq4h}q 8]| \Bڻ4>aw1wꉙW@c.n!v 3F|ί?JX%uɃ;_ݻW ʪ3?'dn#n[ I{`p5Q4!qY<ϣ3EhOZҜf`&W{Q@}C]_o&dZU[6 HݪLa֎mbsl£仜(;ƙ#"b yHXEfB2MFsc=vUoHgc <5PU`b`}F|= ϕry Lձބ{ bo3U&FW`UԦ7} 3&uGf?8PёՓd2NH)q^ u/䘲GC笾L@ryg/kH24s} ٺJ2Py(C7{tn#6C~5'@T>Z`XVQ宇 :BXن gCs9`v٦zV΃ʂ-Cuif|$"Tj_3 N* P9 ɟ`*GHwW*& _2าƓ4 KU23rN{? v۲@bbEuw5m 8H#_V'p 5-Ou ;V. .H`- $Fi* ٿ?S{ǎN4R0W/Ħ7>)rQQ_q8`$V1ۯwv*9q< 7vs{. u&^gɴxfbKqJ).'xw )uФ *k) !A50SFޯ, 4Ұs u M!CZ5K3ţˑz3j ~g׵Mp^ )A*QյTxǮ"ԵJqu Mh,3A@F p}Ӎ>@Wƣ"8#tϿmci3bxB( j&KI, M;ч {ndHzrjr+N 'jCE!בٿ/{`qzyɑ-◳R`6BTD8Ҁ0y:۸?/=x㘈Xv%,({geO0Κ@1CH)8s"YXNqE%UxjA;I&S7X2b*@jY.&k̈6QcwA~߼M}Н*vl.|}ǹ5E 3ko _upd)K=;^e li{n!2fd[uAsDrB 0Q1K Q8^6l#CWcwZYO%.- -KHZa|Kjr⬀Vྒ1r=~7#M}?h-8d7 +kMsDi좫%'b[ô[Rz$r\U9^}'bAk zO>n\Fh+kGCu:F0ˮTRC> PGB:_ ܓ߸ ۸T|ّ[2Q 'aiO!j vuL/8o)FAiIOlk+ ?p`eO3ND\07%V=g".Ye BpaSӇ.:AS~$~K ęoOb W]} l]ۀ@TWc;ҹ%Flw1jbRlKC;)jArҐPڨtu L'~!+Dʼ-klV(Q(8=! HvMEFl\lj\їlךB`:<”p6)y9%V6䋍Q5gJrփZ1[ZArٖQ;{#~ 77P/?'s1[:`&f^J10ξXףA[bd ]SoH!z [dmc t Welּ (DqB|4ΡIiKLKQ%_Hܗ+kp N8]:K=; XA*4QGŞ Qס0.$δVH> )zK)~U.Pt_,?R2䳘6$˅'Db"ByԊ Zo&lQaXϫI=+E6k["wWJKD<ԦzB.VъYƈЃv"Nae?S mo弑 AP=Pl=Rޫ(~%t@o5TXf zF#ٳc̳RjMAxEW'>8vH~X"v]z}uzU_PE0"Ј$T@lPNtMmXJX7 q`#| i(ZqϪVS@n0^\ƅ(Dʯ*<*[6pwGve{ɦ8Y̆)FVO-vtq_߼Ϥp^(YT.kTo#`\k§!a-oyd{;ǥ46yx!uq[1oB|@S~ A3aeF~NO.ҵ1n,H&J|{P@ޣ^D;" LGrAI&S?s'ƣ%݄qi2EX(Q'&"(vxih[b*_%GqN]^ Sh(&tF}h2eU T sz¢ހv(kd U8˼.(*R=85]X D݅Qi,ɹ+Z ;Ke@\_oW'.;eժW$"Q'wJO&nv'㧩 !(FȟH_pփ9:{Z󒃡w6iӱ3BQ !ؐ+N# hۯ~a{ 5F[2,mǚ'U@B8J,g0s6VF)3ш(%sg{Vĥ$$%{G8{\,OU}ߨ /1qK+168|ɶ7vVhr˧.r˧.:4AI&S?Xxj2ީ7 Do%6Ɂzf`Wqfy{(1EB?$BR_8&$? ïv1teux un W}-o98#焓6Ӄͪw/8 G8L^1ъyoM[NzgivO F@Cv cԕF`VwpIYk0}yE"zP^N!זaceRG;3|N4GHx cﳹzI2kQpԜnLNSNl.KL>"˅AL+I4L1*X{bK.\hHv߫T8l쇄}L}c=ewjzveގSrV*#ː)_d1JZbf"()K?exJ[N =q y1F?um!ox pGN+&lI̧(Xc3sW9+9K!]i$E{֚̀*'UG@.Y,mH4$mSx]RxqalQ z OO~b|Cɤ3! Ѳ2!vؒD+{Y%8_4UQрF2ebnER @/\a $mgKSd؂)  +ٕa!y]~?f]살ƈ ig7[ɶ }F„p-$9Oev@jhz$7v$7>h?dBU|!bDm3} %2~ZIɿ*bVRyg)$yPl,TSMG~ a;̩ x oQN0cR3L3{Otmm8+r L>6DJ@I.*=gcFZ8ʪ= VU*eho:,;%'4MWg8@aR|S-P!  B̦(y@Xg,AI&S<A70XӰ@6? /v~{IȘ`hyzN";b |v8;)_.4Ltp?QͨUVc/ $)(~AQ{CάA<>Wi/ Х7Юou/$ذiP Onu40NySw{t ܈i+[^܍sm{1)%>T)a.BLa08<%ڕS,BI/KEwa &D:D3ݰg1/ ܤ:+#U?tx $=0?ט!@{0 :g#+uX:xi`U?;J7 Jr\G:G5InԍZ>yIm/h;nLJBlr}Y5=?CâŲZI+7pˍE**R \BܖiY:[[-v m_ᯈhyq"WX|_Y%Jy ioT+٦)Q4punXep욝mQR)MaӦK~g!W:s'Y H|h̷؟?CrH nYyo+U*YW%\ODasŸxkZ{K<63x66P;%.<֝V9im7໧\o9JJF)},`J>ۚ@7֜BVY5 3T&+Eq05(4*0Saes3&U}5uJۑQMd@WAI&S{;?[~=ZC56OXlq &/D&>@vJ57DόlŃEDSNWi2--niLH 7@wѵ,CϮU^ @ΪJEKBR~j,)B&"Zej+:94z|<"ED\e>/ G-xy^T c4zc{8a-Y; 4a*j)=4[ߙVΓdWd+w0T(_(,r]Q.$v;&9jpO #dh)9S582!xȽ%]A^) ;lg`2nS: pb!kߙ@ nHh2HCG^eʌC8y'x~@T.LD_\ oDP-6l4ڀ>W==f ߱Ʀvv,)Ћ}dcwjbM(`g52yyEgo`8{@<T#40ݚ`(N_!Nv+M5epe*uwwJطtС`X ]t>7<G6W=rֵ[:+PzGeq'o4C]APxnFv9ɟyNע^o| KѠli~ TUU]&}Զh#N>T6JAAI&S<#{Qw1SM&u@N S5Pf'}b +ikc'!}- 6MI q>V:n~4j ?>d"*|B( 9NUݒNȊMz$^o͝19d26O8x_f}Oƹ6=`vSX`w~i+z~7 \tD,hS3FXX9UR\=]A?țfwtd]_|;-pW< JyR}HQ>?[vYUR  xT |?:v){e1pʈhŲ y!iM6a>-v Q{C3%U{qr =0UNπ%K)H<O#5^2_ՠL9t2aZH:K=`o+~:dzq:> P=' v Y:{ʀ5 Tq>mjqdը->h/y0.&LXMK{qZeaLކiJG\QmK30]& )RY"ZՊȫ}ODI#ʊ@Dyv8rEne'^[S<#t+]ۛ-8k6(s~ԛcfU4a)I/#ZIC! $R;:ƛxBAH& M}mx qSNטrsr4. fx SգΝX ֿYxIRr*4ڱdtTVViCI!48Q&^/i6]d^G@9'7u |!>Od< kC q\K/ 4@)+ YHesT~!43&*Nq}1Ȃo&8z07zh)"P@D櫭'a&d?9>x\A6I_#ʥV XSXbDN hJ);4%Ril\/V)sHO8=ɿB8uk<!ٳB< ߡ){ vVtwZHM?D A(I&Sk/3 5;Th[ u]y NUy*g,EMh, ~x}QZbu^ 2SWMA Vk-IXQnBT u :am%z׉I{8Y1h #R$t ʒDcngb.3@CxXU&2 !@8 K/#0bx8Xv+Oճ<bb‘!XVTTُd6^JTD21~ʬF*u4򨁤TQ6oKTߨm-<Ҳ|r-nL> nz+E2(Ll"HI$l7iG$tp?Sxfzq'Lwys"~ܓٱ@H*KFg#w88LqJVK%a$.hn 3 5=DqV@_c]?rթO|DѶQВ7܀ 3nfp3w1w,X LlNp_dCN&qmtV @މqkcc톨lOP.lyd5C- vJE\%]a4$^thV=Ҿcى9؝e͆(3-{us-)afܝN5w͖9ͤZ*2Z3GZw(h?ĹզC<%6Ǐ 4KIBwB0sli=!3B1 64#!t*ASoU3./nW{L ke/CY s@)/F{;Bj*`e)ͻ'+L!2kŐ!&V)ʶSP0UFx$ZM3XQM5nR]V/|ϛxwӤ}f%"GӡƩ1,nXv>wH/[<=t۴~{[V p-Ww)>X e!+}WfůvȬ>SPHG fD48 0]] \ DMt}CЅCM.)lhG8 zd4\uЪ 5!WGL׌+Lph B9p1|O0s*Yy|l%{[nor[pA_2c,o R.M/xq&"RLSLJ ;k09IKjxR,9ۛ.:+\ m-WvQpn_gf,,W$]༙V"iwWZ-8"#sޜ{mAFE<_`v0 Z%پ d'sdE9UZ{i!t1$g%,B۝$L{;Yz "*8˝^Ё:etD,0.6M8r07~ H8*>f+/(逈40|l9ėE˘ML g|8ŋMUKйoGNaw8̿!oZ˩ x|{Z-,h8e1/Õ͵HdH")Tx6owʃFrD Bҝ(!_"ϗPu.Ҩ?~)lgjDô2sgEx d‘:)Xb"ctn)` {^WSi AlIAhLG䑭EY\ƨ_0GA>BT<,'|@X )!%_&LH,*W}4Kpwq0_+hE)6up؟Eo/%#%blSb0-}==$⧆1D/; 7 C("o4'@AE,_9Zxup{O7S0=[55'cz_m Gvy")jAXOMmեϟ u)/O=f"V{]JrwhzdtDef}RZѝ]z,!SQ{ oL'UA!qT= Op\.{~?yA njDô1}eK@>-Pyƴ=|Pc㢞! b FʰiFѐdqAIAlLDi~)rTi2j;+]AG ZԂE7%v|/`嬩q"u.ovCXar>zi4ob)}؄a*#9 /&KC,yeP@am`CΐE5ܳpF4R; r$tBSd `s@ {4s9.|ݖJ A<[A ^6oRPK0tDef}z{kmk% K/F;<ÅpeƸR_#h4DÚNQ"P ܽ ްx6v7q2jDô1}eK@=7)~~!ՠhs2D4a. p@||J_w(; 8Z#>u_!*t_$Z=oɂk)Z-8 Vr҃?𬕼7xKwQQT"CJW@"x5A5IAlLL}t [)vBYN;7跉ak߾h )s= 3QpdaV*hBME ]Y+R/%-siW]@ ևrU % Pu wxepu&Cf }EkَϦ>+sҊJ9\w˗ 9Ο"&';-m^Kt]TjD˩\gl9<Ֆbp2El ],$-*7[ƃfMWh&H6|AYI Re0# knPOuwHר8\-I۴W˙L q%])4@_yU15`+eɴyy2!%_AQ,Լ6)qhYX5 FGԵfiؑd<+cvCk/RMt'r H[d㣡 /'c_ Yqqp 0>H%i%k$plcv0Rɕ7ută"C)s?)j>xaŦEX%U|"AwE4L_87S0>nYCw& ;~ޕ*OMc/gi~N<З4%#"; ¬F|z&Dd C0 \18cx6?q_Z> ؟[ h|%Y.&l,8fTM>M/i^,]S<5d.W{j4@ӎwA_kVrtDef}zl0tW 9 n՜XE}OdIWNeljDô1}eK r,$ $'4 + @G땳7I,DFeAIAhL}m@He1dAYi5<;1[^.ɞx} AI Re0#Xcα38^4n5x h`rP#xr EKH#3IvP%{Gy*1e#.ȕt7Vpq_zrpF z'8=.NjKhftJ3CEe6%-@ AE4L_87S0>nY9?l!__Pr{ .޽uj885ꗝCB7GMx=)6Cԓ4 ̕95r#~سr 'DX\ ΰwxtDef}z{kmc=zX1w4!P"N5hN΅j<,7Ѵz/SAnjDô1}eK@=7)~~!ՠgZڅ>bgÜ @%.<| AIAhL6Xw' 9|H j:fJ$)>?朐it#7Uh ";ę*y՜K߲π' fe*_XCF9MZFI%Xff5J7i|3='@6w4\,M"Gt>y H~AI Re0#6X&OoA8UJTyo.b(.3(7RAAgݬɪAA!IO>UB;u>R3^waЮ֜!Jw|2EM  ¼'0HzZ_C1n.lADI&So9I3`X?c3"؉> B.O);cjpW qs̸uY z/KQ`M`vHZ0n*_cAbE +'@PjD;efkHh7@.AIAhLG!($0tJwì-c@}JOr- mEƮ6'W("RۨFc6_?Xz^]rE3&n8J m97mt`ox`AE,O.rg. @ܷ 1#EqPnGqEjgtǐ )ijD;ef|PCXp8ɆR[pd8 JAIAlLG\\Q"h<qeR֨[8pH꜍tz]ԷHJ Fr=[I#=a K?Ƀ2:ody>uJ=zJYU[hW)e9`'ૹ=%FāJ:oik6l)AE,_YPGJz `A[f ,RA"loAN-$O|!meCy,( /2.l4<;9WAjXiV VtDef $/!r~#Ύ)^Q[\ jDeg?=pɈ69\| Xhv!p X0AIAlLGH{݈U8QXxN%폿>vQLQ%,3fʫqA,E,Od@Ϳ w%Wj+kj;-ni u5K ݡ!QL'VmMjDeg?q(Rq@ w@ ~5wӌ!S`DkAPIAlLL#R\1$#ʾyΎTKtǔꂸqSO_lO@KBjo< bojDe{q`3tQbRf^`cyjGF_v ŀAtI Re0!4:: V9mv]R$ՙ~&smD`QdJW+jake>y.NO:6NK2-@o?0L{ςBa*]q]4Z䥙%u .'ʡ4׺HЊ0vUBSk9aPhOVg#ٸDGr9j0M)Ϊ}u$,X,*<|4 Gq 6ف8}܄sj<PB} ۈgQnP@;܄x~g}ZPE~j,Z/ -2$%"'=h["b.^XeS|iZ΋:L~qpE==Aqirݚ&/AAE,OuwƱȜۍ'~~!V\.W1\ˀ7u5]oQDR: T{^ʦ 6ۡ;);LjD03JOnsDUAIAlLG:9 rS|*@#aԠ{C G4{647qoĕ`v[}GɱWlf&4Z;mD?WaBה?%P?A r6}9p5!`~b.g̫V^4!sPw"giuesJxPqa4˒q/MȹYaZ7hAIsG33LUh1_+R@oJBbQ:7ť]g8='AE,_YPGJz `A[f ,RA"loAN-$i>\LN^[İiWX#hD/nqƤ}Xa8tDef $dSjs}?w i kCll1RAZ:jDeg?=pɈ6We@h6мkɛ(2–cNQ[GKxLJn+:cH1cy-'](R_3AY8e0 A\{o# l.ԑ@}Wx-MPEL&?uFOȸƗU*'42v4ԕu[LjDejs@ %fKG!F# 3 oJL.HA1>9~}?^5T>Z|ݤxqc-7SPn.Y_cm46tXr|kZI[61GrӒo}dus&s%b?K0=zM.ʞ,NDhf^/ξO\y5/%( s1u> 3NoP{ClܽQ*@JڈPނ;$uG4LXqj_*wM`rՋ9<JDW86l!j~I^Ffw7ے461ÔxI{wH:bBK1p/͔qc ?.Gl lw[|")0RA%U[: !g~+D1&ANI Re0#XA &fǾ.\_k8!N5=~n0?+?:']ת83rD? CxN./V5rz\5$lbȥD=KM32,#WT.b|U%ӰV!.E}3XŴ-/;|2%YӇ fV2 $y?y/ QX3h5q[g7v^bB6U>#9RdV(~p bk /d ]INJu|TB;v_.d Q/0h)p<[E0of{t͝vWZ =vE_YXI1JbwFx8K,%ک L[$njfH} k0v2׍e#t{{_+H- h؁ *5ug2f/H%B>(e'7 jك̟&jy·+G܁p+ݘ}W=c9:N{~?R :$ް/йpX%U+0Q+{N/S{6Q(MCD9֮67S(Ԕe&x5P`_#J?#!_BQOI8-8R9@yVÒP˻&;f@"drs,ZEF.Gʈ%\ efLZz5wa}j 3nGzaʱȡ8' zAoIX]ٟg\u>.Q fwC~1V#$*9<]UBۼTaVD jd-+>u̦Nn? BO^DdFK]ΑOq6OEնT>!J;*]nʞ 9g@}ӹ tEʄ>Lh2Sl$dB}- Y818{Sfqڟr^oE|9e//=r/MuvZqe#Q["zNrf2 Z?F:Q:)EphU!泽HX2.= X>GX\ږ=?x >xc7np$P, 7"by9T2в ^T /Yn{b鹟pL5or&wþf뺦gg_6*k'M[;re\ZKJ*Xϫĉ}-zVufX`G MGiV !#F +!ޖӤ(ܸ2n AI&S,`WөaU`1oFbO*k)b . piiUx)bޤ$%P ʑpg?b;"T?":u5AB{΀KKn|_qNn6!=ͯ0\n7c`ΦAXz-БQlyY > 'QMvW[,f_Z_vP W )1.JDGgBrXL\5ӥnC?~9/Xچ0,00t2彊7T D ho/ָ%?Q,KH\ ݪS+[B^l++0TF;9Q}TTBRLc2v=[²!T|'=`)[XWz<"L+ t&T4R|F^Ք shlIRft}[G2r#$~b$mm4R(*w=#~ʡ}' \K7DO+!|jq{l`_.fL"/z{oXP- ˃YT; s/EI)=kIsQß.t(|qMP ,ũP*qc:hi$408@ޓזF\c^=ڶ>00k2nxǴ G:w],֊/[ ] m!Թ/aC0z[PT jXڙ%0 8AHԒ,RoD2xe:VJЃ&h݀~B= dV^MRf:[2;F}2$򋚤Uim,I'WcsZSY}?J,S eqmʋ鎡7},#dKo3c :fW1@:? 77 0[5J->MOZ)`sI#MGtp,1vG G YIaГ䪡baɷ1#+ A!>WY{f\m3--}_V)<&O(jg;=U>t7D[ wCdI#%ϊrM9@jxU5}lqW7q;RlvO!YMK>4  ⽣ 4J*!hj{ɘ;`: BW;1J]ecqJ;c@QeG1yD4DfuѤH 0Ui${{?x,̸SZ;WSgq"i iI66/ѵ#X a6jk/o冔_Ζ+s\0{5#hRhpv̐k*y ykmڔFB቟$9$ւx˚~szk; zpxd| )LU8Ta%%\ڝ4Prl^8H$ H!w*De=ILY$qJ2y@On_ Sx#l*ˑ0 !Әض;{l''0<֭0Q£Z98VkW:GN)/v^RF -Va+BÍ M#`\]h}N S,$ 0 !"o.cO$Q7#*;~=OZH$!%F:KEdm*-&ޡz4ǴL)3%"މD ٬0bED~]JXy]*Hr>2}B_T,uBR'rV6bWw K54!PrECPm= E1ƽ5Td)z(=CK?NXv)M{ :?4zwRi4oj"?~jr?-oT-6.n1Y[I2nd]J:,ILd]4/N~NJif\67jmVYӢX_G _GX5~zFgsvm!a]}_My{i߬˼$gaE c1Z'?CFȑKTxIy\MoFmJݎDv(\dTԷFG <URw^dMxz+ ]PG eAI&S7XS,6#6NbQ={XѓFĜ@4ɑ^FYxYM>'oË&v\1k(ɦW#+ pjrH|01RVbڑoi&fv\ yViY ǯbF7E٧*"C9ߵVx~*m@"hy zVJWD:u췃"hvAbUּͩim9A%+ʜfzQƬ$>M)7BGg<\jn'zj8ռ6IegcME8pt$ ĆvEOD( Dm\#Bftq;Y~*0jYK!җZ^'q]Ig+9xGF똿i!zJLHYYbA+l=grS+` .y^nx2bpg?rs!GEc8/+AU˖ \%Hq1[O9Vi8lm0X]l%pPP50F<.O?';m&($VNl D@/F(=wӱ>΢-򎛗T^zT"FFOdy>;_/*t(7Ocn!̫"ҹ1~@?Qk"=+Ñh 7XG`O[,+V#:{l&[{] 1g0g@eљ ~- bRzo}M:\bڎnmOǚ'%"&@r_NFh۴u I΄+Q.sLAjbFH`y.Rޞ4c核~Na~d+î-ɟ{vSpG2J2U[W{Z߼oHL#sl_ L:trofʙ^>`'ǖ XsCmأ]?UX&M>C|cxh>,ۓ6=42bȦs$[_Z{3;]~ OQ8NmT4 /?c `NI8L瞚P:bY؄ auX ]6hho7SO] ZK{6y3FWHV5 9eyX!*7nIޖ4iH8m -٥.ajqDJu)bB=)wjO;{jNhb }Y(5<[O}dD\Wmi b_,zᙴ0)R ˌEе@2o8wL/@V':ȸTm:?ǡD ۢV?luSZq s޿ ysTv9ц=INx{ i;6ZSC:aƸ@uТ)=$IB<#m~r{QwqhpA/U0#cBhʁd5#@KAA UXogQ[gIT!w흸(Pyg㸇ws/mۭ590aM]2,\EE#w)n\wS) d6)_s|SUٙ/էp1"̆?cE}aZp4 RRVa#B A-h%6[rʗJxn?M[)]R*yv040w_X'rG9!*uGpDwte4kfL*'LrĠGMĒ2x:{yu:% Ef"~#V;kۑU{>yf.KQm`lߛ?>y'lW?KG 6:նRzpRÎees&7ΧAFQ)]ZeQ}Yn !<H)Dyk'aXlns7 F`1 ^t  *@C}&5W^);\&@KboMG&"T.n? AI&S7X^6MW7_?X1RZ,hɣbN KH(Θd0@%LJ{cF"ny2UOM|JHo{L8"\]6}L#%`?O9旑Z('cڟwQΞ}ъW B^$2!Uwg%4 aSvɸdOEJng[j!\c‘,E'HY.V֕IŞ hꎮaۂa{ޥn*ZX0(VSǕ_K]*'IRIaڕF(EJe.-+)/Hs~;3Q>ƈ!|΍BVpme>;F=σ%ҹ`P+_D >rj$@hNznE$ʒpvNE#yQ[NKu>=}KS(_(v@ hQx˶t7y2bf=dec#FEt+ri.aB>M臡nٽ(k/z\# _Cg i^(EĬpgeDZuK}:1P$TMd$_.Ӄ]6]P_OR7)`j$:Gڤui۽o<$C,K9J"gvXsA :~G@R++%e֏o)ka_5noT >9ALM uG8RaZl'N6K t 2 %evAR\mNcrT :f7fL? Bxp|t>[M Ry;hTOFÑ 4dĂ*yls; ZA3k;![13Aɫ8xAy%đrMA"w5 7[bn%`]k6 ׺}_2.YDeG#~?:alsoju۶?hBvd&(:,.+dGl=de⤀Vү̀L?`E.!fmi*'1r9-rvBu2`fiBP ZtӔB[o,E/-t9nr"*P"v8uWk.wU'¿ȕ{p Q:`s0dQrӵ|+Os)7 y+RR͛?'Y} 6S$IpcR>jK|_:kNV(x暂%gkE}9!Qo ?A"@=Bq& LY'w/vg3ڎ"EOr{O;h=-t3-bԾ#Q߂:W%$wC򄪘".;k#Tu :K+֢j ͐qp K1`^QF9T!u T޶4?"Sm`բIX70]e: ǗԝQ j-FN: dy/5@tGek8'a6jH86tфA ?W}Cv m܋:Ik̞*㈴?'Hcm7 = m>\Hi@ǔ-e:(Kp9<Wʃ%ЛefM&ͷQR`ֶF}-A.tx]XVhB{_ݳW$5G){5}*eAvoQ3{2hRl$(\ipaXE=>|bƉ ɒ"(q-hvxaK;0%!d\;-N>N7Dmd]z܈$f(.?vc ͫCX}Z1K)/{ =*,m %Mtvh;]6 M }xu?\w垔E|.2<|53u"NX9P, S)b` RqJ~^$<!I<'(>72f&`&`Ry6@=*dgf՟ͦ:O7O{ј\kz9]ApOxq^_k} %Hni~6ޅM sl{Vt Ux 1O',q*d O:9 vnJO 4\Z=_N>o_Sٟ) !/(Hrc4쑅w:bVCN4'*?TXJZr;"47U!Zr'pHt~-@e0(?sK1ྤ\@ AI&S/X[c襹7KræyMꎵ>'z{߂4[s/s= '  zwӆej] ~r+=Rw4~=\99L&ymS)lIX床M67 6Q]M b-'iIg"ͧXi[Yð3ªH%@B|c%ѳqGs-DA=x :\]x4Zp"b[[EX s#Ή 0lҷdtC;̄=0XKL#Cٲy?ASzr+HtzO3[{2PwUR/@m :o睓yNx[7ԫ;J؄xOD韮𷮃ܫggdτ ^{UYo-Adn!{ ^ <`bAi-ypCɩxsS1'{[ eW 苯*/(aSkhrޗZ Diz|lG^&4 <$;{P6ߥF1{'= IT]ly\AǬ.B3Pw0DJnL;V7JuF?{)Iw-(_BڒkN=JUefBQ:TkrlB҆#nq bAau"uԲf(jPFVkjݯwNN#> Vy^'(hS#bÞ(a@tl])t`|1J0UYhEJ'dbY)g0Ռ5}Ep0=VAW}>;MFU"r c~0 S23X[g?p{J{^eYΧ$ J/XԿ)d NJ*Mj8L;U66XNSoyX\_pePPc }[ ^c8 -2t D?*$:}Et^ A)6?IX'.OrTW&'3x^ :KVl|' 0#Z*h݊F&D*Qş]!S(*EmHi YD/(6يp`廅 g)~ `Zq}!N{g^ȣ=}Ȱo@ c҅fM:gR~ԄB:;,uq33#pCgd%˼ao6/klJT S(p E\i`rZ0TJg^W 0~y{,ɦ:`YƫMn}d.׎r>JlWY"KA:*]V'P7M{rJүs7t=4^ROYꊃ8BqۘJ}hXe0㶳 f\Im||طPFsB_}MP5AgvXT'/z"Z`kR^8g~zҢN:cՊլo\}e;›T2F,zG}M.K;Vl,~7Ef)#vYt/e;m fK% 8.d1d_ia{x19/NÒM}>8xSFV?2ЫqX%/P , 6c`]3<@[k !UwIa DGj0J;DGtsԈjay?h >3a>3|S8࢛| 3b cZO"tB* EOC],ƌ)F5<*e&{H5ɭVB|,@R2^-ҏQ-N N3<@5$ a{xi@^x?sH~t Zbw`HI^ 8l{ܧ-Qt vAhy3B t.&i̶r+F9|W3*1Ih HB1ugI|Nz X!Tͭ>O&ui0'6CFuU5.1R PiPB6Ts[{Ss 19Tg #Է%7d'j Z1sRE TF^v4v :D0R|B9KyܰJsߨPH!}QɂiS@f]jT%toKI6?h,LbIW>Pr vA"o/w~UJՌL?G=q"@;xc"j;|S }~&nYCF_,W nĸ-P>ٱ /#6/XT9zֽ"K*jM⶜X\J{>c8YוLzf|`I%}&f6udLp+FJz]הjn og91>꘽V*$=$f xZn07~U#u%2]l({d jAK? gor_چ%.;gPvQM eE<1Wv~ƳMUVߟ)i٧$eh'ccGfW45v>a]p =⭾:hvt 0vz -}iгW}]sg|K"WVOdd0DovmE(wb xAo7C07߬=[oOjƅ՚'F MZEG eu;\!0sV׳=cLI}A1t?s#XnCc>t@xVY(h~aV5ԟ@ Hj:٨GS!7O#bgQ>EBWrKcA}aFn^Z5*֬Mm/T ܩFصb'v"V9ͭE[9A1jձkKhN$*.XFimƒOI,Bªsj8LxI"yV5 )P?N3bjgg@*lY+NRnW|WiL.tK/\/Sj$2h͌^S4fWG,_c5,XغKd-?4b lJx2~爵4bivFgy*,#%lz!V7!dͣQ:zwl=gçIHRX6,O"};_wSq89dBw2C rNVÌA~sEc/n.Ǹ `[Tz%vUi+yB▌, ~2s&tEt``vP}#) 7t]f#3ِ!Y??ULE_ja%8 ]Q%u粵ɠ9Z.wԢ u#BZӤ1jYmuB:D}EL$iX{3M&f;-,ZRÉ߯o{j.Dh,, QHŅ,n,sٱ1[pWhЪWAOߖ' Nm|Cck~IH ܮ[̈́rþ"3pQx2Fizv"`oLA ڙV&`?bcA: 73#lRRSvBƌ\''ɒ&g\G>Y֫UwqRpecQ{ww%n;I\83ܒ3|VGRSr{/>bRDUPT.1c 2A8zvipsnm]j蠫* fŧZ_YN<{Mc0,7v]f! ÁZ$Kld]#z¬r_}ɜ_RfA9JYd{x{1`EXs\fu\3k7Z%r+EecnSHp)qǴb\ kLv`v-Uv̐PGt՘әLߔ9#9d;LOP\-UeG&"< {­ X}u⾐lSjaBaTv/ Z :oFxDd0lnYHS] ПYtVhkJh C1峑~fC: [xe3( lZi>Ld$ (׼<{wrf1X9{6C;f^c4Ga6H~8~\&k>yN\g&Fp oCML7"LS>%s|:Sn+"fEy@8nM-Y4|q r';~ųXʿAcicd~3 w&jÝ=@&$<%tnjLXىy뗹(Y$::eɎ6wq@Uh C(>5, EM ?Y]bm)P3 A6I&S' #'0ѐV?4*:|vdKjqQuiI]rI^)gsxvzTXEs83 3Xy~?;;jVV7xeG'~.Id/Nq/dIVym|FEܗ8A_԰n J;s8m?Zg:./PL>C=9I4fTNJpGu>[~ҩjӝ+!bXy kQ]s({E }ubIlvv%2Ĥא]598nɵP x^[|w F5hfH9bP2O$od-rl'V?E53NSGKs"cX@NAVp+[!+lf5AҴ(#Ȼ/YLv CGqWu.ѱLYf s|OCԃ;pCS \/iV0hʅاas П)*`9 0`wǮFp7^ ii|”YL9y<:h#IG[eGK2E@eF{Ð c H{1zЦp*#&'G|y;/jT0t@ҲסE[L\$hnm m}KCʦ)K]Ny ~]I {P S,yڽ]/ ?qygfԩ lV_3f^_)o ,\\)%ڕr,0~UR>נuh?yM]mh$2o,N3O~_-m3ghcM>rgƆŊV:kC{WsygjWpv(A,3'{`󖇓VgـD'^b0&P9MC{\sT +$J _ʮSHs*#gXuĴs$)';@{@>.}f`w['*,vUEBEE*]6b[H#K~&l,Q_-F~3sA FbV1nBg$kݿ)Jt ZZ ApWg(Z%9! ԡjP"YT|YW3HU1D\=ʷf#nNDZMHV a 7uD=zw ۩6ψsvY0[2tO4ũ8%Vw6菧9gƷ9$cud)߰|Xiy1#rN_5H=[uAzzD 윘íHxj;&~a(meSjۗ.u 6QŢS+G=V6 ."pD֬-k=[̈NO#S S, B/+D2 B;8}0e]9ԇ([哢-JꭄE1{;}.XR\F#N mLswJa=IaGdVpZZ]|R&-HyV. cNRȎ+|. *GQ!};Pk*NDspb˻.^вLB~; JN  ̚* 3- Oۧ][A 'G/b7Hc^VWn3 qiy"( HHe_krG!̕%qZC0AtY֏`lrFZ<+3Aw`b@XEQ%"!>[sp쮱kժɩeDc b3Gm`MgqYָhP~0a?>%c~+_DW9C?:;f32{ejMװwpY>Fx+M_k+B !5{C"HOD&'~ /vp0,ErDF8p^Jwݟdv-܆E!nGg^X)@Jy)S~bxYC윳7o ŗw-< kDًFd2NCD vYB0J4܉H?(ᯓdFY@ɬqQ{l 39ݒk „&jTz1V! fZ|˜-(;]jƟ v0a:0ϰkڲ'oijqT:Oo~8(7$ 𧊋`^ݦ=3IuY.D7*6hG7e30I*3PjMăƇl@ AWI&SNdJ^Y&bbOܢKp"g.HUN&]t D8m8W#]6ȿJ_|S*W}뺍H㷻e3UFnp{E[fR 죲Lfi I=L+}m?cxh_^jcITg!d( ?Ѹ"wN7(m Eh+M#3ɦ"E Ur3@jf\gÕ7a٬5$6N:"ÔPyK֔mm5~@RD6C5Z#qK&KSjCpKm6_֕c6#,2uIu ꨘ7>s \Pf5x­Ҁ].)0MaJ9/S 8|6?bsњV- A!W3>V@31Z[[iɣAYћUJ'}"Wfwl)מ;gw[|#1r^$'x%50-@Vd>; CcЮ dNudCC?{]/Q<ô~LLa$7E~{&&^V^"6Q}\\xeb|061k C[څ8o3MbshI)ʼn< !Y$0#?7}C2s^.u@A)ߜ7ԾkĔK/`}~/o.!Yw:$7>+>yg[&A6vcS'݈=Uqc_0x6h(ĆUe镕 rh#w:*-c5 _b;m11x#`rPJp>NHn:٨u  D7Bjk?exۚ݇.R mê9l!5 tjv]GE;Ƞf{E~gYtSQ MЛ>kn tN_b`1E"厏z*hWpTx௾k-r nc:JW-)`]m KO?b@:gEߊLeu~1}vi+BKpW}/*1_X╕%߶tS :dž PL#eX'(Qj6ڴ8u%_˹R qSL#&.*Sl?%2çs 6irꌲ._Fe|3SAyI&S<#g9 iNժ:u `@4:-yʠoaֿALj@, sxTM?7*G4J,d209QX:zXLyK|o\ely_6wwm owKOV+DdW8P`;7n?uJ`jDXC?Juaf5AGUo'=Z{@ ) ta?F-#pܿJWW21çqZ’e_>l4- ߅ϲ9FlO6$j6,~.[ፐċ^:3[Lh_x0&ls8KXU*rc>V!gq  mM; {,i&rtjoe1[En;v]Eۀ b}Ih[e);,zԋ,xsZXa[l/t9 ;l E?#0Cs*½۰yqG' ilQ^{iG5s0눝yΙ^`4zf1&ur(FDis> vY.co_i[?Ypw+uMZ =MAr̋R\Y7婊c|i!dz!OsģyKSb0"ږ{PǪ*5o{D3 7&ͥ>nN 񉨄&qjO0JH1d|QOj,Sw1B9l7?T1I#i(fCЁ6ug&IiC)i~9pn ffS@EP3'cN^eš+0qAI&SK&ֱghu\}*VaZmAJj4C&75ߋxǧі>81TAE<_YPGJzz_CDe樂dSCtDeEvA<AjDeghA@GKAIAhLG <={|PAE,_YPGJz `A[{L7 ZπAtDeftmBjDeg?aKAIAlLG 9HPA#E,_YPGJz `A[{L@ON HABtDeftmBDjDeg?aHAIIAlLGPAgE,_YPGJz `A[{L7 ZρAtDeftlBjDeg?aGAIAlLqPAE,_YPGJz `A[{L@ON HAtDeftlBjDeg?aAIAlLX(Q<돦SJ, !Lfmڃrqˆ$U@Y'w6E_()hw ʈ`V Dٿ_TAE,_YPGJz `A[{L;(疧܃7CtDefvCFjDeg?a蠠C AIAlLLO %/6aC%cÌQ}3J|Yt-Mpv&~$(~-4N, moovlmvhd @ 5trak\tkhd @8$edtselst  mdia mdhd<U-hdlrvideVideoHandler Xminfvmhd$dinfdref url  stblstsdavc18HHLavc60.31.102 libx264;avcCd*gd*@x'Z 2h"colrnclxpaspbtrt22sttsstssctts stscstszvSgXTHFd\]dTIFMQFFLQFFLQFFLQFFLQFFLQFFRQFFJRFG AT,v0 [  >p{ &vpi|rgTdmZ`uqfunPYe^biQPLeLLmRZMX*~ W i #  duXGEOTEFOTEFLTEFKTEFXGJstco0budtaZmeta!hdlrmdirappl-ilst%toodataLavf60.16.100picom-12.5/assets/geometry-change.mp4000066400000000000000000003647441471504570600176050ustar00rootroot00000000000000 ftypisomisomiso2avc1mp41freeۯmdatEH, #x264 - core 164 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=16 deblock=1:0:0 analyse=0x3:0x133 me=tesa subme=11 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=24 chroma_me=1 trellis=2 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=0 chroma_qp_offset=-2 threads=14 lookahead_threads=3 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=16 b_pyramid=2 b_adapt=2 b_bias=0 direct=3 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=60 rc=crf mbtree=1 crf=28.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00Ee;XBK};SH{QJ˜{ p"JœA7^޼_Wi\T&jRDdŕ%@ C:ASdoyfz$ud@IlO-o~I)w<zs)W.BrdSCG !ϻ^n WgY 2#%mWc28&NJ_-w_pFH* ]y„19u󇥋܉>~"-]tH[ jPxy'Jm3"y,6>yil<8n?g^ݓ{6$NvÌ~NYax6\(TY."4,.6K1]Egfyp0ǿ@Y[`$ؖź;_pF>OM[f+nH߂y]RGɓz⤒x}t-!8ψAR~+@*!9aG,S! \0xu Ϫ8 ״E8˴f/ jF'W1"ؿ!K; 'OQ2{+J~$B@i [HV3QDz-#' x:rJѨ)Lħ{0',,ؽM`srҷXF"ޡ*R:}Y9߻,2X')h5ŃO[Yn*|'< 3` "O.^x@ր^,z@:]YR* r^|~7f(37BEZ59MG 5 `X3"/ ,ERGTDN,!u6^>OJސ ^jXUd8aՖӂ I B~)Rx6=]0Q_ayJ : fVu#vVX$Ӊю$|8P#.6 A~lF)Ej5BoBI`M_l}J+y )=_I>r;D)˝*BWx|˜ <*nM,Đ.ga"Ks i_='>f1JĒk muVDuP+_1BE., j%Ԥ5FP C`grF<%%(u c9iT>Hw<#B*p<"O‰{lQ{E3/h"N]nsgC']{Q"x{^bu6}8K #$)tO918 #}L|I̢5stJ ZԯE|#΂'9#Tg$ ^JFnD{' >񊾴Npt εE^v3Kzm5`ZA]Mv#h?[W#cW< gb-zL/#۝CRE[qx7nH݆HfTX5#_sXapj͐xvr[;e6q at]"_40trf^*yyVʆρ~ivdACz[m%:[)v:ǟr:$T͙Lq35'5$Q#)qtT3iw(H[1} }>B  ],+4=RS 0?^T8,S{#{]<` ,Jn~*OphKɢ SU ^jݍZ&+\Ul`UMQcZ78Njm4 2WjVB4 ޥ} i㙑3R O6Cx D9,I:P ћoM?(Bc%ا@A,3c0Qtx?>ȐǙB{seOAJmG{T#g)?>"O׎NM;Sx 1nm&Zlvٰb4ZG0d䫾\jq*J<& ESK۱6*a6'm SXbo`Ĺy18rچ(a9Cj#L>c4<46iYmZtWx'qQ~nM?SM7$9Lc&-f:efx2 .ŶqYmbO~`,eku %GpU=oA%Q>oS2z=CNK]^Zayr2"G}dUOI#X[9kY5㉼ۋ8iXWpʃݴQmտe`UPq|7,pҿ20M $+ b|v?>O+E3-)s9nݺzRWmJ8:a9`)Cܧ2@XEUM4Z2r\[!+yAFYs&n6F ;}ݕ+MR"߸&BWUڒ@Cjk#$/|,-a  Qo4VKxK~pĠ`nZߙ3Cu[g+ʅ=¨\uO;qwNN|X \#|EYJ)rWYhpNb)q?Z.Scص ;OH@T#Pj,0xЬ'rf=ġQՄBuӮ1&X6EWt Sà\ʚ@нS/5r A4&SQSڿF* j{QHZ]ͼD6 _Н{euMx 9[bXgo9\V97' #W[#lLIG`fYB-7}1<۬PK/7bf$%>|~i=V,[],u1qr᚛hZCUϥ"t IUc,7ڝ/tmqڪ vBh Vq]:AZ=5qE&`Bwl4nq\T.+z/ٮǯL[k^ z w0\>=D(&I⩒fNg*L` qM2xFל=Seē.@yTP"+ r_5bFM!5ć"n,z!}*wҞ.ʼn㝐`w! MoKh4nM2ER, 53ipYD0ucb{JTTEK@L2@2M%|;zoZc=CYq~goj6Fv)i;F-,rȷ2 H\ƧY,WӇ@3!2pMv2?iX6+}EZgyN2ssזHI [^7<8f!ѥ6XunF8XH :KMS5*(}¬^$vKad<>N,)`(FI  cN&.S0 Sj&hJwl{ u Ykg*m˪4$\++k8J$g*8N(~gcC"Y&czpJ >;>,okQ:XS=!o& TYMx@bR%6.y(d\'=q?[tA(de0#٨?~o "_R2ws`Fԉ z؄``HnWĜe\u09L,~9{˟j"y8 y&19E6_Hq=Sh2aw~',㖩W<.DDHcG _ۮ vFc0շ ځVBz4#ϘkB*#lxA^g.Iǽ^H4v^ ,.]`; %mE޺i|'πq/o3ML] Av=׷rJ"Idj ~) j9T`RGE|<@9nhx<|nGh)6ЧBЫQg*Ѥ3 'z|rs5^Ov9Íta%kT㩎A0L+#C_:&15Aiyq`3)7sdlP( }&l)*¶ǗN|ĝX leLE,rc,\wg3'E5d\!'%$0"Ҳ05-}8~=~*} ީr=wcѭREe<1q!Q1[`ʮ];twWb.{bEh&KprF- 664j q}{-`>EP+y 1y5ǶFWwQUSiM})ެ͵_Tƒe?j/x^=GBhDЪLߘDAqtz1뜽>QUB)k_xQ4cM1K<~fXDs60y* 9c5}01Sm_]O5XNXאJyނ&PIp|a} |z:?!.">q?{v4;T3>E$Bxn'>tfVlBH0$[51QA@N@Zɔ+ƌBso=cU< \f'xA=d:lPy %z> @<$-`ehI1HKMYz 0CVB*_@Opp3KѸH6EnNL0ĎX_gK|DC"h ( ~uYi8iRѵQװ/vk~|ֻ7#"{Bn ꤠQփQM( g[>[| CޫtT_6?jTr@BMQog~ӚhV̆ݽEh`lo3J*#M?llOx8ϣM[Jyg]@2I޸DBEA–FNAHWe0oH툒k/ø48Ԛz7YI l򴣁GsJ˓3O++ {f6('*Zf.gRsgә2N_N疐eÞ_Wq6Y{Ũ XeL";2\'AN rTۊCuNKW@nHr_ovՔƜdN4Ӯײ@fH-_j+\MN-L|6 oȼJIvS:JL{:ٍ>ɔ7|懙Ռ.z5~%WR9~8 ɼ Ҹ'ԫf}3K+R"Lދ} upXψ[FKa!0rk: KQk89ʙ8-"7շm}F'9_á]y`VV_^e]|-,Ҭܨeu ,eac.7F><,[fZ4x=l;-P( T+ C67U? r`0J;-f"/5P&C`K#o<>N(! G2ǺePꨩ̠s,Ϳhk]VYtG^ <$% ]nykl)\pݗݹ]iTEK']i4+"Чkؼեg}G? %b˶aΚ"lb#ujJ!$jL'zl\PK<),qAP_L++].@Z`~$#ql!4afV7Z Bu$8okQĩ]bBNRP"W)8 : _ch?dpD{|9顿0ij]= `qVVD:7T#a;d9(^Kɠ`K  &E 9"P\vRc|=s$+ >Vz^E.|RSc9g5um)JSy !.:Sc,˫G'|8f+K-\._b?[*fX(N6x /Vhh$9yLzwʏg\auA}-$zՁyse>Hg7_܃^ŁZ!}ؚ۲` }5bcrNAXgk&SmsmK1|bpM;;[sIqR,{d3ɡt3K~‘T-#o/ӣoaorxPb O~wЋ%A:6kPhk ȥ)g.Q@(/hR0CMD_&J|jt }xsW:-qt (2~ՙ0]>%WGR#zz;m':p؈<=x$~58Y/f3g_WynI8EIAP{S J̴zF'E,+pmU"@0'ݾ=#aK*QOH/\AvR%DA2X07N0yVLjBnkf@%`Jq{g`.C ]-+ ES+%TR-̮f Z}O˗d ҂.On !0?7Uwĭ#A„WYyq=Į?#T<־l|jDNY,MZȻpݼ0B0fw46^=(_!±\|*@9 Aa4oZɔ+;yQ><%Hw3l D2S5q~Dָ NAxO'+#+LPtQ>!PODRcuܯn]JYg3XKc})=!-҂pT'F𾡑a9+njL*U ?64m%߽m "oJk}@Bѡu[V XBcX.TA`i/I83"?bՠӔ3Vz!h4JJEyBFx}U}fJ y l/.V KќW/ _~V]T9dPvYȹ*RxD$ 5G"2LةS-7Yq;fWk:I玽ȒQPJf\^Kr0Ak謜D(z"[8._}&Do7%%4xW;SccOlȅWʀ4L^k`s wy?$+slҵ)6d$Eӓ两:$ PWkݰ }E|y(tHR`0)?D>R<{H1uhZ-h 7lOQb~[*P zvEnNx#DR@j:& g*'7zD,v 9.B>eCZ?n U~@Ai2!O aiIs^f:Hq&8|p`ą4raA_Pm& S^9g:u;~2,ufm r{מQq9i1)ʠj)xQ|2)ORt] |lq&6C$˳IDWe #OȬ!+E3[>YPƲ"~6"[tH1Mh7 @ʀ/"Aqt~@Ze0%px`9},J͕9r-D} c4Q%1lH!腚z(2"7;D5߱e8B^{$ÝL(ZúO,}9ڧ.qdBstgd@U4ҨPUˡϑ6җul3P٘ZYԐTJ9E]9J$Z9*'d,CB%sb;VMV:xnتX_(9vVyf w"& 'q-Q=l@2C#(ac#n^dx7PPXn1 kH9:s*u7\ {5pl2:wk0ƦǛ ;$ۄ Nw}/s]#Ÿ{+7|,X'!ͶG u/%h+}^|bifU׻Hw8_<'Z?:aVy B'`Rc9t^ *4<0߇=qA]e7 x)wR,x;Xڢ NŹWvLT N Cy >72t\w Tm}7|*)t p:6VYnd@Ri ZgaI^k~i{hyhCzrԃSvƲl\79*7duO0m`ׄ^sm`uAyV:P) wum$a0zqaP=}q7@S(Q$?[¦P+Nê»$Z*U p6|hT[z"$[F5 TGH򷮙 4N"l항X͎-ܕy_QpY }5{\n'GqeҜ7C6aIf:C8v"t1dk('ZgeE5{E`μ g^(0}҅[@ځtAsP -K_`M~^i UƀTGV.'SEBZ ߇-tqϤ^9FK~@UI6WgQnZsюo&1-^iE昤aT7?i'ƨ2 iW(:؀L} (IoScՕ[͛WHYRA5+&R~g4CJ0Z:߲nyчW4$GJn*$_e?Ϡc#cu:tӆAʈ=K*.%oF#t Qy RJtJ ݛ{A>PA׷HZ_|r?7dBc4dTuw5UN(ey^/?l0YBc!CǍ#p9Uqh9_A+?5 3ЋM֠ nqj!mYCk@$镒H}YIGx,Լ1$q4:@5 3ЋM֠ nqj!^ k@Ce2 ƈ$ݷ`>/LkzR9&:5 3ЋM֠ nqX(9q'$QR 6SPb?m*%5 XrX(,CH5 {vvݕɜ5 {vvuq5 {vvd0:CqOX1E.0㴟@oYޞ(Wע4:CqOXaBp%qu8 Ud N BY*po7&:CqOX&ZqS,@cjL8YNԱs Sj@#86:CqOX&ZqS,]W雨!fOU4Epl@eVBF:CqjN"uĺ$h ǑARsE$ Z])kc1B]U;f&m5JsFZ|kó͍KRIˀzS;ۆwú8Q)4F/n3bXhet5ՅSD*<KW<`ˑ.Cx3{g֪}Nhhٞ44(zS b@W4`l^{1\a4q$G~n]G̽wh}zw#ٿr_]1zDYMVLe@cGt g!1u^U%puhشf€ >y,Do8`pdmI5*qLa#便 Xq1"!OAKkk_LM;#QC??qIſ῰l]}rEЦ('hpяg﹉a|. ?j)jqv,bJSc-Xx^[6>6or/CA'8D#QVM|D]z'Ť ~GZ OC+8+[ y*ղU68X*l4^h5MyOrBa4ͪ+;$#$nGXd܍/u1KtgIui<l&Zwf@<Ϧľ] L9f0F8.yVEXk}K(^|/-YțuUdkAKp;d:\yL]`kJIb욢=7݄rW#|RIt>//ynѰ˽0XYX-e,3)j#f뷻뜧ORx-gW 1]4F୹YO}Z.\L$+Wa=pS(NpLLY H#D#@1PaLܵJjQ2oXs-}'w  !Rt@oDܹy5ƕ}\S7x.Bs :|~WЯOJ) gW=zA+\ )hi0$K/8([xg^[vׇL.ssIL2 el <AnHᢹ:ݬY5S xQ$ˋןpkr]_IfQⲁR1c0CWk-FOܣ(!ǐ&ok^W4N(y 8 Gw*H]'@%'@%p,jWPg$VMHS81kVbn` <"`Kڬ ۾k#B5ea=.Od xkhL9YU>޴7@P#$evm;T ~f5gug/ALƨN\^&Տă:Xsśb4K7FAK@J{daS&<3kN1Y_(c@`tGZ,;NZcYKƌHIw޶cnv3&ɻAu 5b|)8)sqKrAR(mcj4LxT'͝V1cc%$ r qjߤXog-T(9Szjig`H*9Δ/`Aɭ~>g[>SY`^M~3Qr]!M5KP$Qrg .]rУ=Chxl8oPP<2,̗:suBpe("n1ZfY W_(hEO-*ٛOWW9uT.b-D2!6K@XQY2F>{ Ql ^+1J-? wY)A)2f] ,ըbBAǞX:xfrف@ܷ_E)~?*N:[tε$ \_z18/z|U(9lBvs5*9P'kR8E ԥ SD@QRy XZj^l)=9<@@gmD;ROϕٟܝf0hxv==qCgh)d@nD/*7rp}pHdbb^2̊@hu;5CiH ~ {-A'a [7ݑ̀8 }>Orݮ#?zhp{\7EH;~VvoE#Ͱ 21>*;u&`Q4PԝQ܎|]~xRTe]ާe0EMZ,6u"ڠb:Pl3|j\d|;5"l  TJJV!$UD}btFNؒ#~ mSE k3 X 'N"Z1$*hv8ߌ ;Fd8 rA-D`D ȱxyg+fΘ ޼yې95I`/GXp~}z/HŅ :Sx0PWl<is?o#7]?"_e'3?_='n^RE)g$,Jh~ !Ŷ,i߂%'f_d)'̪ ^ YoԴg7ₗX*)flebywרt}ؙakIJFLS()s@ eV}߇8ɶ3`hX+YOA+ɩx/PǑOi|OE*bSZTl 8b_T?9$#AKkk_) z9W ZQr! Zbj\B\|42IQZ`M}˅~5tl (n̦ZQysKs{[4D‹w(Zh8i]zgte}nnM|?xyf#X Xe::ݴޘn\0 `N> 1DA^ +$/|r"BnOz:qb>xTZNÙU }/N]pCc*`Q2 aJOwv!v9.(P W@c2%F1l9B} `R=DƧ|ЕS]戭A;gTLH"!dJCZvjbJʦq`Qϖ?fD Ha/FB+hȫLb^qKn=Ee8”T`* (z=Ķ^gac̢2})6'4R8Qϥ[V42Sә}lN$a$1Պo:v?N&_.kĉFe8Wi5((Ba|Aד)~#~,5pĔHiy)(2m`8[T'R'w2P /(4@7#Y%|QnLl(YMjeK)$de)UzCM/a7n\x^<+W'8C:ӂ )b 83~:Qd o͓9OEtRJoRuGAUOSTmpwh~}ѝpcў8vWG _ESRm9¼2h1f]].AC[J a\uX/l^_ 8+eB;~oE-'ɪ[vN֒UWtLƜ1ǵڷNpwb1(U$Ndeܒ<),0ΧsJrV\c*e2SmĐB]/ ~ϋ3dxSK&TGzqS_5N1d蒘<^d( 3ĺ'aIĝ_SKlvk錂_#$VUWC*] t'$)ff0ϳ*8SL8"Af"Ê#Oc7Z}ȫlHh =)gassg-P'nvNn+ yk:CmT,c~J7vo?{>{N7{ E 4s\lt`PO.Za\3f5pgv^4 -!تe5G:f_tf@ Qb?jK1d(3tl4{{S5CB,9}2sGU! C+%רd6̉N^cfqMñH TDΚG{%CV]e)*Ѡ:*aOS5d&kBYC ]ߪd2Ԏ*/1W,q$|RfҾ{^\f%k)Lt m\N )ϔ.TFͯ B SLJGD5z`ge{l46& hߨBdy%0>ܥAih8A_V)Q&0Ys(k"B˩폰zT f8]'j YҞqU w'z.V$ȏI)&CF@cPm?M[|ec͐PךM$BN(({G, b\8W'M]26`3L)["rHȠ|0Yg9-.|݁{7UR,}nBMTD>)#-E'LnӠԚG AQ˫27XqT ftDKXʰ`$M5f#LΪOoߜ;]uNﶥfMwK5|A3-)壧&>Q POL7B0Oa}'@4N3i+x0uC8RY&FTi"THNFZd)Y զ/ea[5j^"}u:$eSԭBQ*9yZJ]ТZY 4z#GiLɎ9n>:3 S"u#9b OcVjMA{eX&ꞯŽ=ma[?Fe,&Q{xdn-WABA}BMjj|I+-%.G|RJƞKBw\c̉MG mP ׋a0;- +Rg|Ÿ|8U>ZxmKSxjx\PW'd} n`' դJT{JaYG{ƒ=}(B(l־IuvaƖ.KA a&K1[)#FPbSDA@O #drN~!L-|3=M|IyJ2pVmD%@ #k5drֱ 4S ҂3W"jر:)m˸O91GS'˱-R3e:UTO=h!bX ܬ Јt0h RuaMA8'."ds-0] R(T>V~"eNnԭdUVc8>ն.١[`}ω"Y}OgLrL&:CbE AC J'-%Y2SmһOLR^]̩yJj(2r2Fӗ ][L3 'd9&uـbwGh J=Jݤl8 =.b$ mG =`qV7ug/s5/3~޶V)>D A%k]2j41Z#,H,xݲ40pyb6XӼM9weŖ ~NQdXݠ4A3,ҢC{" Xv8β# ԕ})y[H?^bI>=V&)D9RpMwB6D|RAKkjLNZ1(=$ZL%(ޒ$^&'a0I*> : X27"/⮁l x@Da$jĸڸ1wZVj,-)gi[޿ &ua ʯDyK`,`G;aZ=9u{)STI_Dwɷ>W>~fIғ qkEOYʽ_pa1i*"#s8ڳQ?dxNyQsۙR|l;g(~ u t-ʪ]-IB%UA~!\6D)%&-fS?f]Ti0 <ޭG`J%eΰ+C 74`Cva(6JXuB9v{RBv0Ţ^\ !vtݒ n)=RgݵJ0g\2 \0xlm>%-#[}/sgEFiGǭdL|:#ؕ=1x]^iό=_T1xhlVHbl( [cEe[B~+Na'|<Բ-Pj&$"F Qc'sӺ1؞GiPmW5kA}:aMآ5aurY,q`/ vUhR4&E.}>k>yg#2. x|ӵB<9 QG;bG&<Y~yr{Ws?-8# ٯ( |%!Y#YqLx F\e?@-/.;lAY'e^PD+ZB\tq'8^4((jd^W^$f\5R3q=m[:q2O=K9Zc0HeחaA>Po<s $z?VfU1ShSi&z5>Iz(hKlXsY~}D-P>a^ <%IkfԅM~{̟gt[ V5 uOo͓5Z}D/jA1N)I8ּjȣ},5=K„?[|EU;Ĕ~*NA^0F^9'"rawU8Rʑ3vA.$ZeR<#9#<nY 4mY㯵 q5jK U$rS,qsHH6,D]P(54BmZcp"lglA`lmHN}ɂ FTUN2&n*H4Hrb"\b@9C5S5WerۨYۼb"w7lJ1 DҥP恱!ĖLHC!J/F}݋\,[tә]} n4+0Ʉ a (CT”a&!`tzcYT gC /VP? ;C2vT>`O:[&@Nu% 1[Eӟ\bG}5A[(]zLH\g]L*.jw1D8Aa^b( LR^>R?}U&?X2%|q*J_{ֶ-+[b`9_yód=ܥmmHMw~h/!}HDU.xgS~4ln/p2iF,TN].|Bo Ru-$axf5BlxTsZXێX Ukݒ[mA'Ҙ0?6uP1= {%y ȁP]x6%Q11vU;YfTД٪CnEŋԡp+bB.`ւ0C:`%C2\i*eu& gt @[t8E)ri-C 0jhfv= ICaRxk<9w )浦v\q>B-b^2"~5L9iViZ -%u7m2ʄ5=|!wO2O;S+ne䉗)!_6a1v5_Is/+ӱ5.t9 ocfNXͪd[ɯbH+ۜv Ƭ}9{NTU0v]wRtT0Kq ]"w<;_@(,[]$Yl  2wwͮ);QUY>W!#6+`MѪ4@ʜf95{o.L}oRi5}mgrsoyaܳ[q"r%V .~= KK+Xp (4(:BJOuqaC weeBߠ%K?[|W3TJDʍ^|nC/pF&5ńLkjo=nJ898ы878~c!S1W0pbDnq ;%ã48󵫀 :Cj`d;~zQY8<ˀ8n 18 !! ~I>hVӇYEk!惔/qViتrx V \pymj2U&RKUҕ;:CMbNbN)l[@C&R>6,w0z$>?:oӧ *yVԪUcꐽRZ79`߭8'&XK ^so+Jl_x5A@cU0=T궀>,j> P2b0I Y\gE yUA_CgHxtp)߼g+<ӑ6BEA&>Pbrb f_a;zF"n(?vHQ^fm,p|*05zOcBIc,USnl}ȗR/5l\]ӿ\%@a*(CCtU WUm855Ib-SJ+p X |"&f6:C 0 2F:C 05ɟy>p#V:C 05ɟ3ȁ!nlԞՈoYK"f:Csil]AxT1Qo SAP -ғ)  T r˓O)aƨ$殗JFS:>!G8A٪W5C$^Jõ*Dq7ƙ]_G};Nj6n?\չmDo~i\VFAA>P/j73r|D9K}ۯ(XYwl@)|~P\! 56+c E`*DshגƼ@ ] 56kM#* 56j%N :C9ՏW܍s! :C9Տo`B(-% :C9ՏQ-@Khc :C9ՏQ-@x1@PdcA P -)ՓvMU : 83ֲvQG. >vP,(*tJu]cZ:U:I:!uH)yOyOtݷH)r՝l&~}I)(~ĕJ~~Nj(NFC-KCHY!^0QPY˧`X(‹J8R9+bVKqm`|N+S>WC?2.KL4mQ[39Ƕ@g!Ze 3;҇ 9rҞf|U&E lԀ*S_?c^s֖I}})g q0 `z]f? n$?ceYaM" wIkQWtЕwAe 2 +YxTu \];ixMpH$e2F X2~^C $I~pŽOR#" ٙ?i)e[3fTPr){裳ض}z.98((%UeYCm5/@N WQo['R T"{xD)[&@e [wn]z D-f ڟ9H52$N*THcv(d:܀=17d@'~ Y,i9jZY&V<)3}ɴeTk2w:>6ShWARjXKo%E6`!I=Pnd.1hvU5ʄNOvsB>!co@q(X8 >+[e[6a/@cR Ga(5zH%֔n{)8)S*J \ NKO aK=Gj<\] Jh!.iCyd nb>&qJ~8Q?;(z,4k3v@+q^JA"skZJG^aUimi!Y}:: @$;r7*ܗxA6ĎsqWA ҍk؍^[0r;D/oWKvEﰕP+˛-դ h4 ӈ$ 3!\!}A#Kkk_)7V ޿?z -slٺ((U,/tԀiTjB4n3 p2ni,lemCk[(cY(𭼠R?=Al[*^aGAyH $wh!]p)`JrT7/MIb5x%WQnCh,w]$YE2xDPׯT~(Ӛ79{Q=>pdJ?s/8mR}Pѕf6l$n@~ee9HPp `l4-'xEAc˲nߣF,PnTgʥu#Cfr3 lD[~e!%JeQ+oZg>]tc3ԳY쭯&c@(hɫ'j~V"*R&(}Fz[9PL8i%e?ͧwfu`ZR>RQ] 5/'p]׺?gdn,߻;1( Ou95qӪ2~kk },%> 5e PZok֬I.'AW$ c -lƴZBb8\ɎmoMR>~A7+)G?#L ʨ2cJ?$~` d_#W#`˴e ۙA=![̌E`i#&ypX!fןIʀ6S.H@šHMbi0KTĒ,Po.C6HĀ4ZB#DK[ }`*ƜJ m\>j>Iu" jZWxВuA#ņ | rEi@#{,U@k{Toγ5bh{(Vc!^vuЪ9.nLHgRS*«B&]Uwm1l)nCdPm]Ɠսե&#m ʨ$G~&hS8Ќ`6x 3HIҫsE]6>MUېWW]TEIE(b%|PM1bbwlIW: /8eG5NՋf\ݏ3'̞˜'W] OhQcM0A3)'bo5.W3 ~X 70h̭i ?k pl{Ⱥ]Ə6^TޮU!isZ)0`;jy&JjG Hz5Up1㜛F~{ٲ+N|Dq{{dY6*!⦹o[lzf°B?u矷f(?l4$ | !XDR+ .ԬN+1gt(}yplTcgXPg\*nH6Xljٴ~Fƨ|g-lٙ–Mu+ݑ8P Ie]Tܝ@G3Ej{ŷvPdcBbQEG?G λt mGsܼXjJuR]GC`Zr;m<}*ukl^:GN6mJS8$}7Xl+}|la̴dcrVb?;q$@94FjhM>8H|Jq-1"O8 ꮹw,޾l@۴ky8ZG n4[^+_p-ac|lRJJ؍&28B>i(-" emضtWKh`μL7Q.>O4=?w Vo/ck%9%=V EK5VcvBy;ϪӱT#`'7 alD]L<Pn$d`!n9] X,Km{^/`ӌr)?IVTy1 ).=b=~6e+T|r#"Z˘lT3*"̽곱`GX%x/b%jVɗTC hdp}])cȖQ?r2B},\-@DyN$Vk&kw0HJ*hiiꁜhu0[ 1Uk=WeoSXf^Q=PwΌ(CĎsD?NMc T*4 Gn4tq G[ l}"D!ĩ'HiuD$%{xO BTGKDynD9wpݕhI y>s+zj0k !/;ZF+%t'K{~FD5|!ZZFf;eK ö:y)-&Xg&ŠY]wIب 9QN<(%@y4XRF1@o={]vj\aA-S-)_,Ql353rkCB_Q1jb[#+Bonl`W"5pYah,9q~(! b8ljY[2omY3/S=EzoE1aE`u!ӑ.aRW+]*6ߔfVk&dd0Wv)OOK+Kz)$G!S)})LZ*J7al/wn„ '^a_!{/y&`DñY]_ 1ȟ c6Ș''`ܰp@,P}["hD}Z8 J&!8׀LGH<V0~?~]h%XONYǩAWi_ĸc(W4,$̆g5M_hJN/8W݆ 5m `]t>Aƚr8EE:{C7x%c19c*Q9+">I#VLs+0 _L81ZAzȽp̭X!1x)~7T*2p}j?Fcx$D>>R`ʣ`WtsnMҾ"r=wwɏAbJ:(1n_&R'o 827oAqT=@Y}f#T"UD<6zbOV-P٧摴R]F `eǚR-[oR$v8^㚄9N+sfhlGRsx:RT1\obK@G1+r"_fZ8 (1@еF#6׼1Z=S8x 9TU=~u -OTObi T-QPCZ iI9p_j?,Og:G؁Qۮ9I5,,R22b/哢E^*WTL'Ĝh6D*iy~;j>V_BF/3aA&eü'&GOxE-aYB8kVbC)*dxP g|lP,‹x CTTw}eBH4XiM-{@ղ.6:ɧ6#c q SoA4Ə *eevuYDM"NtP` ]W7:p=Z7 0JKŖ/"&5)> }(? SW_0yįrTbovv&2X)M8 s')c]՗}⨷-IX5igHW2v>K+Ԃ )-~fԏ 卪:f^~kVBH'[S(9j5~9 _Hrjcy!vF%M+B'ud*HV 0 Qp[tQ\.T0S$s}+vƷN}< j';pԢ gj@CA5cKkk_)_l7cUMmp|'(@h1~Ȉw2(CK\ *eQ 4ۯ< 9|N[쯱*!_w#+@bY]3MJ}FE'l~2X|3mY\W\GЯ6f.H {5ch _GW䭓8rDZt &cITLBXr\1Z2f"ٮM9v\pGeBD'МυdI$c{{7q1^/#;cO,=Y!{ ih}1t BpXwݠdT hg^p9UY'˟9Z٠ Nm t K=,WlS4_ 'Vtj3.\{FO)`epij照HWbx@ rwd Cs&ڏ06| l](l}sVCWIUnfQA(Y,ټhI0[SyjIK&鷠$ ʵ0l_\/Ov),ڀ6e,-mSqgZUaE-#㛠H7:7L#Sh' ]p5IHjYʾ-t]H:QMlo=Qҧ^R_=>heu BsHަkfjahQ+*$f~*nlF;1Xwfd0'5Sm^/ʌ/>_;{h FÈY\ŨM(Ldxi T @68=>vr@Q^|_  \oh_Dhqk9BsmbJx Kh:d_ M!#pgA UG+b;!NY6 VܾʣQ4ܪP/#3~`a_Z|5dgJvtWH@(<Mڣl{/mػtHZ3xK^fvj",[R$?"_Gg!e0$dۿH~ؼ`M5&OY3tԡ~ff ](4a$=[lKz=U aHT+$^m4CF,C 1cOvbF!Deo*7&@C5*2]I;{>CJ.G',X%iJaDqsCvG*I=-/aq5ˡ[Ý7x˓{{ &`,W9Kgl;)eQZz'@~F@AIu€Ww +AE)_4Z1bD36]("rҒU҆#\Oϋָg\RW\Fh>Zbgb8Uiu5tCq0kjrvQK2ʅtꨤu GdT AXc@BIA IAB3~_T*UƮ:jb0_𬲹ϓ݇ˬ }𸢫)!u^\(1hy/[b܊{"*7tQm$]a-#bUFEr§&(>d]Fh<ʩAGLVb˨I*Z0{ v`N[g2fDZ~/ށ1՘mcF-` ͬ Fү럿 bqB!ٌa`P/i<FgL`ы)3aHRh]gTm J+aoڪ Q$kABq;5('):Ps5')t"ě}4Z(Yow֗ q0;Nwh#R<vYnNxEsZ]l@c0Dd6Į*)SnyJ<\Y >l~󳺧 H!=02.xSw>̶ [eicGkrJ 5ߘ=/vNȖ6g~{wBqs[voQh89rV 7\U/@J~6a"(,&ו4#xXV@VsG:)p 1&UAP&w;5Y ]W ࢢ^~ƨ'/tEg@ ,3tj4QoDX0^|%} B'Ւ(L5֪1Ot${3l/bܵ@єLFק>KtpW,oY`w.SN)˰.%lu'SEJXqIKӝXA$ΜhI9ы$ RKpY:[ TGdA}ah}CTl|A X'O4c>*;f'u;(B~_(zĞGՄ!zhtvf6{6F8=vd8%rF"-Y_6i$#+*WTt\pڌt[7[irK CvyjC O5_{ [;-1puTw6z29*C6y/Bd0{~\ޚz̠&kh"ч=uAn}'Oڕ,Nô(.`;ބ~T+eS膌'8KlPm,%~v׍ V4+Җѩs.A&Ď3|^=e{o C #F5.ztnea]ݳi)YNs0,Zpc.]ڀnXP~˧R@ XێHeEY\ldž)Ps2䭲va8Mwuj(f+-n-r>׈hǢ!&]1'?"'ŮH:NF*~1"!,]OtwRdRň #ORMYi@T4$CaݲmЃA@KpnYBmnpӆ N X'We.y3NESD $ဍ<`Y]zՒ&!5JiqDL[P߂jh33!Ӣw }zlT .ņ:"堦sđQ(&Vص8Q^J8 @*~UbGi@eNI:t$fz*_0߸KqAN?=ȅjPξ $ND@_8Bx4)"J[]\ӯjka,,R^ihlVDwTGM>@KްuC_Sn\mAvNհ;{͢!R'"Y<j1>DW[ uը9Mr~͋1;!p'm1@fՏYұ{yC(\J5/ *h$#*]~?ځkV" GNMVkU:BEָWe#kŒ'NJc;kɰ—Jjo 5S\t6VQL !-* ԅ_7y'JHi &O;ޛ1ݗ<'[&{6բx' F":DpZU ?0яΪyqWV.lG(OtSˏ6eѲ׏K9iljQ[~䳤x+zUR1'Zd-uz]቗RLt7BY-EAUKkjL 칀e'*膲_ [8@Z>›y?q:~:vO!\AHd{Љ[5.+ ©|hhО=yډQP sᑍNqQqƎ|2@˿A@܌:v@ŏ\D|ZNֈH__aH`c銽 8ҖeCor&00-!Ko:+əf]F M|ΡT:īui ͢M\#T&b雳E_7ǫd I -YRQxp!IR?>yp~\QuR3irRV cs|φB'X猔4EϤ~xAr~t,T5Sv`?1?u+.gFxz&$\Jyts&|Pl£XFY*/V`9~i5I2I_8 ðK@qMJʀEuu^D;HD^ ^|m40紐H#PMrv`R?GSGofbQFcIyh3,WbbgtE6{V>8<$O4Wȓ6qۈ2^Uyɔ 42,U֞B'e,&CmSNGȬYu ͬ ;SKS3_9.n|uPICO(ڍpĽ<( ,G79MCoJBYYEKLm_Gh.0$&~RZF* zf}0DtZR5"U 7[ڭvuMETٝk~=p; v,=)EƄ#|Jk[=pbBXNrF#LW9 ^ jWUVDε@TU^>ZZ@]p4g}b$\r9WNC!'E~,ˑI!@ƦmWqjrzzEӡ^uOPnF+VUǂ~ A]:BF*vlھ;xTNL+\{ZcL0ϼ0XW f=NsOw+*r^sdR+CYQi1ˊ2f8HB/KOوExHF[ {!+Q|rK J<2Bʣ!7Rb;#EG?:~=.up:[S+Q8G{y$&JR +*~tzo[ӠO 0MGƨz3!ȈHs\  BA=)!N3s%)zk ځy}W;*HQ~.ԝNHczoKa~8C|cxnW(<jy`X:=T5m@EPJ譭#YFսmn b~\UH-ԡSp7J,Qw螢\=?_ɆŽpΚHmjqLJp{KNT\ʢ5*>J`Ng6E\H;~"Pӫ/wcCs[n,QːcZjd62+߈"|98lgjĹ W2[bX8Y'v=1\?U(-Ȣv֩6p} ~1t)QHmTܦ  A]2(!]a>0WZ /筐h\:s X['2 3Uoz& (b$h~lWq@ZYsg|GDs,t+DTkpk7yӧw|vq^~ގB#|sWyt{ E{e:\EEcs换~xpŰݲH^,e0!Q%HTW$4Y:ZKsScr ;g>]R0_bax}^I?FgAYZKXWl:5љw{ZjS' pr7;f|*ədr,o9IM@ߖ]w qǮ4ln"LR3lP,>Xr{aIE&X7KİHdfU)U&d>qӹNѸ&ԸI6Sx"Q!N%؍;p.}q'Tzkfu_6o*d18e~F0U8xQ+ (ɠCy D$b/ZFБӽƏ.!ثp2|_Z=O\'i){IFhW ? k( ѩ7G`܄hocN8e"TZO?AWCJo:Q}snE%¦gDAo2,aw} 1z|]O25@J%<w_<3lR@mevtσ]SR=8 ILo>&0Vh[ئjGx`PEKˏbQȗJ@o' }tfsJtewF %20MeVTcBýq+fG}cic<\*8V Y&@GgT  D05A%_QܨǤ _$`-בM >;ۻ/B !y͹9VGs߬=7(Z]&VDv?'rdHY^hk-c!8>,7۵iRe/Ec8 2i)^P\5Kj4hɠS -=-w jDJRtdﯾw:_ J@Lb p^:Y\4UjpIR;-8iz:esN,k>0w8+FZd&Ku{bg+gSfטHV0B=8^.Z .0q4_6~!5z+KH;C87G"R(UF4' * &dsL:*AˆelM2{V *β*f\ I;]•h:,nHBt NR`P|uIAq1JMUj@Cp`CaLSRDv-`?3if_-9ML݂xN\"]նBXu2AM>FmȌAk3)A,%MHy/԰u"Ēͪ?a9 4bq}ПDKʅocb63F^5fQWKA%KтaLר/Jh~OS Uw]ƪ> b (k/n…2$|:c#ROS6*FwZe'&YpTX®x7{)<#ȿ/xc:ˣ>VWS9t{-y΄830 tz.VO<)f1ěQLҔBu"3=h&yPeA̠!iQר4} x)t௲Ghi֬a1 >45_݁': X c㑺9hty:0Xnfh6<8:Rux ;εsل+SAoda;s{PJd &9*E׊ .8t}O9$z 2謞l%@!iS-BD͠f,NBG4[G ~՞>B$x˱W_u3r.iyQCT Zy!ك7_ F;bLݟוL;%<>Zx,J }vX|`I( mHR|1Tf> 3Hky4*Wr_\ уlPm?k?<[E)#'C^WYC(7Np"Z|雋&wSK":_,_K 1Wr![G| B/s4$na#oP"N{4u\"~Uf}zəUt5oӡH칃 ľ18P4QaDe:B~K&#,"Nb$NX?ڑ:mG̚?T~^ɴ.cˣ+[o/0YZb0ie c7 Hpx2^J6ӷO%F\d&S߿'}xsc{t#vq%؆qQD']m=vJZ=kj&/WUI^6N{ kgTI_G-R'a^IhqǮ1eCHЯ %?qGߠhqu4Eω2J"uM ]oL7vywD16ŇB^UEW5ems ke(:ȽYB݆g;-6W*Rjb}20Y{}H8ڒ{t\i 0 ;ml/)uRʟvsPdCyYY, 爤By3Ŀj0[r)^/izx|f 42 tu<^*DaXCilԩ+7iKV7c~} {e TdɒB.[nk 5" ע:a@0Էҝn}+fIq88&5sFӡ( 4'TicϴjMtMfJv! x \I;Ŵ6ँx3+`-fTYj-LeP9pJrL\DW\$i"k-s;r63sSȴCߴءf2w`lz3՚@ > 5A:.(sPxȳ3|hԜHyOU^B̄ߛg}znb"!:$m8smJ߃j눾V8B)l3<5뱏<կ Aنc[Dr*H+g~uΎ|}-$mi>LLFdZ ~У=2 Qʤϛvys8QFPbӛ2Uz/1) $d\h(pT(HDIV rg1MWə:?=f]^@+s],ۿB;2Gfh^fxa9']fЭ ,o3^~{CQXovg9@)Ɋ $)4yz!ar3fR!0MBXd5:g>l|oX'0̪J=̯͗Tv1?! *meT)#2W J!J$:lj53Z!N\lFy2O :Dc[ 40 ,bښ_GY _Hia *ҍSamä5]Q| C&k ݃zXBh1 ^˔2^iC:mu7L9_X?E8.[&rIaA'NGK((a$%k.8A`Gzk >gy"EMv˭ uiF POhHu`x'wz}`\@+[A=|l Se/0{),N7բ _| i<'Ff:}+*S&vBWPҒ{1q@ly ҈´隋5½%ߐ12y8^ rX6o}r닮,قɮ1}]#Ő$s<ø-sD4X!~HzyZBG^k/?PD]φmH3C=$c4܀M|տ)-IjOґKS%[y̽474D&o~@.My$Yig(vsV eуَ+q u-[+7tNX5UD5+:i7Έ[ݬnyEi g ]2lY5.ЈnտpLcWʀ[p N.MYQx#|]޻rwT[lF]TJ.j5,/|3hTͣsAd Z5p &awQq)D"i,tQ!(ylMNx)H?Kƺ<`Nᮃi~DXPhwO,!E8W'hICf8w\ҁѾon=𸵭Y!ONp&wK MxAxgy=ҲJqTk@\^Agu'qKf6kGmtL) ui#?ǢIb]]mmU 3ƄidQHi$Ab+PLrRQ."JQWT}>.+`ٸN&7o V*/(R ,[NֳvpZ &?yXÍVCa&Q"LlөYNХl{}Z_aw^*+a\V,T,ЈKʩ֖sKUS3؜Z|hx`c^tohnH˟4An>P_<Mڟ6WGI~YxMIb?,{#q ' j]:)G2+watw%J1ѓsݜ`x6t)J"$y)<&L*I$9pXƨgaxF!\="<ޖ&H&(IODBSʹ98g&-'+m+<p@pʤk ͉bSHe_90]o-]D1޹x-TWrC~A=PY,((p2ȯb[mx-? y &pZyOп%ne𓏖}A!yKtٽ4L Q/Wu˗+]bbP4ġ(|PW6dT~:Djˍr%󔪋8Nþ#S'.KFF3@q_c!.,^a Y$־MP_Ԥ~6lV.` .,T ]PRd4{äI^hἙ2 /UVh&. jIDnH"Q!Ui3\<]E)xZOeW=1,?Gekc%Dj]GbfYP?=δ!qv^w EzQu5Q=yzР/k1\2rgSa&biFP0ڪ/oz'D"I䀳dr.- ׿'FK?nM0ȨJ UB#Stam`QgEk"ėQ"fVvHW($ QG!"Wmr/5ژV:]dƓ?}|syv?bO7*@[kHowCONӞ`=Ri.7OK95~gauKMm_y@xngcb|xoOL]_Wr60Cu-qsI0ښWۆ@#bKxmUcgy`l ;DҔoi`C?Wx؂Mr|ob bGz!bMwd?$Tx,Ivcb$]^]FP uˆf0jMzF&v YWB]\x̆[:н_ц{!MފIt?Av:Bߙ͟m.J_MЩF6nugLS[e&'s&CR&xҜuzնMXmƑδ 9>szxX֜Ֆ!ȯx6 ;a3Qt{F`hYHʐÏ#{  |⤣#  <9?x}wXl߀`V3  [&XˋƅH+e f<>}N  ;%I"Lyy)fk4vAOK d!xUpRz2<!rrdi{wiw$\yL&@*̂##iWp:Zp )WIq~Z] Tk~fk$AbXC#"o}ZEX빾 n+sR\3 s녶'бL lbsj>~J\/42P0W3sN{&uV̷ѨK0@sL|GRtsVa"|j5Y:ߧ^ס$;d,|hV_:,eV՘훙 >V' O~d@C {fmKxM3R/{b3{ N6cs2= ߗ9 ovTc'i{T̿ZŽJ)甓7Dx=:,ͮpGfF Owo97?4 ™:C k_mV?p(G`GR┾i\}O3o\v/$GA"iX~2\^,*|q([bzEY+[|cB=kf ++Koy/O)]Kr_S ]T6<2#mE꿮i$?fiTҲ^#~:" 9y ګgd_͊wWd*z3,I̶,)8_1D#ƥ*ٴWq O^LIvN0MB}R31q+X:L,g`XB٦FB8(pPiߊw>Dhԣ߫YFKp|Ɓߗ9 \pYؾ.>-DAJf)Dwq8_'kMTYкR'Y%|pkb挵g~#: e' w^qם)M#G[[T̤FR\%-]KED--v )C-[Z1lR^@<YGllm%YAoz "ߦS0g>MCTU8Jg"]P >!\J7^))65\,nv"ΕwOUM"fJ ,R=X!wߚgP Pr`Ztؓ~ĻB~$.Yq ^ރdעԢP$ȟ 'V7iU *Ky+foI95״ʩ5oV@Z(V:6yƺmN4Ԃ h_]x$$D"z&p8ҁ%\j ;닃krЪ]0"^Dқw'f.¢zp$ ,u)P>,+䟄M[MqbsYT}v@pzǚ_)=X;9`S8@5].(sS~'-;|ʷIU’kH]Yݶ tjv{X$iQS@]U#j6i~~=A4-B=XoeϹu 1@OŒ*f^As/gid52HϏ.xV.%g/_9 &0z)A5˂i-YҬ½>amKz-!TlMX8큮!tͫJ~m/,88p e{J8BD/`I_7J:_>}27ǂX(sی1!հ/yA xź:_ ZbGҺȗ9_.GLyҜo(vP)y LX7K4q_s!P dRs3] )e~h9i Ζb '\Ԕߠ$ټ'+G %QGg?zYi?fH1*CG\g8,k_!1Nuq 0 jRDH~_g(*6H">OЂٿؐWXԿ%IRq5ub1bCe[lc" oA~F>PߚaHY0^T@"f{A+ۖO܄ne@ƴ+VJZZ+mwdPݳ@&) 2hźVSlXIN02z/\Y^o2 ?< ?9S3mz^~):ڶH6sgw-?Ib,z<֥gqk$aTY-3RElh95^]•GQT m H)/}.6p9;#ߌ:S {>V$@RgUH^M)% 9͜_9-:">aд+4n66`n$^Ι\{V:CuߘAU֡BaEz7ZdfNhǔ$\DySdQпR醜My$G-G޳œL8n82<ѓ^5⬊%u^G2esvkFLønբfaC4ʻUwrxj6._1R[ rĎGHAsP @瀆<0`!_u'W(%:g(r\bo6(,֟z&aU鸝On ̒$:1Rjj!)y #{ ]ݴ.>j} 5g E`*푨%.aŸ #ek|v={LOѨnҔ|?.%2AB_5xda{p3;>"0]zNƼZaz,[76 $WR?>InC%2K YlϽhCNc< ()XH_;;6\yW o d4Mq۬=D0իwwm% o_5Ix4ʥ+^MqiayI4g嫓UL\ `z^B+@#gneGr\bcXAr\jI\[ca^ t&0KOS(2DTo P7^,W$拆Nb[etn2$T)E(}@l#I_9O6kP C~ EV͇v. &!= $"ƙLW n`@œT\<(=m"6N&g>l(Y؎!>:oqSѷ7֖?(8vr@PZe|s&|25TBq1SqBۡ}e`oZ/ ^ShK8v5=/5k+Y2('na)/ UU"T2&B_ gb/GYDQo2x@Ag(#4W (CFLet>_nMV)9pN9H$WCE%խnĂG;V&B3 H-&fdYHhOd PqJ6 :"4fcY QX{c%aMNy!9x5 \*j<|:C>0PHRJ@=_t mR/8vJzS$힘f.i l$F#ۣ}()'e>faX&V*e0ΆA 5|j wNFq+?C_{:~!Э֮pq 38Pďnxb`K8⁏tj%Ո+$Gĸ9'ī\0Q{W7a>Zvv4՜,@Z9ɱ=*>ǂUQ,2FJ^B >FZ,`TM)n̛0oy*ј. =+D}L/qL6hiA}Q8.K/%˜p{AO08:X.M-Rg\+G5Rɵ~3v` PASز$# n>Y,U.)I??|aOģ6PpJwף|IHcŻ>OvcdV`6 uYld[oLZBݎPY_2_23_mvKA*EJ|PwL[#E8X$!< IUK3%V&HguV?#R5jrҕޠF!-7cJs;͂nq:l#-U $a431(7chMvMjcK5j偅Pwj\3ʍq.H 2Eē0#;OQwzͲ 08g5j`hF*$SvxQ k|Muz ^`&T7|&TRGò+5B)& n[A$_?(W||ߨvoXOXb5_oN6%c1!^'Vr.0^=ui@uT0ȼ-W,Y q~Q% 5]'~WP}@V̷ގB+:C7,M$z4H,^jzK'6y-:C7%a ^\=P17ɓu/2 ?:C7H"L?sgkxw:#>V3m"5M; I:C7Q,5%j>QM} SyţVv@YoVKx .JIP:C6G? eRX:Ƌ; r"+f&:b3|_PDrV?azcP{1ߙIA3P -R`I-tԙ"@]7MEy0 b-2X8[6kz5%I1D^g}O<.wnڷ*CmU]@uwڡ󏪕G{ZϦ LD{?kjJiˁI؛" {3ycQQ*+vy6C\X(8o`SF,4ewA#U1[H^P]OˁAU,9Ϳ|ckkދШmoyg'hAf$HdqDDz-Z>7P{ AA>PAP} 5 vk GQP2IC:[pRdɬ3/ar8TZʗ: D:=Yz룜ZM=xL"REijT-$‹\6 8d"@T%ݥڗ2\kV54_c)"a' FS®8v8Xz8i';@t )i %PA p,kpfѧ_&IDz<ׁZ)| NXf54TZlس`Hk桛bGFBt#I8m]I8J4 ]f%IeQ C;3L AFv5L 7L=h4Y[2Hs6/ g[9*G5L~I?| |cT&4lĖԊ:$K+!(N8"[f'M5h!oP( (E`:YO!3"+m_V^v+%60? QgN]FzŴ+%J:Ck[7X(M1c/b$paM_"b:euAaɦy ?$4X xEI:Ck[7X(M1c/b$paM_"b:euAaɦyXӈ+U*4D:Ck[7X(M1c/b$paM_"b:euAaɦyw+ J)ɂiK:C*\0~#W" o չ0^4$g7?Qv<3gnYt\7D:C48?i궻 H \uCn3a" ӛ4KݙÍ|AK:C48?i궻 H \uCn3֐Rz[51mɎΩ2FƃIa yGH:C48?i궻 H \uCnQHtqoWY1oۚKDE0BJ&:C48?i궻 H"`?a3RNpG Z-2?MYtAAP -RLG Ñ44^7%@MUB<" Q|E]#O× !,}WkV]^,n){Q㣖ʌ0󉭘d[/!,iO ə375є]PH'##8µApFk5D6P;1z覅rvú?zEB6F EOQ[A>PA+@;"L0"C6;R!+Ŀ*gR:- oN+#K˲ʲCuu_A1p1)&^<F57 E\AjӉcgDEdؿ3dbj?w@ V 9V57^ST@I FȢR5?>)Ygk)1}4f57^Q jφe(g5^NXl QQ&׭.3v57pV&@ᗄ&-w?ol&|.ҟ557pV&@ᗄ&-w?olZfلmb' 3:Cc`Ofqm5&Rfϩq*91:Cc`Ofqmhq2 Cq"3:CUX̫|EckwEݾ& !.:CA%B&8SYETA2ak\a.:CA%B&8SYETA2ak\`2:CA%B&8yc)<\SV?Gv脚?ӄ@<AP -R`Dxmi>ٳ$C)~/KoD`rTڸ{n:z0Z ֬Gp w܍zx TcD)XI"uh>tDDعZtѦPA4:e /Y*T$s$-P~̒v@u߯ifpF Q 5iΏh}DupD&{Xo~/5jH0-b *t3f{WWKI_.&5jH0-LoJy^l@^Q(+65% :! ` ҷr;Z#/~ls#v͵,F5% :Ӏd.xZ<° TąnSA-V5?A^{vԆ!'@CZ-f5?A^zµѠx̂n2v5F  ut1ld31Ew ݃Z$(P˭L8ɖ:C1!cEf4kH>ӋlRX׽J@6ɦ:C1!ZWfzUT%;.W^‘6ɶ:C1!ZWfzUT%;.W^‘6:C1!ZWfzUT%;.W^‘8:C1!ZWfzUT%;.Wo0C)5! "@uA8:C1!ZWfzUT%;.Wo0C)5! "@uAZ:Cgk ]U&?O1&i?̗G=ioC3,ʅ1)((3AP -RLG 5ۡ,q-0; pa,Rh/hũ%ör`}S\FMrg/Y&rK:QxdOݴ{³30Li0^lV_/N:(`&y$- Yͤ;5_Č}K}n~zuVȪ$:V'd9.p uKr6`zW~R^=B%8"*`KY8+^x}Q~LVT~ccu95yŻ Jے3В]|X;ÒDxa1 N苟.j fGX)khb];_K SѤ["cwPnZ-;nmG]BmŴg.|T÷j(L!mV(-Ux8o_H\(TSئGfoB{>㧻CRPaE3 )6$맷YAxSry J;IA+ % iKC%ڟvƬk8XC+٪-޸jSz\Nj+X FVbBwA#KkkkTLGtZQ7(EK\ ʭ8OC^"ȢkjcYn=k_(+g\Ti>/{|AgQ -$Sz,9&$}0Gh% fՕnHc0#TgZE=dy^1pW40 Uy pqy+|H4PD 9)ZxA5XOC*S4}) ){Z bsɆVZ9KHQD6kY3&56UY+B6$,k[uW Y׶Eٲ~G3QP}.AI*L]COl! CCoip[xT)q^ `\.\Mb '!lM6hLzU~CMe@jA3-28NAQ'~UgU eJ{ &A4En$4o|r Jkӳӎم9fԜIꏍp*-W5ػ\Lum#诿㒼iIjflޱNkb$_AmEصHN%^Z{ͶBq}t YPMo44ɉH>EyLGJ24}Ʋ$`Oև=8#Wa@Lj5T v@>7c}6z  12Kf~W|,. =D{^ZS𖣷AACKd`E}bs:_4kkE1WF:P.R`!5j(^oY=S{!ʾrpAS-R)1bVd@.m Zr,哖{:QN6L@ai.IL%28i{`>X 놸YK Ḓ;v>#\ڸq.FM*5q)~77m~qXxCCUtj!jܮ[SN6h^C:!#/0?|{#(f.᭍4ܼlkr|#w_(!@;q27V0?HK.\T S8Y*WՉ73\JU\ A6S$HF$/V!|Sm?\1Qې(R'qDs2 :F07%LQ'(>.~r!A˜'v+mOVAsKkkkTL 3)r+S7/vϷ"DhI +1=92T e+7ixLȕ$7SRQ| z&H˹v=Mj։x(>T־O$y0!+tù(2/М%1A~*,|]5QWAFxy<'5=uKr-[b1r\"~hy|Qmu2q ;Q I%!V/ݬbeI$?memK)q4` Ǝ)rNjDO[HV=B1zW30J'hu[qrϲA;QAe| g'3IhWd0Y:X*# ` ܪ5 Avf:C8rbn'b9g̔!twW32Mt~/\zݦ)"rFzJh#ɟ]z&J~r0+FB9(:| "NXpr]e2zM1t1@.Gh8_@m8A̞g* C { RQɅ O0P]BXյ*u Q1(A2t|d[TJ'Gvs{%L)7j2 4OI>W20#.?~~:!1Gu[oSc \{jb̝S%3c x2&3p-^+"su!BU;LEL_!㳃d}|<ӌ:k-2pwuq!,KtV䮤5d5/ #D)FWar'ɥx_$T >+,}jز$2`?Sm"=Re5<1k\O\ ׋ݺ0-Y4=,s5j1*XL=atcG6*3٠yH {?I5p4Eb{=7CQ5DP]Z[--3,Mg N<,LM=9]VB m5E]䌡$xRš{,Jևp評ĭQn0 + 6ѯ `gkCʸ.}HRʬÃ-xZ(ǮÌe[wf뛴dj\E劆.;q\*1$;(4c~f&JRpBJYSÛHLrf<| fkM =[ WYH8(h: T"A5.ʬ,/ANJ1?b7H)&;o V`(NdoXjˁjRWnF\٢XuEp]5EFSa|bt'J3|M(()Q׭P%pֳ9gi>6j{Y;<@I:yϦ1B:Q*bNl1}{_,r)xs|V{A>PA_B<.U*G{?1mL%n96e(=t*~1LYA }ъ3. Ľ'lR$\7O :\.WLҍIT_Gсc}=8'3шDG|{yYM` W@ 43CxΆmH7wDѻ/:Xáª#m%jٻZëD1Lk .)iƀDM|ZD%fr BaO'9J^w)B=r5cuA[HU(ٌj(k`1ɣBTY%@Нx?oa䂸I𬸕1 T˽ZʷLWk.0Yx*xfaU`%qb؁e렞_*V]uZNXX{ 4c@;4H$$&-!9ަ|lbC vuUL&]uŻӝ{º7z5֮mrMa:J@-Z/ʃe'3A#K_-2Ghs P?$a5)(d#Ic5;NBoE_HIcd;j8 i]GܠsA3#y¸f ~#b^yh/abiu!緑{.v#|1CBT~"(HApFrt k3_"m"z3oEУX| }=ܑiR q3+HrvI'A`ɝƂ$&`k^XÍr*Ǹdeur7gَAŲ@~P[\Jp[;&$(y7o];>>q .; ̺ wdt[r4fq4kd&,9qìP_)@k@YjK >ΧPOG+c !(GQm D}z)&cM?A d:/ؑ] XXOӇ fAffU+b.{8uNO犼C;-4W:L9xUy<*#9#L u頍U@7vmQ3ܷ .Fg,[$г}=L,"5ʎu";Б4#X/U(dNEN#:BIx(X#d8W H"gtyzCz^߆10/W.R'@կqYv P񺅮Enku$  iv"u?!#:Bp$`Ns1@&s-J~0Y}OV_7zY#MԠ2[uKXIIWՇ!NCpqr2q| l8UdDq.)W̬XðtY88A#P -RKjLW\ 5u[T,7߻kYuzג`uI+7nXKG &g4@`M1& Ŝ"@i5}up&I0iCF&y8?m^ӤO_Oo&cG0R1m1JFpY :cgRLEϭN& 8밈t46%:KITA̓\.P)C|)e'szV:J)$0^w>=Kmە7;;5g7=3q&q' QQ qQ//MZ; 'eCE0=$P7hQѯn_U02 O@D恖6S;SUA+V>P_w#TɼVsI2<ڭK IItl%+O@U3Oz:i$=mh365͟t"Plo!`yы6b N^{/EN: RI2Hik5Q"Fyt@ KXReNMZUߒ%W;;pRaKl\ -ƿ/Rس'Jx3pF1IjN [dp(ԔF]n/}3F5/yQ˸]̶:z1n/uQu;QU< ۨ}遻޻ߘliY2RL7W̕-Hd]0G2|m>{"?):?Cn4*|n6m4OwiwD ^8YRK<2~i >@.zЏtF.3f:C* l4!$80DŠﴩ\=*3v:C-Y] u2+M; WA4sP -RJLGX*nw[@$Wdh0˝LrI2壆BgwPÒQiV APWKgDMن"B%So[ Z|u 'C5hP%L\@m@ܪV%C5hP%L\@mh$C5hQ^џl_ OXm^C5q Y+X@~'C5qoka48C5qE+MP(1D:C7֨lQFD:C7֨2ރ ̰[@D&:C7֨2ރ ̰[AD6:C7ֲ`‚֝$DF:C7ֲ|8 Jn@DiʉsHT 'DV:C7ֲξ~ڑx^IxJf!(+Df:C7ֲ|=fu#[zH(]ЀiAEsP -RLGqe;2Zŧ| M[# D`Jw=@He%w:@O;$Aa/wz8HiIc瓴<Ɔ`U.Q2"+/0w7{ 6 u؏R-ULԥ| | 4oshԻ%%xi3Z:PraXMIb=Ս)eπy1hD\i@`( 1a5 Zكd6QUk= gݻ5vJ?3oQ,A^D(.IBXK=pݏg P~-TuL#'eDp'B/V/~&Nbo}b!#M.\А?3FPfS~0Q&:u0/IvA-܂BYSq۲qÑ$Ɋ, 6DƐh(>3II4Hb *_+ .?/*j o V@8AL>PAgtG/m<>\QwuT5GPcCE$ಀT5GPcCE$ಁT5GPcCE$ಀT5)F~,@T5<ʻqT5<ʻpT5<ʻpU:C0zFF U:C0z* Z eU&:C0z* Z eU6:C0z* Z !#̀UF:C0zz`cs W*UV:C0zz`cs W*6Uf:C$A@ N4 =d Ŏj\ȹBvfyye0kAUP -R`D0*Cw{8)R4"Lj୆3jESz^J|,} K],8Y@?IPju4@PG+D/➧o?.+GgT Q3AiCrY Ħ}o>HpցhSb[aQ0THHn`׬  ܃Q)>̾r(L.޹ƋKK0r `0| q2GuDaWn_Mk Xa,7ZISTz$ir,˵EW2GqmBpܣ8? !Vݞ:Ն?M+ 0*`ږY-c,aQ[mHL% (m$$c,\={GM?ی 9U9EkwPb4tm&إ6"N %MY+AžY 9*{HhWriQJc_*g\v q5I=ȻDψkHVW'ta>)Nr/aفQF`2ONg)>v&L!EMKS~ô^W/úKi`5-B|_sb5r/3K%HJTHAcPhʏ/f:Zx&UTФr؎;i1,kʿ~n>5a$>W{PY0!uC0]WߔW<A3%,V{g$-c_|iAa\BwRz۲!K[`pOa|s]V=決 :KǻS`$w/x*7]2[3x#2dW έ Lg.H,.*QTX{ ] ^ɻ%e}ԠL"10A]KkkkT`+]Ĕ)jwAT/A4-^C"qc~O'>TZhkng#:m~XDٻ, &hoMF%[r`U}&ޅC,[gTt=#&kzyN=yILJK@ FaK,O ŸɶɟD<>JQ.!Z-}4ETvqT;ʴ0Ǥێ5`bچ e Ѵ/1^{|L>7`|X$Be/fOP8飛Q;R_٠r86fQDbV;Ky2Ūꯦ &^_`7D% (SFenF:> j/\߿mp/YΎ}54d.֘:Wg9 ܥKύ(殣!gWJcbf,_pnRdScہ0TfH&[?n, pe2^;2:3RFCfaHs` }e[J;UQ-5^לKጏ<,q] bwѶEO8ի 1-u(YsFE)r;:~Բ',F۫'z g!ɽ h͇r~^\s0X`/]y(.C?A~?詪vg+Y縌ڑ:yexp4Y$^q<2tˠR#69 !/ߙr>wPړ > =8!(v# rb oɉچ|̋q?F,w`Jpz/q'a@hޅW!ІhzÉuGB4 ' >o^ݸy1g4 YE)pC1uC[njLZD*~Z gRi@6$ /i :d;ST68ܫ;)or<4)l"H@%(e75;#:'Q󵧆WobiWz縭/ tOKLN-k392X}L!pPxX@?eY =Z8;2Il@YX}!1OW{D- 0.ç0ЬBFf ?w?״^]:j>.,VȁY^Xm]3GpLA&-r*Y^ .Տ9Re:CSTdf7w/{{xE (MݛH=x5ڝֹ$d<

Ts>O8)簻יC?\ZqLjuO#E] Fj.W~4%Ks UHR^+&Բx F;^Yo^ Pb>eU bS.XQ Ae-d`E2菬wͦgetcfq);~AꡀzGlj~V#ਪl^rJ*5o#1$I6 tYzJ[L1|9JchnSDžΞ 4ؗR|pnHnmB+|=UCO79}K~ ߺV9wvOx!`g4cXz&AmKD`ESL\.]s\{/ v;:N QJC@ߊ a$ @WH6`UV쮏sp.Ӛ`iPR'[U ͐J16mW؝,CӥjDNنujQWHl <<PwN Уڐ2Z܈XY' NNJGQ1j=W#tc5.hrp*%6cHgyQ,%/yzMN悕8bEDiM^hp>rPmb?٪^26%lB2<5CD;/_m[3tY[WeX-̾FZ΁Uձ[*htP6˅b}]}@yIFΑ#Ԑ{13b${h<1.y&u-Au-R)v+fE4hĬjvC:nY)KSR*~޵c 88\SRgbOdepN Jhzs4K>"NwcjHz5^ܣ )l?@ϭeenMhsi jZiYlaʐ@Ӯ=p}V Ȇi_:٨%k>,E$ -CݯzX|07cOٜGdI'{^ͱ~JPDǙp:XB#|vb٠]IX\{7M1Vp@!/Үisk&tH{pX|#m{4sVt4Զ }_87 (}:C8r97Y'3:YJ 5%z(M=Hef5WqIJUQ"kqrESkV͓E'sFzs=R054dA~KkkkTLgQfF߱Mrd쵑p71P+>#l݁(v(6?*"[|VuU)&&),yZss\=32s hsZ.TsEfɵ}ynj)d*2U+W2}8 bM[kiínl,b Nz6@o J%;f imc9ˑfzva]|%**V<@drYib6yj= ౱c"9'7y^, 3Ӑvά" %~ş Zw\Chx{E!]~DjKbI ҏ<[5?Ogen?*çEVpil",5+M:=㤣# fyA>PE4+ġ&w3Sw|m[h]_jm%%J0GH gmn3%WK0bxCkARF#o'deXe6{N!3Ę@ :C8<$Em||Ő]9Ĩ J!' :xh2Bۚd2ha VO36e =#CՈe<}a/CVd^(sNޙ,=O:+PӴuA&>P b\;GgFCTr 쪇l"Rx.wׯvP;.}D\K^*  /P'5za{0gD #I o),:=r+*o4հu[m@8.hP&uH@,+!ĻrɤѮEcкUNIx-_e{l`ھM[%HAP -ړ)Q&-SktS)s لr<˵ t5I瘹;#YMjHe ^y8ީ|%dH*WŭQtt200SۏH#=dU5 DI@O ӾN3qaƌPK8zx;Dz>*< "; X"lw0ؿtߏ*0JhګF2R;p!è33z[+ђ1}dˡP@6`#( 2N,KTw[X֪$.=#{*IE _ˆgIVG0 &Af>P_ љ#FSW#ֲJH s?և? (wo <̏-fȷ6t Qi`5jƊY:ܿ.~7%ٖmy969^@ Pwy fkxDK2e8yN:#M x!k l;2iTq| ' uY r)p勗ظq=qJkntUH?Wބ-;jhӀڒK˼1ݕ#3GNJ+ J#V5ǣ@]MxIElcC=y*SKd7t07PE>?_-IF悽 DeYmC@*FV&XÄIGikx~!BtU s2IDYJ` 2f Lv:Cl0qgunjq522HUfrl{}:\DQtd njNn6o#$g*Sqfɽ:ZУ[ 'hrj +y |Zɿ @s~'DfglS0aft[ ?Nv7=@|=xh!ñZ 8&)3آn+j4ߥ` |FR^al%i\PyIKlpʄS/ '2w];=],;ry7(aIKz$&A[%@Z(-QPs zg5D;ևH\eUr=#%ѳ](na0 (52dQu*E% 9q079BzTz}g~F{P4KW OI94BxqHc R.#Y3e,)Wl2$Xv*AKkk_LG<W&Fh#fZ œj$Il:QG&bHq%NJONaxxdwYzx[O}@P<[:)hZ@(_yF[y8ŖhQ"TN%j.(V߰pA7+P[soxJD9ڈqa!,dk2(;,˵6,2MVvSokuws!W[կZijɻ bOv~,V3eܸMG}M Q'H:2DEYoȄ9z;"xq 4ݼ~2!-koiYe:<|$}tR)UOpY7Ϲ\8Gs]?XJQ&R%{E 5_Њ.b`.Dvȷ$XVktrq>{t9\M jϳP~tΫAXNn4.!u_,Fघud1jmT5|%QY1TKgYP~u$Eg=̜!kU.;u2Ig y>A>PA3֬X,x0jbA)"ץ=ߣ.)F7M%]\QiVXzǏyaz#3jxp?%E} {LP~xB _cƶ5c,W69?9@#;]u8&\U| mU/=W*-!p(rUChD5/OMzc֩5Z1xܽ+JCҮ^uB:C5J ]ěݾNypPqPn}6SdB5d_^#sW:pqsJ:C5J 0t)׈PjaݓSl7J``6YFWIyr*1nV:)af|N:C5I#f ԤOt%_tUL#T4Y^ayx$?sJ~(ASP -R)ߊyDF<̲LHI{&fW?{}14`@hIrRE. Z@3|(4ˤqR qZ bIuT涓ȓ5ue>7A6>PEŸ@*-o;A *R XOL/KC9nTؐV14&5]3'j?Ӓl']^oA.0WS7F:Cyl'3дF_<˦0&M*P^)NAדP -R`C l&(R7y! 8N$`eXg21!7Av>PA6Yq.0bfȔƲP9|H-w8jcZZv5f568-6dɄ8B&w7I,e|E6nʼn S8:Cxr10aj@Qɚd(h;J; c3[e -MOK}%ϷB&q:k7 82O쒫m}!7,7Gf uqU @AE<Ԃfc0hLeha/oDg8G1/|v 5|6L횋CHǝՑ\m0 C;dmŠ6(bs5@:ȡq~'\J[탶߸.TΎ4f~Y=(RE4`zAG$_!B?n IE0rU %CZ֚\%M\xIppګf$>w#1Pq6bQԶwKB zt4 N%aO39oX;`ίUTw[㶜P*ӛt qmYz>WfO;s;AG[(n/QaO0m7tʠLk &i{r*+G@J@"Le@1e4PpXNRnD-ldKwbdWu&= ^X`I{%ü Lems_QWq#>``(#zABTNJܓְb1^Uֲc7 n*7/J#^I{h';c3N_j=X,U5ݬ^ *-}:u+谋;yFه$wmf &3S XO0)NŸONXuhCG;Oʹ De.tÅl2A/q0E] d1EU;)u;y_8= @DiI ?:W/fTBX/, D H,$j-D"W'lREhbj7F5=+ eMh]~f a!$ gطd K-WT'7]4vmD_57jy얙+!mU ۱2tD_&R>ҧ/ H{r+l 1'ABph6r@k "3f*Д XV&@2̃gRȐ,I@>'כ,6A/\( LR:)y!$Bᕅh| (8!G-oݿkNbPITo;7`F-,2b3oGAoN~'t"nZn9Xn{nJkOÁKU FqpFѲB!&,b&ۏ5s^1JD<0>3HVfV߈Ut/ F(ud}$m#HcG+#t30X5FD1'Ou:8^'6uO|,ֈa5 Qr@1w\FC .oiъщIדB0zU[4]A,#Cjs5r=\!'ljr>Ǣ z&-$$iW nP@c drww1ꌊ1c\#TfaSPNZS6Tdp Sً!ӗy̿"ޱhzG\' mt~Yf|ɬPXŕ'F7i;[ AwU~js4O_r8P= +Jz\ Ӹ:vę'~ dOμp}@9~m#ʩ2f^G{fI@TT7@z^R]:Y. y~`u3'g `"wbB}zm րc+[9$G|.%|AY6t#5@p?ohr|B.'^ ȍήD]y[׺ݚќi57UWEjbJx%BWS/g 8MC!"߃x?>Nq1Z>j:t ,/-qW8 mA+)?yyUq\u['Q[NlͥD?t`"խX(\UEYh=m^Tu@YAp+lf{hך$uF_1N}2=̵qBZޕQz0Dg6QO <$iuȈQBiFe} f}d8 g$'SHȥk+Ӗ|nS)ZFEq`bZjͫ#45{B@4قޱ0ӎ:!&i}gM8޸y~HnfglkhM5Fb Z?FQʈI/a4g%cuWkEͬfVwFU$b u%a~rg(=;DzA U(\,` 9a$DU 5pFcyNr-ab!SVOzO< Q9ڌ 5b6=ݷ&HzFZkJ`G ƆIjSܫ$=z]$'.zGM-#r{hSR(1,aFEdz"byEC'ʋۗB8(*72ک/k_.ޞ^NT}t܊Ӷ}1g%P9#$'g>. y|Ż{TNA!+b 0_ayD `@[9ڣE.X>dcz߿] }bUYD3M͟ OfcJv D'+=靴:_zTJ\𠃟Ji 9Zys(v[$M] W\҂! aGyOap2$Rnr% q䧭j==2֏|iɉ Bc*,?qsp؜-+auvN Oy4s?[69\+`镸4:«[tiZ>\fك|f^Sb)"nc#Ejp,xOcХ-R^CӺqϳQ*֭`U o_b d G2qR($Ѯ̏nP <egLmMJ(4πkG'є؞%aF(Fr` &OcH#҆C:˧W r[Q < ^Ye}{^skZ},}Kja/.yo$~jcQaL W{*)iSb|ZbGu-2E 3P*\VԸM K+@FOsIc*DKXf$eM@~_= ?ΠlXjzFTdN ZWN/hۚw89>Ĥ _S(YcC?޿~ۿQfJj**\mjƹ/ߢ{oS ˵Ki*yڪ8ZnWr9-6P8[ IJ*<"UKGVN?-ۗ3eND468M 'qķ5Jv9G~*;  -ե][i}o:)D  ޷N7D1|G쾽PY r 5"HMw PVaHWVaC U/w\@ˎs[KGD1,ć`zfS׎lA{Z)r]XAhL_"r?ɋ;\ s:`/ T$v!{a~8Ui!D>.z2\fk {a`^ݿo2, ;DrV d"-"+!]d +$TlNzi>k*[GC LEl ;6K(uךS@3;lb R|=_  qJ )` ffbKLW;ZX|ә&o@`k,|}AÒgյ589L~2Qx $'ư9ؕ"6c̡ 7g"=OE&=nxJ7+eCdE %H޶.JW&9Y)}@XDl 00Ye$YY8l.ŸbAOWjFi.k%*²K-q9 |ser0/^un"'rL6|'.7L_0P!Cɸݮ o/$9}a#Qug`tIP[^}ӷd*w/VЮ?Kƙ6GgR{ȞD-@S"C[ɦ |&gwo#%YwzbkF^Y{-d:Ӝ>Gs{ԐqUb)rvփmãjF e;X3/oXl=_wQ35a*qkQDx{IK`A ľ Z&S=gNf5uUfEѹ>"WSwwT܁[clIWmCk e~.nI;:o#oR ~b,ei )~v~ m݋4 3̞P3 :[j.iQTg;g=/TpYhn/q7(f]8l]}ECKz01m\FV&% •'tiçb;m8#OuKm_-[bOw 絍 /zBٕ+~Roo炛ozUvpU=l#:US/&MRwBC-pe]CpVh$[ ,F;9ն6Yq.JS!KA'Wӄ 8u \$(-' V_Iüvj'7^Wr tJK*Y h zwVߋ<6G6?7o 7X4xWW[̰Z.']8/L7mQ{y轂X4kf,Ri|D mm8 YQ)z:$<8QDbfH3x@,Y`R]E.LwC޴ Vrj˿O d 7ٳcH>u@t͟oA+`/BdZ. > dJsy:eη-5n#ǏT}ZUYމ=~^>JKW++oXyw ]"`A(HFE86[UcE0.E=cKN lAP#m;2kkQ:GsȮ?I#{qMsԛGTzUB]%"I?=0j^BJH;tɥw[D:)\CxfL;^!I缌HbH}Heciԋm Ddj":|LsׯEKTxN"@H!6c<1q mFnhu4{ @T(Տ8R<1㉴r{Zϟ<;v*}o|LwƆjXGiI<İDp&`M~?A0e0X~in&XY^(s%{r;8Hk[M (Dy{ζCRq^:;]krznuzn%LOti^.qe/cI 'l桌2uLe1Gy5!bJ@@:/ǵ& Z{qIpֆ9-))7 JSkڇ7og\JRKf߇#k&@[|"g}={@e/Eg0b Tk6@uZH?gYs3--9?ԓd^tөObSk|Ab.i@%,A4yH.#Irw ؟`ZNLQJ&Ɵ#o؂#~h)y-pz)|e* }U[y:ynFhBQK>%'r?. 8%KXEA5}*وa/׎`ީ X=C |n&|0̵ %Jl>FGo@X6U.e;H婓y0˹Ws$xf k୮ 2Ďsh~27_*z>a`j?bvDZISǀ"/h7(w겥RN|o =b$2^]{J0<ի@][fhW8A 5g%%OX R9ÞG_yܓ*kDEm2(!A8GZ&S+z;?K4*cؐԮMI{Jg'ʜ4J]YcI @8\=hPg& l $RGQ-R ~JV7#}A11?6+$\ٿie|O!3gFasL _fmzeз"nOK*&0+- <]'gdG!AufWuDҸ po[žT{ kTWH]ۢ4T; :ư a&4ʥ =]FD+~IAcDJABA&{ ȪTgTq&f܌pbQ2#S`~gSɕ;wHAHE`e^OE14a^ѕkۀh\rDäx/izmu+G XYgŁh@|ZvFd;?U҅b) -e;Nbud ,_1fJO[!_dF&ZuZd#\i~{9 ۲^`fs=;8<fD$ ^vpeC! GΟrAO[,8 @E:I䤼3-"$ၪ#{cʁ~osU& :\/Ɲ|yLB5?_f@I>*.݄parrWDwugyI-| QKC@Pn(_5Ev Z;5Arue+P%/Fj߸KymfoM̀v`^/: sp g`o!5a=hN29Uҽ-Ngj@㇢3Qb|`~Ԩ蔵G',VNٮ.8AL+һù-9lɚtFFddD %GqGV+YV,u9SrAAO#?}D;b-줿@t8»En̚ g7r(%0ILj?F2O{;˅eB'S9y% |v$vRkA&zOoIZc^N}k<'w|8׿,_]j"hl9ɖh,@ϬCcmN}!3P,mmZ anr&۹x׻qq+jEl7'{8zFWэҸE-_bO4hKrR1:E& Xg54<ܧw^(i7;+Gddu!Nd'?کavp,Ei(EICOd.LYJ99ywcy\ՙѝV EROo>wgX&~_Ry]ݽ\t}iaCcLoxA #OW^,~vf^5Ikx&Qa*/"%HH`ʻH-N'w> 3"YX Y8|Рq$kIpm:7R ShZ☦!$NEїc馧ћ϶1{Lkb=, [Dp]ֳY%Mbb'8q_ mϗ:ddG\rFsf}Y×mAЮgZײ &'@O6QK뛖h7leQQ/ri<>ƏQ24uudy~?C8d >PJVaPFoܗQE暻Z(}a=WR)uRw1RYfŷ cAnTVygmD2BNN  _l0\h+. BeNkI|>0AIWe03ߌ!3m)w>ƬB\%xr펳`},xg{GW&w\NSOVUoFz; $7 n@p=(w]42\kЀs9x$ .87 _F9-r2*-[6! v};;&ݯz'򖵿,s,̕qV{ɝiS~1DGpq$g^ :zb gW^I nqoIakM6`m6)3 &V2#Σ;1OsT ?iZUhd?_jʓ (7` @rGP( gH چrü]e=boI=_i**6+ȫhDZ|yZ)": L-zę'I-v }E U>=X N \!8p&gkڬA6iΖtR9 O9&9؉C<.WZhv~ 4ZI|ҝ?܊\8.+Α%>L]2oei7-Lc)G1R6-Zښc]U+F]O_49"C;y$_J$TIo,vpYȭqҩES\hv~_h0Ѝ i>+CvZn1E3)O;Rio?gcVM4//KAX:DA:AY4gZ&S3.`m`rkeIHpK $i<3E")'hg3IHJ'S.*DkV3FykD,JBC+8X7LN}tǖc_&Si.yo!&p3c)cD|f 4YEpY̹N3e}"~ 0r{D(\D5oW@&|y?N}! !^V%[t5574^DZk`ᆰZ-#z8,9w o1;љ_M"dN#Z`l RA:iHRXjY.<K_fu,t+b`beߔ/K/U]Fc.4%[%QIJFF(Oڣnz6TՐ ((8+Vmpޖ٘jeSz0!,f˥zc^FѶ x7EéXM>851n;ri* g܆׃ (GZo7gŀ)N]SNս#c.<"pNO{+1wI$1AFku 3/<898֍ g$Oi`ylS[|gb*%_#T#8#y]ٙ-D5+koLdUdJKqU^qu@z YhfO<>]fe9um?Q?ac"$5F ?ɜ{{]+(0e+x1w-a)MAoMvC7B>:a)Ydpd]t/@@ݬѸ*z 0E8 hOI*tz{tǠlo77O)#eƸЉR$d* ~Q%Ev˫ |اy."s/]e𝶤!OIg|=4 L AaTo#\R80!&/؆VG: _E~ր[^sC7ʇvnu3^y}Z_ڌ#wl0]:X)* JAjGL/L6]wnZx*>P՝a֘Ù祰gX_n 4@n-v-~9*2T'Bx5ٓQ"j4db5%>=8g] O #-i[+'P"';jTD<f>C{44%{amvWF9~ry A.(N|f#)g(b_`0PqDߊ#"eP9 sYmAU7 IZdY"Hba1q4Jn=2.>E[\i(a8v-PvF7bֆ$lE0dgkңe4 H jUbb=T1׷4--!cgR4<2ʓȸ_hp&sn/c *3[1+-[$ 5j]PhWKn2\ۍ'0MmkNMqI,m(+- D}~]&4PUI+ g┫$I8iI;B_oBA]fj92vݞ33/M%{y>֝a|LCGQvGo: &fA)J *t7u/k2.b5)H9B |"FB!p^7|PXa_AfL.=r)OS'dKv g3иO k~+!4C}ggG'nڼX}pH>/NΈ YݥB Vy DhSAX_N[Y˨ɣD'Iug^T#D@~E Օn? !yo+c,Ix`p_V9}򸭣8ۗp)`eW6^c_]sYj>?@ފ$|4 [M!y; nw<((ځm6P(Q@bqΟc/KSOc٠n4('Ƽo& N3"Wx*}@!;X(N 4)>2ˍ [JdlN5w&('.'nIpW:%";V^]va5׷>Td-61f~h"VWTF@)KE&dkf ~ \4*DQ1[RʭpτaL 1t?+#wDh ʁAidwe0;y<^ŕ_w%[FZN8)m)XG.P(PÀ~˾F®dR}dDt`YHnsP~џo㧅3LT o#ypPƪp)ӽ6yg.(3 ْ7ԥgnq7x|m3GC3/`%s9:Z`pTP$QЖX zAK {)myWS_hA37LQ>#3uvb<+)%]0c/jRa$Yc^ H[+>J(QHdN.IWj ZbZ،2) qC9 88A&|BuMʓ=9Zf3[GTW/Ƶc vH~~P*7M}iC,>U4a>^l&:9V H2ҤOcԘcj$[ྔj ?V_ojZKB ,l/|_FV3b T 1K t60qy*jL?1f1R;(O5dzNYu3K%2scSSb*E cSI:y {Nn'B^ZL^Y)n2@yGc0V9^$Qiĸp48fBrM"murGw"gQmQ^`[[]l47bNs)ɵ `sV]7Q:љ> )4c#ND_t. 2 N3|Ћ[D5Yw0N9\Ksю3Rф B,+bݏj{ \;n"WhrxB4FZ: m*}e|҃r<O?m7kcR=+/@ Ɵ4^Uvbo U1;ጘ1)@MG|Mh(" $ ԥ-̨|~u&N1 _;Daρ 5\enkHC>g7@02hYVP"n$n 1CF.+Njt?ep:I|YC-qAs`HL8rfuİgY+$Isāa˩1-]ZCk{yZ@AqhL# zY D=r!@~F+(u!R5`U3)yidQXPnMH j5D1څ5K`nj8dxs r &¹59@jB..bRlA$b20 b 5LqFm BS t?35,g߉T?OS Jc䗆)-3k iAqH2nz-Pe3bavyqH퇮,Q1{9$crRLrf!I8V]ȡWCNDQZ<C˯_kb 9!G9L㦏HMȰKH JC Gޠ~öd qV>UNK^k+~iX *Eav*NAu*L_pgɽ}lCw.I 34WA/c?C\hz*vSȟPVe&F&V$ _k Ne@-Sr<9wVpst~OYsh fN [  =ȁ>zχxq"Jwb_.%[a9BhB]VhUYR !N/Js-o\:d׻,HrALܙh2ACȹILIKPE"k$#1.]`K ܬ#;)+Co }8D,yTud(% P*.(dLvVvp =Q)liҏ!xe20Ӕb@̙[r4/9Oƻ.10RP?I p H% Gh Yo? "򋕧SY ԹXBr^ es7h>1l9DLȱDuJԷdl/lY#d wX\a@@I3S (4meKt{;iJ Ey18K}`5?~95'hhH ȉ鳺deR*.! ũⱇ, }'/"§D-:s 1TwN~L܉Ai{8 @?eHc,.,(@;ЎWvV҂"F0NT|LNO[Iڑ-LHԏe9yZ᫷op-Ж8np6j૊3nxL#c_%͵cD!x1W=#X,G/a1VSO;j2bXbSz#ϖ{̢ ;Viyv:C4߿e̓&Tonr.nlTi0Pi0) U6 o>/'?!8_28Π̋[DH4c?i즱N<"X?lO&FK}Lqk |%h__مka/:]ױAs]Ud6@BU&8)1%!hg <$=X&J_EE+T!Xbkwcz>|VH}_җsΪsq.LX^.OJˣμ3*n5bgUXJ|y`r::B& a{bEX&޲Qͩ`t旌k$M 8yЍ?т~?dE-dvLij0!a4UigIQ}pB?!Je8mZԉDNOl_ & Ay-)2]; X97ⶏ0+ c7@ԸpSxzPs{Ey'J]r4vC%VOr#Эf؛?  cIUè`NHWB #9 B?8Y xc l.(Y$n p4Y:VaBɝ^SbQ1" AFM=M17Ɲ.Y`m1- !.2O@N&T;d)t]# YF`Y<̐dnS%EF<|y3mPgo%oŽCJkL"*";Jf_}^)eS š2K\uDIlRB؉Rw궙ꞯw/Jdi]e.'!ƉgFn-~Bl:E.Bý|v d/I@/PУPj7Si4۪'lgK=  3ѰC.t:p5$dR(  S,TW4k:E\œmhxlǹr@n"h rMzE$uq~SM>km i,\11͢Ϸ!}íhK抸;hP(HWV+inٞ&y]fy,q79>tii/7/Г?2cڦpoDSfH72܃û~lQSp$t1䢩_p^t(l)7I)%d k}[F,xD8 G|f։SM45}1^&=LS#"pv $2Y9r/EHbPgvu &#^x0+Q}PAȮl^|J3 iCVd=9+YOqa7WRȮH3:t -[; (XWFI;nF nr/5 Z+|>h:"9`#户1{ͣMcI*d,BnPrLr*1f#:ި  pR5eBlT5I?t&eZ+E`Ss6 eުCs/u\pihKJA+QR }hęN+I~FKR`WdBտEf`c<'|bHfs'O"?Q-KK|U_Wxo %TZTބ>,8tɾ5r[mc.Of[AS!Z~C_Z0zWLvc:-^Hh_fj󹿵%UXo6&R))ӺeUE̬ȑ碖S *×p8:LBǬ|2F$tkDAurhf:C ipq&:Eׁ-A*=ُe#N(:2K21oN^&v-QǼ4;J2/77YӬ }]So;7KFBe!Ի!JWIF]A! cAv9T=1ҟJ' ƠKqV$V}&Zv6񈅉^((KOD[ېb/2иT[Rvs5 0N0qGP%W#k4ӊ V\ԢИ =O3:yp*bO.ە"ѷPmE7膶Ea l{I^]KAP -D`MOR+T's[ p4l0"E4]`2H̀!I9CuO(^af~'=Z 2Ot0ر/ӯIR r9 ^ֶA}uԳ]zL]=fC`9 t0Jռu "N&RZtmj"A2dEύtQ!OV<3HX'M /Z4F$7 ?Q/X|ѯW(QU;`>|F=Vb@bUG<VeP,Da(e\2Rی?ѱV:Y?8a+rMI ٓ:|"8Up.!pӑlS]GޏnfqJ%Q'~锤]j`uPeQR< ˝h_.̐lb:(hԗw(cgFI9@.o0l/2ܧ_ZF.z>d%=^+_ EJ(rglL)^#+Xʣ Ic %^$+lnLz S/{ePpv75j3W$HTK4aTPnt=ˡ-IeOw.4pZAJ_c+7 vl_]y:?^?ck'3E|@#d N鵦wQr_`j՝سdĸ|!cp/,ۂQ'Xf1UJ9cmÌswH<ᩜ'A{0* W-A7F*ݿ^$I,3Y2o畠c'^#Lkpxt4nE4=hIA>PEq~T "1!h9j@V=%Ŵ:XޟSYw+ڏBuP-m6m%}؈(m,}:FqF-΢Aef$ōKu ]<ލ*2LO:Lʸ@je&:}_Cg B~c{iHj(:C辯 :o *.ɱo(B⣶99:uXɶԣPM;tV2gιW Wy@!: eֿ,65>k;lk3Cur& ,5 N:dDLuB 8ۊKV4 :8AKk_2i$P"~tͤQNq{n6VtJ@yl!ʍy-ajI6@=]7џd!&&_8Pt$daRqJ}|߸xd )#_OK>جlj2 &3];*T6zCZ߯("v؋ 3H"ZG tq-yAn9j VixA.Gś} 4br_xֺ\Ƙ #2AZEkl(gF?t;U!U@ibi3u/xDNH(ʱHJJVXePi9]0))r]dn&/2] ikl:fzc=V K])7+ܬ3'`&`/^lrßf!6V Qo` tn]. iF.בUD;<9P0T}=tuk͛Q= pD_|052Q$0~$ZA>PAet: b:p >Ș"WK0eЮo$Gno;yoowd"&Q}Zj' [An'] o45ő++v*CZET\D8C_qwN(OAue&"+_jS4.~ z @R5RLT9' VYU?*+dAPg';bӏl;[yTA.iAR&5RLT9'%Ĕ]ӰB*YgӍi ePg{MkEb,dZ[j46v>R65PL@LTĀLY[* B^P}`G¢YD5rj3ܽ2A7Q8Q%:Cj@)`=LJgC32':CjEP{i39-Y:.>ț ?,:Cİ`, Aa ˫ a~!!68 4:CI<$ ~ŦT >[@osƋyA:CI<4y#Gpq//Rp %ԉ(ɉB:CKD > "7Z=kD$sRǁ@cc.Nlr9+3aAF:C-b5WD@4_-d_uC UVStnR-YD0E:C4Nύܮ/-k*|5[Z+쒒^7KLTR1ߙE:PAP -R`C VγZ|0a:'F#lK/L> d㢎^AJ  2VYGNLs`dF>?=zf>{lOpx;L۷,Y]jAv>PA>h9jŸ 5N'x {=q7`BKJj?/(?í ?Ĥev(d;s=Ύ3Fs1tRN&5z^6$./zeHXvP5GQb}_h+Jӻȁm\yOO^(g23LsT1u.PCF5-eȓ2# vL@wq'~0_y-ҐїVU7V5`y.<}7BQ6p xb)/f5`y._?)#48;<FFESvoRen|5'I439'&E% !g* o HU sVOkf/1CMTMqmo\JKQNMHOHOLN_@=879757226m32/0116<:::<<^ nE70<Y2.9+)(##""!(+/m<!!! !!:4*%gHFNR;8;R;9<_DQNRO?@QBC4>.jOYVVV]jb\)+08EFJInRNG;3655567stco0budtaZmeta!hdlrmdirappl-ilst%toodataLavf60.16.100picom-12.5/assets/next.css000066400000000000000000000627341471504570600155670ustar00rootroot00000000000000html { line-height: 1.15; -webkit-text-size-adjust: 100%; } body { margin: 0; } main { display: block; } h1 { font-size: 2em; margin: 0.67em 0; } hr { box-sizing: content-box; height: 0; overflow: visible; } pre { font-family: monospace, monospace; font-size: 1em; } a { background-color: transparent; } abbr[title] { border-bottom: none; text-decoration: underline; text-decoration: underline dotted; } b, strong { font-weight: bolder; } code, kbd, samp { font-family: monospace, monospace; font-size: 1em; } small { font-size: 80%; } sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sub { bottom: -0.25em; } sup { top: -0.5em; } img { border-style: none; } button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } button, select { text-transform: none; } button, [type="button"], [type="reset"], [type="submit"] { -webkit-appearance: button; } button::-moz-focus-inner, [type="button"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner { border-style: none; padding: 0; } button:-moz-focusring, [type="button"]:-moz-focusring, [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring { outline: 1px dotted ButtonText; } fieldset { padding: 0.35em 0.75em 0.625em; } legend { box-sizing: border-box; color: inherit; display: table; max-width: 100%; padding: 0; white-space: normal; } progress { vertical-align: baseline; } textarea { overflow: auto; } [type="checkbox"], [type="radio"] { box-sizing: border-box; padding: 0; } [type="number"]::-webkit-inner-spin-button, [type="number"]::-webkit-outer-spin-button { height: auto; } [type="search"] { -webkit-appearance: textfield; outline-offset: -2px; } [type="search"]::-webkit-search-decoration { -webkit-appearance: none; } ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; } details { display: block; } summary { display: list-item; } template { display: none; } [hidden] { display: none; } :root { --font-family-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-family-serif: Georgia, Cambria, "Times New Roman", Times, serif; --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --font-body: var(--font-family-sans); --font-heading: var(--font-family-sans); --font-subheading: var(--font-family-serif); --font-heading-weight: 300; --background: #fff; --background-panel: #fafafa; --text: rgba(0, 0, 0, 0.87); --text-secondary: rgba(0, 0, 0, 0.54); --text-disabled: rgba(0, 0, 0, 0.38); --link: #3f51b5; --link-hover: #3f51b5; --border: rgba(0, 0, 0, 0.06); --divider: rgba(0, 0, 0, 0.12); --h1-font-size: 2.5em; --h2-font-size: 2em; --h3-font-size: 1.75em; --h4-font-size: 1.5em; --h5-font-size: 1.25em; --h6-font-size: 1em; --code-text: var(--text); --code-background: rgba(255, 230, 102, 0.2); --tip: rgb(76, 174, 79); --tip-border: rgba(76, 174, 79, 0.4); --tip-background: rgba(76, 174, 79, 0.1); --note: rgb(32, 148, 243); --note-border: rgba(32, 148, 243, 0.4); --note-background: rgba(32, 148, 243, 0.1); --caution: rgb(211, 49, 49); --caution-border: rgba(211, 49, 49, 0.4); --caution-background: rgba(211, 49, 49, 0.1); --important: rgb(244, 64, 52); --important-border: rgba(244, 64, 52, 0.4); --important-background: rgba(244, 64, 52, 0.1); --warning: rgb(255, 153, 0); --warning-border: rgba(255, 153, 0, 0.4); --warning-background: rgba(255, 153, 0, 0.1); --hljs-color: #383a42; --hljs-background: #fafafa; --hljs-comment: #a0a1a7; --hljs-keyword: #a626a4; --hljs-section: #e45649; --hljs-literal: #0184bb; --hljs-string: #50a14f; --hljs-built-in: #c18401; --hljs-variable: #986801; --hljs-symbol: #4078f2; } html[data-theme="dark"] { --link: #f48fb1; --link-hover: #f48fb1; --text: #fff; --text-secondary: rgba(255, 255, 255, 0.7); --background: #303030; --background-panel: rgba(0, 0, 0, 0.2); --border: rgba(255, 255, 255, 0.12); --divider: rgba(255, 255, 255, 0.12); --code-text: var(--text); --code-background: rgba(255, 229, 100, 0.2); --tip-background: rgba(76, 174, 79, 0.05); --note-background: rgba(32, 148, 243, 0.05); --caution-background: rgba(211, 49, 49, 0.05); --important-background: rgba(244, 64, 52, 0.05); --warning-background: rgba(255, 153, 0, 0.05); --hljs-color: #abb2bf; --hljs-background: #282c34; --hljs-background: var(--background-panel); --hljs-comment: #5c6370; --hljs-keyword: #c678dd; --hljs-section: #e06c75; --hljs-literal: #56b6c2; --hljs-string: #98c379; --hljs-built-in: #e6c07b; --hljs-variable: #d19a66; --hljs-symbol: #61aeee; } body { font-family: var(--font-body); font-size: 1rem; line-height: 1.5; background-color: var(--background); color: var(--text); } @media screen and (min-width: 768px) { body.toc2 { padding-left: 15em; padding-right: 0; } #toc.toc2 { position: fixed; top: 0; left: 0; width: 15em; height: 100%; overflow: auto; border-top: none !important; border-right: 1px solid var(--divider); background-color: var(--background-panel); padding: 1em 1em 0 1em; box-sizing: border-box; } body.toc2.toc-right { padding-left: 0; padding-right: 15em; } body.toc2.toc-right #toc.toc2 { border-right: none !important; border-left: 1px solid var(--divider); left: auto; right: 0; } } @media screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } #toc.toc2 { width: 20em; } body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } h1, h2, h3, h4, #toctitle, h5, h6 { font-family: var(--font-heading); font-weight: var(--font-heading-weight); } h1 { font-size: var(--h1-font-size); } h2 { font-size: var(--h2-font-size); } h3 { font-size: var(--h3-font-size); } h4, #toctitle { font-size: var(--h4-font-size); } h5 { font-size: var(--h5-font-size); } h6 { font-size: var(--h6-font-size); } hr { border: 0; border-bottom: 1px solid var(--divider); margin: 1em 0; } pre, code { font-family: var(--font-family-monospace); } img, object, svg { display: inline-block; vertical-align: middle; } em em { font-style: normal; } strong strong { font-weight: normal; } a { color: var(--link); text-decoration: none; } a:hover { color: var(--link-hover); text-decoration: underline; } #header, #content, #footnotes { max-width: 100%; margin-left: auto; margin-right: auto; } @media screen and (min-width: 768px) { #header, #content, #footnotes { max-width: 60em; } } #header { border-bottom: 1px solid var(--border); } #header > h1:first-child { color: var(--text); margin-bottom: 0; } #header #toc { border-top: 1px solid var(--border); padding-top: 1em; } #header #toc ul { list-style: none; padding-left: 1.25em; } #header #toc > ul { margin-left: 0; padding-left: 0; } #header #toc a { text-decoration: none; } #header #toc a:active { text-decoration: underline; } #header .details { display: flex; flex-flow: row wrap; padding-bottom: 0.25em; color: var(--text-secondary); } #header .details span.email a { color: var(--text); } #header .details br { display: none; } #header .details br + span:before { content: "\00a0\2013\00a0"; } #header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: var(--text-secondary); } #header .details br + span#revremark:before { content: "\00a0|\00a0"; } #header #revnumber { text-transform: capitalize; } #header #revnumber:after { content: "\00a0"; } #content #toc { background: var(--background-panel); border: 1px solid var(--border); border-radius: 4px; margin-bottom: 1.25em; padding: 1.25em; } #content #toc > :first-child { margin-top: 0; } #content #toc > :last-child { margin-bottom: 0; } #footnotes hr { width: 7em; margin-bottom: 0.5em; } #footnotes .footnote { font-size: 0.9em; } #footer { max-width: 100%; background: rgba(0, 0, 0, 0.8); padding: 1em; margin-top: 1em; } #footer-text { color: rgba(255, 255, 255, 0.8); } .sect1 > h2 { border-bottom: 1px solid var(--divider); padding-bottom: 0.3rem; } h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, h5 > a.anchor, h6 > a.anchor, #toctitle > a.anchor, .content > .title > a.anchor { position: absolute; display: inline-block; width: 2ex; margin-left: -2ex; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; font-size: 0.85em; margin-top: 0.15em; } h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before, #toctitle > a.anchor:before, .content > .title > a.anchor:before { content: "#"; display: block; } h1:hover > a.anchor, h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, h3 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover, #toctitle:hover > a.anchor, #toctitle > a.anchor:hover, .content > .title:hover > a.anchor, .content > .title > a.anchor:hover { visibility: visible; } h1 > a.link, h2 > a.link, h3 > a.link, h5 > a.link, h6 > a.link, #toctitle > a.link, .content > .title > a.link { text-decoration: none; } :not(pre) > code { font-size: 0.93em; font-style: normal !important; letter-spacing: 0; padding: 0.2em 0.5ex 0.1em 0.5ex; word-spacing: -0.15em; border-radius: 4px; line-height: 1.4; text-rendering: optimizeSpeed; word-wrap: break-word; background-color: var(--code-background); color: var(--code-text); background-color: var(--code-background); color: var(--code-text); } :not(pre) > code.nobreak { word-wrap: normal; } :not(pre) > code.nowrap { white-space: nowrap; } pre { line-height: 1.4; } pre > code { display: block; } pre .nowrap, pre .nowrap pre { white-space: pre; word-wrap: normal; } table { background: var(--background); margin-bottom: 1.25em; border: 1px solid var(--border); border-spacing: 0; } table thead, table tfoot { background: var(--background); font-weight: var(--font-weight-medium, 500); } table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: var(--text-secondary); text-align: left; } table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: var(--text); } table tr.even, table tr.alt { background: rgba(0, 0, 0, 0.04); } table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.6; } table.tableblock { max-width: 100%; border-collapse: separate; } p.tableblock { margin-top: 0; } p.tableblock:last-child { margin-bottom: 0; } td.tableblock > .content { margin-bottom: 1.25em; } td.tableblock > .content > :last-child { margin-bottom: -1.25em; } table.tableblock, th.tableblock, td.tableblock { border: 0 solid var(--border); } table.grid-all > thead > tr > .tableblock, table.grid-all > tbody > tr > .tableblock { border-width: 0 1px 1px 0; } table.grid-all > tfoot > tr > .tableblock { border-width: 1px 1px 0 0; } table.grid-cols > * > tr > .tableblock { border-width: 0 1px 0 0; } table.grid-rows > thead > tr > .tableblock, table.grid-rows > tbody > tr > .tableblock { border-width: 0 0 1px 0; } table.grid-rows > tfoot > tr > .tableblock { border-width: 1px 0 0 0; } table.grid-all > * > tr > .tableblock:last-child, table.grid-cols > * > tr > .tableblock:last-child { border-right-width: 0; } table.grid-all > tbody > tr:last-child > .tableblock, table.grid-all > thead:last-child > tr > .tableblock, table.grid-rows > tbody > tr:last-child > .tableblock, table.grid-rows > thead:last-child > tr > .tableblock { border-bottom-width: 0; } table.frame-all { border-width: 1px; } table.frame-sides { border-width: 0 1px; } table.frame-topbot, table.frame-ends { border-width: 1px 0; } table.stripes-all tr, table.stripes-odd tr:nth-of-type(odd), table.stripes-even tr:nth-of-type(even), table.stripes-hover tr:hover { background: rgba(0, 0, 0, 0.04); } th.halign-left, td.halign-left { text-align: left; } th.halign-right, td.halign-right { text-align: right; } th.halign-center, td.halign-center { text-align: center; } th.valign-top, td.valign-top { vertical-align: top; } th.valign-bottom, td.valign-bottom { vertical-align: bottom; } th.valign-middle, td.valign-middle { vertical-align: middle; } table thead th, table tfoot th { font-weight: var(--font-weight-medium); } tbody tr th { display: table-cell; line-height: 1.6; background: var(--background); } tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: var(--text-secondary); font-weight: var(--font-weight-medium); } p.tableblock > code:only-child { background: none; padding: 0; } p.tableblock { font-size: 1em; } details, .audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1em; } .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { color: var(--text-secondary); text-align: left; font-family: var(--font-subheading); font-weight: var(--font-heading-weight); font-size: 1rem; font-style: italic; } .paragraph.lead > p, #preamble > .sectionbody > [class="paragraph"]:first-of-type p { font-size: 1.2rem; line-height: 1.6; color: var(--text); } .exampleblock > .content { background: var(--background-panel); border: 1px solid var(--border); border-radius: 4px; margin-bottom: 1.25em; padding: 1.25em; } .exampleblock > .content > :first-child { margin-top: 0; } .exampleblock > .content > :last-child { margin-bottom: 0; } .sidebarblock { background: var(--background-panel); border: 1px solid var(--border); border-radius: 4px; margin-bottom: 1.25em; padding: 1.25em; } .sidebarblock > :first-child { margin-top: 0; } .sidebarblock > :last-child { margin-bottom: 0; } .sidebarblock > .content > .title { font-size: 1rem; font-weight: bold; text-transform: uppercase; text-align: left; color: var(--text-secondary); margin-top: 0; margin-bottom: 1rem; } :not(.openblock) > .content .paragraph:first-child > p:first-child { margin-top: 0; } :not(.openblock) > .content .paragraph:last-child > p:last-child { margin-bottom: 0; } .literalblock pre, .listingblock > .content > pre { border: 1px solid var(--border); border-radius: 4px; word-wrap: break-word; overflow-x: auto; padding: 1rem; font-size: 0.9rem; } .literalblock pre, .listingblock > .content > pre:not(.highlight), .listingblock > .content > pre[class="highlight"], .listingblock > .content > pre[class^="highlight "] { background: var(--background-panel); } .listingblock > .content { position: relative; } .listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.8rem; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: inherit; opacity: 0.5; } .listingblock:hover code[data-lang]:before { display: block; } .quoteblock, .verseblock { margin: 1.25em 0; padding-left: 1.25em; border-left: 6px solid var(--divider); } .quoteblock .attribution, .verseblock .attribution { font-size: 0.9rem; line-height: 1.45; font-style: italic; } .quoteblock .attribution br, .verseblock .attribution br { display: none; } .quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: var(--text-secondary); } .quoteblock blockquote { margin: 1em 0; padding: 0; border: none; } .quoteblock blockquote, .quoteblock p { font-style: italic; } .verseblock pre { font-family: var(--font-body); font-style: italic; font-weight: 300; } .verseblock pre strong { font-weight: 400; } .admonitionblock { border-left: 6px solid var(--divider); border-radius: 4px; background-color: var(--background-panel); } .admonitionblock table { border: 0; border-collapse: collapse; background: transparent; } .admonitionblock td.icon { position: absolute; font-weight: bold; text-transform: uppercase; padding: 1rem; } .admonitionblock td.icon i::after { content: attr(title); font-family: var(--font-body); font-weight: bold; font-size: 1rem; } .admonitionblock td.content { padding: 3rem 1rem 1rem; } .admonitionblock.note { border-color: var(--note-border); background-color: var(--note-background); } .admonitionblock.note td.icon { color: var(--note); } .admonitionblock.tip { border-color: var(--tip-border); background-color: var(--tip-background); } .admonitionblock.tip td.icon { color: var(--tip); } .admonitionblock.warning { border-color: var(--warning-border); background-color: var(--warning-background); } .admonitionblock.warning td.icon { color: var(--warning); } .admonitionblock.caution { border-color: var(--caution-border); background-color: var(--caution-background); } .admonitionblock.caution td.icon { color: var(--caution); } .admonitionblock.important { border-color: var(--important-border); background-color: var(--important-background); } .admonitionblock.important td.icon { color: var(--important); } ol, ul { padding-left: 1.25em; } dl dt { font-weight: bold; } dl dd { margin-left: 1.25em; } dl dd > :first-child { margin-top: 0.5em; } ul.checklist { list-style-type: none; padding-left: 0; margin-left: 0.625em; } ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1.25em; font-size: 0.8em; position: relative; bottom: 0.125em; } ul.checklist li > p:first-child > input[type="checkbox"]:first-child { margin-right: 0.25em; } .hdlist > table, .colist > table { border: 0; background: none; } .hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } td.hdlist1, td.hdlist2 { vertical-align: top; } td.hdlist1 { font-weight: bold; } td.hdlist2 :first-child { margin-top: 0; } td.hdlist2 :last-child { margin-bottom: 0; } .literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } .colist td:not([class]):first-child { padding: 0.4em 0.75em 0 0.75em; line-height: 1; vertical-align: top; } .colist td:not([class]):first-child img { max-width: none; } .colist td:not([class]):last-child { padding: 0.25em 0; } .keyseq, .menuseq, .menuref { color: var(--text-secondary); } kbd { display: inline-block; font-family: var(--font-family-monospace); font-size: 0.75em; line-height: 1.45; margin: 0 0.15em; padding: 0.2em 0.5em; color: var(--text-secondary); background-color: var(--background-panel); border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 1px 0 var(--border), 0 0 0 0.1em var(--background-panel) inset; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } .keyseq kbd:first-child { margin-left: 0; } .keyseq kbd:last-child { margin-right: 0; } .menuseq b:not(.caret), .menuref { font-weight: inherit; } .menuseq { word-spacing: -0.02em; } .menuseq b.caret { font-size: 1.25em; line-height: 0.8; } .menuseq i.caret { font-weight: bold; text-align: center; width: 0.45em; } b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } b.button:before { content: "["; padding: 0 3px 0 2px; } b.button:after { content: "]"; padding: 0 2px 0 3px; } .left { float: left !important; } .right { float: right !important; } .text-left { text-align: left !important; } .text-right { text-align: right !important; } .text-center { text-align: center !important; } .text-justify { text-align: justify !important; } .hide { display: none; } .center { margin-left: auto; margin-right: auto; } .stretch { width: 100%; } .unbreakable { page-break-inside: avoid; } .big { font-size: larger; } .small { font-size: smaller; } .underline { text-decoration: underline; } .overline { text-decoration: overline; } .line-through { text-decoration: line-through; } .aqua { color: var(--aqua, #00bfbf); } .aqua-background { background-color: var(--aqua-background, #00fafa); } .black { color: var(--black, black); } .black-background { background-color: var(--black-background, black); } .blue { color: var(--blue, #0000bf); } .blue-background { background-color: var(--blue-background, #0000fa); } .fuchsia { color: var(--fuchsia, #bf00bf); } .fuchsia-background { background-color: var(--fuchsia-background, #fa00fa); } .gray { color: var(--gray, #606060); } .gray-background { background-color: var(--gray-background, #7d7d7d); } .green { color: var(--green, #006000); } .green-background { background-color: var(--green-background, #007d00); } .lime { color: var(--lime, #00bf00); } .lime-background { background-color: var(--lime-background, #00fa00); } .maroon { color: var(--maroon, #600000); } .maroon-background { background-color: var(--maroon-background, #7d0000); } .navy { color: var(--navy, #000060); } .navy-background { background-color: var(--navy-background, #00007d); } .olive { color: var(--olive, #606000); } .olive-background { background-color: var(--olive-background, #7d7d00); } .purple { color: var(--purple, #600060); } .purple-background { background-color: var(--purple-background, #7d007d); } .red { color: var(--red, #bf0000); } .red-background { background-color: var(--red-background, #fa0000); } .silver { color: var(--silver, #909090); } .silver-background { background-color: var(--silver-background, #bcbcbc); } .teal { color: var(--teal, #006060); } .teal-background { background-color: var(--teal-background, #007d7d); } .white { color: var(--white, #bfbfbf); } .white-background { background-color: var(--white-background, #fafafa); } .yellow { color: var(--yellow, #bfbf00); } .yellow-background { background-color: var(--yellow-background, #fafa00); } span.icon > .fa { cursor: default; } a span.icon > .fa { cursor: inherit; } .conum[data-value] { display: inline-block; color: var(--background) !important; background-color: var(--text); border-radius: 50%; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: var(--font-body); font-style: normal; font-weight: 500; } .conum[data-value] * { color: var(--text) !important; } .conum[data-value] + b { display: none; } .conum[data-value]:after { content: attr(data-value); } pre .conum[data-value] { position: relative; top: -0.125em; } b.conum * { color: inherit !important; } .conum:not([data-value]):empty { display: none; } .hljs { display: block; overflow-x: auto; color: var(--hljs-color); background: var(--hljs-background); } .hljs-comment, .hljs-quote { color: var(--hljs-comment); font-style: italic; } .hljs-doctag, .hljs-keyword, .hljs-formula { color: var(--hljs-keyword); } .hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { color: var(--hljs-section); } .hljs-literal { color: var(--hljs-literal); } .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { color: var(--hljs-string); } .hljs-built_in, .hljs-class .hljs-title { color: var(--hljs-built-in); } .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { color: var(--hljs-variable); } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { color: var(--hljs-symbol); } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-link { text-decoration: underline; } .print-only { display: none !important; } @media print { * { box-shadow: none; text-shadow: none !important; } html { font-size: 80%; } a { color: inherit !important; text-decoration: underline !important; } abbr[title]:after { content: " (" attr(title) ")"; } pre, blockquote, tr, img, object, svg { page-break-inside: avoid; } thead { display: table-header-group; } image, object, embed, svg { max-width: 100%; } p, blockquote, dt, td.content { font-size: 1em; orphans: 3; widows: 3; } h2, h3, #toctitle, .sidebarblock > .content > .title { page-break-after: avoid; } #toc, .sidebarblock, .exampleblock > .content { background: none !important; } .admonitionblock, .quoteblock, .verseblock { page-break-inside: avoid; } #header, #content, #footnotes { padding: 0 1.5rem; } } picom-12.5/assets/slide.mp4000066400000000000000000001056751471504570600156230ustar00rootroot00000000000000 ftypisomisomiso2avc1mp41freemdatEH, #x264 - core 164 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=34 lookahead_threads=5 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00 e#oY3k/ٛnybZ}应T1   !@@bWxc@N646}8/%bb5uOx z s ?S{Σ-js&GUQjRzu6 !mQ y?.<'yh+gdT}]$*zB;o?jcL_j-άl\Yk3=K72*1A0vdabL__AVK=:P~S)$&7ϙPf| cuO0vT7ltC _3x"8#YP(k<șL 7IԵ)Jz6,O4 e^vna;د!8/n9$nt2fN5^^4ƨĜz hb/F,P? ,02K`Ar("o:\hjN{s0ԷVK6gdR8 ٦/Ka,PJ0yNjbs #= N~=TC,7" eVeP[TwnlTtk1༁('R^/D茒܂ZSpO߷|x+ (bRB& Bkp|]'P$#zN @ Lꎌ`o(HX{l(I pPQHg:f@[w?=0*DHo9>M"&?\:zVjy?a.|ҳuXJg'"hdB"`ػ Í=//&=T@  W%%zz,B"*uף&lKH7}bQ TDLsV)l8H| 2>ICOўZN-G+82ttWDcz 0Oipc g^&֓D3젟VM$o[Mz}}:@LIE"dz:r|GEk$ڹ"'_؍:N~/,6ԕF(aFb`2x~n|7Iy!Qqi>0[ nfֹ~Y! *Ɗ+B$Go7R31Y0*Q֚Ҕ~[Tj6X?).k]EF%_K; E=JũG/F EsqYϩ9vH=qA3pG_KߛrK0<؎VEpcgиl}C|Ur@ي'xwi͚~ZwC%ɃUMeVO+fn> a^$i'[w&E8{Ymw' 鈆PpB7VIz9:Sf?5gV"0hj}B.h?@)vS޲XfT *${ĐLL->Np<jqƜJ3l-*5׶mG_i [9ƒ`-U(TiKE+vU =?WBq#55}"_U8kzvwB~D WV\/rjn6T%L5cN"q7ZAKQOuj,0Jw>_Gp?`{ A5 ?̽*q"Mfd_zјxFu!4v${h)IMb-"^\ ^ +9gran+ޥI_M[a\`[qi?d t!εƘM3 ~1= N'|ҡ,<7 =*kWE2/-Q`d4ij,RӸ>Hy*WkW~| _fߊ# ܠ$d0Ѷ:e_9d&m)~SHA$lB?KZ왤C- JJ[^̴ \80N#f?J=ړk+#xzlԺĴyl͞YYjMl-aԪ0 +Ƨ{~ٽ<-C5[Kb'P݃ gEcjDeg2@,π1qAhIAhLGGZ:`(Hp(-]rNKs`2z tR}dS,pASqtAE,_&3=ꌥ+qnL&$Y1- )RtDeVݒ)PJCjDeg WhAIAlLG{k(v&əNЄ=J0QAE,_&3=ꌥ+ZZQ"FMqp*aCtDe"2BjDegh<?iAIAlLG YT<`\@vOkRRAE,_&3=ꌥ)/P . D-tDeVݒDD/jDeg WMoHA4IAlLGQARE,_&3=ꌥ+ZZQ"FMqp*aCqtDe"2BsjDegh<?GAxIAlLqRAE,_&3=ꌥ)/P . DtDeVݒDDjDeg WMofAIAlL.ۀ Rي_zey`ȹUd}-b\`fT.(|JՂ*-Jnc5D2c7|`gPy}gOa"1Ungk85> N( ='7>ԥ}K`mR3)KYXȀ27hi J!=$ SAok6JOP13" δ*;TO cYډH .{F8rgSD<*牎]`_rv % @Wy7j _j'zKuC1?Z¼zu(y}=4]#(-c7ӢtH4#!3ޞnݙiqמf@4](j,fQUIW3MgU)pc lU8þ.g*_1 gAIAlLLoX}z^^ T2lVeWR}uiFSP-gCmG]s`zLC&)U~. *Py3p9f1XNv۔ `GiU4Ì˟Čeynhj]?g: b׽VL/FTg 0"i.ڹ8pǫYd'n)_ /`8s.Am&3Dv^>|_]K/ Đd,?.Gt*;Mۢ{E:CRgШ]8yg n<7daP ( ~Uțtz"SVjz- 9KjX,<% njDW2Vv MWv|ypK&!e ٮ;; Sm QF 5/Csp>C?rzQw㺺@/Ov[ZGOX}"j0 Iܔ4" ZpD*_ukifp\^*j_BJlHy{b4:PCR`qtF)dP1iㅇT.)OϕʤRWo%lx ^ZIuT|flYڵ>hzR!n )QHS7M&87'jCFߡǠdNѽ)q0-@S7%$N3mdmS/G $bϚv5]p(nz{ʍF::pl]=ۅ:h梙 gʜ0Ua qW>#--3s?|'b8k7ۻ|`bb͈"Aa#7ީnXd~N쐁NO ~i bjyݑ)1f$CvOXM@[½5D> %,zOoſ 0!(IbE}'PgjGF7DÞ*;$OK -`{+e˹4tAX#1]ߏDwd7 PED&S2hWX)r>rO+ wDI*2tL?cgMt\!K讇85um;*jQ$Ρזژp\.,4WK2FB?li 6g'PUgfI!D0H]988{Uu߂"NiO aLykGrĆ-?$:4xz(;СNh{"[T9~۰;Nk!*=?ܨLgp"ӓ[ӻ jDr\&HXRnnߣLײ.ysbwRӗ|,}8空tXP_0ɍyi+ǢuzAa؟3F;sKf.@F34B浕G{61SڢhpӤ4 (iEacbq뫻HӮzs`Vo k#%]N t;5AI Re0#́M%EOvd^I{%tOmiC_ܝ0l$:4>|>J_gDH]׎%b^1~D@_̆Wp~FTdxWy,R4[]v7Y3{gp1=0:աɊ6:?E>)g\x݂km q#[m6bF8SU.nalK`N™$"Hebu|r-sa/wwG_*v)4cs#h6{W^|YlaDbm2I2[K*pJ :vISX,aw>95,6"Z@ֹ|ٽ} ^ü`|weWRKFN5(p{5~f82.; GxKVbE4ExqBcHhZuWOWqGI6_{Zzd4;B()ؽX&j7uiRbPTc&gP40 lYndyz0O._XG0a]egR#ٟJ=*4OɃ1 /%^Ľg:w?_C 9fVt U&_. 5K;8(H7e*;0[Au b>Oh:JP߳żQ q-~j16RH%jrb~lWhb𭑀o&JyRv(,KB B̸M&񵝀İ\JnRݛzyod{eǡ1[eT/(ԉl#GV[Pf%JTC ?1<މKQ9oeW$F[VLe\~tފ0 E9e`)6Ak ܰ#٠%[&6^ g"25:;} 'm (SzTDBb2?Ўy 0UiA!IMXo}w~PN k77`jcF kfʅ.'/HHdxayOr K[xlf^\->Ъ.5J)GK#88Yl]Ģ2R@6j.@ԝdROtwm8ٴs͝,4@b&cgsUx"r IfS B;`3g>uܣxQNN|$Zq_jjwPH0ipud^"MAomyC"!<]Eb`K~vTYţҴur 0WV/v{WK: [jaX=^F#Ch Dp$n"vZbun*Bprk9j^Y@qm>&PFqZ.prNI: Uq/.I#&ڞQ]g t>u3y,q0eBVR] KoRq0l@GU  R0KՐ:!?EՅoCs3+ |*5= }9+i< ܳT>*i>iܱ е?O JA n+zY᫕#[\d_&@I϶^(ԙ 97KˏaMM~=H֙x=MG,PWEǻOr`t]THfWW#I^?Μ}7qKw=5/"AIcZ?0C`:Ц2 Pǐ|r7[7 ^/԰aNV<DN߸]1ݦ(mj ->PV>,+eʵpAq0YVUGl C8n*1AќTV]F3<&33.^5J;xdA yT2,WzJ~Rl#4#AU c`>+_8a'!G]^^I\>:7P!~& q[!y7m5w)c`E22O /~5rV}w; 3Cԡ9XϝqW}Q \†Zpsy'ȾFݘ,roJRQIh8A/ uX|Xyt[*fg="Us5wMlMzv{(*TjE +og&qh Cqnʊ9hwH.lSѴV_#έKUFtdP٬erp!+a]gLv/[ܭW"=ηr,muδ:G j?ϫ]9,zᘋߣV`3u\<$]7[Ǫ H_AwsmqGq(t[:o%*~Nb_-|-xo˶ϻ]ϡ IľB QR;_ MO>}#V xiu2n=WtN.(&U"$f?' g=)j>i3f5( zHp7̃ CK3J'oA@jD548_q)gzCaڲЅti|Ef i9hsDU#nAc׷K ]!dz Aڊ|jd϶ w@#C|<`"FyJء4ǼL^Bh;.~oKLPb-Eg2#,M;'z8 ]_%,;8%_Fl;u-hdv2!"n!o4!xfETK>iLA41)im$Buu@AcI&S?\)63SpW2nB.ьqקQ{h>nߟs邑0$%!fD]A*9TQ3'M$)tK*-1o;ȧ0KWK U5Rt NHCYН˦da4H 2*ǥ_V)3qS ԣPC&`#5om)V^m6.-;1SYKwy MFA6i3P׍}?r $otLP,h w\NGz%"~с-uBti{l)eHJGHsũh:L2XhHur{ Ot$ٍ%xC߽OZ'V~ЫD&r| ɀ2NQ : U=*NNx=@q1P$Y/A!!W %xBHH+=M8Cܠ@fC >OAI&S?{rQCA'NE, Qq6JhqZ׶=$*;΢VW߽ȕe- zfʿ/!-LycDϭ5^ymQ'v3oQWuxǒlF%@q29wfRGVrZ %d̹orX39~t !yXmwKUl ]O+ӆ,eoFjUD]!~z O%2P$U8V- xU3?7n^E2CXkg  L oVJQɰ>5,;MCt/'}QRmlֈw{w=È L&.1Ifn3~&|}9A~ q95(~a@%q[Ãj{m[w y]*@f786.o\ǜPF\KJVvLDK\,YlXcXȹl۠%L$ƱN _~ b([nWH]z u2'j95=w[K826OAQk-F7F$ :X|\fϚ o_+FT@ٓRTUAUF!1T-NֺNzզ[÷_L_ůz1ѥj\{rCE"C:5$E :V. IVWtv^q"_Ǣ/x"Nȍ1J/$ I֟9ag2CpqlPye7a4.pKcW"0ك Rb;+5a璢|* loíqY&EkeiE20fyU[qf؛" p"GDE<@DJ;7Ы#SΘA*w٤w!m /")PxQBi4TxqW!~2zp㕌}V܂͠w pX6_0,0mRcFK]szhë)9![Yf[ϙ !!޴Pb,koT!BS&F=X!P@JlzNI/Xh5lMN _Fו:@pbp-=bA "<\x .U1VyS<1~In"$RE/DgSQ]ɗO+N)CVۯWĵuY04=R|y?>|Bpr f =S2FƇ_n,y2pPw5õJ3ֳZ;i/c7 4GOb'  |ui6:L%$ש_s Kabsi[wN>A U.NL}X9ظڐ΢>*g+KÐ_כLyB;Lk3uJRzlpB l*#]j2iZzo"5 A.{O } GzHfL#014%ˆkL%۸c l@U蠢Y^krHFP" ݨ!6[1YW] q >L'hq[n0Z:,HBwV(GG!N>Q SЍZêC0AÀPAjMTf[:ūD383)}g֦S hkH#dN>J?, _aQ'[;.~ֆ'>^>B8ޗ3篲M7Q?M8=pإ*# N_QxU}jsOkw/M+(,DVPB3 z"5RA*!|є֠2@ ?hb wH)áRb)b#'"=qY?ַqbw|̠@3v9S/D< C[(ـbYIwbV:! Vϩ$!Xl6G~rN/DL*R?[7?Xp SP)xו=rDK1n:iafip͆.g@670aLq6Эr  tYwi.pfsY#yCA=rQ2>XS7nLj'NsFQrİv]L/fW ݗ]B"O=,I "] }V5 N}.6IyZ~^ ܀p.ebꓵ`zF~N/?mSnBFh2UjO]^շ[22ʘuZp^!L=L! 0rENs7OV Ks'۞/&_$o i$XTl3wLhy] 9xYQ|@.k/ r8y5RZ7~֪,õMD Z|&sodI "cۼp؁4 /R{ 6 8czy [)Ƕ"^%l$;ȅMqeN6w2,C PAIMPC Z4Q_@b~* }6b&%U0h~^vDg`T+/FN(r|x!JSse5,XēI% OYJZ :L1Ǩ9hs@N6@+ݲ/Irx1|3 VCPnTHa  5 62 ԑIzH㮫 kG P"SEw@cfwL[#Bt}HI# x)f튦/%ɅotuYrg@Qx2dN-H,ZǠ 0pH:`/d>Ћz.7] w`F.>yj3޹GACbm !HeMK܇`EœS0evՒf3TfN$$&Aclᓫ:P r@;LG zKD+B]6Sv- CLAE<_& -3 Pd"zwU>mnip1,|rЈVԱ T_39o:q<?;Ds$CY֤d&P[[TuoZGYA tDqi5ؒ? T"_n  Z ,63TgvfHCu,S?_ꦹd\hTI"\Wĩ23'Q=6ũs/G3kԺkx2zՔOńt>:d-`qZЫgڹ;]hX+lH>*#rNJ 2=.;w#_jDehv,W$R0*qShռ"O:-4A IAhLG7퉁ZD)}'S [CSr"U\%d$lL/NZ[gh[hL@uG 1orWd/q T_]Daf4 vA+E,_&3gݻE´6BTSc;rLzYNA4Nqߝ ٬n ,3 "ZJtDev ۧ/ 3\4NM)'@fo]‡lZLjDegoM) # JZgt͈S+c5P`0!AQIAlLG?z"S,rPm"6!&|dQ>YUyBCw)}$5u7j7fWZ0 )oAoE,_&3=8ힹS\B̋ءAH ;*k깳5J(p+FC: XtDeގ>M`KlO’To6`,ʠP\WjDegoM) !"]Igh* UAIAlLG Ό tLr MkAE,_&3=ꌐ`n1ư)qʖoFўݼ$πStDeގ>M`KJP:]RjDegoM) !"]C%  / 1HAIAlLGcAE,_&3=ꌐ`n1ư)@L٪@(#\T}OtDeގ>M`K lDTPjDegoM) !"\wZ*//gAIAlLG Fs=^5av-n@k~%pEτ+,[Z7m%G@UZ"T˾N{bnQk.?7/]A͇K6iOJlc D-^k2Xŋ*>tVsjHaav[P9OhA;E,_&3=ꌐ`n1ư)@5ڭi -@)tÙD_7AOZtDeގ>M`K lDTh\jDegoM) !"\wZ*/8!@Oɗs9Qj*ګH9AAIAlLGj.~@'Xņb Ztfn7ЀsPhf۸a8ү- (i|sm,cN}f⨯00m$3[.м6 Q ED߯W:> ]h$d U:zn W AE,_&3=ꌐ`n1ư)@5ڭi -@)/SEQ)6+83a  m5U*iGLZ=Eٻ65M>_V|lHM Z1Z!6s_P02TW9VU/&;lNtZCjۣrtDeގ>M`K lDT&?[60$NamA*jA)jDegoM) !"\wZ*/`-Zh* (g"u%*b*lL3) qK,1å͌2_)B'Lri97Bȝw;4L<3ʶ! >kGpbuIMNJkj1)4`F=pڲ5TEBgY#x0AIAlLGǔ]P.~&? 9Eݼyܳ.Y" "Tyd3+`nM7d [jK_2a6Q̷̹}HViݢZn#2~H&W]X{RAE,_&3=ꌐ`n1ư)@5ڭi -@,QLvt=UUÒ܈_`*d!!6;y'L"s (Pxޢ? +"ݣ:VU Нѐ'~')Ttf*~NOXa9#2a*c!HgdtDeގ>M`K lVl ?_­ ,:iQfMK yx exA|a@ٛg/3c`7o8)k*$IIp8!oM`K lDUp26BwcMG/54z8L=V8bydTqVƝ7r?[\)5 %?"隤/c%cIiq2;=?h'u r&2,' jDegoM) !"\wZ*6#E<`#˶1R/&k] Bb 4۷\xahbjsH^4V1Uڧ!ʏhbjy)ϫ ,D19V)E15)T('s2&\@25c:FY|A IAlLG4:9@iW՗Tж%bQ(ޫ|uɲ wgPv8D1m q&6ՖtOKz$o=}(t`SA(& u98;OӞ?)TdjaddK]𦅍\ m&XxcxAz%l;@繊^!Ek摄 MV3aW{az E~m5xN;ri-29HYO(iXPN)+iX8q lIɌzG5#;/Y7IR0gv>n@29vA+E,_&3=ꌐ`n1ư)@5ڭiuܼvS(-50'opx9G c`?9RZ 9Ǎeu1M`K l)a[ߠ뎈?ȭn Hh+sk;(R&p ۪A,N{o? ?i T(]AoE,_&3=ꌐ`n1ư)@5ڭi y?OWI r` hԠRXaf1H4:|7&wtDeގ>M`K l0TK$N#hsE%SEBwW@ғ2|`y-]kjDegoM) !"\wZGdi]"+Z}s~yG TAIAlLGxkaH݇YuYAhE;iϮP;?w‚=0?'B+TFwًۭ5xkPe&bλn ~%Il b579xYmᆰ]c"4o[붎?߅[Gd񩥀{#t : S+b%㓬?qpAE,_&3=ꌐ`n1ư)@5ڭi =t Q ~ΎLVy ZP[tDeގ>M`K l=/UڄmRWjDegoM) !"\wZ5xkmd`:F1{AZAIAlLGNCHrwu"]z' cAE,_&3=ꌐ`n1ưkFL(R: UtDeގ>M`K @~M`JIlaq@`|O\jDegoM) mf@&AAIAlL onU)T&.p&yg0_dĚ"s)J~wUS˱H3& 9[L鿀2?WOId]98 Z}Ϭ,EͪKf4.LY(sd0840lAE,_&3=ꌐ`n2BVN֩!Yzz]o#z@BYtDeގ>M`JIl>̊E1 9ށ\jDegoM) mf@\'wTyã[p®f_ZAIAlL-]| O]eU3FkCw!h״m^AF.yA}:n|W4Ae͕*~%}Ee#l/W,_;@[z PWn"Ͳ'MdِUʿGnO&Mq\Oz?/; 2VK,ˆ߲ Y6OY9c:h$y ? im?K'89@X7^."i{SoX"N.8PG]C WP=!HN񹼞Ե4a3,ݐj$*B AE,_&'+Ż+^unRLT 4E<MI%üNSh!zuV)xK]̘@a.VO} 3e]폫bm"ON7L+¹,6s{ZȁJX_38`tDesoiɣ>!V SGΠMC >&!wxKpYAjDé/FeCsLɫ]MVF5l)o*sF3|VDDúP"S/ST1%K%>~& e\2p{.ܒ}ϽybQTͱZfǪ"9 Qq96QR{ ; K*|10@|AIAlLL6Xw?\ǩ!ӧ ̱ɞJH ͣGGq yMVC%Ry$2 uE, ՠhU{J+d҅1)]@?^'1.n|Oqk[*4io2}x`D%y=v"DH9TFe>e;<+!]_EFD{/<] ډ0 HV64jLm-`J'oDy}yg$trP}e(NL5<hԪ1j |<zX/,B1 (J)zQX@=َcD`9?:<]}U,8+c6^F[ Ոey . _, yBhx$)şgjl>aDAI Re0#\^f΁;X$!HيLTNdImvl?9Yuc>>Mw y1zm瓃"fRl,Il64cz>c/c`4銃/FPn_k@k8&[x%{7+k4s|`?u4R0 A,I&S<́nxiuou;o^p)t> BXW!E;2M#H-m e5+N6/Ҋ˶H,> rT*4b6v3և/+C)&xwtXFܐuZN:Μ}4uЅߛ%vȔN4^)C(U];JpS,,*;_B!! * `ʭ*Wev[`N?v&:%>t'6)9{NpEeY3[C]\IO_[2jD9`1[[`P^;Κ.B0vwX1^r{=ʽ#գM~wb?$qtN>+A 䴭''``ʪ`dgIs?"Al>αՀV),P6Ơm]2C [˿(=j<KcR1.)TC ή@O.e1D-ưKANI&S<=ݒCe,zoHX6ާSc^eQ3#"|6HίFrof?F"=Ep9J-Vt'*EFk# qEB/(Uԟ *{ NFz,aŽ[xҸ#\(H: p,!sb:7O(lS _3?s" 2J6sTy`lIUrv0\E>>ΔD6aoQg8Q TǿzMßmjD7up!fۆr#L-Z6LwȲSģ =⭺2VT7+fȪ֍cg=g?x=:+𵡬E`w\ 9:j͂Gg,&Xg,2]FLtbHvvGfWh69BkƲ~^m='T^K!+ِuM[ `xArI&S?.L#vSYJ$=fTR츄AO"?0'-$KrJiC+ž0eAE<_&|]h9'+@e?y>p G`?"P@tD\l{WFB1Aömp! x>ׅdJQ+v g| x9` 6xRu#JŪ{g{p_+;:FjDeghsgGAIAhLX. RAE,_&3=ꌊOFtDe ܕl*|?FjDeghsgGAIAlL oAmoovlmvhd@trak\tkhd@8$edtselst}mdia mdhd<xU-hdlrvideVideoHandler(minfvmhd$dinfdref url stblstsdavc18HHLavc60.31.102 libx264;avcCd*gd*@x'Z 2h"colrnclxpaspbtrt  sttsxstsscttsustscxstszx7keIuxVGlUGFmVHHLUGFKVHHjdFk 9mSz#cz^^s\[YoWVLgSTlSlv p{ot_[^gYUXcTSp]`^d}HO |iJKVJJKstco0budtaZmeta!hdlrmdirappl-ilst%toodataLavf60.16.100picom-12.5/bin/000077500000000000000000000000001471504570600133315ustar00rootroot00000000000000picom-12.5/bin/picom-trans000077500000000000000000000270541471504570600155230ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: MIT # # picom-trans # Copyright (c) 2021, Subhaditya Nath # Based on previous works of Christopher Jeffrey # # # Conforming to POSIX-1.2007 # https://pubs.opengroup.org/onlinepubs/9699919799 # # Usage: # $ picom-trans [options] [+|-]opacity # By window id # $ picom-trans -w "$WINDOWID" 75 # By name # $ picom-trans -n "urxvt" 75 # By current window # $ picom-trans -c 75 # By selection # $ picom-trans 75 # $ picom-trans -s 75 # Increment current window 5% # $ picom-trans -c +5 # Delete current window's opacity # $ picom-trans -c --delete # Toggle current window's opacity between 90 and unset # $ picom-trans -c --toggle 90 # Reset all windows # $ picom-trans --reset # Save $0 now to print correct value while printing from functions. # Printing errormsgs from functions using "$0" prints the function name # instead of the executable name. EXE_NAME="$0" # Instead of printing the full path to this file (e.g. /usr/bin/picom-trans) # only print the base name (i.e. picom-trans) EXE_NAME="$(basename "$EXE_NAME")" print_usage() { #{{ echo "Usage: $EXE_NAME [options] [+|-]opacity" echo "" echo "Options:" echo " -h, --help Print this help message." echo " -o, --opacity OPACITY Specify the new opacity value in range 0-100 for the window. If" echo " prefixed with + or -, increment or decrement from the current" echo " opacity of the target window." echo "" echo "Actions:" echo " -g, --get Print current opacity of the target window." echo " -d, --delete Delete opacity of the target window." echo " -t, --toggle Toggle the target window's opacity, i.e. set if not already set" echo " and delete else." echo " -r, --reset Reset opacity for all windows." echo "" echo "Window Selection:" echo " -s, --select Select target window with mouse cursor. (DEFAULT)" echo " -c, --current Select the currently active window as target." echo " -n, --name WINDOW_NAME Specify and try to match a window name." echo " -w, --window WINDOW_ID Specify the window id of the target window." } #}} parse_args() { #{{ i=1 # because we start from "$1", not from "$0" while [ $i -le $# ] do #### [START] Convert GNU longopts to POSIX equivalents #### if [ "$1" = "--${1##--}" ] # check if $1 is a longopt then # Catch invalid options case "$1" in (--opacity=|--name=|--window=) echo "$EXE_NAME: option ${1%=} needs a value" >&2 exit 1;; (--opacity|--name|--window) test $i -eq $# \ && echo "$EXE_NAME: option $1 needs a value" >&2 \ && exit 1;; esac # Separate "--ARG=VAL" into "--ARG" "VAL" case "$1" in (--opacity=*|--name=*|--window=*) ARG="$(echo "$1" | sed -E 's/(--[^=]+)=.*$/\1/')" VAL="${1##${ARG}=}" shift && set -- "$ARG" "$VAL" "$@" esac # Turn into short form case "$1" in (--help|--opacity|--get|--delete|--toggle|--reset|--select|--current|--name|--window) ARG=${1#-} # remove one '-' from prefix ARG="$(echo "$ARG" | cut -c -2)" # get first two characters shift && set -- "$ARG" "$@" esac # If the argument still starts with --, it is an invalid argument case "$1" in (--*) echo "$EXE_NAME: illegal option $1" >&2 exit 1 esac fi #### [END] Convert GNU longopts to POSIX equivalents #### #### [START] Prepend '-o' to standalone opacity values #### # Iterate over every argument and check if it is an opacity without the -o # option in the previous argument. If so, then prepend the -o option. # e.g. Turn this - # picom-trans -c +10 -s # into this - # picom-trans -c -o +10 -s # # NOTE: Don't touch arguments that are preceded by -o, -w, or -n (i.e. the # options that take a value.) # e.g. This - # picom-trans -w 75 -o 90 # should NOT be turned into this - # picom-trans -w -o 75 -o 90 # We ensure this by checking the "$#"th (i.e. the last) argument. If # argument is an option that needs a value, we don't do anything to $1. # # NOTE: we are using printf because most echo implementations aren't # POSIX-compliant. For example, according to POSIX.1-2017, echo doesn't # support any options, so, # $ echo "-n" # should output - # -n # But it doesn't. It instead interprets the "-n" as the option -n, which, # in most implementations, means that the trailing newline should not be # printed. if echo "$1" | grep -qE '^[+-]?[[:digit:]]+%?$' && \ ! eval "printf '%s' \"\${$#}\"" | grep -q '^-[hdtrgsc]*[own]$' # NOTE: eval "printf '%s' \"\${$#}\"" means 'print the last argument' # NOTE: The letters inside the first square brackets (ie. hdtrgsc) are # the same as those in the getopts argument, minus those that are # followed by a ':' # NOTE: The letters inside the second square brackets (ie. own) are # the same as those in the getopts argument, minus those that are # NOT followed by a ':' then set -- "$@" "-o" i=$(( i + 1 )) fi #### [END] Prepend '-o' to standalone opacity values #### # Prepare for next iteration ARG="$1" shift && set -- "$@" "$ARG" i=$(( i + 1 )) done # NOTE: DO NOT ATTEMPT TO USE "$OPTIND" INSIDE THE getopts LOOP # - https://github.com/yshui/picom/pull/634#discussion_r654571535 # - https://www.mail-archive.com/austin-group-l%40opengroup.org/msg04112.html OPTIND=1 while getopts 'ho:dtrgsn:w:c' OPTION do case "$OPTION" in (h) print_usage; exit 0;; (o) target_opacity="$OPTARG";; (d) action=delete;; (t) action=toggle;; (r) action=reset;; (g) action=get;; (s) winidtype=; winid=;; (n) winidtype=-name; winid="$OPTARG";; (w) winidtype=-id; winid="$OPTARG";; (c) winidtype=-id; winid="$(get_focused_window_id)";; (\?) exit 1 esac done } #}} get_target_window_id() { #{{ # Get the output of xwininfo if test -z "$winidtype" then xwininfo_output="$(xwininfo -children -frame)" elif test "$winidtype" = "-name" then xwininfo_output="$(xwininfo -children -name "$winid")" elif test "$winidtype" = "-id" then # First, check if supplied window id is valid if ! echo "$winid" | grep -Eiq '^[[:space:]]*(0x[[:xdigit:]]+|[[:digit:]]+)[[:space:]]*$' then echo "Bad window ID" >&2 exit 1 fi xwininfo_output="$(xwininfo -children -id "$winid")" fi # Extract window id from xwininfo output winid="$(echo "$xwininfo_output" | sed -n 's/^xwininfo:.*: \(0x[[:xdigit:]]*\).*$/\1/p')" if test -z "$winid" then echo "Failed to find window" >&2 exit 1 fi # Make sure it's not root window if echo "$xwininfo_output" | grep -Fq "Parent window id: 0x0" then echo "Cannot set opacity on root window" >&2 exit 1 fi # If it's not the topmost window, get the topmost window if ! echo "$xwininfo_output" | grep -q 'Parent window id: 0x[[:xdigit:]]* (the root window)' then window_tree="$(xwininfo -root -tree)" if test -z "$window_tree" then echo "Failed to get root window tree" >&2 exit 1 fi # Find the highest ancestor of the target window winid="$(echo "$window_tree" \ | sed -n "/^\s*$winid/q;s/^ \(0x[[:xdigit:]]*\).*/\1/p" \ | tail -n 1)" if test -z "$winid" then echo "Failed to find window in window tree" >&2 exit 1 fi fi if test -z "$winid" then echo "Failed to find the highest parent window below root of the selected window" >&2 exit 1 fi echo "$winid" } #}} get_focused_window_id() { #{{ id="$(xprop -root -notype -f _NET_ACTIVE_WINDOW 32x '$0' _NET_ACTIVE_WINDOW)" echo "${id#_NET_ACTIVE_WINDOW}" } #}} get_current_opacity() { #{{ # Gets current opacity in the range 0-100 # Doesn't output anything if opacity isn't set cur="$(xprop -id "$winid" -notype -f _NET_WM_WINDOW_OPACITY 32c '$0' _NET_WM_WINDOW_OPACITY)" cur="${cur#_NET_WM_WINDOW_OPACITY}" cur="${cur%:*}" test -n "$cur" && cur=$(( cur * 100 / 0xffffffff )) echo "$cur" } #}} get_opacity() { #{{ cur="$(get_current_opacity)" test -z "$cur" && cur=100 # Unset opacity means fully opaque echo "$cur" exit 0 } #}} delete_opacity() { #{{ xprop -id "$winid" -remove _NET_WM_WINDOW_OPACITY exit 0 } #}} reset_opacity() # Reset opacity of all windows { #{{ for winid in $(xwininfo -root -tree | sed -n 's/^ \(0x[[:xdigit:]]*\).*/\1/p') do xprop -id "$winid" -remove _NET_WM_WINDOW_OPACITY 2>/dev/null done exit 0 } #}} set_opacity() { #{{ if ! echo "$target_opacity" | grep -qE '^[+-]?[[:digit:]]+%?$' then if test -z "$target_opacity" then echo "No opacity specified" >&2 else echo "Invalid opacity specified: $target_opacity" >&2 fi exit 1 fi # strip trailing '%' sign, if any target_opacity="${target_opacity%%%}" if echo "$target_opacity" | grep -q '^[+-]' then current_opacity="$(get_current_opacity)" test -z "$current_opacity" && current_opacity=100 target_opacity=$(( current_opacity + target_opacity )) fi test $target_opacity -lt 0 && target_opacity=0 test $target_opacity -gt 100 && target_opacity=100 target_opacity=$(( target_opacity * 0xffffffff / 100 )) xprop -id "$winid" -f _NET_WM_WINDOW_OPACITY 32c \ -set _NET_WM_WINDOW_OPACITY "$target_opacity" exit $? } #}} toggle_opacity() { #{{ # If opacity is currently set, unset it. # If opacity is currently unset, set opacity to the supplied value. If no # value is supplied, we default to 100%. if test -z "$(get_current_opacity)" then test -n "$target_opacity" || target_opacity=100 set_opacity else delete_opacity fi } #}} # Warn about rename of compton to picom case "$0" in *compton-trans*) echo "Warning: compton has been renamed, please use picom-trans instead" >&2;; esac # Check if both xwininfo and xprop are available if ! command -v xprop >/dev/null || ! command -v xwininfo >/dev/null then echo "The command xwininfo or xprop is not available. They might reside in a package named xwininfo, xprop, x11-utils, xorg-xprop, or xorg-xwininfo" >&2 exit 1 fi # No arguments given. Show help. if test $# -eq 0 then print_usage >&2 exit 1 fi # Variables # action is set to 'set' by default action=set winid= winidtype= target_opacity= # If there's only one argument, and it's a valid opacity # then take it as target_opacity. Else, parse all arguments. if test $# -eq 1 && echo "$1" | grep -qE '^[+-]?[[:digit:]]+%?$' then target_opacity=$1 shift else parse_args "$@" fi # reset_opacity doesn't need $winid case $action in (reset) reset_opacity;; esac # Any other action needs $winid # # NOTE: Do NOT change the order of winid= and winidtype= below # the output of get_target_window_id depends on $winidtype # # NOTE: If get_target_window_id returns with a non-zero $? # that must mean that some error occured. So, exit with that same $? # winid=$(get_target_window_id) || exit $? winidtype=-id case $action in (set) set_opacity;; (get) get_opacity;; (delete) delete_opacity;; (toggle) toggle_opacity;; esac # We should never reach this part of the file echo "This sentence shouldn't have been printed. Please file a bug report." >&2 exit 128 # vim:ft=sh:ts=4:sts=4:sw=2:et:fdm=marker:fmr=#{{,#}}:nowrap picom-12.5/compton-default-fshader-win.glsl000066400000000000000000000003511471504570600207510ustar00rootroot00000000000000uniform float opacity; uniform bool invert_color; uniform sampler2D tex; void main() { vec4 c = texture2D(tex, gl_TexCoord[0]); if (invert_color) c = vec4(vec3(c.a, c.a, c.a) - vec3(c), c.a); c *= opacity; gl_FragColor = c; } picom-12.5/compton-fake-transparency-fshader-win.glsl000066400000000000000000000007471471504570600227530ustar00rootroot00000000000000uniform float opacity; uniform bool invert_color; uniform sampler2D tex; void main() { vec4 c = texture2D(tex, gl_TexCoord[0]); { // Change vec4(1.0, 1.0, 1.0, 1.0) to your desired color vec4 vdiff = abs(vec4(1.0, 1.0, 1.0, 1.0) - c); float diff = max(max(max(vdiff.r, vdiff.g), vdiff.b), vdiff.a); // Change 0.8 to your desired opacity if (diff < 0.001) c *= 0.8; } if (invert_color) c = vec4(vec3(c.a, c.a, c.a) - vec3(c), c.a); c *= opacity; gl_FragColor = c; } picom-12.5/compton.desktop000066400000000000000000000004631471504570600156360ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Type=Application NoDisplay=true Name=compton GenericName=X compositor Comment=An X compositor Categories=Utility; Keywords=compositor;composite manager;window effects;transparency;opacity; TryExec=compton Exec=compton Icon=compton # Thanks to quequotion for providing this file! picom-12.5/data/000077500000000000000000000000001471504570600134725ustar00rootroot00000000000000picom-12.5/data/animation_presets.conf000066400000000000000000000145111471504570600200670ustar00rootroot00000000000000disappear = { opacity = { duration = "placeholder0"; start = "window-raw-opacity-before"; end = "window-raw-opacity"; }; blur-opacity = "opacity"; shadow-opacity = "opacity"; offset-x = "(1 - scale-x) / 2 * window-width"; offset-y = "(1 - scale-y) / 2 * window-height"; scale-x = { curve = "cubic-bezier(0.21, 0.02, 0.76, 0.36)"; duration = "placeholder0"; start = 1; end = "placeholder1"; }; scale-y = "scale-x"; shadow-scale-x = "scale-x"; shadow-scale-y = "scale-y"; shadow-offset-x = "offset-x"; shadow-offset-y = "offset-y"; # See comments in tools/animgen.c for syntax *knobs = { scale = 0.95; duration = 0.2; }; *placeholders = ((0, "duration"),(1, "scale")); }; appear = { opacity = { duration = "placeholder0"; start = "window-raw-opacity-before"; end = "window-raw-opacity"; }; blur-opacity = "opacity"; shadow-opacity = "opacity"; offset-x = "(1 - scale-x) / 2 * window-width"; offset-y = "(1 - scale-y) / 2 * window-height"; scale-x = { curve = "cubic-bezier(0.24, 0.64, 0.79, 0.98)"; duration = "placeholder0"; start = "placeholder1"; end = 1; }; scale-y = "scale-x"; shadow-scale-x = "scale-x"; shadow-scale-y = "scale-y"; shadow-offset-x = "offset-x"; shadow-offset-y = "offset-y"; *knobs = { scale = 0.95; duration = 0.2; }; *placeholders = ((0, "duration"),(1, "scale")); }; slide-out = { v-timing = { curve = "cubic-bezier(0.21, 0.02, 0.76, 0.36)"; start = 0; duration = "placeholder0"; end = "window-width * placeholder1 + window-height * placeholder2"; }; offset-x = "v-timing * placeholder3"; offset-y = "v-timing * (1 - placeholder3)"; shadow-offset-x = "offset-x"; shadow-offset-y = "offset-y"; crop-x = "window-x"; crop-y = "window-y"; crop-width = "window-width"; crop-height = "window-height"; opacity = { start = "window-raw-opacity-before"; end = "window-raw-opacity-before"; duration = "placeholder0"; }; blur-opacity = "opacity"; shadow-opacity = "opacity"; *knobs = { duration = 0.2; direction = (2, ["up", "down", "left", "right"]); }; *placeholders = ( (0, "duration"), (1, "direction", [0, 0, -1, 1]), (2, "direction", [-1, 1, 0, 0]), (3, "direction", [0, 0, 1, 1]), ); }; slide-in = { v-timing = { curve = "cubic-bezier(0.24, 0.64, 0.79, 0.98)"; start = "window-width * placeholder1 + window-height * placeholder2"; duration = "placeholder0"; end = 0; }; offset-x = "v-timing * placeholder3"; offset-y = "v-timing * (1 - placeholder3)"; shadow-offset-x = "offset-x"; shadow-offset-y = "offset-y"; crop-x = "window-x"; crop-y = "window-y"; crop-width = "window-width"; crop-height = "window-height"; *knobs = { duration = 0.2; direction = (2, ["up", "down", "left", "right"]); }; *placeholders = ( (0, "duration"), (1, "direction", [0, 0, -1, 1]), (2, "direction", [-1, 1, 0, 0]), (3, "direction", [0, 0, 1, 1]), ); }; fly-out = { v-timing = { curve = "cubic-bezier(0.05, 0, 0.69, -0.05)"; duration = "placeholder0"; start = 0; end = "window-height * placeholder2 + window-y * placeholder5 + window-width * placeholder1 + window-x * placeholder4"; }; offset-x = "v-timing * placeholder3"; offset-y = "v-timing * (1 - placeholder3)"; shadow-offset-x = "offset-x"; shadow-offset-y = "offset-y"; opacity = { start = "window-raw-opacity-before"; end = "window-raw-opacity-before"; duration = "placeholder0"; }; shadow-opacity = "opacity"; blur-opacity = "opacity"; *knobs = { duration = 0.2; direction = (0, ["up", "down", "left", "right"]); }; *placeholders = ( (0, "duration"), (1, "direction", [0, 0, -1, 1]), # left/right -> +1/-1 for X axis (2, "direction", [-1, 1, 0, 0]), # up/down -> +1/-1 for Y axis (3, "direction", [0, 0, 1, 1]), # X/Y axis switch (4, "direction", [0, 0, -1, 0]), # whether window X is added (5, "direction", [-1, 0, 0, 0]), # whether window Y is added ); }; fly-in = { v-timing = { curve = "cubic-bezier(0.17, 0.67, 0.68, 1.03)"; end = 0; duration = "placeholder0"; start = "window-height * placeholder2 + window-y * placeholder5 + window-width * placeholder1 + window-x * placeholder4"; }; offset-x = "v-timing * placeholder3"; offset-y = "v-timing * (1 - placeholder3)"; shadow-offset-x = "offset-x"; shadow-offset-y = "offset-y"; *knobs = { duration = 0.2; direction = (0, ["up", "down", "left", "right"]); }; *placeholders = ( (0, "duration"), (1, "direction", [0, 0, -1, 1]), # left/right -> +1/-1 for X axis (2, "direction", [-1, 1, 0, 0]), # up/down -> +1/-1 for Y axis (3, "direction", [0, 0, 1, 1]), # X/Y axis switch (4, "direction", [0, 0, -1, 0]), # whether window X is added (5, "direction", [-1, 0, 0, 0]), # whether window Y is added ); }; geometry-change = { scale-x = { curve = "cubic-bezier(0.07, 0.65, 0, 1)"; duration = "placeholder0"; start = "window-width-before / window-width"; end = 1; }; scale-y = { curve = "cubic-bezier(0.07, 0.65, 0, 1)"; duration = "placeholder0"; start = "window-height-before / window-height"; end = 1; }; shadow-scale-x = "scale-x"; shadow-scale-y = "scale-y"; offset-x = { curve = "cubic-bezier(0.07, 0.65, 0, 1)"; duration = "placeholder0"; start = "window-x-before - window-x"; end = 0; }; offset-y = { curve = "cubic-bezier(0.07, 0.65, 0, 1)"; duration = "placeholder0"; start = "window-y-before - window-y"; end = 0; }; saved-image-blend = { duration = "placeholder0"; start = 1; end = 0; }; shadow-offset-x = "offset-x"; shadow-offset-y = "offset-y"; *knobs = { duration = 0.4; }; *placeholders = ((0, "duration")); }; picom-12.5/dbus-examples/000077500000000000000000000000001471504570600153325ustar00rootroot00000000000000picom-12.5/dbus-examples/cdbus-driver.sh000077500000000000000000000032441471504570600202650ustar00rootroot00000000000000#!/bin/sh if [ -z "$SED" ]; then SED="sed" command -v gsed > /dev/null && SED="gsed" fi # === Get connection parameters === dpy=$(echo -n "$DISPLAY" | tr -c '[:alnum:]' _) if [ -z "$dpy" ]; then echo "Cannot find display." exit 1 fi service="com.github.chjj.compton.${dpy}" interface='com.github.chjj.compton' object='/com/github/chjj/compton' type_win='uint32' type_enum='uint32' # === DBus methods === # List all window ID compton manages (except destroyed ones) dbus-send --print-reply --dest="$service" "$object" "${interface}.list_win" # Get window ID of currently focused window focused=$(dbus-send --print-reply --dest="$service" "$object" "${interface}.find_win" string:focused | $SED -n 's/^[[:space:]]*'${type_win}'[[:space:]]*\([[:digit:]]*\).*/\1/p') if [ -n "$focused" ]; then # Get invert_color_force property of the window dbus-send --print-reply --dest="$service" "$object" "${interface}.win_get" "${type_win}:${focused}" string:invert_color_force # Set the window to have inverted color dbus-send --print-reply --dest="$service" "$object" "${interface}.win_set" "${type_win}:${focused}" string:invert_color_force "${type_enum}:1" else echo "Cannot find focused window." fi # Reset compton sleep 3 dbus-send --print-reply --dest="$service" "$object" "${interface}.reset" # Undirect window sleep 3 dbus-send --print-reply --dest="$service" "$object" "${interface}.opts_set" string:redirected_force "${type_enum}:0" # Revert back to auto sleep 3 dbus-send --print-reply --dest="$service" "$object" "${interface}.opts_set" string:redirected_force "${type_enum}:2" # Force repaint dbus-send --print-reply --dest="$service" "$object" "${interface}.repaint" picom-12.5/dbus-examples/inverter.sh000077500000000000000000000043621471504570600175340ustar00rootroot00000000000000#!/bin/sh # == Declare stderr function === stderr() { printf "\033[1;31m%s\n\033[0m" "$@" >&2 } # === Verify `picom --dbus` status === if [ -z "$(dbus-send --session --dest=org.freedesktop.DBus --type=method_call --print-reply /org/freedesktop/DBus org.freedesktop.DBus.ListNames | grep compton)" ]; then stderr "picom DBus interface unavailable" if [ -n "$(pgrep picom)" ]; then stderr "picom running without dbus interface" #killall picom & # Causes all windows to flicker away and come back ugly. #picom --dbus & # Causes all windows to flicker away and come back beautiful else stderr "picom not running" fi exit 1 fi # === Setup sed === SED="${SED:-$(command -v gsed || printf 'sed')}" # === Get connection parameters === dpy=$(printf "$DISPLAY" | tr -c '[:alnum:]' _) if [ -z "$dpy" ]; then stderr "Cannot find display." exit 1 fi service="com.github.chjj.compton.${dpy}" interface="com.github.chjj.compton" picom_dbus="dbus-send --print-reply --dest="${service}" / "${interface}"." type_win='uint32' type_enum='uint32' # === Color Inversion === # Get window ID of window to invert if [ -z "$1" -o "$1" = "selected" ]; then window=$(xwininfo -frame | sed -n 's/^xwininfo: Window id: \(0x[[:xdigit:]][[:xdigit:]]*\).*/\1/p') # Select window by mouse elif [ "$1" = "focused" ]; then # Ensure we are tracking focus window=$(${picom_dbus}find_win string:focused | $SED -n 's/^[[:space:]]*'${type_win}'[[:space:]]*\([[:digit:]]*\).*/\1/p') # Query picom for the active window elif echo "$1" | grep -Eiq '^([[:digit:]][[:digit:]]*|0x[[:xdigit:]][[:xdigit:]]*)$'; then window="$1" # Accept user-specified window-id if the format is correct else echo "$0" "[ selected | focused | window-id ]" fi # Color invert the selected or focused window if [ -n "$window" ]; then invert_status="$(${picom_dbus}win_get "${type_win}:${window}" string:invert_color | $SED -n 's/^[[:space:]]*boolean[[:space:]]*\([[:alpha:]]*\).*/\1/p')" if [ "$invert_status" = true ]; then invert=0 # Set the window to have normal color else invert=1 # Set the window to have inverted color fi ${picom_dbus}win_set "${type_win}:${window}" string:invert_color_force "${type_enum}:${invert}" & else stderr "Cannot find $1 window." exit 1 fi exit 0 picom-12.5/desc.txt000066400000000000000000000001021471504570600142310ustar00rootroot00000000000000Compton is a X compositing window manager, fork of xcompmgr-dana. picom-12.5/flake.lock000066400000000000000000000037761471504570600145320ustar00rootroot00000000000000{ "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": 1721287717, "narHash": "sha256-i5F24BL4FaJCOE0twnIPaltgDNeA44CqLsj/TqBAIsQ=", "owner": "nixos", "repo": "nixpkgs", "rev": "56375296f413158b095ce493799cc8d237d70739", "type": "github" }, "original": { "owner": "nixos", "repo": "nixpkgs", "type": "github" } }, "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 } picom-12.5/flake.nix000066400000000000000000000061671471504570600143750ustar00rootroot00000000000000{ inputs = { flake-utils.url = github:numtide/flake-utils; nixpkgs.url = github:nixos/nixpkgs; 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 # like lib.lists.remove, but takes a list of elements to remove removeFromList = toRemove: list: pkgs.lib.foldl (l: e: pkgs.lib.remove e l) list toRemove; overlay = self: super: { picom = super.picom.overrideAttrs (oldAttrs: rec { version = "11"; pname = "picom"; nativeBuildInputs = (removeFromList [ self.asciidoc ] oldAttrs.nativeBuildInputs) ++ [ self.asciidoctor ]; buildInputs = [ self.pcre2 self.xorg.xcbutil self.libepoxy ] ++ (removeFromList [ self.xorg.libXinerama self.xorg.libXext 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; }) (final: prev: { llvmPackages_18 = prev.llvmPackages_18 // { stdenv = final.withCFlags "-fno-omit-frame-pointer" prev.llvmPackages_18.stdenv; }; }) ]; }; overlays = [overlay]; mkDevShell = p: p.overrideAttrs (o: { nativeBuildInputs = o.nativeBuildInputs ++ (with pkgs; [ clang-tools_18 llvmPackages_18.clang-unwrapped.python llvmPackages_18.libllvm 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" export UBSAN_OPTIONS="disable_coredump=0:unmap_shadow_on_exit=1:print_stacktrace=1" export ASAN_OPTIONS="disable_coredump=0:unmap_shadow_on_exit=1:abort_on_error=1" ''; }); in rec { inherit overlay overlays ; packages = { default = pkgs.picom; }; devShells.default = mkDevShell packages.default; devShells.useClang = devShells.default.override { inherit (pkgs.llvmPackages_18) 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_18.stdenv; }; }); } picom-12.5/include/000077500000000000000000000000001471504570600142045ustar00rootroot00000000000000picom-12.5/include/meson.build000066400000000000000000000001441471504570600163450ustar00rootroot00000000000000# SPDX-License-Identifier: MPL-2.0 # Copyright (c) Yuxuan Shui subdirs('picom') picom-12.5/include/picom/000077500000000000000000000000001471504570600153135ustar00rootroot00000000000000picom-12.5/include/picom/api.h000066400000000000000000000024621471504570600162410ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #define PICOM_API_MAJOR (0UL) #define PICOM_API_MINOR (1UL) struct backend_base; /// The entry point of a backend plugin. Called after the backend is initialized. typedef void (*picom_backend_plugin_entrypoint)(struct backend_base *backend, void *user_data); struct picom_api { /// Add a plugin for a specific backend. The plugin's entry point will be called /// when the specified backend is initialized. /// /// @param backend_name The name of the backend to add the plugin to. /// @param major The major version of the backend API interface this plugin /// is compatible with. /// @param minor The minor version of the backend API interface this plugin /// is compatible with. /// @param entrypoint The entry point of the plugin. /// @param user_data The user data to pass to the plugin's entry point. bool (*add_backend_plugin)(const char *backend_name, uint64_t major, uint64_t minor, picom_backend_plugin_entrypoint entrypoint, void *user_data); }; const struct picom_api * picom_api_get_interfaces(uint64_t major, uint64_t minor, const char *context); picom-12.5/include/picom/backend.h000066400000000000000000000444171471504570600170650ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include "types.h" #define PICOM_BACKEND_MAJOR (1UL) #define PICOM_BACKEND_MINOR (0UL) #define PICOM_BACKEND_MAKE_VERSION(major, minor) ((major) * 1000 + (minor)) typedef pixman_region32_t region_t; struct xvisual_info { /// Bit depth of the red component int red_size; /// Bit depth of the green component int green_size; /// Bit depth of the blue component int blue_size; /// Bit depth of the alpha component int alpha_size; /// The depth of X visual int visual_depth; xcb_visualid_t visual; }; typedef struct session session_t; struct win; struct ev_loop; struct backend_operations; typedef struct backend_base backend_t; // 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 shader_attributes { // Whether the shader needs to be render regardless of whether the window is // updated. SHADER_ATTRIBUTE_ANIMATED = 1, }; struct gaussian_blur_args { int size; double deviation; }; struct box_blur_args { int size; }; struct kernel_blur_args { struct conv **kernels; int kernel_count; }; struct dual_kawase_blur_args { int size; 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; /// Scale factor for the horizontal and vertical direction (X for horizontal, /// Y for vertical). vec2 scale; /// Corner radius of the source image BEFORE scaling. The corners of /// the source image will be rounded. double corner_radius; /// Effective size of the source image BEFORE scaling, set where the corners /// of the image are. ivec2 effective_size; /// Border width of the source image BEFORE scaling. 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_WINDOW_SAVED, 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. backend_t *(*init)(session_t *, xcb_window_t) __attribute__((nonnull(1))); void (*deinit)(backend_t *backend_data) __attribute__((nonnull(1))); /// Called when rendering will be stopped for an unknown amount of /// time (e.g. when screen is unredirected). Free some resources. /// /// Optional, not yet used void (*pause)(backend_t *backend_data, session_t *ps); /// Called before rendering is resumed /// /// Optional, not yet used void (*resume)(backend_t *backend_data, session_t *ps); /// 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); // =========== Rendering ============ /// Called before when a new frame starts. /// /// Optional void (*prepare)(backend_t *backend_data, const region_t *reg_damage); /// 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) __attribute__((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) __attribute__((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) __attribute__((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) __attribute__((nonnull(1, 3, 4, 5))); /// 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`: /// /// 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. /// /// 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) __attribute__((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) __attribute__((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) __attribute__((nonnull(1))); // ============ Resource management =========== /// Create a shader object from a shader source. /// /// Optional void *(*create_shader)(backend_t *backend_data, const char *source) __attribute__((nonnull(1, 2))); /// Free a shader object. /// /// Required if create_shader is present. void (*destroy_shader)(backend_t *backend_data, void *shader) __attribute__((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) __attribute__((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) __attribute__((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) __attribute__((nonnull(1, 2))); // =========== Query =========== /// Get backend quirks /// @return a bitmask of `enum backend_quirk`. uint32_t (*quirks)(struct backend_base *backend_data) __attribute__((nonnull(1))); /// Get the version of the backend void (*version)(struct backend_base *backend_data, uint64_t *major, uint64_t *minor) __attribute__((nonnull(1, 2, 3))); /// 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) __attribute__((nonnull(1))); /// Return the capabilities of an image. uint32_t (*image_capabilities)(struct backend_base *backend_data, image_handle image) __attribute__((nonnull(1, 2))); /// Get the attributes of a shader. /// /// Optional, Returns a bitmask of attributes, see `shader_attributes`. uint64_t (*get_shader_attributes)(backend_t *backend_data, void *shader) __attribute__((nonnull(1, 2))); /// 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. /// 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)(backend_t *backend_data); // =========== Post-processing ============ /// 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); // =========== 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); }; struct backend_base { struct backend_operations ops; struct x_connection *c; struct ev_loop *loop; /// Whether the backend can accept new render request at the moment bool busy; // ... }; /// Register a new backend, `major` and `minor` should be the version of the picom backend /// interface. You should just pass `PICOM_BACKEND_MAJOR` and `PICOM_BACKEND_MINOR` here. /// `name` is the name of the backend, `init` is the function to initialize the backend, /// `can_present` should be true if the backend can present the back buffer to the screen, /// false otherwise (e.g. if the backend does off screen rendering, etc.) bool backend_register(uint64_t major, uint64_t minor, const char *name, struct backend_base *(*init)(session_t *ps, xcb_window_t target), bool can_present); /// Define a backend entry point. (Note constructor priority 202 is used here because 1xx /// is reversed by test.h, and 201 is used for logging initialization.) #define BACKEND_ENTRYPOINT(func) static void __attribute__((constructor(202))) func(void) picom-12.5/include/picom/meson.build000066400000000000000000000002541471504570600174560ustar00rootroot00000000000000# SPDX-License-Identifier: MPL-2.0 # Copyright (c) Yuxuan Shui api_headers = [ 'api.h' 'backend.h' ] install_headers(api_headers, subdir: 'picom') picom-12.5/include/picom/types.h000066400000000000000000000075201471504570600166340ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once /// Some common types #include #include #include #include enum blur_method { BLUR_METHOD_NONE = 0, BLUR_METHOD_KERNEL, BLUR_METHOD_BOX, BLUR_METHOD_GAUSSIAN, BLUR_METHOD_DUAL_KAWASE, BLUR_METHOD_INVALID, }; /// Enumeration type to represent switches. typedef enum { OFF = 0, // false ON, // true UNSET } switch_t; enum tristate { TRI_FALSE = -1, TRI_UNKNOWN = 0, TRI_TRUE = 1 }; /// Return value if it's not TRI_UNKNOWN, otherwise return fallback. static inline enum tristate tri_or(enum tristate value, enum tristate fallback) { return value ?: fallback; } static inline bool tri_or_bool(enum tristate value, bool fallback) { return value == TRI_UNKNOWN ? fallback : value == TRI_TRUE; } static inline enum tristate tri_from_bool(bool value) { return value ? TRI_TRUE : TRI_FALSE; } /// A structure representing margins around a rectangle. typedef struct { int top; int left; int bottom; int right; } margin_t; struct color { double red, green, blue, alpha; }; 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; struct ibox { ivec2 origin; ivec2 size; }; static const vec2 SCALE_IDENTITY = {1.0, 1.0}; static inline vec2 ivec2_as(ivec2 a) { return (vec2){ .x = a.x, .y = a.y, }; } 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, }; } /// Saturating cast from a vec2 to a ivec2 static inline ivec2 vec2_as(vec2 a) { return (ivec2){ .x = (int)fmin(fmax(a.x, INT_MIN), INT_MAX), .y = (int)fmin(fmax(a.y, INT_MIN), INT_MAX), }; } static inline vec2 vec2_add(vec2 a, vec2 b) { return (vec2){ .x = a.x + b.x, .y = a.y + b.y, }; } static inline vec2 vec2_ceil(vec2 a) { return (vec2){ .x = ceil(a.x), .y = ceil(a.y), }; } static inline vec2 vec2_floor(vec2 a) { return (vec2){ .x = floor(a.x), .y = floor(a.y), }; } static inline bool vec2_eq(vec2 a, vec2 b) { return a.x == b.x && a.y == b.y; } static inline vec2 vec2_scale(vec2 a, vec2 scale) { return (vec2){ .x = a.x * scale.x, .y = a.y * scale.y, }; } /// Check if two boxes have a non-zero intersection area. static inline bool ibox_overlap(struct ibox a, struct ibox b) { if (a.size.width <= 0 || a.size.height <= 0 || b.size.width <= 0 || b.size.height <= 0) { return false; } if (a.origin.x <= INT_MAX - a.size.width && a.origin.y <= INT_MAX - a.size.height && (a.origin.x + a.size.width <= b.origin.x || a.origin.y + a.size.height <= b.origin.y)) { return false; } if (b.origin.x <= INT_MAX - b.size.width && b.origin.y <= INT_MAX - b.size.height && (b.origin.x + b.size.width <= a.origin.x || b.origin.y + b.size.height <= a.origin.y)) { return false; } return true; } static inline bool ibox_eq(struct ibox a, struct ibox b) { return ivec2_eq(a.origin, b.origin) && ivec2_eq(a.size, b.size); } static inline ivec2 ivec2_scale_ceil(ivec2 a, vec2 scale) { vec2 scaled = vec2_scale(ivec2_as(a), scale); return vec2_as(vec2_ceil(scaled)); } static inline ivec2 ivec2_scale_floor(ivec2 a, vec2 scale) { vec2 scaled = vec2_scale(ivec2_as(a), scale); return vec2_as(vec2_floor(scaled)); } #define MARGIN_INIT \ { 0, 0, 0, 0 } picom-12.5/man/000077500000000000000000000000001471504570600133345ustar00rootroot00000000000000picom-12.5/man/meson.build000066400000000000000000000013771471504570600155060ustar00rootroot00000000000000mans = ['picom.1', 'picom-inspect.1', 'picom-trans.1'] if get_option('with_docs') a2x = find_program('asciidoctor') foreach m : mans custom_target( m, output: [m], input: [m + '.adoc'], command: [ a2x, '-a', 'picom-version=v' + meson.project_version(), '--backend', 'manpage', '@INPUT@', '-D', meson.current_build_dir(), ], install: true, install_dir: join_paths(get_option('mandir'), 'man1'), ) custom_target( m + '.html', output: [m + '.html'], input: [m + '.adoc'], command: [ a2x, '-a', 'picom-version=v' + meson.project_version(), '--backend', 'html', '@INPUT@', '-D', meson.current_build_dir(), ], install_dir: get_option('datadir') / 'doc' / 'picom', ) endforeach endif picom-12.5/man/picom-inspect.1.adoc000066400000000000000000000023301471504570600170730ustar00rootroot00000000000000= picom-inspect(1) Yuxuan Shui :doctype: manpage :mansource: picom-inspect :manversion: {picom-version} :manmanual: 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* accepts all options that *picom* does. 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. *picom-inspect* also accepts some extra options: ::: *--monitor*:: Keep *picom-inspect* running in a loop, and dump information every time something changed about a window. 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), xref:picom.1.adoc[*picom*(1)] picom-12.5/man/picom-trans.1.adoc000066400000000000000000000050161471504570600165610ustar00rootroot00000000000000= picom-trans(1) Yuxuan Shui :doctype: manpage :mansource: picom :manversion: {picom-version} :manmanual: User Commands NAME ---- picom-trans - an opacity setter tool SYNOPSIS -------- *picom-trans* [-w _WINDOW_ID_] [-n _WINDOW_NAME_] [-c] [-s] _OPACITY_ DESCRIPTION ----------- *picom-trans* is a bash script that sets __NET_WM_WINDOW_OPACITY_ attribute of a window using standard X11 command-line utilities, including *xprop*(1) and *xwininfo*(1). It is similar to *transset*(1) or *transset-df*(1). OPTIONS ------- *-w*, *--window*=_WINDOW_ID_:: Specify the window id of the target window. *-n*, *--name*=_WINDOW_NAME_:: Specify and try to match a window name. *-c*, *--current*:: Specify the currently active window as target. Only works if EWMH '_NET_ACTIVE_WINDOW' property exists on root window. *-s*, *--select*:: Select target window with mouse cursor. This is the default if no window has been specified. *-o*, *--opacity*=_OPACITY_:: Specify the new opacity value for the window. This value can be anywhere from 1-100. If it is prefixed with a plus or minus (+/-), this will increment or decrement from the target window's current opacity instead. *-g*, *--get*:: Print the target window's opacity instead of setting it. *-d*, *--delete*:: Delete opacity of the target window instead of setting it. *-t*, *--toggle*:: Toggle the target window's opacity: Set opacity if not already set, and delete if already set. *-r*, *--reset*:: Reset opacity for all windows instead of setting it. EXAMPLES -------- * Set the opacity of the window with specific window ID to 75%: + ------------ picom-trans -w "$WINDOWID" 75 ------------ * Set the opacity of the window with the name "urxvt" to 75%: + ------------ picom-trans -n "urxvt" 75 ------------ * Set current window to opacity of 75%: + ------------ picom-trans -c 75 ------------ * Select target window and set opacity to 75%: + ------------ picom-trans -s 75 ------------ * Increment opacity of current active window by 5%: + ------------ picom-trans -c +5 ------------ * Decrement opacity of current active window by 5%: + ------------ picom-trans -c -- -5 ------------ * Delete current window's opacity: + ------------ picom-trans -c --delete ------------ * Toggle current window's opacity between 90 and unset + ------------ picom-trans -c --toggle 90 ------------ * Reset all windows: + ------------ picom-trans --reset ------------ BUGS ---- Please submit bug reports to . SEE ALSO -------- xref:picom.1.adoc[*picom*(1)], *xprop*(1), *xwininfo*(1) picom-12.5/man/picom.1.adoc000066400000000000000000001610101471504570600154310ustar00rootroot00000000000000= picom(1) Yuxuan Shui :doctype: manpage :mansource: picom :manversion: {picom-version} :manmanual: User Commands :source-highlighter: highlight.js :highlightjs-languages: glsl :toc: right NAME ---- picom - a compositor for X11 SYNOPSIS -------- *picom* [_OPTIONS_] DESCRIPTION ----------- picom is a compositor based on Dana Jansens' version of xcompmgr (which itself was written by Keith Packard). It includes some improvements over the original xcompmgr, like window frame opacity and inactive window transparency. OPTIONS ------- *-h*, *--help*:: Get the usage text embedded in program code, which may be more up-to-date than this man page. *-r*, *--shadow-radius*=_RADIUS_:: The blur radius for shadows, in pixels. (defaults to 12) *-o*, *--shadow-opacity*=_OPACITY_:: The opacity of shadows. (0.0 - 1.0, defaults to 0.75) *-l*, *--shadow-offset-x*=_OFFSET_:: The left offset for shadows, in pixels. (defaults to -15) *-t*, *--shadow-offset-y*=_OFFSET_:: The top offset for shadows, in pixels. (defaults to -15) *-I*, *--fade-in-step*=_OPACITY_STEP_:: Opacity change between steps while fading in. (0.01 - 1.0, defaults to 0.028) *-O*, *--fade-out-step*=_OPACITY_STEP_:: Opacity change between steps while fading out. (0.01 - 1.0, defaults to 0.03) *-D*, *--fade-delta*=_MILLISECONDS_:: The time between steps in fade step, in milliseconds. (> 0, defaults to 10) *-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. [[fading]]*-f*, *--fading*:: Fade windows in/out when opening/closing and when opacity changes, unless *--no-fading-openclose* is used. [[inactive-opacity]]*-i*, *--inactive-opacity*=_OPACITY_:: Opacity of inactive windows. (0.1 - 1.0, defaults to 1.0). Using this option is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific opacity. *-e*, *--frame-opacity*=_OPACITY_:: Opacity of window titlebars and borders. (0.1 - 1.0, disabled by default) *-b*, *--daemon*:: Daemonize process. Fork to background after initialization. This option can only be set from the command line, setting this in the configuration file will have no effect. *--log-level*:: 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-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. *--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). *--config* _PATH_:: Look for configuration file at the path. See *CONFIGURATION FILES* section below for where picom looks for a configuration file by default. Use `/dev/null` to avoid loading configuration file. *--write-pid-path* _PATH_:: Write process ID to a file. it is recommended to use an absolute path. *--plugins* _PATH_:: Specify plugins to load. Plugins will first be searched in current working directory (unless specified in the config file, in which case this step is skipped), then in `$XDG_CONFIG_HOME/picom/plugins`, then in `$XDG_CONFIG_DIRS/picom/plugins`. If all of the above fail, the plugin name is passed directly to the dynamic loader. Can be specified multiple times to load more than one plugins. *--shadow-color* _STRING_:: Color of shadow, as a hex string (e.g. _#000000_) *--shadow-red* _VALUE_:: Red color value of shadow (0.0 - 1.0, defaults to 0). *--shadow-green* _VALUE_:: Green color value of shadow (0.0 - 1.0, defaults to 0). *--shadow-blue* _VALUE_:: Blue color value of shadow (0.0 - 1.0, defaults to 0). [[inactive-opacity-override]]*--inactive-opacity-override*:: Let inactive opacity set by *-i* override the __NET_WM_WINDOW_OPACITY_ values of windows. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific opacity. [[active-opacity]]*--active-opacity* _OPACITY_:: Default opacity for active windows. (0.0 - 1.0, defaults to 1.0). Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific opacity. [[inactive-dim]]*--inactive-dim* _VALUE_:: Dim inactive windows. (0.0 - 1.0, defaults to 0.0). Using this option is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific dim levels. [[corner-radius]]*--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]]*--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*. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific corner radius. [[rounded-corners-exclude]]*--rounded-corners-exclude* _CONDITION_:: Exclude conditions for rounded corners. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific corner radius. *--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]]*--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. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific rules. [[mark-ovredir-focused]]*--mark-ovredir-focused*:: Mark override-redirect windows that doesn't have a child window with _WM_STATE_ focused. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific rules. *--no-fading-openclose*:: Do not fade on window open/close. *--no-fading-destroyed-argb*:: Do not fade destroyed ARGB windows with WM frame. Workaround of bugs in Openbox, Fluxbox, etc. [[shadow-ignore-shaped]]*--shadow-ignore-shaped*:: Do not paint shadows on shaped windows. Note shaped windows here means windows setting its shape through X Shape extension. Those using ARGB background is beyond our control. Deprecated, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific shadow. [[detect-rounded-corners]]*--detect-rounded-corners*:: 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 _pass:[_]NET_WM_WINDOW_OPACITY_ on client windows, useful for window managers not passing _pass:[_]NET_WM_WINDOW_OPACITY_ of client windows to frame windows. *--vsync*, *--no-vsync*:: Enable/disable VSync. *--use-ewmh-active-win*:: 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 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. [[unredir-if-possible-exclude]]*--unredir-if-possible-exclude* _CONDITION_:: Conditions of windows that shouldn't be considered full-screen for unredirecting screen. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific unredirect. [[shadow-exclude]]*--shadow-exclude* _CONDITION_:: Specify a list of conditions of windows that should have no shadow. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific shadow. [[clip-shadow-above]]*--clip-shadow-above* _CONDITION_:: Specify a list of conditions of windows that should have no shadow painted over, such as a dock window. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific shadow clipping. [[fade-exclude]]*--fade-exclude* _CONDITION_:: Specify a list of conditions of windows that should not be faded. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific fading. [[focus-exclude]]*--focus-exclude* _CONDITION_:: Specify a list of conditions of windows that should always be considered focused. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way for doing this. *--inactive-dim-fixed*:: Use fixed inactive dim value, instead of adjusting according to window opacity. [[detect-transient]]*--detect-transient*:: Use _WM_TRANSIENT_FOR_ to group windows, and consider windows in the same group focused at the same time. [[detect-client-leader]]*--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. *--blur-method*, *--blur-size*, *--blur-deviation*, *--blur-strength*:: Parameters for background blurring, see the *BLUR* section for more information. *--blur-background*:: 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-frame*:: 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-fixed*:: Use fixed blur strength rather than adjusting according to window opacity. *--blur-kern* _MATRIX_:: Specify the blur convolution kernel, with the following format: + ---- WIDTH,HEIGHT,ELE1,ELE2,ELE3,ELE4,ELE5... ---- + In other words, the matrix is formatted as a list of comma separated numbers. The first two numbers must be integers, which specify the width and height of the matrix. They must be odd numbers. Then, the following `width * height - 1` numbers specifies the numbers in the matrix, row by row, excluding the center element. + The elements are finite floating point numbers. The decimal pointer has to be _._ (a period), scientific notation is not supported. + The element in the center will either be 1.0 or varying based on opacity, depending on whether you have *--blur-background-fixed*. Yet the automatic adjustment of blur factor may not work well with a custom blur kernel. + 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 (See *BLUR*). [[blur-background-exclude]]*--blur-background-exclude* _CONDITION_:: Exclude conditions for background blur. *--resize-damage* _INTEGER_:: 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*. Only works with *--legacy-backends*. Shrinking doesn't function correctly. [[invert-color-include]]*--invert-color-include* _CONDITION_:: Specify a list of conditions of windows that should be painted with inverted color. Resource-hogging, and is not well tested. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to do this. [[opacity-rule]]*--opacity-rule* _OPACITY_:__CONDITION__:: Specify a list of opacity rules, in the format `PERCENT:PATTERN`, like `50:name pass:[*]= "Firefox"`. picom-trans is recommended over this. Note we don't make any guarantee about possible conflicts with other programs that set _pass:[_]NET_WM_WINDOW_OPACITY_ on frame or client windows. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific opacity. *--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. + -- * `xrender` backend performs all rendering operations with X Render extension. It is what `xcompmgr` uses, and is generally a safe fallback when you encounter rendering artifacts or instability. * `glx` (OpenGL) backend performs all rendering operations with OpenGL. It is more friendly to some VSync methods, and has significantly superior performance on color inversion (*--invert-color-include*) or blur (*--blur-background*). It requires proper OpenGL 2.0 support from your driver and hardware. You may wish to look at the GLX performance optimization options below. *--xrender-sync-fence* might be needed on some systems to avoid delay in changes of screen contents. * `xr_glx_hybrid` backend renders the updated screen contents with X Render and presents it on the screen with GLX. It attempts to address the rendering issues some users encountered with GLX backend and enables the better VSync of GLX backends. *--vsync-use-glfinish* might fix some rendering issues with this backend. -- *--glx-no-stencil*:: 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-rebind-pixmap*:: 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 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. 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. *--dbus*:: Enable remote control via D-Bus. See the *D-BUS API* section below for more details. *--benchmark* _CYCLES_:: Benchmark mode. Repeatedly paint until reaching the specified cycles. *--benchmark-wid* _WINDOW_ID_:: Specify window ID to repaint in benchmark mode. If omitted or is 0, the whole screen is repainted. *--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. *--max-brightness*:: 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) *--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 xref:_shader_interface[*SHADER INTERFACE*] below for more details on the interface. [[window-shader-fg-rule]]*--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. Using this is discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific shaders. *--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. WINDOW RULES ------------ Window rules allow you to set window-specific options which can be used to change appearance of windows based on certain conditions. Note there are other options that also cover some of the functionality of window rules, but window rules are more flexible and powerful. If you are creating a fresh configuration file, it is recommended to use window rules instead of the other options. Following is a list of all the options that are superseded by window rules: <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>. As well as the xref:wintypes[*wintypes*] configuration file option. If window rules option is used, none of the above options will have any effect. And warning messages will be issued. When the window rules option is used, the compositor will also behave somewhat differently in certain cases. One such case is that fullscreen windows will no longer have their rounded corners disabled by default. If you are currently using some of these options and want to switch to window rules, or if you want to keep the existing behavior, see the xref:_migrating_old_rules[*Migrating old rules*] section for how to convert them. === Syntax Window rules are only available in the configuration file. To set window rules, set the `rules` option in the configuration file to something like this: [listing] rules = ( { match = "focused"; opacity = 1; }, { match = "name = 'firefox'"; shadow = true; }, # ... and so on ) `rules = ( ... )` sets the option to a list, which can contain multiple sub-items. For `rules`, each sub-item must be a group (i.e. `{ key = value; ... }`), representing a condition and a set of options to apply when the condition is met. These sub-items are matched in the order they appear in the configuration file, options are applied as the conditions are matched. If the same option is set multiple times, the last one will take effect. Within each sub-item, these keys are available: :: match::: The condition string to match windows with. See the xref:_format_of_conditions[*FORMAT OF CONDITIONS*] section below for the syntax of condition strings. If not specified, the rule will always match. shadow::: Whether to draw shadow under the matching window. 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. fade::: Whether to fade the matching window in/out when opening/closing it. When animations are used, this will have no effect. This can only be used to disable fading animations enabled by option <>. opacity::: Opacity of the matching window. (0.0 - 1.0). If not explicitly set by a rule, the opacity value from the window properties (e.g. pass:[_]NET_WM_WINDOW_OPACITY) will be used. dim::: Dim level of the matching window. Larger value means more dimming. (0.0 - 1.0) corner-radius::: Corner radius of the matching window in number of pixels. 0 means no corner rounding. blur-background::: Whether the background of the matching window should be blurred. invert-color::: Whether to invert the color of the matching window. clip-shadow-above::: Whether to prevent the matching window from being painted over by shadows. unredir::: Whether the matching window should cause the compositor to unredirect the screen, and whether it should trigger the screen to be redirected again if it is currently unredirected. This could be a boolean value, if _true_, the screen will be unredirected if the matching window meets certain conditions; if _false_, it will never cause the screen to be unredirected. If the screen is currently unredirected, and there is no other window that will trigger unredirection, both of these choices will cause the screen to be redirected again. To control that behavior as well, you can set `unredir` to either _preferred_, such windows will not cause the screen to be redirected in this situation, and will behave like `true` otherwise; or _passive_, which not only won't cause redirection in this case, but also won't actively cause the screen to be unredirected. The last possible value for this option is _forced_, any of the windows having their `unredir` set to `forced` will cause the screen to be unredirected unconditionally. The value of the _pass:[_]NET_WM_BYPASS_COMPOSITOR_ property on the window will be considered iff `unredir` is not explicitly set by any rule. transparent-clipping::: Whether to make the matching window clip other windows like opaque windows do, instead of blending on top of them. When applied to transparent windows, this means nothing will be painted under the transparent parts of the window, essentially cuts a hole in the screen. shader::: GLSL fragment shader path for rendering window contents. See section xref:_shader_interface[*SHADER INTERFACE*] below for more details on the interface. [[window-rules-animations]]animations::: Define window-specific animation scripts. The format of this option is the same as the top-level _animations_ option. You can find more information in the xref:_animations[*ANIMATIONS*] section. If animation scripts are defined in multiple matching rules, they will be merged together. If multiple matching rules contain animation scripts for the same trigger, the last one will take effect, the same as other options. === Migrating old rules Most of the rule options should 1:1 map to the new window rules. Here is a list of the non-trivial ones and how to achieve the same effect with window rules. *Inactive dimming and opacity*:: This includes options <>, <>, <>, <>, <>, and <>. When using the window rules, the compositor no longer have an "active window" concept, as it is easy to achieve with window rules. You can use `match = "focused || group_focused"` to match windows that would have been considered active with the old options. Then you can set the opacity and dim level for matched windows accordingly. <> and <> can be achieved by adding `|| wmwin` and `|| override_redirect` to the match string, respectively. <> can be achieved by setting `opacity-override = true`. + NOTE: Setting _opacity_ explicitly with a rule will override the opacity value from the window properties (i.e. _pass:[_]NET_WM_WINDOW_OPACITY_), which is used by tools like `picom-trans` for setting the opacity of window. If you would like to keep using tools like `picom-trans`, you can choose to set the opacity only for windows without the opacity property by matching `! _NET_WM_WINDOW_OPACITY`. *Active window*:: This includes option <>. This option was only used to influence what windows are considered active, to apply inactive opacity and dimming. Since with window rules you no longer need the compositor to help you decide what is active and what is not (see above), this option is no longer needed. *Rounded corners and fullscreen windows*:: Rounded corners are no longer automatically disabled for fullscreen windows. If you want to disable rounded corners for fullscreen windows, you can use the following rule: + ---- rules = ( { match = "fullscreen"; corner-radius = 0; }, ) ---- 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. Formal grammar for a condition looks like this: Condition <- Term ('||' Term)* Term <- Item ('&&' Item)* Item <- '!'? Target '@'? ('[' Index ']')? (Operator Pattern)? | '(' Condition ')' Concretely speaking, a condition is a sequence of one or more simple pattern matching __Item__s, joined by logical operators `&&` (and) and `||` (or). `&&` has higher precedence than `||`. Both operators are left-associative. Parentheses can be used to raise precedence. If an _Item_ has a leading negation operator (`!`), the result of the item is negated. Inside an _Item_: _Target_:: is either a predefined target name, or the name of a window property to match. Supported predefined targets are: ::: `x`, `y`, `x2`, `y2`:::: Window coordinates, from the top-left corner of the window `(x, y)` to the bottom-right corner `(x2, y2)`. `width`, `height`:::: Size of the window. `widthb`, `heightb`:::: Like `width` and `height`, but including the window border. `border_width`:::: Width of the window border. `fullscreen`:::: Whether the window is fullscreen. If *--no-ewmh-fullscreen* is set, this is determined by the window size and position; otherwise, it is determined by the _pass:[_]NET_WM_STATE_FULLSCREEN_ property. `override_redirect`:::: Whether the window is override-redirect. `argb`:::: Whether the window has an ARGB visual. `focused`:::: Whether the window is focused. `group_focused`:::: Whether the window is in the same window group as the focused window. This requires <> or <>. `wmwin`:::: Whether the window looks like a WM window, i.e. has no client window and is not override-redirected. [[c2-bounding-shaped]]`bounding_shaped`:::: Whether the window has a bounding shape. `rounded_corners`:::: Whether the window bounding shape only has rounded corners, and is otherwise rectangular. This implies <>. Requires <>. This has no relation to <>. `window_type`:::: Window type, as defined by _pass:[_]NET_WM_WINDOW_TYPE_. Name only, e.g. _normal_ means _pass:[_]NET_WM_WINDOW_TYPE_NORMAL_. Because a window can have multiple types, testing for equality succeeds if any of the window's types match. `name`:::: Name of the window. This is either _pass:[_]NET_WM_NAME_ or _pass:[_]WM_NAME_. `class_i`, `class_g`:::: Instance and general class of the window. This is the first and second value of _pass:[_]WM_CLASS_, respectively. `role`:::: Window role. This is the value of _pass:[_]WM_WINDOW_ROLE_. + _Target_ can be followed by an optional `@` if the window attribute should be be looked up on client window. Otherwise the frame window will be used. _Index_:: is the index number of the property to look up. For example, `[2]` returns the third value of 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. None of the predefined targets have multiple values, so do not use this with them. _Operator_ and _Pattern_:: define how _Target_ will be matched. They can be omitted together, in which case the existence of the window property is checked when _Target_ is not a predefined target; for a predefined _Target_, omitting _Operator_ and _Pattern_ is equivalent to writing `!= 0`. + Available operators change depends on the type of _Target_ being matched. If the target is a number, the operators are `=`, `>`, `<`, `>=`, `pass:[<=]`, as well as their negation, obtained by prefixing the operator with `!` (e.g. `!=`, `!>`, etc.). If the target is a string, the operators are `=` (strict equal), `pass:[*]=` (substring match), `^=` (starts with), `%=` (match with glob), `~=` (match with regex), as well as their case insensitive variants `?=`, `pass:[*]?=`, `^?=`, `%?=`, `~?=`. String operators can be negated by prefixing the operator with `!` as well (e.g. `!=`, `!pass:[*]=`, etc.). + _Pattern_ is either an integer or a string enclosed by single or double quotes. Python-3-style escape sequences are supported for strings. Boolean values are interpreted as integers, i.e. writing `true` is equivalent to `1`, and `false` `0`. Examples: # If the window is focused focused focused = 1 # If the window is not override-redirected !override_redirect override_redirect = false override_redirect != true override_redirect != 1 # If the window is a menu window_type *= "menu" _NET_WM_WINDOW_TYPE@ *= "MENU" # If the window is marked hidden: _NET_WM_STATE contains _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@[*] *?= "sticky" # If the window name contains "Firefox", ignore case name *?= "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 = 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] < 20 || !_NET_FRAME_EXTENTS@ # The pattern here will be parsed as "dd4" name = "\x64\x64\o64" # These two are equivalent name = 'Firefox' || name = 'Chromium' && class_i = 'Navigator' name = 'Firefox' || (name = 'Chromium' && class_i = 'Navigator') ANIMATIONS ---------- picom supports xref:fading[fading] animation when you open or close a window. In addition to that, picom also has a very powerful animation script system, which can be used to animate many aspects of a window based on certain triggers. Animation scripts can be defined in your configuration file by setting the option _animations_. It is also possible to define animations per-window using the xref:_window_rules[*WINDOW RULES*] system, by setting the <> option in a rule. (Read the rest of this section first before you go there.) The basic syntax of the _animations_ option is as follows: ---- animations = ({ triggers = [ ... ]; suppressions = [ ... ]; # more options follow ... }, { # another animation script }, ...) ---- `animations = ( ... )` sets _animations_ to a list, which can contain multiple sub-items, each item is an animation script. An animation script is a group containing multiple entries (i.e. `{ key = value; ... }`). All animation scripts share some common options, like _triggers_ and _suppressions_, they also contain more options that either defines the actual animation, or selects an animation preset. === Common options _triggers_::: A list of triggers specifying when this animation should be started. Each trigger can have at most one animation script associated to it, otherwise the behavior is undefined, and a warning will be issued. Valid triggers are: :::: _open_:: When a window is opened. _close_:: When a window is closed. _show_:: When a minimized or iconified window is shown. _hide_:: When a window is minimized or iconified. _increase-opacity_:: When the opacity of a window is increased. _decrease-opacity_:: When the opacity of a window is decreased. [[trigger-geometry]]_geometry_:: When the geometry of a window is changed. (EXPERIMENTAL) + WARNING: The _geometry_ trigger is experimental. Using this means you accept the caveat that geometry animations will also trigger when you manually resize or move a window, like when you drag the window around with your mouse. _suppressions_::: Which other animations should be suppressed when this animation is running. Normally, if another trigger is activated while an animation is already running, the animation in progress will be interrupted and the new animation will start. If you want to prevent this, you can set the `suppressions` option to a list of triggers that should be suppressed. This is optional, the default value for this is an empty list. === Presets Defining an animation is a bit involved. To make animations more approachable, without you having to learn the nitty-gritty details of the script system, picom provides a number of presets that you can use by just specifying a handful of options. To choose a preset, add a _preset_ option to an animation script group, like this: ---- animations = ({ triggers = [ "close", "hide" ]; preset = "slide-out"; direction = "down"; ... }, ...) ---- Some presets have additional options that you can set to customize the animation. In this example, the _slide-out_ preset has a _direction_ option specifying the direction of the sliding animation. ifndef::env-web[] NOTE: Describing animations with only words is difficult. We have short video clips showing off each preset, but sadly they cannot be included in this manpage. The web version of this document hosted on our website at https://picom.app[] on the other hand, does have those clips. endif::[] The following presets are available: :: + _slide-in_, _slide-out_::: + Show/hide the window with a sliding animation. + -- ifdef::env-web[] video::assets/slide.mp4[width=400] endif::[] -- + -- *Options*::: _direction_:: The sliding direction, valid values are _up_, _down_, _left_, _right_. _duration_:: Duration of the animation in seconds. (Can be fractional). -- + _fly-in_, _fly-out_::: + Show/hide the window with a flying animation. + -- ifdef::env-web[] video::assets/fly.mp4[width=400] endif::[] -- + -- *Options*::: _direction_:: The flying direction, valid values are _up_, _down_, _left_, _right_. _duration_:: Duration of the animation in seconds. -- + _appear_, _disappear_::: + Show/hide the window with a combination of scaling and fading. + -- ifdef::env-web[] video::assets/appear.mp4[width=400] endif::[] -- + -- *Options*::: _scale_:: The scaling factor of the window, 1.0 means no scaling. _duration_:: Duration of the animation in seconds. -- + _geometry-change_::: + Animate the geometry (i.e. size and position) change of the window. + WARNING: This makes use of both the <> trigger, and the <> output variable. Both of these features are experimental and may not work as expected. + -- ifdef::env-web[] video::assets/geometry-change.mp4[width=400] endif::[] -- + -- *Options*::: _duration_:: Duration of the animation in seconds. -- === Advanced If the existing presets don't meet your needs, it is always possible to define your own animations. To put it simply, an animation script is just a collection of variables, and how their values should be computed. Animation scripts, when running, are evaluated once per frame, and the values of some of the variables are then used to animate the window. -- *Basic syntax* To concretely illustrate what the above means, here is an example: ---- # this animation script does nothing to your windows by the way. animations = ({ # common options, these are not part of the collection of variables triggers = [ "open" ]; # variables a = 10; b = "a * 10"; c = "a + b"; d = { curve = "cubic-bezier(0.25, 0.1, 0.25, 1.0)"; duration = 0.5; delay = 0; start = 0; end = 1; }; # more options follow # ... }, ...) ---- A variable can be defined as a number, an expression, or a timing function. In the example above, _a_ is defined to be a number (10), _b_ is defined to be the result of the expression `a * 10`, and _c_ similarly. Expression used to define one variable can refer to other variables in the same script. This is how you can create complex animations. Where the variables are defined in the script does not matter, as long as no circular references exist. NOTE: Because variable names can contain dashes (`-`), minus signs in expressions must be surrounded by spaces. For example, `a - 10` means `a` minus `10`, whereas `a-10` is a variable named `a-10`. _d_ is a timing function, which is a group with several options specifying its behavior. Timing functions are what drives an animation. If no timing function is defined in an animation script, nothing will be animated and the animation will end instantly. These options are valid for a timing function: ::: _curve_:: Type of the curve and its parameters. It can be _linear_, which takes no parameters and defines a linear curve; or _cubic-bezier_, which takes four parameters for the four control points of the cubic bezier curve; or _step_, which takes one or two parameters, the first is the number of steps, the second is the "jumpterm", which can be _jump-start_, _jump-end_, _jump-none_, or _jump-both_. This option is optional, is not specified, the curve will be linear. _delay_:: The number of seconds to wait before the value starts changing. Optional, defaults to 0. _duration_:: The number of seconds it will take for the value to go from _start_ to _end_ once it starts changing. Mandatory. And must be greater than 0. _start_:: The start value of the variable. Mandatory. _end_:: The end value of the variable. Mandatory. All options except _curve_ can be set to expressions. Timing function options are not variables themselves. NOTE: If any of _delay_, _duration_, _start_, or _end_ is defined with an expression, the expression will be evaluated only once when the animation starts. The values of _delay_, _duration_, _start_, and _end_ will then be fixed for the duration of the animation. The total duration of an animation is determined by the duration of the timing function with the longest duration. The animation will end when the longest timing function ends. Once an animation ends, its effects on the window will be removed. There isn't any restriction on what you can name the variables. Obviously they cannot conflict with the names of common options (_triggers_, _suppressions_, and _preset_), but other than that, you can name them whatever you want as long as libconfig allows it. Some variable names have special meanings as we will see below. -- -- *Output variables* Now you know how to write an animation script. But what we just wrote doesn't actually do anything to the window. To animate a window, we define a set of special variable names which we will call "output variables". If you define variables with these names, their values will be used to animate the window. For example, if you define an animation script like this: ---- animations = ({ triggers = [ "open" ]; offset-x = { duration = 2; start = 0; end = 100; }; }, ...) ---- Then when a window opens, it will move 100 pixels to the right over the course of 2 seconds. WARNING: Although we did say you can name your variables whatever you want, if some of them become output variables in the future, your animation script will behave unexpectedly. To avoid this kind of problems, we reserve several classes of variable names which we will never use for special variables. These are: 1) any names that start with a single letter followed by a dash (e.g. `a-`, `b-`, etc.); 2) any names that start with `var-`, `tmp-`, or `user-`. If you need to define a non-output variable, use one of these names. Currently, these output variables are supported: ::: _offset-x_, _offset-y_:: The offset of the window in the X and Y direction, respectively. The window body will be moved by this amount. Note this does not affect the shadow, so if you define these but not _shadow-offset-x_ or _shadow-offset-y_, the shadow will remain where the window was without the animation. _shadow-offset-x_, _shadow-offset-y_:: The offset of the shadow in the X and Y direction, respectively. The shadow will be moved by this amount. _opacity_:: The opacity of the window. This is a number between 0 and 1. _blur-opacity_:: The opacity of the blur behind the window. This is a number between 0 and 1. _shadow-opacity_:: The opacity of the shadow. This is a number between 0 and 1. _scale-x_, _scale-y_, _shadow-scale-x_, _shadow-scale-y_:: The scaling factor of the window and shadow in the X and Y direction, respectively. 1.0 means no scaling. The window body and the shadow are scaled independently. _crop-x_, _crop-y_, _crop-width_, _crop-height_:: These four values combined defines a rectangle on the screen. The window and its shadow will be cropped to this rectangle. If not defined, the window and shadow will not be cropped. [[saved-image-blend]]_saved-image-blend_:: When the window's geometry changes, its content will often change drastically, creating a jarring discontinuity. This output variable allows you to blend the window's content before and after the geometry change, the before and after images will be stretched appropriately to match the animation. This way you can smoothly animated geometry changes. This is a number between 0 and 1. 0 means the saved image is not used, whereas 1 means you will only see the saved image. (EXPERIMENTAL) + WARNING: The _saved-image-blend_ variable is experimental. It might work incorrectly, cause visual artifacts, or slow down your system. You are welcome to open an issue on GitHub if you encounter any problems to help us improve it, though resolution is not guaranteed. All coordinates are in pixels, and are in the coordinate system of the screen. Sizes are also in pixels. IMPORTANT: If an output variable name is not defined in your animation script, it will take the default value for whichever state the window is in. Specifically, if you don't define an _opacity_ variable in the animation script for the "close" or "hide" trigger, a closed window will, by default, have 0 opacity. So you will just see it disappear instantly. Oftentimes, you will want to set _opacity_ to 1 to make the window visible for the duration of the animation. -- -- *Context variables* Now you know how to animate a window. But this is still not powerful enough to support most animations you might want to define. For example, if you want your window to fly out the right side of your screen, the amount of pixels it has to move depends on where it is on the screen, and its width. For the last piece of the puzzle, we have context variables. A context variable is a variable picom defines for you, and you can use them in expressions like any other variables. Their values reflect certain attributes of the window you are animating. WARNING: If you define a variable with the same name as a context variable, your variable will shadow the context variable. Since more context variables can be added in the future, this can be difficult to avoid. Thus, the same rule for output variables applies here as well: if you need to define a temporary variable, use one of the reserved names. Currently, these context variables are defined: ::: _window-x_, _window-y_:: The coordinates of the top-left corner of the window. _window-width_, _window-height_:: The size of the window. _window-x-before_, _window-y-before_, _window-width-before_, _window-height-before_:: The size and coordinates of the window from the previous frame. This is only meaningfully different from the normal window geometry variables inside animations triggered by the _geometry_ trigger. _window-monitor-x_, _window-monitor-y_, _window-monitor-width_, _window-monitor-height_:: Defines the rectangle which reflects the monitor the window is on. If the window is not fully contained in any monitor, the rectangle will reflect the entire virtual screen. _window-raw-opacity-before_, _window-raw-opacity_:: Animation triggers are usually accompanied by a change in the window's opacity. For example, when a window is opened, its opacity changes from 0 to 1. These two variables reflect the opacity of the window for the previous and current frame. They are useful if you want to smoothly transition the window's opacity. IMPORTANT: All of the _window-*-before_ variables are updated every frame, and reflects the state of the window in the previous frame. Which means they will only be meaningful for a single frame, when an animation has just been triggered. Which means you should only use them to define the _start_, _end_, _duration_, or _delay_ values of a timing function, since these values are only evaluated once when the animation starts. -- === Share your animations If you have created an animation script that you think is particularly cool, you are encouraged to share it with the community. You can submit an issue or a pull request to picom on GitHub, and get a chance to have your animation included as one of the presets, so it can be used by everyone. 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`. When `@include` directive is used in the config file, picom will first search for the included file in the parent directory of `picom.conf`, then in `$XDG_CONFIG_HOME/picom/include/`, then in `$XDG_CONFIG_DIRS/picom/include`. picom uses general libconfig configuration file format. A sample configuration file is available as `picom.sample.conf` in the source tree. Most of command line switches can be used as options in configuration file as well. For example, *--vsync* option documented above can be set in the configuration file using `vsync = `. Command line options will always overwrite the settings in the configuration file. Some options can only be set in the configuration file. Such options include `rules` (see xref:_window_rules[*WINDOW RULES*]), `animations` (see xref:_animations[*ANIMATIONS*]), `wintypes` (see below). Window-type-specific settings allow you to set window-specific options based on the window type. These settings are exposed only in configuration file. The format of this option is as follows: [#wintypes] ------------ wintypes: { WINDOW_TYPE = { fade = BOOL; shadow = BOOL; opacity = FLOAT; focus = BOOL; blur-background = BOOL; full-shadow = BOOL; clip-shadow-above = BOOL; redir-ignore = BOOL; }; }; ------------ WARNING: Using this is highly discouraged, see the xref:_window_rules[*WINDOW RULES*] section for the recommended way to set window-specific options. IMPORTANT: According to the window manager specification, a window can have multiple types. But due to the limitation of how _wintypes_ was implemented, if a window has multiple types, then for the purpose of applying `wintypes` options, one of the window types will be chosen at random. Again, you are recommended to use xref:_window_rules[*WINDOW RULES*] instead. _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.) blur-background::: 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 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`. BLUR ---- You can configure how the window background is blurred using a 'blur' section in your configuration file. Here is an example: -------- blur: { method = "gaussian"; size = 10; deviation = 5.0; }; -------- 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 not supported by the legacy backends. (default: none) *size*::: An integer. The size of the blur kernel, required by _gaussian_ and _box_ blur methods. For the _kernel_ method, the size is included in the kernel. Corresponds to the *--blur-size* command line option (default: 3). *deviation*::: A floating point number. The standard deviation for the _gaussian_ blur method. Corresponds to the *--blur-deviation* command line option (default: 0.84089642). *strength*::: 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-kern* option. Corresponds to the *--blur-kern* command line option. SIGNALS ------- * picom reinitializes itself upon receiving `SIGUSR1`. D-BUS API --------- It's possible to control picom via D-Bus messages, by running picom with *--dbus* and send messages to `com.github.chjj.compton.`. `` is the display used by picom, with all non-alphanumeric characters transformed to underscores. For `DISPLAY=:0.0` you should use `com.github.chjj.compton._0_0`, for example. The D-Bus methods and signals are not yet stable, thus undocumented right now. EXAMPLES -------- * Disable configuration file parsing: + ------------ $ picom --config /dev/null ------------ * Run picom with client-side shadow and fading: + ------------ $ 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 -bcf -i 0.8 -e 0.8 --no-fading-openclose ------------ * Draw white shadows: + ------------ $ picom -c --shadow-red 1 --shadow-green 1 --shadow-blue 1 ------------ * Avoid drawing shadows on wbar window: + ------------ $ picom -c --shadow-exclude 'class_g = "wbar"' ------------ * Enable VSync with GLX backend: + ------------ $ picom --backend glx --vsync ------------ BUGS ---- Please submit bug reports to . Out dated information in this man page is considered a bug. RESOURCES --------- Homepage: SEE ALSO -------- *xcompmgr*(1), xref:picom-inspect.1.adoc[*picom-inspect*(1)], xref:picom-trans.1.adoc[*picom-trans*(1)] picom-12.5/media/000077500000000000000000000000001471504570600136405ustar00rootroot00000000000000picom-12.5/media/compton.svg000066400000000000000000000133441471504570600160450ustar00rootroot00000000000000 picom-12.5/media/icons/000077500000000000000000000000001471504570600147535ustar00rootroot00000000000000picom-12.5/media/icons/48x48/000077500000000000000000000000001471504570600155525ustar00rootroot00000000000000picom-12.5/media/icons/48x48/compton.png000066400000000000000000000046171471504570600177470ustar00rootroot00000000000000PNG  IHDR00WtEXtSoftwareAdobe ImageReadyqe< 1IDATxY{lv|g`l\RPʑ*4VHH}RM+AC!(RDTHqZ4X":<|O};}wwmHgvovonjeNxNc TQph\&@Z)Y3譭6mu\+¥`^'!%+ύ> c>"L2 j=M<=>ňAv c'ʛjߩ%%߄ZQT6`ꄈ>K3v_4.70CYloU $;)P#i|xoY.I%ax` T kx/<7FK68 ʀ'px$1J+64"FS"SINÅ'MAv p#>ĉ&p~qGkfsxr8F2Ʃ@bOm$t$7F"q'|֡OdHiIbu;9x^ w[#wC0EHv?Vp!P,k|[‚~ i]&BS@;>@&+#Q^/ĒIx!~,TJGByмā@H `i5ﬨek`.L55I򵖖4v#xk _N'3aro8VzpUeF(zIxnML~ O0N X E&; ,a e ^oXBu>kk^t߉+ڃN;1 + #xʀ3嚠Q';ɑ JNŅQ9Qw]-z sJ iAVk l˗Kvb)tjECbڑ&YTKhF#]!x>8ibbP1Հg@3j3m(ˀ,8982KEܮ3 Tpڵ:2 w̵& #Iads"bt#щ>7'o$!ՖߘkuSGf3bj8%g҉g~6Ĩ th3'aeTԖ`KAriBׇPHMD$4[2Gtn3QWtk2{J9 ۵Dk޳DӟM 㸝hKoA6))YA3KIՖ VQD*x& :kfJ:: ]ItܧlTB+sr_QMFt v{F !c ̛1R+}?c۷ EM U.NJgFhtZ5Ջ`k5u4hpDSPCmí5l;XF2ڟy~cફ熆؍ZuP[Ro}1##Ioo[9g``jm 9Q?\W8ܚel8!9t,ܖsw 18IR< YtV< |/pRp6aT OTUh_\Pƈh `sdx,X6}Z[+ۀ:ȦSkKABDdS0FHᤣi$a+OrF^a/O? sC+IENDB`picom-12.5/meson.build000066400000000000000000000114211471504570600147220ustar00rootroot00000000000000project('picom', 'c', version: '12.5', default_options: ['c_std=c11', 'warning_level=1']) cc = meson.get_compiler('c') # use git describe if that's available git = find_program('git', required: false) if git.found() gitv = run_command('git', 'rev-parse', '--short=7', 'HEAD', check: false) if gitv.returncode() == 0 commit_hash_short = gitv.stdout().strip() endif git_upstream = run_command('git', 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}', check: false) if git_upstream.returncode() == 0 remote = git_upstream.stdout().strip().split('/')[0] else remote = 'origin' endif git_repository = run_command('git', 'remote', 'get-url', remote, check: false) if git_repository.returncode() == 0 repository = git_repository.stdout().strip() endif endif add_global_arguments('-DPICOM_VERSION="v'+meson.project_version()+'"', language: 'c') if is_variable('repository') add_global_arguments('-DPICOM_FULL_VERSION="v'+meson.project_version()+' ('+repository+' revision '+commit_hash_short+')"', language: 'c') elif is_variable('commit_hash_short') add_global_arguments('-DPICOM_FULL_VERSION="v'+meson.project_version()+' (revision '+commit_hash_short+')"', language: 'c') else add_global_arguments('-DPICOM_FULL_VERSION="v'+meson.project_version()+'"', language: 'c') endif if get_option('buildtype') == 'release' add_global_arguments('-DNDEBUG', language: 'c') endif if get_option('sanitize') sanitizers = ['address', 'undefined'] if cc.has_argument('-fsanitize=integer') sanitizers += ['integer'] endif if cc.has_argument('-fsanitize=nullability') sanitizers += ['nullability'] endif add_global_arguments('-fsanitize='+','.join(sanitizers), language: 'c') add_global_link_arguments('-fsanitize='+','.join(sanitizers), language: 'c') if cc.has_argument('-fno-sanitize=unsigned-integer-overflow') add_global_arguments('-fno-sanitize=unsigned-integer-overflow', language: 'c') endif if cc.has_argument('-fno-sanitize=unsigned-shift-base') # uthash does a lot of this add_global_arguments('-fno-sanitize=unsigned-shift-base', language: 'c') endif endif if get_option('llvm_coverage') if cc.get_id() != 'clang' error('option \'llvm_coverage\' requires clang') endif coverage_flags = ['-fprofile-instr-generate', '-fcoverage-mapping'] add_global_arguments(coverage_flags, language: 'c') add_global_link_arguments(coverage_flags, language: 'c') endif if get_option('modularize') if not cc.has_argument('-fmodules') error('option \'modularize\' requires clang') endif add_global_arguments(['-fmodules', '-fmodule-map-file='+ meson.current_source_dir()+ '/src/picom.modulemap'], language: 'c') endif add_global_arguments('-D_GNU_SOURCE', language: 'c') if cc.has_header('stdc-predef.h') add_global_arguments('-DHAS_STDC_PREDEF_H', language: 'c') endif 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') subdir('src') subdir('man') subdir('tools') 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') pkgconf = import('pkgconfig') picom_pc = pkgconf.generate( name: 'picom-api', description: 'picom API headers', requires: [ 'pixman-1', 'xcb' ], subdirs: 'picom', ) if get_option('compton') install_data('compton.desktop', install_dir: 'share/applications') install_data('media/icons/48x48/compton.png', install_dir: 'share/icons/hicolor/48x48/apps') install_data('media/compton.svg', install_dir: 'share/icons/hicolor/scalable/apps') meson.add_install_script('meson/install.sh') endif picom-12.5/meson/000077500000000000000000000000001471504570600137025ustar00rootroot00000000000000picom-12.5/meson/install.sh000077500000000000000000000006601471504570600157110ustar00rootroot00000000000000#!/bin/sh if [ ! -e "${MESON_INSTALL_DESTDIR_PREFIX}/bin/compton" ]; then echo "Linking picom to ${MESON_INSTALL_DESTDIR_PREFIX}/bin/compton" ln -s picom "${MESON_INSTALL_DESTDIR_PREFIX}/bin/compton" fi if [ ! -e "${MESON_INSTALL_DESTDIR_PREFIX}/bin/compton-trans" ]; then echo "Linking picom-trans to ${MESON_INSTALL_DESTDIR_PREFIX}/bin/compton-trans" ln -s picom-trans "${MESON_INSTALL_DESTDIR_PREFIX}/bin/compton-trans" fi picom-12.5/meson_options.txt000066400000000000000000000024021471504570600162140ustar00rootroot00000000000000option('sanitize', type: 'boolean', value: false, description: 'Build with sanitizers enabled (deprecated)') 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') option('opengl', type: 'boolean', value: true, description: 'Enable features that require opengl (opengl backend, and opengl vsync methods)') option('dbus', type: 'boolean', value: true, description: 'Enable support for D-Bus remote control') option('xrescheck', type: 'boolean', value: false, description: 'Enable X resource leak checker (for debug only)') option('compton', type: 'boolean', value: true, description: 'Install backwards compat with compton') option('with_docs', type: 'boolean', value: false, description: 'Build documentation and man pages') option('unittest', type: 'boolean', value: false, description: 'Enable unittests in the code') # Experimental options option('modularize', type: 'boolean', value: false, description: 'Build with clang\'s module system (experimental)') option('llvm_coverage', type: 'boolean', value: false, description: 'Use LLVM source-based code coverage, instead of --coverage. Do not use with b_coverage.') picom-12.5/picom-dbus.desktop000066400000000000000000000004371471504570600162220ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Type=Application NoDisplay=true Name=picom (dbus) GenericName=X compositor (dbus) Comment=An X compositor with dbus backend enabled Categories=Utility; Keywords=compositor;composite manager;window effects;transparency;opacity; TryExec=picom Exec=picom --dbus picom-12.5/picom.desktop000066400000000000000000000005171471504570600152660ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Type=Application NoDisplay=false Name=picom GenericName=X compositor Comment=An X compositor 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 picom-12.5/picom.sample.conf000066400000000000000000000216441471504570600160260ustar00rootroot00000000000000################################# # 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. # # Can be set per-window using rules. # # Default: false shadow = true; # The blur radius for shadows, in pixels. # # Default: 12 shadow-radius = 7; # The opacity of shadows. # # Range: 0.0 - 1.0 # Default: 0.75 # shadow-opacity = .75 # The left offset for shadows, in pixels. # # Default: -15 shadow-offset-x = -7; # The top offset for shadows, in pixels. # # Default: -15 shadow-offset-y = -7; # Hex string color value of shadow. Formatted like "#RRGGBB", e.g. "#C0FFEE". # # Default: #000000 # shadow-color = "#000000" # Crop shadow of a window fully on a particular monitor to that monitor. This is # currently implemented using the X RandR extension. # # Default: false # crop-shadow-to-monitor = false ################################# # Fading # ################################# # Fade windows in/out when opening/closing and when opacity changes, # unless no-fading-openclose is used. Can be set per-window using rules. # # Default: false fading = true; # Opacity change between steps while fading in. (0.01 - 1.0, defaults to 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; # The time between steps in fade step, in milliseconds. (> 0, defaults to 10) # fade-delta = 10 # 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 window titlebars and borders. # # Range: 0.1 - 1.0 # Default: 1.0 (disabled) frame-opacity = 0.7; # Use fixed inactive dim value, instead of adjusting according to window opacity. # # Default: false # inactive-dim-fixed = true ################################# # 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`. # # Default: 0 (disabled) corner-radius = 0 ################################# # Blur # ################################# # Parameters for background blurring, see BLUR section in the man page for more information. # blur-method = # blur-size = 12 # # blur-deviation = false # # blur-strength = 5 # Blur background of semi-transparent / ARGB windows. # Can be set per-window using rules. # # Default: false # blur-background = false # Blur background of windows when the window frame is not opaque. # Implies: # blur-background # # Default: false # blur-background-frame = false # Use fixed blur strength rather than adjusting according to window opacity. # # Default: false # 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"; # Can also be a pre-defined kernel, see the man page. # # Default: "" blur-kern = "3x3box"; ################################# # General Settings # ################################# # Enable remote control via D-Bus. See the man page for more details. # # Default: false # 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 `egl`. # # Default: "xrender" backend = "glx" # Use higher precision during rendering, and apply dither when presenting the # rendered screen. Reduces banding artifacts, but may cause performance # degradation. Only works with OpenGL. dithered-present = false; # Enable/disable VSync. # # Default: false vsync = true; # Try to detect windows with rounded corners and don't consider them # shaped windows. The accuracy is not very high, unfortunately. # # Has nothing to do with `corner-radius`. # # Default: 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. # # Default: false detect-client-opacity = true; # Use EWMH '_NET_ACTIVE_WINDOW' to determine currently focused window, # rather than listening to 'FocusIn'/'FocusOut' event. May be more accurate, # provided that the WM supports it. # # Default: false # 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. # # Default: false # unredir-if-possible = false # Delay before unredirecting the window, in milliseconds. # # Default: 0. # unredir-if-possible-delay = 0 # Use 'WM_TRANSIENT_FOR' to group windows, and consider windows # in the same group focused at the same time. # # Default: 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. # # Default: false # detect-client-leader = false # Use of damage information for rendering. This cause the only the part of the # screen that has actually changed to be redrawn, instead of the whole screen # every time. Should improve performance. # # Default: false use-damage = true; # Use X Sync fence to wait for the completion of rendering of other windows, # before using their content to render the current screen. # # Required for explicit sync drivers, such as nvidia. # # Default: false # xrender-sync-fence = false # GLX backend: Use specified GLSL fragment shader for rendering window # contents. Read the man page for a detailed explanation of the interface. # # Can be set per-window using rules. # # window-shader-fg = "default" # Force all windows to be painted with blending. Useful if you # have a `window-shader-fg` that could turn opaque pixels transparent. # # Default: false # 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. # # Default: false # 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 (disabled) # max-brightness = 1.0 # Make transparent windows clip other windows like non-transparent windows do, # instead of blending on top of them. e.g. placing a transparent window on top # of another window will cut a "hole" in that window, and show the desktop background # underneath. # # Default: false # transparent-clipping = false # Set the log level. Possible values are: # "trace", "debug", "info", "warn", "error" # in increasing level of importance. Case insensitive. # 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. # # Default: "warn" # 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" # Write process ID to a file. # write-pid-path = "/path/to/your/log/file" # Rule-based per-window options. # # See WINDOW RULES section in the man page for how these work. rules: ({ match = "window_type = 'tooltip'"; fade = false; shadow = true; opacity = 0.75; full-shadow = false; }, { match = "window_type = 'dock' || " "window_type = 'desktop' || " "_GTK_FRAME_EXTENTS@"; blur-background = false; }, { match = "window_type != 'dock'"; # shader = "my_shader.frag"; }, { match = "window_type = 'dock' || " "window_type = 'desktop'"; corner-radius = 0; }, { match = "name = 'Notification' || " "class_g = 'Conky' || " "class_g ?= 'Notify-osd' || " "class_g = 'Cairo-clock' || " "_GTK_FRAME_EXTENTS@"; shadow = false; }) # `@include` directive can be used to include additional configuration files. # Relative paths are search either in the parent of this configuration file # (when the configuration is loaded through a symlink, the symlink will be # resolved first). Or in `$XDG_CONFIG_HOME/picom/include`. # # @include "extra.conf" picom-12.5/src/000077500000000000000000000000001471504570600133505ustar00rootroot00000000000000picom-12.5/src/api.c000066400000000000000000000047111471504570600142700ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include "compiler.h" #include "log.h" #include "utils/list.h" #include "utils/misc.h" struct backend_plugins { UT_hash_handle hh; const char *backend_name; struct list_node plugins; } *backend_plugins; struct backend_plugin { const char *backend_name; picom_backend_plugin_entrypoint entrypoint; void *user_data; struct list_node siblings; }; static bool add_backend_plugin(const char *backend_name, uint64_t major, uint64_t minor, picom_backend_plugin_entrypoint entrypoint, void *user_data) { if (major != PICOM_BACKEND_MAJOR || minor > PICOM_BACKEND_MINOR) { log_error("Cannot add plugin for backend %s, because the requested " "version %" PRIu64 ".%" PRIu64 " is incompatible with the our " "%lu.%lu", backend_name, major, minor, PICOM_BACKEND_MAJOR, PICOM_BACKEND_MINOR); return false; } auto plugin = ccalloc(1, struct backend_plugin); plugin->backend_name = backend_name; plugin->entrypoint = entrypoint; plugin->user_data = user_data; struct backend_plugins *plugins = NULL; HASH_FIND_STR(backend_plugins, backend_name, plugins); if (!plugins) { plugins = ccalloc(1, struct backend_plugins); plugins->backend_name = strdup(backend_name); list_init_head(&plugins->plugins); HASH_ADD_STR(backend_plugins, backend_name, plugins); } list_insert_after(&plugins->plugins, &plugin->siblings); return true; } void api_backend_plugins_invoke(const char *backend_name, struct backend_base *backend) { struct backend_plugins *plugins = NULL; HASH_FIND_STR(backend_plugins, backend_name, plugins); if (!plugins) { return; } list_foreach(struct backend_plugin, plugin, &plugins->plugins, siblings) { plugin->entrypoint(backend, plugin->user_data); } } static struct picom_api picom_api = { .add_backend_plugin = add_backend_plugin, }; PICOM_PUBLIC_API const struct picom_api * picom_api_get_interfaces(uint64_t major, uint64_t minor, const char *context) { if (major != PICOM_API_MAJOR || minor > PICOM_API_MINOR) { log_error("Cannot provide API interfaces to %s, because the requested" "version %" PRIu64 ".%" PRIu64 " is incompatible with our " "%lu.%lu", context, major, minor, PICOM_API_MAJOR, PICOM_API_MINOR); return NULL; } return &picom_api; } picom-12.5/src/api_internal.h000066400000000000000000000003771471504570600161750ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui struct backend_base; /// Invoke all backend plugins for the specified backend. void api_backend_plugins_invoke(const char *backend_name, struct backend_base *backend); picom-12.5/src/atom.c000066400000000000000000000117641471504570600144650ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include "atom.h" #include "compiler.h" #include "log.h" #include "utils/cache.h" #include "utils/misc.h" struct atom_entry { struct cache_handle entry; UT_hash_handle hh; xcb_atom_t atom; }; 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(keylen), atom_name), NULL); xcb_atom_t atom = XCB_NONE; if (reply) { log_debug("Atom %.*s is %d", (int)keylen, atom_name, reply->atom); atom = reply->atom; free(reply); } else { log_error("Failed to intern atoms"); 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 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) { 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 picom-12.5/src/atom.h000066400000000000000000000047271471504570600144730ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include "utils/meta.h" // clang-format off // Splitted into 2 lists because of the limitation of our macros #define ATOM_LIST1 \ _NET_WM_WINDOW_OPACITY, \ _NET_FRAME_EXTENTS, \ WM_STATE, \ _NET_WM_NAME, \ _NET_WM_PID, \ WM_NAME, \ WM_CLASS, \ WM_ICON_NAME, \ WM_TRANSIENT_FOR, \ WM_WINDOW_ROLE, \ WM_CLIENT_LEADER, \ WM_CLIENT_MACHINE, \ _NET_ACTIVE_WINDOW, \ _COMPTON_SHADOW, \ COMPTON_VERSION, \ _NET_WM_WINDOW_TYPE, \ _XROOTPMAP_ID, \ ESETROOT_PMAP_ID, \ _XSETROOT_ID #define ATOM_LIST2 \ _NET_WM_WINDOW_TYPE_DESKTOP, \ _NET_WM_WINDOW_TYPE_DOCK, \ _NET_WM_WINDOW_TYPE_TOOLBAR, \ _NET_WM_WINDOW_TYPE_MENU, \ _NET_WM_WINDOW_TYPE_UTILITY, \ _NET_WM_WINDOW_TYPE_SPLASH, \ _NET_WM_WINDOW_TYPE_DIALOG, \ _NET_WM_WINDOW_TYPE_NORMAL, \ _NET_WM_WINDOW_TYPE_DROPDOWN_MENU, \ _NET_WM_WINDOW_TYPE_POPUP_MENU, \ _NET_WM_WINDOW_TYPE_TOOLTIP, \ _NET_WM_WINDOW_TYPE_NOTIFICATION, \ _NET_WM_WINDOW_TYPE_COMBO, \ _NET_WM_WINDOW_TYPE_DND, \ _NET_WM_STATE, \ _NET_WM_STATE_FULLSCREEN, \ _NET_WM_BYPASS_COMPOSITOR, \ UTF8_STRING, \ C_STRING // clang-format on #define ATOM_DEF(x) xcb_atom_t a##x struct atom_entry; struct atom { LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST1); LIST_APPLY(ATOM_DEF, SEP_COLON, ATOM_LIST2); }; /// 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); 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); } 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); picom-12.5/src/backend/000077500000000000000000000000001471504570600147375ustar00rootroot00000000000000picom-12.5/src/backend/backend.c000066400000000000000000000125071471504570600164770ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "region.h" #include "renderer/layout.h" #include "wm/win.h" #include "x.h" #include "backend.h" static struct backend_info { UT_hash_handle hh; const char *name; struct backend_base *(*init)(session_t *ps, xcb_window_t target); bool can_present; } *backend_registry = NULL; PICOM_PUBLIC_API bool backend_register(uint64_t major, uint64_t minor, const char *name, struct backend_base *(*init)(session_t *ps, xcb_window_t target), bool can_present) { if (major != PICOM_BACKEND_MAJOR) { log_error("Backend %s has incompatible major version %" PRIu64 ", expected %lu", name, major, PICOM_BACKEND_MAJOR); return false; } if (minor > PICOM_BACKEND_MINOR) { log_error("Backend %s has incompatible minor version %" PRIu64 ", expected %lu", name, minor, PICOM_BACKEND_MINOR); return false; } struct backend_info *info = NULL; HASH_FIND_STR(backend_registry, name, info); if (info) { log_error("Backend %s is already registered", name); return false; } info = cmalloc(struct backend_info); info->name = name; info->init = init; info->can_present = can_present; HASH_ADD_KEYPTR(hh, backend_registry, info->name, strlen(info->name), info); return true; } struct backend_info *backend_find(const char *name) { struct backend_info *info = NULL; HASH_FIND_STR(backend_registry, name, info); return info; } struct backend_base * backend_init(struct backend_info *info, session_t *ps, xcb_window_t target) { return info->init(ps, target); } struct backend_info *backend_iter(void) { return backend_registry; } struct backend_info *backend_iter_next(struct backend_info *info) { return info->hh.next; } const char *backend_name(struct backend_info *info) { return info->name; } bool backend_can_present(struct backend_info *info) { return info->can_present; } /// 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; } if (cmd->blit.opacity < 1. / MAX_ALPHA) { continue; } 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; } 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; } succeeded = backend->ops.blur(backend, cmd->origin, target, &cmd->blur); break; case BACKEND_COMMAND_INVALID: default: assert(false); } } return succeeded; } 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_WINDOW_SAVED: return "window_saved"; case BACKEND_COMMAND_SOURCE_SHADOW: return "shadow"; case BACKEND_COMMAND_SOURCE_BACKGROUND: return "background"; } unreachable(); } void log_backend_command_(enum log_level level, const char *func, const struct backend_command *cmd) { if (level < log_get_level_tls()) { return; } 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; } } // vim: set noet sw=8 ts=8 : picom-12.5/src/backend/backend.h000066400000000000000000000023111471504570600164740ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018, Yuxuan Shui #pragma once #include #include #include "log.h" bool backend_execute(struct backend_base *backend, image_handle target, unsigned ncmds, const struct backend_command cmds[ncmds]); struct backend_info *backend_find(const char *name); struct backend_base * backend_init(struct backend_info *info, session_t *ps, xcb_window_t target); struct backend_info *backend_iter(void); struct backend_info *backend_iter_next(struct backend_info *info); const char *backend_name(struct backend_info *info); bool backend_can_present(struct backend_info *info); 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)); /// Define a backend entry point. (Note constructor priority 202 is used here because 1xx /// is reversed by test.h, and 201 is used for logging initialization.) #define BACKEND_ENTRYPOINT(func) static void __attribute__((constructor(202))) func(void) picom-12.5/src/backend/backend_common.c000066400000000000000000000335401471504570600200470ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include #include "common.h" #include "config.h" #include "log.h" #include "utils/kernel.h" #include "utils/misc.h" #include "wm/win.h" #include "x.h" #include "backend_common.h" /** * Generate a 1x1 Picture of a particular color. */ 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, 1, 1); if (!pixmap) { return XCB_NONE; } pa.repeat = 1; picture = x_create_picture_with_standard_and_pixmap( c, argb ? XCB_PICT_STANDARD_ARGB_32 : XCB_PICT_STANDARD_A_8, pixmap, XCB_RENDER_CP_REPEAT, &pa); if (!picture) { xcb_free_pixmap(c->c, pixmap); return XCB_NONE; } col.alpha = (uint16_t)(a * 0xffff); col.red = (uint16_t)(r * 0xffff); col.green = (uint16_t)(g * 0xffff); col.blue = (uint16_t)(b * 0xffff); rect.x = 0; rect.y = 0; rect.width = 1; rect.height = 1; 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(struct x_connection *c, const conv *kernel, double opacity, int width, int height) { /* * We classify shadows into 4 kinds of regions * r = shadow radius * (0, 0) is the top left of the window itself * -r r width-r width+r * -r +-----+---------+-----+ * | 1 | 2 | 1 | * r +-----+---------+-----+ * | 2 | 3 | 2 | * height-r +-----+---------+-----+ * | 1 | 2 | 1 | * height+r +-----+---------+-----+ */ xcb_image_t *ximage; const double *shadow_sum = kernel->rsum; assert(shadow_sum); // We only support square kernels for shadow assert(kernel->w == kernel->h); int d = kernel->w; int r = d / 2; int swidth = width + r * 2, sheight = height + r * 2; assert(d % 2 == 1); assert(d > 0); 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 long sstride = ximage->stride; // If the window body is smaller than the kernel, we do convolution directly if (width < r * 2 && height < r * 2) { for (int y = 0; y < sheight; y++) { 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 * opacity); } } return ximage; } if (height < r * 2) { // Implies width >= r * 2 // If the window height is smaller than the kernel, we divide // the window like this: // -r r width-r width+r // +------+-------------+------+ // | | | | // +------+-------------+------+ for (int y = 0; y < sheight; y++) { for (int x = 0; x < r * 2; x++) { double sum = sum_kernel_normalized(kernel, d - x - 1, d - y - 1, d, height) * 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 * opacity; memset(&data[y * sstride + r * 2], (uint8_t)sum, (size_t)(width - 2 * r)); } return ximage; } if (width < r * 2) { // Similarly, for width smaller than kernel for (int y = 0; y < r * 2; y++) { for (int x = 0; x < swidth; x++) { double sum = sum_kernel_normalized(kernel, d - x - 1, d - y - 1, width, d) * 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 * opacity; for (int y = r * 2; y < height; y++) { data[y * sstride + x] = (uint8_t)sum; } } return ximage; } // Implies: width >= r * 2 && height >= r * 2 // Fill part 3 for (int y = r; y < height + r; y++) { memset(data + sstride * y + r, (uint8_t)(255 * opacity), (size_t)width); } // Part 1 for (int y = 0; y < r * 2; y++) { for (int x = 0; x < r * 2; x++) { double tmpsum = shadow_sum[y * d + x] * opacity * 255.0; data[y * sstride + x] = (uint8_t)tmpsum; data[(sheight - y - 1) * sstride + x] = (uint8_t)tmpsum; data[(sheight - y - 1) * sstride + (swidth - x - 1)] = (uint8_t)tmpsum; data[y * sstride + (swidth - x - 1)] = (uint8_t)tmpsum; } } // Part 2, top/bottom for (int y = 0; y < r * 2; y++) { double tmpsum = shadow_sum[d * y + d - 1] * opacity * 255.0; memset(&data[y * sstride + r * 2], (uint8_t)tmpsum, (size_t)(width - r * 2)); memset(&data[(sheight - y - 1) * sstride + r * 2], (uint8_t)tmpsum, (size_t)(width - r * 2)); } // Part 2, left/right for (int x = 0; x < r * 2; x++) { double tmpsum = shadow_sum[d * (d - 1) + x] * opacity * 255.0; for (int y = r * 2; y < height; y++) { data[y * sstride + x] = (uint8_t)tmpsum; data[y * sstride + (swidth - x - 1)] = (uint8_t)tmpsum; } } return ximage; } /** * Generate shadow Picture for a window. */ 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_image_t *shadow_image = NULL; xcb_pixmap_t shadow_pixmap = XCB_NONE, shadow_pixmap_argb = XCB_NONE; xcb_render_picture_t shadow_picture = XCB_NONE, shadow_picture_argb = XCB_NONE; xcb_gcontext_t gc = XCB_NONE; shadow_image = make_shadow(c, kernel, opacity, width, height); if (!shadow_image) { log_error("Failed to make shadow"); return false; } shadow_pixmap = x_create_pixmap(c, 8, shadow_image->width, shadow_image->height); shadow_pixmap_argb = x_create_pixmap(c, 32, shadow_image->width, shadow_image->height); if (!shadow_pixmap || !shadow_pixmap_argb) { log_error("Failed to create shadow pixmaps"); goto shadow_picture_err; } shadow_picture = x_create_picture_with_standard_and_pixmap( c, XCB_PICT_STANDARD_A_8, shadow_pixmap, 0, NULL); shadow_picture_argb = x_create_picture_with_standard_and_pixmap( 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(c); 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->c) * 4 - 1024; auto maximum_row = to_u16_checked(clamp(maximum_image_size / shadow_image->stride, 0, UINT16_MAX)); if (maximum_row <= 0) { // TODO(yshui) Upload image with XShm log_error("X server request size limit is too restrictive, or the shadow " "image is too wide for us to send a single row of the shadow " "image. Shadow size: %dx%d", width, height); goto shadow_picture_err; } for (uint32_t row = 0; row < shadow_image->height; row += maximum_row) { auto batch_height = maximum_row; if (batch_height > shadow_image->height - row) { batch_height = to_u16_checked(shadow_image->height - row); } auto offset = (size_t)row * shadow_image->stride / sizeof(*shadow_image->data); 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->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; xcb_free_gc(c->c, gc); xcb_image_destroy(shadow_image); xcb_free_pixmap(c->c, shadow_pixmap); x_free_picture(c, shadow_picture); x_free_picture(c, shadow_picture_argb); return true; shadow_picture_err: if (shadow_image) { xcb_image_destroy(shadow_image); } if (shadow_pixmap) { xcb_free_pixmap(c->c, shadow_pixmap); } if (shadow_pixmap_argb) { xcb_free_pixmap(c->c, shadow_pixmap_argb); } if (shadow_picture) { x_free_picture(c, shadow_picture); } if (shadow_picture_argb) { x_free_picture(c, shadow_picture_argb); } if (gc) { xcb_free_gc(c->c, gc); } return false; } static struct conv **generate_box_blur_kernel(struct box_blur_args *args, int *kernel_count) { int r = args->size * 2 + 1; assert(r > 0); auto ret = ccalloc(2, struct conv *); ret[0] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); ret[1] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); ret[0]->w = r; ret[0]->h = 1; ret[1]->w = 1; ret[1]->h = r; for (int i = 0; i < r; i++) { ret[0]->data[i] = 1; ret[1]->data[i] = 1; } *kernel_count = 2; return ret; } static struct conv ** generate_gaussian_blur_kernel(struct gaussian_blur_args *args, int *kernel_count) { int r = args->size * 2 + 1; assert(r > 0); auto ret = ccalloc(2, struct conv *); ret[0] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); ret[1] = cvalloc(sizeof(struct conv) + sizeof(double) * (size_t)r); ret[0]->w = r; ret[0]->h = 1; ret[1]->w = 1; ret[1]->h = r; for (int i = 0; i <= args->size; i++) { ret[0]->data[i] = ret[0]->data[r - i - 1] = 1.0 / (sqrt(2.0 * M_PI) * args->deviation) * exp(-(args->size - i) * (args->size - i) / (2 * args->deviation * args->deviation)); ret[1]->data[i] = ret[1]->data[r - i - 1] = ret[0]->data[i]; } *kernel_count = 2; return ret; } /// Generate blur kernels for gaussian and box blur methods. Generated kernel is not /// normalized, and the center element will always be 1. struct conv **generate_blur_kernel(enum blur_method method, void *args, int *kernel_count) { switch (method) { case BLUR_METHOD_BOX: return generate_box_blur_kernel(args, kernel_count); case BLUR_METHOD_GAUSSIAN: return generate_gaussian_blur_kernel(args, kernel_count); default: break; } return NULL; } /// Generate kernel parameters for dual-kawase blur method. Falls back on approximating /// standard gauss radius if strength is zero or below. struct dual_kawase_params *generate_dual_kawase_params(void *args) { struct dual_kawase_blur_args *blur_args = args; static const struct { int iterations; /// Number of down- and upsample iterations float offset; /// Sample offset in half-pixels 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 }; auto params = ccalloc(1, struct dual_kawase_params); params->iterations = 0; params->offset = 1.0F; if (blur_args->strength <= 0 && blur_args->size) { // find highest level that approximates blur-strength with the selected // gaussian blur-radius int lvl = 1; while (strength_levels[lvl - 1].min_radius < blur_args->size && lvl < 20) { ++lvl; } blur_args->strength = lvl; } if (blur_args->strength <= 0) { // default value blur_args->strength = 5; } assert(blur_args->strength > 0 && blur_args->strength <= 20); params->iterations = strength_levels[blur_args->strength - 1].iterations; params->offset = strength_levels[blur_args->strength - 1].offset; // Expand sample area to cover the smallest texture / highest selected iteration: // - 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)ceilf(params->offset) + 1; return params; } void init_backend_base(struct backend_base *base, session_t *ps) { base->c = &ps->c; base->loop = ps->loop; base->busy = false; base->ops = (struct backend_operations){}; } uint32_t backend_no_quirks(struct backend_base *base attr_unused) { return 0; } picom-12.5/src/backend/backend_common.h000066400000000000000000000023371471504570600200540ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include "config.h" struct session; struct win; struct conv; struct backend_base; struct backend_operations; struct x_connection; struct dual_kawase_params { /// Number of downsample passes int iterations; /// Pixel offset for down- and upsample float offset; /// Save area around blur target (@ref resize_width, @ref resize_height) int expand; }; 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 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); uint32_t backend_no_quirks(struct backend_base *base attr_unused); picom-12.5/src/backend/driver.c000066400000000000000000000052721471504570600164040ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include "common.h" #include "compiler.h" #include "log.h" #include "driver.h" /// 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) { 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 // There's no way to query the X server about what driver is loaded, so RANDR is // our best shot. auto randr_version = xcb_randr_query_version_reply( c, xcb_randr_query_version(c, XCB_RANDR_MAJOR_VERSION, XCB_RANDR_MINOR_VERSION), NULL); if (randr_version && (randr_version->major_version > 1 || randr_version->minor_version >= 4)) { auto r = xcb_randr_get_providers_reply( c, xcb_randr_get_providers(c, window), NULL); if (r == NULL) { log_warn("Failed to get RANDR providers"); free(randr_version); return 0; } auto providers = xcb_randr_get_providers_providers(r); for (auto i = 0; i < xcb_randr_get_providers_providers_length(r); i++) { auto r2 = xcb_randr_get_provider_info_reply( c, xcb_randr_get_provider_info(c, providers[i], r->timestamp), NULL); if (r2 == NULL) { continue; } if (r2->num_outputs == 0) { free(r2); continue; } auto name_len = xcb_randr_get_provider_info_name_length(r2); assert(name_len >= 0); auto name = strndup(xcb_randr_get_provider_info_name(r2), (size_t)name_len); if (strcasestr(name, "modesetting") != NULL) { ret |= DRIVER_MODESETTING; } else if (strcasestr(name, "Radeon") != NULL) { // Be conservative, add both radeon drivers ret |= DRIVER_AMDGPU | DRIVER_RADEON; } else if (strcasestr(name, "NVIDIA") != NULL) { ret |= DRIVER_NVIDIA; } else if (strcasestr(name, "nouveau") != NULL) { ret |= DRIVER_NOUVEAU; } else if (strcasestr(name, "Intel") != NULL) { ret |= DRIVER_INTEL; } free(name); free(r2); } free(r); } free(randr_version); // If the backend supports driver detection, use that as well if (backend_data && backend_data->ops.detect_driver) { ret |= backend_data->ops.detect_driver(backend_data); } return ret; } picom-12.5/src/backend/driver.h000066400000000000000000000040461471504570600164070ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include "config.h" #include "utils/misc.h" struct session; struct backend_base; // A list of known driver quirks: // * NVIDIA driver doesn't like seeing the same pixmap under different // ids, so avoid naming the pixmap again when it didn't actually change. /// A list of possible drivers. /// The driver situation is a bit complicated. There are two drivers we care about: the /// DDX, and the OpenGL driver. They are usually paired, but not always, since there is /// also the generic modesetting driver. /// This enum represents _both_ drivers. enum driver { DRIVER_AMDGPU = 1, // AMDGPU for DDX, radeonsi for OpenGL DRIVER_RADEON = 2, // ATI for DDX, mesa r600 for OpenGL DRIVER_FGLRX = 4, DRIVER_NVIDIA = 8, DRIVER_NOUVEAU = 16, DRIVER_INTEL = 32, DRIVER_MODESETTING = 64, }; static const char *driver_names[] = { "AMDGPU", "Radeon", "fglrx", "NVIDIA", "nouveau", "Intel", "modesetting", }; /// Return a list of all drivers currently in use by the X server. /// Note, this is a best-effort test, so no guarantee all drivers will be detected. enum driver detect_driver(xcb_connection_t *, struct backend_base *, xcb_window_t); /// 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)) { seen_drivers[driver_count++] = driver_names[i]; } } if (driver_count > 0) { printf("%s", seen_drivers[0]); for (int i = 1; i < driver_count; i++) { printf(", %s", seen_drivers[i]); } } printf("\n"); } picom-12.5/src/backend/dummy/000077500000000000000000000000001471504570600160725ustar00rootroot00000000000000picom-12.5/src/backend/dummy/dummy.c000066400000000000000000000165121471504570600173760ustar00rootroot00000000000000#include #include #include #include "backend/backend.h" #include "backend/backend_common.h" #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "region.h" #include "utils/misc.h" #include "utils/uthash_extra.h" #include "x.h" struct dummy_image { enum backend_image_format format; xcb_pixmap_t pixmap; struct list_node siblings; UT_hash_handle hh; }; struct dummy_data { struct backend_base base; struct dummy_image *pixmap_images; struct list_node non_pixmap_images; struct dummy_image back_buffer; }; const 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->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, 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->pixmap_images, &img->pixmap, tmp); if (!tmp) { log_warn("Using an invalid (possibly freed) image"); assert(false); } } 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 *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; } 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->pixmap_images, &pixmap, img); if (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; HASH_ADD_INT(dummy->pixmap_images, pixmap, img); return (image_handle)img; } xcb_pixmap_t dummy_release_image(backend_t *base, image_handle image) { auto dummy = (struct dummy_data *)base; if ((struct dummy_image *)image == &dummy->back_buffer) { return XCB_NONE; } auto img = (struct dummy_image *)image; 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_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_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; } 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, enum backend_image_format format attr_unused, void *args attr_unused) { static int dummy_context; return &dummy_context; } void dummy_destroy_blur_context(struct backend_base *base attr_unused, void *ctx attr_unused) { } void dummy_get_blur_size(void *ctx attr_unused, int *width, int *height) { // 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; } static int dummy_max_buffer_age(struct backend_base *base attr_unused) { return 5; } #define PICOM_BACKEND_DUMMY_MAJOR (0UL) #define PICOM_BACKEND_DUMMY_MINOR (1UL) static void dummy_version(struct backend_base * /*base*/, uint64_t *major, uint64_t *minor) { *major = PICOM_BACKEND_DUMMY_MAJOR; *minor = PICOM_BACKEND_DUMMY_MINOR; } const struct backend_operations dummy_ops = { .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, .quirks = backend_no_quirks, .version = dummy_version, .release_image = dummy_release_image, .init = dummy_init, .deinit = dummy_deinit, .buffer_age = dummy_buffer_age, .max_buffer_age = dummy_max_buffer_age, .create_blur_context = dummy_create_blur_context, .destroy_blur_context = dummy_destroy_blur_context, .get_blur_size = dummy_get_blur_size, }; BACKEND_ENTRYPOINT(dummy_register) { if (!backend_register(PICOM_BACKEND_MAJOR, PICOM_BACKEND_MINOR, "dummy", dummy_ops.init, false)) { log_error("Failed to register dummy backend"); } } picom-12.5/src/backend/gl/000077500000000000000000000000001471504570600153415ustar00rootroot00000000000000picom-12.5/src/backend/gl/blur.c000066400000000000000000000710521471504570600164560ustar00rootroot00000000000000#include #include #include "backend/backend_common.h" #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, GLfloat); auto indices = ccalloc(nrects * 6, GLuint); gl_mask_rects_to_coords(origin, nrects, rects, SCALE_IDENTITY, 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, GLfloat); auto indices_resized = ccalloc(nrects_resized * 6, GLuint); gl_mask_rects_to_coords(origin, nrects_resized, rects_resized, SCALE_IDENTITY, 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); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBindVertexArray(gd->vertex_array_objects[0]); glBindBuffer(GL_ARRAY_BUFFER, gd->buffer_objects[0]); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gd->buffer_objects[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_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, NULL); glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, (void *)(sizeof(GLfloat) * 2)); glBindVertexArray(gd->vertex_array_objects[1]); glBindBuffer(GL_ARRAY_BUFFER, gd->buffer_objects[2]); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gd->buffer_objects[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_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, NULL); glVertexAttribPointer(vert_in_texcoord_loc, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 4, (void *)(sizeof(GLfloat) * 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, gd->vertex_array_objects, 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, gd->vertex_array_objects, 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); // Invalidate buffer data glBindBuffer(GL_ARRAY_BUFFER, gd->buffer_objects[0]); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gd->buffer_objects[1]); glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * nrects * 16, NULL, GL_STREAM_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * nrects * 6, NULL, GL_STREAM_DRAW); glBindBuffer(GL_ARRAY_BUFFER, gd->buffer_objects[2]); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gd->buffer_objects[3]); glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord_resized) * nrects_resized * 16, NULL, GL_STREAM_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices_resized) * nrects_resized * 6, NULL, GL_STREAM_DRAW); // Cleanup vertex array states glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glBindVertexArray(0); 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; } picom-12.5/src/backend/gl/egl.c000066400000000000000000000261511471504570600162610ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 /* * Copyright (c) 2022 Yuxuan Shui */ #include #include #include #include #include #include "backend/backend.h" #include "backend/backend_common.h" #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "picom.h" #include "utils/misc.h" #include "x.h" #include "egl.h" #include "gl_common.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; // Destroy EGL context if (gd->ctx != EGL_NO_CONTEXT) { gl_deinit(&gd->gl); 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); } const 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; } log_warn("The egl backend is still experimental, use with care."); 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_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_RENDERABLE_TYPE, EGL_OPENGL_BIT, 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"); } } static int egl_max_buffer_age(backend_t *base attr_unused) { if (!eglext.has_EGL_EXT_buffer_age) { return 0; } return 5; // Why? } #define PICOM_BACKEND_EGL_MAJOR (0UL) #define PICOM_BACKEND_EGL_MINOR (1UL) static void egl_version(struct backend_base * /*base*/, uint64_t *major, uint64_t *minor) { *major = PICOM_BACKEND_EGL_MAJOR; *minor = PICOM_BACKEND_EGL_MINOR; } const 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, .version = egl_version, .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 = egl_max_buffer_age, }; 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 } BACKEND_ENTRYPOINT(egl_register) { if (!backend_register(PICOM_BACKEND_MAJOR, PICOM_BACKEND_MINOR, "egl", egl_ops.init, true)) { log_error("Failed to register egl backend"); } } picom-12.5/src/backend/gl/egl.h000066400000000000000000000007131471504570600162620ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include #include 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); picom-12.5/src/backend/gl/gl_common.c000066400000000000000000001153631471504570600174700ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include #include #include #include // for xcb_render_fixed_t, XXX #include #include "backend/backend_common.h" #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "region.h" #include "utils/misc.h" #include "gl_common.h" 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) { log_trace("===\n%s\n===", shader_str); bool success = false; GLuint shader = glCreateShader(shader_type); if (!shader) { log_error("Failed to create shader with type %#x.", shader_type); goto end; } glShaderSource(shader, 1, &shader_str, NULL); glCompileShader(shader); // Get shader status { GLint status = GL_FALSE; glGetShaderiv(shader, GL_COMPILE_STATUS, &status); if (status == GL_FALSE) { GLint log_len = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_len); if (log_len) { char log[log_len + 1]; glGetShaderInfoLog(shader, log_len, NULL, log); log_error("Failed to compile shader with type %d: %s", shader_type, log); } goto end; } } success = true; end: if (shader && !success) { glDeleteShader(shader); shader = 0; } gl_check_err(); return shader; } GLuint gl_create_program(const GLuint *const shaders, int nshaders) { bool success = false; GLuint program = glCreateProgram(); if (!program) { log_error("Failed to create program."); goto end; } 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 (status == GL_FALSE) { GLint log_len = 0; glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_len); if (log_len) { char log[log_len + 1]; glGetProgramInfoLog(program, log_len, NULL, log); log_error("Failed to link program: %s", log); } goto end; } } success = true; end: if (program) { 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 NULL-terminated arrays of vertex and fragment shader * strings. */ 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 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; } } 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; } } 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; } /** * @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 (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); } /* * @brief Implements recursive part of gl_average_texture_color. * * @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 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 * texture in following render iteration. * Otherwise we simply will switch source and destination textures * between each other on each render iteration. */ static GLuint _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; const int from_width = next_power_of_two(width); const int from_height = next_power_of_two(height); const int to_width = from_width > max_width ? from_width / 2 : from_width; const int to_height = from_height > max_height ? from_height / 2 : from_height; // Prepare coordinates GLint coord[] = { // top left 0, 0, // vertex coord 0, 0, // texture coord // top right to_width, 0, // vertex coord width, 0, // texture coord // bottom right to_width, to_height, // vertex coord width, height, // texture coord // bottom left 0, to_height, // vertex coord 0, height, // texture coord }; glBufferSubData(GL_ARRAY_BUFFER, 0, (long)sizeof(*coord) * 16, coord); // Prepare framebuffer for new render iteration 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); // Have we downscaled enough? GLuint result; if (to_width > max_width || to_height > max_height) { GLuint new_source_texture = destination_texture; GLuint new_destination_texture = auxiliary_texture != 0 ? auxiliary_texture : source_texture; result = _gl_average_texture_color( new_source_texture, new_destination_texture, 0, fbo, to_width, to_height); } else { result = destination_texture; } return result; } /* * @brief Builds a 1x1 texture which has color corresponding to the average of all * 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(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(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, 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, img->width, img->height, 0, GL_BGR, GL_UNSIGNED_BYTE, NULL); } } // Prepare framebuffer used for rendering and bind it glBindFramebuffer(GL_DRAW_FRAMEBUFFER, gd->temp_fbo); glDrawBuffer(GL_COLOR_ATTACHMENT0); // Enable shaders glUseProgram(gd->brightness_shader.prog); glUniform2f(UNIFORM_TEXSIZE_LOC, (GLfloat)img->width, (GLfloat)img->height); // Prepare vertex attributes glBindVertexArray(gd->vertex_array_objects[0]); glBindBuffer(GL_ARRAY_BUFFER, gd->buffer_objects[0]); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gd->buffer_objects[1]); 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)); // 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_STREAM_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * 6, indices, GL_STREAM_DRAW); // Do actual recursive render to 1x1 texture GLuint result_texture = _gl_average_texture_color( img->texture, img->auxiliary_texture[0], img->auxiliary_texture[1], gd->temp_fbo, img->width, img->height); // Cleanup vertex attributes glDisableVertexAttribArray(vert_coord_loc); glDisableVertexAttribArray(vert_in_texcoord_loc); // Invalidate buffer data glBufferData(GL_ARRAY_BUFFER, (long)sizeof(*coord) * 16, NULL, GL_STREAM_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * 6, NULL, GL_STREAM_DRAW); // Cleanup buffers glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glBindVertexArray(0); // Cleanup shaders glUseProgram(0); // Cleanup framebuffers glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glDrawBuffer(GL_BACK); // Cleanup render textures glBindTexture(GL_TEXTURE_2D, 0); gl_check_err(); 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(GLfloat) * 4, .count = 2, .attribs = {{GL_FLOAT, vert_coord_loc, NULL}, {GL_FLOAT, vert_in_texcoord_loc, ((GLfloat *)NULL) + 2}}, }; /** * Render a region with texture data. * * @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_blit_inner(struct gl_data *gd, GLuint target_fbo, int nrects, GLfloat *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; } 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); glBindVertexArray(gd->vertex_array_objects[0]); glBindBuffer(GL_ARRAY_BUFFER, gd->buffer_objects[0]); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gd->buffer_objects[1]); 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_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); } glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target_fbo); glDrawElements(GL_TRIANGLES, nrects * 6, GL_UNSIGNED_INT, NULL); glDisableVertexAttribArray(vert_coord_loc); glDisableVertexAttribArray(vert_in_texcoord_loc); // Invalidate buffer data glBufferData(GL_ARRAY_BUFFER, vert_attribs->stride * nrects * 4, NULL, GL_STREAM_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, (long)sizeof(*indices) * nrects * 6, NULL, GL_STREAM_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); glBindVertexArray(0); // Cleanup 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); glUseProgram(0); gl_check_err(); } void gl_mask_rects_to_coords(ivec2 origin, int nrects, const rect_t *rects, vec2 scale, GLfloat *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]; // clang-format off memcpy(&coord[i * 16], ((GLfloat[][2]){ // Interleaved vertex and texture coordinates, starting with vertex. {(GLfloat)rect_dst.x1, (GLfloat)rect_dst.y1}, // bottom-left {(GLfloat)(rect_src.x1 / scale.x), (GLfloat)(rect_src.y1 / scale.y)}, {(GLfloat)rect_dst.x2, (GLfloat)rect_dst.y1}, // bottom-right {(GLfloat)(rect_src.x2 / scale.x), (GLfloat)(rect_src.y1 / scale.y)}, {(GLfloat)rect_dst.x2, (GLfloat)rect_dst.y2}, // top-right {(GLfloat)(rect_src.x2 / scale.x), (GLfloat)(rect_src.y2 / scale.y)}, {(GLfloat)rect_dst.x1, (GLfloat)rect_dst.y2}, // top-left {(GLfloat)(rect_src.x1 / scale.x), (GLfloat)(rect_src.y2 / scale.y)}, }), sizeof(GLint[2]) * 8); // clang-format on 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); } } /// 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, GLfloat *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] = (GLfloat)texture_height - current_vertex[1]; } } } /// 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, GLfloat **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(args->target_mask, &nrects); if (!nrects) { // Nothing to paint return 0; } *coord = ccalloc(nrects * 16, GLfloat); *indices = ccalloc(nrects * 6, GLuint); gl_mask_rects_to_coords(origin, nrects, rects, args->scale, *coord, *indices); if (!img->y_inverted) { gl_y_flip_texture(nrects, *coord, img->height); } 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 auto tex_sampler = vec2_eq(args->scale, SCALE_IDENTITY) ? gd->samplers[GL_SAMPLER_REPEAT] : gd->samplers[GL_SAMPLER_REPEAT_SCALE]; 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, tex_sampler}}, [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 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; } 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_; if (source == &gd->back_image) { log_error("Trying to blit from the back texture, this is not allowed"); return false; } GLfloat *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); } 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(gd, fbo, nrects, coord, indices, &gl_blit_vertex_attribs, shader, NUMBER_OF_UNIFORMS, uniforms); free(indices); free(coord); return true; } /// 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); 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; } /// 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 nrects; const rect_t *rects = pixman_region32_rectangles(region, &nrects); if (nrects == 0) { return true; } auto coord = ccalloc(16 * nrects, GLfloat); auto indices = ccalloc(6 * nrects, GLuint); gl_mask_rects_to_coords(origin, nrects, rects, SCALE_IDENTITY, 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(gd, fbo, nrects, coord, indices, &gl_blit_vertex_attribs, shader, ARR_SIZE(uniforms), uniforms); free(indices); free(coord); return true; } 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); } 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); } 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; } if (inner->user_data) { // user_data indicates that the image is a bound X pixmap return BACKEND_IMAGE_CAP_SRC; } return BACKEND_IMAGE_CAP_SRC | BACKEND_IMAGE_CAP_DST; } 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 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_strv(vshader_strv, fshader_strv); if (!ret->prog) { log_error("Failed to create GLSL program."); gl_check_err(); return false; } 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. */ void gl_resize(struct gl_data *gd, int width, int height) { GLint viewport_dimensions[2]; glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); gd->back_image.height = height; gd->back_image.width = width; assert(viewport_dimensions[0] >= gd->back_image.width); assert(viewport_dimensions[1] >= gd->back_image.height); gl_check_err(); } 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; } 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); glDeleteTextures(1, &inner->texture); glDeleteTextures(2, inner->auxiliary_texture); free(inner); gl_check_err(); return pixmap; } 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; } } 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}; if (!gl_shader_from_stringv(vert, frag, out_shader)) { return false; } 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 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}}; 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 true; } 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; } 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; } return ret; } static const struct { GLint filter; GLint wrap; } gl_sampler_params[] = { [GL_SAMPLER_REPEAT] = {GL_NEAREST, GL_REPEAT}, [GL_SAMPLER_REPEAT_SCALE] = {GL_LINEAR, GL_REPEAT}, [GL_SAMPLER_BLUR] = {GL_LINEAR, GL_CLAMP_TO_EDGE}, [GL_SAMPLER_EDGE] = {GL_NEAREST, GL_CLAMP_TO_EDGE}, [GL_SAMPLER_BORDER] = {GL_NEAREST, GL_CLAMP_TO_BORDER}, }; bool gl_init(struct gl_data *gd, session_t *ps) { 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; glGenBuffers(4, gd->buffer_objects); glGenVertexArrays(2, gd->vertex_array_objects); // Initialize GL data structure glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); glEnable(GL_BLEND); // Initialize stencil buffer glDisable(GL_STENCIL_TEST); glStencilMask(0x1); glStencilFunc(GL_EQUAL, 0x1, 0x1); // Set gl viewport to the maximum supported size so we won't have to worry about // it later on when the screen is resized. The corresponding projection matrix can // be set now and won't have to be updated. Since fragments outside the target // buffer are skipped anyways, this should have no impact on performance. GLint viewport_dimensions[2]; glGetIntegerv(GL_MAX_VIEWPORT_DIMS, viewport_dimensions); glViewport(0, 0, viewport_dimensions[0], viewport_dimensions[1]); // Clear screen glClearColor(0.0F, 0.0F, 0.0F, 1.0F); glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 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->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}, {0, 0, 0, 0}, {-1, -1, 0, 1}}; gd->fill_shader.prog = gl_create_program_from_str(fill_vert, fill_frag); gd->fill_shader.uniform_bitmask = (uint32_t)-1; // make sure our uniforms // are not ignored. glUseProgram(gd->fill_shader.prog); glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection_matrix[0]); glUseProgram(0); 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; } 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); if (!gd->brightness_shader.prog) { log_error("Failed to create the brightness shader"); return false; } glUseProgram(gd->brightness_shader.prog); glUniform1i(UNIFORM_TEX_LOC, 0); glUniformMatrix4fv(UNIFORM_PROJECTION_LOC, 1, false, projection_matrix[0]); glUseProgram(0); gd->back_image.width = ps->root_width; gd->back_image.height = ps->root_height; glGenSamplers(GL_MAX_SAMPLERS, gd->samplers); for (size_t i = 0; i < ARR_SIZE(gl_sampler_params); i++) { glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MIN_FILTER, gl_sampler_params[i].filter); glSamplerParameteri(gd->samplers[i], GL_TEXTURE_MAG_FILTER, gl_sampler_params[i].filter); glSamplerParameteri(gd->samplers[i], GL_TEXTURE_WRAP_S, gl_sampler_params[i].wrap); glSamplerParameteri(gd->samplers[i], GL_TEXTURE_WRAP_T, gl_sampler_params[i].wrap); } gd->logger = gl_string_marker_logger_new(); if (gd->logger) { log_add_target_tls(gd->logger); } 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, 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) { 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); glDeleteBuffers(4, gd->buffer_objects); glDeleteVertexArrays(2, gd->vertex_array_objects); glDeleteQueries(2, gd->frame_timing); gl_check_err(); } GLuint gl_new_texture(void) { GLuint texture; glGenTextures(1, &texture); if (!texture) { log_error("Failed to generate texture"); return 0; } return texture; } 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); 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); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); return true; } image_handle gl_back_buffer(struct backend_base *base) { auto gd = (struct gl_data *)base; return (image_handle)&gd->back_image; } 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; } 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(GLfloat) * 4, .count = 1, .attribs = {{GL_FLOAT, 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 glBlendFunc(GL_ZERO, GL_CONSTANT_ALPHA); glBlendColor(0, 0, 0, (GLclampf)alpha); int nrects; const rect_t *rect = pixman_region32_rectangles(reg_op, &nrects); auto coord = ccalloc(nrects * 16, GLfloat); auto indices = ccalloc(nrects * 6, GLuint); 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, 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_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; } 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; } picom-12.5/src/backend/gl/gl_common.h000066400000000000000000000267021471504570600174730ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include #include "backend/backend.h" #include "log.h" #include "region.h" #define CASESTRRET(s) \ case s: return #s 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; // 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 bound GL texture. struct gl_texture { 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 { /// A sampler that repeats the texture, with nearest filtering. GL_SAMPLER_REPEAT = 0, /// A sampler that repeats the texture, with linear filtering. GL_SAMPLER_REPEAT_SCALE, /// 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 { struct backend_base base; // If we are using proprietary NVIDIA driver bool is_nvidia; // 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]; GLuint buffer_objects[4]; GLuint vertex_array_objects[2]; bool dithered_present; GLuint default_mask_texture; /// Called when an gl_texture is decoupled from the texture it refers. Returns /// the decoupled user_data void *(*decouple_texture_user_data)(backend_t *base, void *user_data); /// Release the user data attached to a gl_texture void (*release_user_data)(backend_t *base, struct gl_texture *); struct log_target *logger; }; 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, vec2 scale, GLfloat *coord, GLuint *indices); /// Like `gl_mask_rects_to_coords`, but with `origin` is (0, 0). static inline void gl_mask_rects_to_coords_simple(int nrects, const rect_t *rects, GLfloat *coord, GLuint *indices) { return gl_mask_rects_to_coords((ivec2){0, 0}, nrects, rects, SCALE_IDENTITY, coord, indices); } GLuint gl_create_shader(GLenum shader_type, const char *shader_str); 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); 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(void); xcb_pixmap_t gl_release_image(backend_t *base, image_handle image); image_handle gl_clone(backend_t *base, image_handle image, const region_t *reg_visible); 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); enum device_status gl_device_status(backend_t *base); #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_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, GLfloat *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] = (GLfloat)target_height - current_vertex[1]; } } } /** * Get a textual representation of an OpenGL error. */ static inline const char *gl_get_err_str(GLenum err) { switch (err) { CASESTRRET(GL_NO_ERROR); CASESTRRET(GL_INVALID_ENUM); CASESTRRET(GL_INVALID_VALUE); CASESTRRET(GL_INVALID_OPERATION); CASESTRRET(GL_INVALID_FRAMEBUFFER_OPERATION); CASESTRRET(GL_OUT_OF_MEMORY); CASESTRRET(GL_STACK_UNDERFLOW); CASESTRRET(GL_STACK_OVERFLOW); CASESTRRET(GL_FRAMEBUFFER_UNDEFINED); CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT); CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT); CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER); CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER); CASESTRRET(GL_FRAMEBUFFER_UNSUPPORTED); CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE); CASESTRRET(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS); } return NULL; } /** * Check for GL error. * * http://blog.nobel-joergensen.com/2013/01/29/debugging-opengl-using-glgeterror/ */ static inline void gl_check_err_(const char *func, int line) { GLenum err = GL_NO_ERROR; while (GL_NO_ERROR != (err = glGetError())) { const char *errtext = gl_get_err_str(err); if (errtext) { log_printf(tls_logger, LOG_LEVEL_ERROR, func, "GL error at line %d: %s", line, errtext); } else { log_printf(tls_logger, LOG_LEVEL_ERROR, func, "GL error at line %d: %d", line, err); } } } static inline void gl_clear_err(void) { while (glGetError() != GL_NO_ERROR) { } } #define gl_check_err() gl_check_err_(__func__, __LINE__) /** * Check for GL framebuffer completeness. */ static inline bool gl_check_fb_complete_(const char *func, int line, GLenum fb) { GLenum status = glCheckFramebufferStatus(fb); if (status == GL_FRAMEBUFFER_COMPLETE) { return true; } const char *stattext = gl_get_err_str(status); if (stattext) { log_printf(tls_logger, LOG_LEVEL_ERROR, func, "Framebuffer attachment failed at line %d: %s", line, stattext); } else { log_printf(tls_logger, LOG_LEVEL_ERROR, func, "Framebuffer attachment failed at line %d: %d", line, status); } return false; } static const GLuint vert_coord_loc = 0; static const GLuint vert_in_texcoord_loc = 1; // 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[]; picom-12.5/src/backend/gl/glx.c000066400000000000000000000415541471504570600163100ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* * Compton - a compositor for X11 * * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard * * Copyright (c) 2011-2013, Christopher Jeffrey * Copyright (c) 2019 Yuxuan Shui * See LICENSE-mit for more information. * */ #include #include #include #include #include #include #include #include #include #include #include "backend/backend.h" #include "backend/backend_common.h" #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "picom.h" #include "utils/misc.h" #include "x.h" #include "gl_common.h" #include "glx.h" struct _glx_data { struct gl_data gl; 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); \ break; \ } \ } while (0) 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(c->dpy, c->screen, (int[]){ GLX_RED_SIZE, m.red_size, GLX_GREEN_SIZE, m.green_size, GLX_BLUE_SIZE, m.blue_size, GLX_ALPHA_SIZE, m.alpha_size, GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT, GLX_X_RENDERABLE, True, GLX_CONFIG_CAVEAT, GLX_NONE, None, }, &ncfg); // clang-format on int texture_tgts, y_inverted, texture_fmt; bool found = false; int min_cost = INT_MAX; GLXFBConfig ret; for (int i = 0; i < ncfg; i++) { int depthbuf, stencil, doublebuf, bufsize; 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(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(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."); continue; } int visual; glXGetFBConfigAttribChecked(c->dpy, cfg[i], GLX_VISUAL_ID, &visual); if (m.visual_depth != -1 && 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 // quite common, seen in both open source and proprietary drivers. // // If the FBConfig has a matching depth but its visual doesn't, we // still cannot use it. continue; } // All check passed, we are using this one. found = true; ret = cfg[i]; 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 if (m.alpha_size) { texture_fmt = rgba ? GLX_TEXTURE_FORMAT_RGBA_EXT : GLX_TEXTURE_FORMAT_RGB_EXT; } else { texture_fmt = rgb ? GLX_TEXTURE_FORMAT_RGB_EXT : GLX_TEXTURE_FORMAT_RGBA_EXT; } min_cost = depthbuf + stencil + bufsize * (doublebuf + 1); } free(cfg); if (found) { info->cfg = ret; info->texture_tgts = texture_tgts; info->texture_fmt = texture_fmt; info->y_inverted = y_inverted; } return found; } /** * Free a glx_texture_t. */ static void glx_release_image(backend_t *base, struct gl_texture *tex) { GLXPixmap *p = tex->user_data; // Release binding if (p && tex->texture) { glBindTexture(GL_TEXTURE_2D, tex->texture); glXReleaseTexImageEXT(base->c->dpy, *p, GLX_FRONT_LEFT_EXT); glBindTexture(GL_TEXTURE_2D, 0); } // Free GLX Pixmap if (p) { glXDestroyPixmap(base->c->dpy, *p); *p = 0; } free(p); tex->user_data = NULL; } /** * Destroy GLX related resources. */ void glx_deinit(backend_t *base) { struct _glx_data *gd = (void *)base; gl_deinit(&gd->gl); // Destroy GLX context if (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) { return NULL; } static bool glx_set_swap_interval(int interval, Display *dpy, GLXDrawable drawable) { bool vsync_enabled = false; if (glxext.has_GLX_MESA_swap_control) { vsync_enabled = (glXSwapIntervalMESA((uint)interval) == 0); } if (!vsync_enabled && glxext.has_GLX_SGI_swap_control) { vsync_enabled = (glXSwapIntervalSGI(interval) == 0); } if (!vsync_enabled && glxext.has_GLX_EXT_swap_control) { // glXSwapIntervalEXT doesn't return if it's successful glXSwapIntervalEXT(dpy, drawable, interval); vsync_enabled = true; } return vsync_enabled; } const struct backend_operations glx_ops; /** * Initialize OpenGL. */ static backend_t *glx_init(session_t *ps, xcb_window_t target) { bool success = false; 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->target_win = target; XVisualInfo *pvis = NULL; // Check for GLX extension if (!ps->glx_exists) { log_error("No GLX extension."); goto end; } // Get XVisualInfo int nitems = 0; 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; } // Ensure the visual is double-buffered int value = 0; 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->c.dpy, pvis, GLX_DOUBLEBUFFER, &value) || !value) { log_error("Root visual is not a double buffered GL visual."); goto end; } if (glXGetConfig(ps->c.dpy, pvis, GLX_RGBA, &value) || !value) { log_error("Root visual is a color index visual, not supported"); goto end; } if (!glxext.has_GLX_EXT_texture_from_pixmap) { log_error("GLX_EXT_texture_from_pixmap is not supported by your driver"); goto end; } if (!glxext.has_GLX_ARB_create_context) { log_error("GLX_ARB_create_context is not supported by your driver"); goto end; } // 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(ps->c.dpy, ps->c.screen, &ncfgs); bool found = false; for (int i = 0; i < ncfgs; i++) { int visualid; glXGetFBConfigAttribChecked(ps->c.dpy, cfg[i], GLX_VISUAL_ID, &visualid); if ((VisualID)visualid != pvis->visualid) { continue; } 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) { log_error("Failed to get GLX context."); goto end; } found = true; break; } if (!found) { log_error("Couldn't find a suitable fbconfig for the target window"); goto end; } // Attach GLX context GLXDrawable tgt = gd->target_win; if (!glXMakeCurrent(ps->c.dpy, tgt, gd->ctx)) { log_error("Failed to attach GLX context."); goto end; } if (!gl_init(&gd->gl, ps)) { log_error("Failed to setup OpenGL"); goto end; } gd->gl.decouple_texture_user_data = glx_decouple_user_data; gd->gl.release_user_data = glx_release_image; if (ps->o.vsync) { if (!glx_set_swap_interval(1, ps->c.dpy, tgt)) { log_error("Failed to enable vsync."); } } else { glx_set_swap_interval(0, ps->c.dpy, tgt); } success = true; end: if (pvis) { XFree(pvis); } if (!success) { glx_deinit(&gd->gl.base); return NULL; } return &gd->gl.base; } 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 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->width = r->width; inner->height = r->height; inner->format = BACKEND_IMAGE_FORMAT_PIXMAP; free(r); 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 (!(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, (fbconfig->texture_fmt == GLX_TEXTURE_FORMAT_RGBA_EXT)); GLint attrs[] = { GLX_TEXTURE_FORMAT_EXT, fbconfig->texture_fmt, GLX_TEXTURE_TARGET_EXT, GLX_TEXTURE_2D_EXT, 0, }; inner->y_inverted = fbconfig->y_inverted; glxpixmap = cmalloc(GLXPixmap); inner->pixmap = pixmap; *glxpixmap = glXCreatePixmap(base->c->dpy, fbconfig->cfg, pixmap, attrs); if (!*glxpixmap) { log_error("Failed to create glpixmap for pixmap %#010x", pixmap); goto err; } log_trace("GLXPixmap %#010lx", *glxpixmap); // Create texture inner->user_data = glxpixmap; inner->texture = gl_new_texture(); glBindTexture(GL_TEXTURE_2D, inner->texture); glXBindTexImageEXT(base->c->dpy, *glxpixmap, GLX_FRONT_LEFT_EXT, NULL); glBindTexture(GL_TEXTURE_2D, 0); gl_check_err(); return (image_handle)inner; err: if (glxpixmap && *glxpixmap) { glXDestroyPixmap(base->c->dpy, *glxpixmap); } free(glxpixmap); return NULL; } static bool glx_present(backend_t *base) { struct _glx_data *gd = (void *)base; gl_finish_render(&gd->gl); glXSwapBuffers(base->c->dpy, gd->target_win); return true; } static int glx_buffer_age(backend_t *base) { if (!glxext.has_GLX_EXT_buffer_age) { return -1; } struct _glx_data *gd = (void *)base; unsigned int 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) { bool warn_software_rendering = false; const char *software_renderer_names[] = {"llvmpipe", "SWR", "softpipe"}; 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") == 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; break; } } } #ifdef GLX_MESA_query_renderer if (glxext.has_GLX_MESA_query_renderer) { unsigned int accelerated = 0; glXQueryCurrentRendererIntegerMESA(GLX_RENDERER_ACCELERATED_MESA, &accelerated); printf("* Accelerated: %d\n", accelerated); // Trust GLX_MESA_query_renderer when it's available warn_software_rendering = (accelerated == 0); } #endif 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"); } } static int glx_max_buffer_age(struct backend_base *base attr_unused) { return 5; // Why? } #define PICOM_BACKEND_GLX_MAJOR (0UL) #define PICOM_BACKEND_GLX_MINOR (1UL) static void glx_version(struct backend_base * /*base*/, uint64_t *major, uint64_t *minor) { *major = PICOM_BACKEND_GLX_MAJOR; *minor = PICOM_BACKEND_GLX_MINOR; } const struct backend_operations glx_ops = { .apply_alpha = gl_apply_alpha, .back_buffer = gl_back_buffer, .bind_pixmap = glx_bind_pixmap, .blit = gl_blit, .blur = gl_blur, .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, .version = glx_version, .release_image = gl_release_image, .init = glx_init, .deinit = glx_deinit, .root_change = gl_root_change, .prepare = gl_prepare, .buffer_age = glx_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 = 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 = glx_max_buffer_age, }; struct glxext_info glxext = {0}; void glxext_init(Display *dpy, int screen) { if (glxext.initialized) { return; } glxext.initialized = true; #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); check_ext(GLX_MESA_swap_control); check_ext(GLX_EXT_swap_control); 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 } BACKEND_ENTRYPOINT(glx_register) { if (!backend_register(PICOM_BACKEND_MAJOR, PICOM_BACKEND_MINOR, "glx", glx_ops.init, true)) { log_error("Failed to register glx backend"); } } picom-12.5/src/backend/gl/glx.h000066400000000000000000000016271471504570600163120ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include #include #include "x.h" struct glx_fbconfig_info { GLXFBConfig cfg; int texture_tgts; int texture_fmt; int y_inverted; }; bool glx_find_fbconfig(struct x_connection *c, struct xvisual_info m, struct glx_fbconfig_info *info); struct glxext_info { bool initialized; bool has_GLX_SGI_video_sync; bool has_GLX_SGI_swap_control; bool has_GLX_OML_sync_control; bool has_GLX_MESA_swap_control; bool has_GLX_EXT_swap_control; bool has_GLX_EXT_texture_from_pixmap; 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; void glxext_init(Display *, int screen); picom-12.5/src/backend/gl/meson.build000066400000000000000000000001121471504570600174750ustar00rootroot00000000000000srcs += [ files('blur.c', 'egl.c', 'gl_common.c', 'glx.c', 'shaders.c') ] picom-12.5/src/backend/gl/shaders.c000066400000000000000000000175641471504570600171530ustar00rootroot00000000000000#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; vec2 mask_rectangle_sdf(vec2 point, vec2 half_size) { vec2 d = max(abs(point) - half_size, 0.0); float l = length(d); // Add a small number to avoid 0/0. return vec2(l, l / (max(d.x, d.y) + 1e-8)); } 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; vec2 sdf = mask_rectangle_sdf(maskcoord - mask_size / 2.0f, inner_size / 2.0f); float dist = sdf.x - mask_corner_radius + sdf.y / 2.0f; if (dist > 0.0f) { mask.r *= (1.0f - clamp(dist, 0.0f, sdf.y) / (sdf.y + 1e-8)); } } 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 // Returns 2 number: the distance, and the approximate chord length inside // the pixel around `point`. vec2 rectangle_sdf(vec2 point, vec2 half_size) { vec2 d = max(abs(point) - half_size, 0.0); float l = length(d); // Add a small number to avoid 0/0. return vec2(l, l / (max(d.x, d.y) + 1e-8)); } 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); } if (corner_radius != 0) { // 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; vec2 sdf = rectangle_sdf(texcoord - outer_size / 2.0f, inner_size / 2.0f); // For anti-aliasing, we estimate how much of the pixel is covered by the rounded // rectangle. This differs depends on at what angle the circle sweeps through the // pixel. e.g. if it goes from corner to corner, then the coverage goes from 0 to // 1 when the distance goes from -sqrt(2)/2 to +sqrt(2)/2; if it goes from egde to // edge, then the coverage goes from 0 to 1 when the distance goes from -0.5 to 0.5. // The chord length returned by `rectangle_sdf` is an approximation of this. float rect_distance = sdf.x - corner_radius + sdf.y / 2.0f; // Add a small number to sdf.y to avoid 0/0 if (rect_distance > 0.0f) { c = (1.0f - clamp(rect_distance, 0.0f, sdf.y) / (sdf.y + 1e-8)) * rim_color; } else { float factor = clamp(rect_distance + border_width, 0.0f, sdf.y) / (sdf.y + 1e-8); 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 picom-12.5/src/backend/meson.build000066400000000000000000000003101471504570600170730ustar00rootroot00000000000000# enable xrender srcs += [ files( 'dummy/dummy.c', 'backend.c', 'backend_common.c', 'driver.c', ), ] subdir('xrender') # enable opengl if get_option('opengl') subdir('gl') endif picom-12.5/src/backend/xrender/000077500000000000000000000000001471504570600164065ustar00rootroot00000000000000picom-12.5/src/backend/xrender/meson.build000066400000000000000000000000371471504570600205500ustar00rootroot00000000000000srcs += [ files('xrender.c') ] picom-12.5/src/backend/xrender/xrender.c000066400000000000000000001142711471504570600202270ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include #include #include #include #include #include #include #include "backend/backend.h" #include "backend/backend_common.h" #include "backend/driver.h" #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "picom.h" #include "region.h" #include "utils/kernel.h" #include "utils/misc.h" #include "x.h" 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[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[2]; /// The back buffer we should be painting into int curr_back; /// The corresponding pixmap to the back buffer 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]; // XXX don't know if these are really needed /// 1x1 white picture xcb_render_picture_t white_pixel; /// 1x1 black picture xcb_render_picture_t black_pixel; 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 { enum blur_method method; /// Blur kernels converted to X format struct x_convolution_kernel **x_blur_kernel; int resize_width, resize_height; /// Number of blur kernels int x_blur_kernel_count; }; 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 set_picture_scale(struct x_connection *c, xcb_render_picture_t picture, vec2 scale) { xcb_render_transform_t transform = { .matrix11 = DOUBLE_TO_XFIXED(1.0 / scale.x), .matrix22 = DOUBLE_TO_XFIXED(1.0 / scale.y), .matrix33 = DOUBLE_TO_XFIXED(1.0), }; x_set_error_action_abort(c, xcb_render_set_picture_transform(c->c, picture, transform)); } /// 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, }; x_set_error_action_abort( 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), to_i16_checked(extent.y1), 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; auto extent_to_mask = region_translate_rect(extent, ivec2_neg(ivec2_add(mask_origin, origin))); mask_pict = xrender_process_mask(xd, args->source_mask, extent_to_mask, 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_saturated(args->effective_size.width * args->scale.x); auto const tmpeh = to_u16_saturated(args->effective_size.height * args->scale.y); const xcb_render_color_t dim_color = { .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); 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); } } set_picture_scale(xd->base.c, mask_pict, args->scale); 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 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); vec2 inverse_scale = (vec2){ .x = 1.0 / args->scale.x, .y = 1.0 / args->scale.y, }; if (vec2_eq(args->scale, SCALE_IDENTITY)) { x_set_picture_clip_region( xd->base.c, tmp_pict, to_i16_checked(-origin.x), to_i16_checked(-origin.y), args->target_mask); } else { // We need to scale the target_mask back so it's in the source's // coordinate space. scoped_region_t source_mask_region; pixman_region32_init(&source_mask_region); pixman_region32_copy(&source_mask_region, args->target_mask); region_scale(&source_mask_region, origin, inverse_scale); x_set_picture_clip_region( xd->base.c, tmp_pict, to_i16_checked(-origin.x), to_i16_checked(-origin.y), &source_mask_region); } // Copy source -> tmp 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 (args->color_inverted) { if (inner->has_alpha) { 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->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->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->c, XCB_RENDER_PICT_OP_IN_REVERSE, tmp_pict2, XCB_NONE, tmp_pict, 0, 0, 0, 0, 0, 0, tmpw, tmph); x_free_picture(xd->base.c, tmp_pict2); } else { 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 (args->dim != 0) { // Dim the actually content of window xcb_rectangle_t rect = { .x = 0, .y = 0, .width = tmpw, .height = tmph, }; xcb_render_fill_rectangles(xd->base.c->c, XCB_RENDER_PICT_OP_OVER, tmp_pict, dim_color, 1, &rect); } 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); } set_picture_scale(xd->base.c, tmp_pict, args->scale); // Transformations don't affect the picture's clip region, so we need to // set it again x_set_picture_clip_region(xd->base.c, tmp_pict, to_i16_checked(-origin.x), to_i16_checked(-origin.y), args->target_mask); 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); set_picture_scale(xd->base.c, inner->pict, args->scale); 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 (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 (args->dim != 0) { // Dim the actually content of window xcb_rectangle_t rect = { .x = to_i16_checked(origin.x), .y = to_i16_checked(origin.y), .width = tmpew, .height = tmpeh, }; xcb_render_fill_rectangles( xd->base.c->c, XCB_RENDER_PICT_OP_OVER, target->pict, dim_color, 1, &rect); } } } if (mask_allocated) { x_free_picture(xd->base.c, mask_pict); } xrender_record_back_damage(xd, target, args->target_mask); return true; } 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 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 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 = (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; } 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(args->target_mask, bctx->resize_width, bctx->resize_height); const pixman_box32_t *extent_resized = pixman_region32_extents(®_op_resized); 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"; // Create a buffer for storing blurred picture, make it just big enough // for the blur region 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_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_resized); return false; } region_t clip; pixman_region32_init(&clip); pixman_region32_copy(&clip, ®_op_resized); pixman_region32_translate(&clip, -extent_resized->x1, -extent_resized->y1); x_set_picture_clip_region(c, tmp_picture[0], 0, 0, &clip); x_set_picture_clip_region(c, tmp_picture[1], 0, 0, &clip); pixman_region32_fini(&clip); xcb_render_picture_t src_pict = source->pict; auto mask_pict = xd->alpha_pict[(int)(args->opacity * MAX_ALPHA)]; bool mask_allocated = false; ivec2 mask_pict_origin = {}; if (args->source_mask != NULL) { // Translate the target mask region to the mask's coordinate auto mask_extent = *pixman_region32_extents(args->target_mask); mask_extent = region_translate_rect(mask_extent, ivec2_neg(args->source_mask->origin)); mask_pict_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; } 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: // 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_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->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}; } if (mask_allocated) { x_free_picture(c, mask_pict); } 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 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->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 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 (img->pict == XCB_NONE) { free(img); return NULL; } return (image_handle)img; } 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; } 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 xrender_deinit(backend_t *backend_data) { auto xd = (struct xrender_data *)backend_data; for (int i = 0; i < 256; i++) { x_free_picture(xd->base.c, xd->alpha_pict[i]); } x_free_picture(xd->base.c, xd->target); for (int i = 0; i < 2; 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->c, xd->present_event); } x_free_picture(xd->base.c, xd->white_pixel); x_free_picture(xd->base.c, xd->black_pixel); free(xd); } static bool xrender_present(struct backend_base *base) { auto xd = (struct xrender_data *)base; if (xd->vsync) { // Make sure we got reply from PresentPixmap before waiting for events, // to avoid deadlock auto e = xcb_request_check( 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 false; } // TODO(yshui) don't block wait for present completion 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 false; } assert(pev->evtype == XCB_PRESENT_COMPLETE_NOTIFY); 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; // buffer_age < 0 means that back buffer is empty if (xd->buffer_age[1 - xd->curr_back] > 0) { xd->buffer_age[1 - xd->curr_back]++; } 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); } // Without vsync, we are rendering into the front buffer directly pixman_region32_clear(&xd->back_damaged); return true; } 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. return 1; } return xd->buffer_age[xd->curr_back]; } 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); if (!pixman_region32_not_empty(reg_op) || alpha == 1) { return true; } 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 * 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; } if (method == BLUR_METHOD_DUAL_KAWASE) { log_warn("Blur method 'dual_kawase' is not compatible with the 'xrender' " "backend."); ret->method = BLUR_METHOD_NONE; return ret; } ret->method = BLUR_METHOD_KERNEL; struct conv **kernels; int kernel_count; if (method == BLUR_METHOD_KERNEL) { kernels = ((struct kernel_blur_args *)args)->kernels; kernel_count = ((struct kernel_blur_args *)args)->kernel_count; } else { kernels = generate_blur_kernel(method, args, &kernel_count); } ret->x_blur_kernel = ccalloc(kernel_count, struct x_convolution_kernel *); for (int i = 0; i < kernel_count; i++) { int center = kernels[i]->h * kernels[i]->w / 2; x_create_convolution_kernel(kernels[i], kernels[i]->data[center], &ret->x_blur_kernel[i]); ret->resize_width += kernels[i]->w / 2; ret->resize_height += kernels[i]->h / 2; } ret->x_blur_kernel_count = kernel_count; if (method != BLUR_METHOD_KERNEL) { // Kernels generated by generate_blur_kernel, so we need to free them. for (int i = 0; i < kernel_count; i++) { free(kernels[i]); } free(kernels); } return ret; } 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]); } free(ctx->x_blur_kernel); free(ctx); } 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; } const 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, " "it will be ignored."); } if (ps->o.max_brightness < 1.0) { log_warn("\"max-brightness\" is not supported by the xrender backend, it " "will be ignored."); } 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, false, o, 0, 0, 0); assert(xd->alpha_pict[i] != XCB_NONE); } 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 = 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->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 e = 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; free(e); } xd->present_event = 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"); xd->vsync = false; } } else { 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 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_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"); goto err; } } xd->curr_back = 0; xd->back_image.pict = xd->vsync ? xd->back[xd->curr_back] : xd->target; xd->quirks |= BACKEND_QUIRK_SLOW_BLUR; return &xd->base; err: 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; } static int xrender_max_buffer_age(struct backend_base *base) { return ((struct xrender_data *)base)->vsync ? 2 : 1; } #define PICOM_BACKEND_XRENDER_MAJOR (0UL) #define PICOM_BACKEND_XRENDER_MINOR (1UL) static void xrender_version(struct backend_base * /*base*/, uint64_t *major, uint64_t *minor) { *major = PICOM_BACKEND_XRENDER_MAJOR; *minor = PICOM_BACKEND_XRENDER_MINOR; } const struct backend_operations xrender_ops = { .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, .version = xrender_version, .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 = xrender_max_buffer_age, .create_blur_context = xrender_create_blur_context, .destroy_blur_context = xrender_destroy_blur_context, .get_blur_size = xrender_get_blur_size // end }; BACKEND_ENTRYPOINT(xrender_register) { if (!backend_register(PICOM_BACKEND_MAJOR, PICOM_BACKEND_MINOR, "xrender", xrender_ops.init, true)) { log_error("Failed to register xrender backend"); } } // vim: set noet sw=8 ts=8: picom-12.5/src/c2.c000066400000000000000000001753731471504570600140400ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2018 Yuxuan Shui #include #include #include #include #include #include #include #include #include // libpcre #ifdef CONFIG_REGEX_PCRE #define PCRE2_CODE_UNIT_WIDTH 8 #include #endif #include #include #include "atom.h" #include "common.h" #include "compiler.h" #include "log.h" #include "test.h" #include "utils/str.h" #include "utils/uthash_extra.h" #include "wm/win.h" #include "x.h" #include "c2.h" #pragma GCC diagnostic error "-Wunused-parameter" #define C2_MAX_LEVELS 10 typedef struct c2_condition_node_branch c2_condition_node_branch; typedef struct c2_condition_node_leaf c2_condition_node_leaf; enum c2_condition_node_type { C2_NODE_TYPE_BRANCH, C2_NODE_TYPE_LEAF, C2_NODE_TYPE_TRUE, }; /// Fat, typed pointer to a condition tree node. typedef struct { enum c2_condition_node_type type; union { struct c2_condition_node_branch *b; struct c2_condition_node_leaf *l; }; bool neg; } c2_condition_node_ptr; 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; /// Number of bytes allocated for the string. unsigned int string_capacity; }; struct { int64_t numbers[4]; }; struct { int64_t *array; /// Number of bytes allocated for the array. unsigned int array_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, C2_PROPERTY_TYPE_NONE, } type; bool valid; bool needs_update; }; /// Initializer for c2_ptr_t. static const c2_condition_node_ptr C2_NODE_PTR_INIT = { .type = C2_NODE_TYPE_LEAF, .l = NULL, }; /// Operator of a branch element. typedef enum { C2_B_OUNDEFINED, C2_B_OAND, C2_B_OOR, C2_B_OXOR, } c2_b_op_t; /// Structure for branch element in a window condition struct c2_condition_node_branch { c2_b_op_t op; c2_condition_node_ptr opr1; c2_condition_node_ptr opr2; }; /// Initializer for c2_b_t. #define C2_B_INIT \ {.neg = false, .op = C2_B_OUNDEFINED, .opr1 = C2_PTR_INIT, .opr2 = C2_PTR_INIT} /// Structure for leaf element in a window condition struct c2_condition_node_leaf { 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, } match : 3; bool match_ignorecase : 1; char *tgt; unsigned int target_id; xcb_atom_t tgtatom; bool target_on_client; int index; // 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_PGROUPFOCUSED, 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_PTUNDEFINED, C2_L_PTSTRING, C2_L_PTINT, } ptntype; char *ptnstr; long ptnint; #ifdef CONFIG_REGEX_PCRE pcre2_code *regex_pcre; pcre2_match_data *regex_pcre_match; #endif }; static const unsigned int C2_L_INVALID_TARGET_ID = UINT_MAX; /// Initializer for c2_l_t. static const c2_condition_node_leaf C2_LEAF_NODE_INIT = { .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, .target_id = C2_L_INVALID_TARGET_ID, }; /// Linked list type of conditions. struct c2_condition { c2_condition_node_ptr root; void *data; struct list_node siblings; }; // clang-format off // Predefined targets. static const struct { const char *name; bool is_string; bool deprecated; } C2_PREDEFS[] = { [C2_L_PID] = { "id", false, true }, [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_PGROUPFOCUSED] = { "group_focused", false, }, [C2_L_PWMWIN] = { "wmwin", false, }, [C2_L_PBSHAPED] = { "bounding_shaped", false, }, [C2_L_PROUNDED] = { "rounded_corners", false, }, [C2_L_PCLIENT] = { "client", false, true }, [C2_L_PWINDOWTYPE] = { "window_type", true, }, [C2_L_PLEADER] = { "leader", false, true }, [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 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) { return ret; } char c = src[strlen(needle)]; if (isalnum((unsigned char)c) || '_' == c) { return 1; } return 0; } /** * Combine two condition trees. */ static inline c2_condition_node_ptr c2h_comb_tree(c2_b_op_t op, c2_condition_node_ptr p1, c2_condition_node_ptr p2) { c2_condition_node_ptr p = {.type = C2_NODE_TYPE_BRANCH, .b = NULL}; p.b = cmalloc(struct c2_condition_node_branch); p.b->op = op; p.b->opr1 = p1; p.b->opr2 = p2; return p; } /** * Get the precedence value of a condition branch operator. */ static inline int c2h_b_opp(c2_b_op_t op) { switch (op) { case C2_B_OAND: return 2; case C2_B_OOR: case C2_B_OXOR: return 1; default: break; } assert(0); return 0; } /** * Compare precedence of two condition branch operators. * * Associativity is left-to-right, forever. * * @return positive number if op1 > op2, 0 if op1 == op2 in precedence, * negative number otherwise */ static inline int c2h_b_opcmp(c2_b_op_t op1, c2_b_op_t op2) { return c2h_b_opp(op1) - c2h_b_opp(op2); } static int c2_parse_group(const char *pattern, int offset, c2_condition_node_ptr *presult, int level); static int c2_parse_target(const char *pattern, int offset, c2_condition_node_ptr *presult); static int c2_parse_op(const char *pattern, int offset, c2_condition_node_ptr *presult); static int c2_parse_pattern(const char *pattern, int offset, c2_condition_node_ptr *presult); static int c2_parse_legacy(const char *pattern, int offset, c2_condition_node_ptr *presult); static void c2_free(c2_condition_node_ptr p); static size_t c2_condition_node_to_str(c2_condition_node_ptr p, char *output, size_t len); static const char *c2_condition_node_to_str2(c2_condition_node_ptr ptr); static bool c2_tree_postprocess(struct c2_state *state, xcb_connection_t *c, c2_condition_node_ptr node); /** * Wrapper of c2_free(). */ static inline void c2_freep(c2_condition_node_ptr *pp) { if (pp) { c2_free(*pp); *pp = C2_NODE_PTR_INIT; } } /** * Parse a condition string. */ struct c2_condition *c2_parse(struct list_node *list, const char *pattern, void *data) { if (!pattern) { return NULL; } // Parse the pattern auto result = C2_NODE_PTR_INIT; int offset = -1; if (strlen(pattern) >= 2 && ':' == pattern[1]) { offset = c2_parse_legacy(pattern, 0, &result); } else { offset = c2_parse_group(pattern, 0, &result, 0); } if (offset < 0) { c2_freep(&result); return NULL; } // Insert to pcondlst { auto plptr = cmalloc(struct c2_condition); *plptr = (struct c2_condition){ .root = result, .data = data, }; list_init_head(&plptr->siblings); if (list) { list_insert_after(list, &plptr->siblings); } #ifdef DEBUG_C2 log_trace("(\"%s\"): ", pattern); c2_dump(plptr->ptr); putchar('\n'); #endif return plptr; } } /** * Parse a condition string with a prefix. */ c2_condition * c2_parse_with_prefix(struct list_node *list, 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(list, pattern_start, val); if (!ret && free_value) { free_value(val); } return ret; } TEST_CASE(c2_parse) { char str[1024]; struct c2_condition *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_EQUAL(!cond->root.type, C2_NODE_TYPE_BRANCH); TEST_NOTEQUAL(cond->root.l, NULL); TEST_EQUAL(cond->root.l->op, C2_L_OEQ); TEST_EQUAL(cond->root.l->ptntype, C2_L_PTSTRING); TEST_STREQUAL(cond->root.l->ptnstr, "xterm"); size_t len = c2_condition_node_to_str(cond->root, str, sizeof(str)); TEST_STREQUAL3(str, "name = \"xterm\"", len); struct wm *wm = wm_new(); struct wm_ref *node = wm_new_mock_window(wm, 1); struct win test_win = { .name = "xterm", .tree_ref = node, }; TEST_TRUE(c2_match_one(state, &test_win, cond, NULL)); c2_tree_postprocess(state, NULL, cond->root); TEST_EQUAL(HASH_COUNT(state->tracked_properties), 0); c2_state_free(state); destroy_atoms(atoms); c2_free_condition(cond, NULL); cond = c2_parse(NULL, "argb", NULL); TEST_NOTEQUAL(cond, NULL); TEST_NOTEQUAL(cond->root.type, C2_NODE_TYPE_BRANCH); TEST_EQUAL(cond->root.l->ptntype, C2_L_PTINT); c2_free_condition(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_NOTEQUAL(cond->root.type, C2_NODE_TYPE_BRANCH); TEST_NOTEQUAL(cond->root.l, NULL); TEST_EQUAL(cond->root.l->op, C2_L_OEXISTS); TEST_EQUAL(cond->root.l->match, C2_L_MEXACT); TEST_EQUAL(cond->root.l->predef, C2_L_PUNDEFINED); TEST_TRUE(cond->root.l->target_on_client); TEST_NOTEQUAL(cond->root.l->tgt, NULL); TEST_STREQUAL(cond->root.l->tgt, "_GTK_FRAME_EXTENTS"); atoms = init_mock_atoms(); state = c2_state_new(atoms); c2_tree_postprocess(state, NULL, cond->root); 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_node_to_str(cond->root, str, sizeof(str)); TEST_STREQUAL3(str, "_GTK_FRAME_EXTENTS@[0]", len); c2_free_condition(cond, NULL); cond = c2_parse( NULL, "!(name != \"xterm\" && class_g *= \"XTerm\") || !name != \"yterm\"", NULL); TEST_NOTEQUAL(cond, NULL); TEST_STREQUAL(c2_condition_to_str(cond), "(!(name != \"xterm\" && class_g *= " "\"XTerm\") || name = \"yterm\")"); c2_free_condition(cond, NULL); cond = c2_parse(NULL, "name = \"xterm\" && class_g *= \"XTerm\"", NULL); TEST_NOTEQUAL(cond, NULL); TEST_EQUAL(cond->root.type, C2_NODE_TYPE_BRANCH); TEST_NOTEQUAL(cond->root.b, NULL); TEST_EQUAL(cond->root.b->op, C2_B_OAND); TEST_NOTEQUAL(cond->root.b->opr1.l, NULL); TEST_NOTEQUAL(cond->root.b->opr2.l, NULL); TEST_EQUAL(cond->root.b->opr1.l->op, C2_L_OEQ); TEST_EQUAL(cond->root.b->opr1.l->match, C2_L_MEXACT); TEST_EQUAL(cond->root.b->opr1.l->ptntype, C2_L_PTSTRING); TEST_EQUAL(cond->root.b->opr2.l->op, C2_L_OEQ); TEST_EQUAL(cond->root.b->opr2.l->match, C2_L_MCONTAINS); TEST_EQUAL(cond->root.b->opr2.l->ptntype, C2_L_PTSTRING); TEST_STREQUAL(cond->root.b->opr1.l->tgt, "name"); TEST_EQUAL(cond->root.b->opr1.l->predef, C2_L_PNAME); TEST_STREQUAL(cond->root.b->opr2.l->tgt, "class_g"); TEST_EQUAL(cond->root.b->opr2.l->predef, C2_L_PCLASSG); atoms = init_mock_atoms(); state = c2_state_new(atoms); len = c2_condition_node_to_str(cond->root, str, sizeof(str)); TEST_STREQUAL3(str, "(name = \"xterm\" && class_g *= \"XTerm\")", len); test_win.class_general = "XTerm"; TEST_TRUE(c2_match_one(state, &test_win, cond, NULL)); test_win.class_general = "asdf"; TEST_TRUE(!c2_match_one(state, &test_win, cond, NULL)); c2_free_condition(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->root.l->index, 1); TEST_STREQUAL(cond->root.l->tgt, "_NET_WM_STATE"); TEST_STREQUAL(cond->root.l->ptnstr, "_NET_WM_STATE_HIDDEN"); len = c2_condition_node_to_str(cond->root, str, sizeof(str)); TEST_STREQUAL3(str, "_NET_WM_STATE[1] *= \"_NET_WM_STATE_HIDDEN\"", len); c2_free_condition(cond, NULL); cond = c2_parse(NULL, "_NET_WM_STATE[*]:32a*='_NET_WM_STATE_HIDDEN'", NULL); TEST_EQUAL(cond->root.l->index, -1); len = c2_condition_node_to_str(cond->root, str, sizeof(str)); TEST_STREQUAL3(str, "_NET_WM_STATE[*] *= \"_NET_WM_STATE_HIDDEN\"", len); c2_free_condition(cond, NULL); cond = c2_parse(NULL, "!class_i:0s", NULL); TEST_NOTEQUAL(cond, NULL); len = c2_condition_node_to_str(cond->root, str, sizeof(str)); TEST_STREQUAL3(str, "!class_i", len); c2_free_condition(cond, NULL); cond = c2_parse(NULL, "_NET_WM_STATE = '_NET_WM_STATE_HIDDEN'", NULL); TEST_NOTEQUAL(cond, NULL); c2_free_condition(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_node_to_str(cond->root, str, sizeof(str)); TEST_STREQUAL3(str, rule, len); c2_free_condition(cond, NULL); wm_free_mock_window(wm, test_win.tree_ref); wm_free(wm); } #define c2_error(format, ...) \ do { \ log_error("Pattern \"%s\" pos %d: " format, pattern, offset, ##__VA_ARGS__); \ goto fail; \ } while (0) // TODO(yshui) Not a very good macro, should probably be a function #define C2H_SKIP_SPACES() \ { \ while (isspace((unsigned char)pattern[offset])) \ ++offset; \ } /** * Parse a group in condition string. * * @return offset of next character in string */ static int c2_parse_group(const char *pattern, int offset, c2_condition_node_ptr *presult, int level) { if (!pattern) { return -1; } // Expected end character const char endchar = (offset ? ')' : '\0'); // We use a system that a maximum of 2 elements are kept. When we find // the third element, we combine the elements according to operator // precedence. This design limits operators to have at most two-levels // of precedence and fixed left-to-right associativity. // For storing branch operators. ops[0] is actually unused c2_b_op_t ops[3] = {}; // For storing elements c2_condition_node_ptr eles[2] = {C2_NODE_PTR_INIT, C2_NODE_PTR_INIT}; // Index of next free element slot in eles int elei = 0; // Pointer to the position of next element c2_condition_node_ptr *pele = eles; // Negation flag of next operator bool neg = false; // Whether we are expecting an element immediately, is true at first, or // 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])) { continue; } // Handle end of group if (')' == pattern[offset]) { break; } // Handle "!" if ('!' == pattern[offset]) { if (!next_expected) { c2_error("Unexpected \"!\"."); } neg = !neg; continue; } // Handle AND and OR if ('&' == pattern[offset] || '|' == pattern[offset]) { if (next_expected) { c2_error("Unexpected logical operator."); } next_expected = true; if (mstrncmp("&&", pattern + offset) == 0) { ops[elei] = C2_B_OAND; ++offset; } else if (mstrncmp("||", pattern + offset) == 0) { ops[elei] = C2_B_OOR; ++offset; } else { c2_error("Illegal logical operator."); } continue; } // Parsing an element if (!next_expected) { c2_error("Unexpected expression."); } assert(!elei || ops[elei]); // If we are out of space if (elei == 2) { --elei; // If the first operator has higher or equal precedence, combine // the first two elements if (c2h_b_opcmp(ops[1], ops[2]) >= 0) { eles[0] = c2h_comb_tree(ops[1], eles[0], eles[1]); eles[1] = C2_NODE_PTR_INIT; pele = &eles[elei]; ops[1] = ops[2]; } // Otherwise, combine the second and the incoming one else { eles[1] = c2h_comb_tree(ops[2], eles[1], C2_NODE_PTR_INIT); assert(eles[1].type == C2_NODE_TYPE_BRANCH); pele = &eles[1].b->opr2; } // The last operator always needs to be reset ops[2] = C2_B_OUNDEFINED; } // It's a subgroup if it starts with '(' if ('(' == pattern[offset]) { if ((offset = c2_parse_group(pattern, offset + 1, pele, level + 1)) < 0) { goto fail; } } else { // Otherwise it's a leaf if ((offset = c2_parse_target(pattern, offset, pele)) < 0) { goto fail; } assert(pele->type != C2_NODE_TYPE_BRANCH && pele->l != NULL); if ((offset = c2_parse_op(pattern, offset, pele)) < 0) { goto fail; } 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 (C2_PREDEFS[pele->l->predef].deprecated) { log_warn("Predefined target \"%s\" is " "deprecated. Matching against it will " "always fail. Offending pattern is " "\"%s\"", pele->l->tgt, pattern); } 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; // Apply negation if (neg) { neg = false; pele->neg = !pele->neg; } next_expected = false; ++elei; pele = &eles[elei]; } // Wrong end character? if (pattern[offset] && !endchar) { c2_error("Expected end of string but found '%c'.", pattern[offset]); } if (!pattern[offset] && endchar) { c2_error("Expected '%c' but found end of string.", endchar); } // Handle end of group if (!elei) { c2_error("Empty group."); } else if (next_expected) { c2_error("Missing rule before end of group."); } else if (elei > 1) { assert(2 == elei); assert(ops[1]); eles[0] = c2h_comb_tree(ops[1], eles[0], eles[1]); eles[1] = C2_NODE_PTR_INIT; } *presult = eles[0]; if (')' == pattern[offset]) { ++offset; } return offset; fail: c2_freep(&eles[0]); c2_freep(&eles[1]); return -1; } /** * Parse the target part of a rule. */ static int c2_parse_target(const char *pattern, int offset, c2_condition_node_ptr *presult) { // Initialize leaf presult->type = C2_NODE_TYPE_LEAF; presult->neg = false; presult->l = cmalloc(c2_condition_node_leaf); auto const pleaf = presult->l; *pleaf = C2_LEAF_NODE_INIT; // Parse negation marks while ('!' == pattern[offset]) { presult->neg = !presult->neg; ++offset; C2H_SKIP_SPACES(); } // Copy target name out int tgtlen = 0; for (; pattern[offset] && (isalnum((unsigned char)pattern[offset]) || '_' == pattern[offset] || '.' == pattern[offset]); ++offset) { ++tgtlen; } if (!tgtlen) { c2_error("Empty target."); } pleaf->tgt = strndup(&pattern[offset - tgtlen], (size_t)tgtlen); // Check for predefined targets static const int npredefs = (int)(sizeof(C2_PREDEFS) / sizeof(C2_PREDEFS[0])); for (int i = 0; i < npredefs; ++i) { if (strcmp(C2_PREDEFS[i].name, pleaf->tgt) == 0) { pleaf->predef = i; break; } } C2H_SKIP_SPACES(); // Parse target-on-client flag if ('@' == pattern[offset]) { pleaf->target_on_client = true; ++offset; C2H_SKIP_SPACES(); } // Parse index if ('[' == pattern[offset]) { if (pleaf->predef != C2_L_PUNDEFINED) { c2_error("Predefined targets can't have index."); } offset++; C2H_SKIP_SPACES(); long index = -1; const char *endptr = NULL; if ('*' == pattern[offset]) { index = -1; endptr = pattern + offset + 1; } else { index = strtol(pattern + offset, (char **)&endptr, 0); if (index < 0) { c2_error("Index number invalid."); } } 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 = (int)index; offset = to_int_checked(endptr - pattern); C2H_SKIP_SPACES(); if (pattern[offset] != ']') { c2_error("Index end marker not found."); } ++offset; C2H_SKIP_SPACES(); } // Parse target type and format if (':' == pattern[offset]) { ++offset; C2H_SKIP_SPACES(); // Look for format bool hasformat = false; long format = 0; 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 switch (pattern[offset]) { case 'w': case 'd': case 'c': case 's': case 'a': break; default: c2_error("Invalid type character."); } 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(); // Write format if (hasformat) { 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); } } } return offset; fail: return -1; } /** * Parse the operator part of a leaf. */ static int c2_parse_op(const char *pattern, int offset, c2_condition_node_ptr *presult) { auto const pleaf = presult->l; // Parse negation marks C2H_SKIP_SPACES(); while ('!' == pattern[offset]) { presult->neg = !presult->neg; ++offset; C2H_SKIP_SPACES(); } // Parse qualifiers if ('*' == pattern[offset] || '^' == pattern[offset] || '%' == pattern[offset] || '~' == pattern[offset]) { switch (pattern[offset]) { case '*': pleaf->match = C2_L_MCONTAINS; break; case '^': pleaf->match = C2_L_MSTART; break; case '%': pleaf->match = C2_L_MWILDCARD; break; case '~': pleaf->match = C2_L_MPCRE; break; default: assert(0); } ++offset; C2H_SKIP_SPACES(); } // Parse flags while ('?' == pattern[offset]) { pleaf->match_ignorecase = true; ++offset; C2H_SKIP_SPACES(); } // Parse operator while ('=' == pattern[offset] || '>' == pattern[offset] || '<' == pattern[offset]) { if ('=' == pattern[offset] && C2_L_OGT == pleaf->op) { pleaf->op = C2_L_OGTEQ; } else if ('=' == pattern[offset] && C2_L_OLT == pleaf->op) { pleaf->op = C2_L_OLTEQ; } else if (pleaf->op) { c2_error("Duplicate operator."); } else { switch (pattern[offset]) { case '=': pleaf->op = C2_L_OEQ; break; case '>': pleaf->op = C2_L_OGT; break; case '<': pleaf->op = C2_L_OLT; break; default: assert(0); } } ++offset; C2H_SKIP_SPACES(); } // Check for problems 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; fail: return -1; } /** * Parse the pattern part of a leaf. */ static int c2_parse_pattern(const char *pattern, int offset, c2_condition_node_ptr *presult) { auto const pleaf = presult->l; // Exists operator cannot have pattern if (!pleaf->op) { return offset; } C2H_SKIP_SPACES(); char *endptr = NULL; if (!strcmp_wd("true", &pattern[offset])) { pleaf->ptntype = C2_L_PTINT; pleaf->ptnint = true; offset += 4; // length of "true"; } else if (!strcmp_wd("false", &pattern[offset])) { pleaf->ptntype = C2_L_PTINT; pleaf->ptnint = false; offset += 5; // length of "false"; } else if (pleaf->ptnint = strtol(pattern + offset, &endptr, 0), pattern + offset != endptr) { pleaf->ptntype = C2_L_PTINT; offset = to_int_checked(endptr - pattern); // Make sure we are stopping at the end of a word if (isalnum((unsigned char)pattern[offset])) { c2_error("Trailing characters after a numeric pattern."); } } else { // Parse string patterns bool raw = false; char delim = '\0'; // String flags if (tolower((unsigned char)pattern[offset]) == 'r') { raw = true; ++offset; 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; delim = pattern[offset]; ++offset; } if (pleaf->ptntype != C2_L_PTSTRING) { c2_error("Invalid pattern type."); } // Parse the string now // We can't determine the length of the pattern, so we use the length // to the end of the pattern string -- currently escape sequences // cannot be converted to a string longer than itself. auto tptnstr = ccalloc((strlen(pattern + offset) + 1), char); char *ptptnstr = tptnstr; pleaf->ptnstr = tptnstr; for (; pattern[offset] && delim != pattern[offset]; ++offset) { // Handle escape sequences if it's not a raw string if ('\\' == pattern[offset] && !raw) { switch (pattern[++offset]) { case '\\': *(ptptnstr++) = '\\'; break; case '\'': *(ptptnstr++) = '\''; break; case '\"': *(ptptnstr++) = '\"'; break; case 'a': *(ptptnstr++) = '\a'; break; case 'b': *(ptptnstr++) = '\b'; break; case 'f': *(ptptnstr++) = '\f'; break; case 'n': *(ptptnstr++) = '\n'; break; case 'r': *(ptptnstr++) = '\r'; break; case 't': *(ptptnstr++) = '\t'; break; case 'v': *(ptptnstr++) = '\v'; break; case 'o': case 'x': { scoped_charp tstr = strndup(pattern + offset + 1, 2); char *pstr = NULL; long val = strtol( tstr, &pstr, ('o' == pattern[offset] ? 8 : 16)); if (pstr != &tstr[2] || val <= 0) { c2_error("Invalid octal/hex escape " "sequence."); } 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; } default: c2_error("Invalid escape sequence."); } } else { *(ptptnstr++) = pattern[offset]; } } if (!pattern[offset]) { c2_error("Premature end of pattern string."); } ++offset; *ptptnstr = '\0'; pleaf->ptnstr = strdup(tptnstr); free(tptnstr); } C2H_SKIP_SPACES(); if (!pleaf->ptntype) { c2_error("Invalid pattern type."); } 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_error("String pattern cannot have an arithmetic operator."); } return offset; fail: return -1; } /** * Parse a condition with legacy syntax. */ static int c2_parse_legacy(const char *pattern, int offset, c2_condition_node_ptr *presult) { if (strlen(pattern + offset) < 4 || pattern[offset + 1] != ':' || !strchr(pattern + offset + 2, ':')) { c2_error("Legacy parser: Invalid format."); } // Allocate memory for new leaf auto pleaf = cmalloc(c2_condition_node_leaf); presult->type = C2_NODE_TYPE_LEAF; presult->l = pleaf; presult->neg = false; *pleaf = C2_LEAF_NODE_INIT; pleaf->op = C2_L_OEQ; pleaf->ptntype = C2_L_PTSTRING; // Determine the pattern target switch (pattern[offset]) { 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]); } offset += 2; // Determine the match type switch (pattern[offset]) { case 'e': pleaf->match = C2_L_MEXACT; break; case 'a': pleaf->match = C2_L_MCONTAINS; break; case 's': pleaf->match = C2_L_MSTART; break; case 'w': pleaf->match = C2_L_MWILDCARD; break; case 'p': pleaf->match = C2_L_MPCRE; break; default: c2_error("Type \"%c\" invalid.\n", pattern[offset]); } ++offset; // Determine the pattern flags while (':' != pattern[offset]) { switch (pattern[offset]) { case 'i': pleaf->match_ignorecase = true; break; default: c2_error("Flag \"%c\" invalid.", pattern[offset]); } ++offset; } ++offset; // Copy the pattern pleaf->ptnstr = strdup(pattern + offset); return offset; fail: return -1; } #undef c2_error /** * Do postprocessing on a condition leaf. */ static bool c2_l_postprocess(struct c2_state *state, xcb_connection_t *c, c2_condition_node_leaf *pleaf) { // Get target atom if it's not a predefined one if (pleaf->predef == C2_L_PUNDEFINED) { 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 tracked property name list if (pleaf->tgtatom) { 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 if (pleaf->predef == C2_L_PUNDEFINED) { for (const char *pc = pleaf->tgt; *pc; ++pc) { if (islower((unsigned char)*pc)) { log_warn("Lowercase character in target name \"%s\".", pleaf->tgt); break; } } } // PCRE patterns if (C2_L_PTSTRING == pleaf->ptntype && C2_L_MPCRE == pleaf->match) { #ifdef CONFIG_REGEX_PCRE int errorcode = 0; PCRE2_SIZE erroffset = 0; unsigned int options = 0; // Ignore case flag if (pleaf->match_ignorecase) { options |= PCRE2_CASELESS; } // Compile PCRE expression pleaf->regex_pcre = 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; } 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; #endif } return true; } static bool c2_tree_postprocess(struct c2_state *state, xcb_connection_t *c, c2_condition_node_ptr node) { switch (node.type) { case C2_NODE_TYPE_TRUE: return true; case C2_NODE_TYPE_LEAF: return c2_l_postprocess(state, c, node.l); case C2_NODE_TYPE_BRANCH: return c2_tree_postprocess(state, c, node.b->opr1) && c2_tree_postprocess(state, c, node.b->opr2); default: unreachable(); } } bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, struct list_node *list) { list_foreach(c2_condition, i, list, siblings) { if (!c2_tree_postprocess(state, c, i->root)) { return false; } } return true; } /** * Free a condition tree. */ static void c2_free(c2_condition_node_ptr p) { // For a branch element switch (p.type) { case C2_NODE_TYPE_BRANCH: if (p.b == NULL) { return; } c2_free(p.b->opr1); c2_free(p.b->opr2); free(p.b); return; case C2_NODE_TYPE_LEAF: // For a leaf element if (!p.l) { return; } free(p.l->tgt); free(p.l->ptnstr); #ifdef CONFIG_REGEX_PCRE pcre2_code_free(p.l->regex_pcre); pcre2_match_data_free(p.l->regex_pcre_match); #endif free(p.l); default: return; } } /** * Free a condition tree in c2_lptr_t. */ void c2_free_condition(c2_condition *lp, c2_userdata_free f) { if (!lp) { return; } if (f) { f(lp->data); } lp->data = NULL; c2_free(lp->root); free(lp); } /** * Get a string representation of a rule target. */ static const char *c2h_dump_str_tgt(const c2_condition_node_leaf *pleaf) { if (pleaf->predef != C2_L_PUNDEFINED) { return C2_PREDEFS[pleaf->predef].name; } return pleaf->tgt; } /** * 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 size_t c2_condition_node_to_str(const c2_condition_node_ptr 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; char number[128]; switch (p.type) { case C2_NODE_TYPE_BRANCH: // Branch, i.e. logical operators &&, ||, XOR if (p.b == NULL) { return 0; } if (p.neg) { push_char('!'); } push_char('('); if (len > offset) { offset += c2_condition_node_to_str(p.b->opr1, output + offset, len - offset); } else { offset += c2_condition_node_to_str(p.b->opr1, NULL, 0); } switch (p.b->op) { 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; } if (len > offset) { offset += c2_condition_node_to_str(p.b->opr2, output + offset, len - offset); } else { offset += c2_condition_node_to_str(p.b->opr2, NULL, 0); } push_str(")"); break; case C2_NODE_TYPE_LEAF: // Leaf node if (!p.l) { return 0; } if (C2_L_OEXISTS == p.l->op && p.neg) { push_char('!'); } // Print target name, type, and format const char *target_str = c2h_dump_str_tgt(p.l); push_str(target_str); if (p.l->target_on_client) { push_char('@'); } if (p.l->predef == C2_L_PUNDEFINED) { if (p.l->index < 0) { push_str("[*]"); } else { sprintf(number, "[%d]", p.l->index); push_str(number); } } if (C2_L_OEXISTS == p.l->op) { return offset; } // Print operator push_char(' '); if (C2_L_OEXISTS != p.l->op && p.neg) { push_char('!'); } switch (p.l->match) { case C2_L_MEXACT: 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 (p.l->match_ignorecase) { push_char('?'); } switch (p.l->op) { case C2_L_OEXISTS: break; 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 push_char(' '); switch (p.l->ptntype) { case C2_L_PTINT: sprintf(number, "%ld", p.l->ptnint); push_str(number); break; case C2_L_PTSTRING: // TODO(yshui) Escape string before printing out? push_char('"'); for (int i = 0; p.l->ptnstr[i]; i++) { switch (p.l->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(p.l->ptnstr[i])) { push_char(p.l->ptnstr[i]); } else { sprintf(number, "\\x%02x", (unsigned char)p.l->ptnstr[i]); push_str(number); } break; } } push_char('"'); break; default: assert(0); break; } break; case C2_NODE_TYPE_TRUE: push_str("(default)"); break; default: unreachable(); } #undef push_char #undef push_str return offset; } /// 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_node_to_str2(c2_condition_node_ptr ptr) { static thread_local char buf[4096]; auto len = c2_condition_node_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; } const char *c2_condition_to_str(const c2_condition *ptr) { return c2_condition_node_to_str2(ptr->root); } /// 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; } static inline bool c2_int_op(const c2_condition_node_leaf *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(); } static bool c2_match_once_leaf_int(const struct win *w, const c2_condition_node_leaf *leaf) { // Get the value if (leaf->predef != C2_L_PUNDEFINED) { // A predefined target int64_t predef_target = 0; if (C2_PREDEFS[leaf->predef].deprecated) { return false; } switch (leaf->predef) { 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 = w->a.map_state == XCB_MAP_STATE_VIEWABLE && w->is_focused; break; case C2_L_PGROUPFOCUSED: predef_target = w->a.map_state == XCB_MAP_STATE_VIEWABLE && w->is_group_focused; 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_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 && wm_ref_client_of(w->tree_ref) == NULL; break; default: unreachable(); } return c2_int_op(leaf, predef_target); } // A raw window property if (leaf->target_id == C2_L_INVALID_TARGET_ID) { log_debug("Leaf target ID is invalid, skipping. Most likely a list " "postprocessing failure."); return false; } 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, wm_ref_win_id(w->tree_ref), w->name); return false; } if (values->type == C2_PROPERTY_TYPE_STRING) { log_error("Property %s is not an integer", leaf->tgt); return false; } 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; } } return false; } static bool c2_string_op(const c2_condition_node_leaf *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) == 0; case C2_L_MCONTAINS: return strcasestr(target, leaf->ptnstr); case C2_L_MSTART: return strncasecmp(target, leaf->ptnstr, strlen(leaf->ptnstr)) == 0; case C2_L_MWILDCARD: return !fnmatch(leaf->ptnstr, target, FNM_CASEFOLD); default: unreachable(); } } else { switch (leaf->match) { case C2_L_MEXACT: return strcmp(target, leaf->ptnstr) == 0; case C2_L_MCONTAINS: return strstr(target, leaf->ptnstr); case C2_L_MSTART: return strncmp(target, leaf->ptnstr, strlen(leaf->ptnstr)) == 0; 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 win *w, const c2_condition_node_leaf *leaf) { // A predefined target const char *predef_target = NULL; if (leaf->predef != C2_L_PUNDEFINED) { if (leaf->predef == C2_L_PWINDOWTYPE) { for (unsigned i = 0; i < NUM_WINTYPES; i++) { if (w->window_types & (1 << i) && c2_string_op(leaf, WINTYPES[i].name)) { return true; } } return false; } switch (leaf->predef) { 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; case C2_L_PWINDOWTYPE: default: unreachable(); } if (!predef_target) { return false; } log_verbose("Matching against predefined target %s", predef_target); return c2_string_op(leaf, predef_target); } if (leaf->target_id == C2_L_INVALID_TARGET_ID) { log_debug("Leaf target ID is invalid, skipping. Most likely a list " "postprocessing failure."); return false; } 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, client %#010x (%s)", leaf->tgt, win_id(w), win_client_id(w, false), 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); for (size_t i = 0; i < ntargets; ++i) { 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; } } return false; } 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; } 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(const struct c2_state *state, const struct win *w, const c2_condition_node_ptr leaf) { assert(leaf.type == C2_NODE_TYPE_LEAF); assert(leaf.l); const xcb_window_t wid = (leaf.l->target_on_client ? win_client_id(w, /*fallback_to_self=*/true) : win_id(w)); // Return if wid is missing if (leaf.l->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_node_to_str2(leaf)); unsigned int pattern_type = leaf.l->ptntype; if (pattern_type == C2_L_PTUNDEFINED) { if (leaf.l->target_id == C2_L_INVALID_TARGET_ID) { log_debug("Leaf target ID is invalid, skipping. Most likely a " "list postprocessing failure."); return false; } auto values = &w->c2_state.values[leaf.l->target_id]; if (values->type == C2_PROPERTY_TYPE_STRING) { pattern_type = C2_L_PTSTRING; } else { pattern_type = C2_L_PTINT; } } switch (pattern_type) { // Deal with integer patterns case C2_L_PTINT: return c2_match_once_leaf_int(w, leaf.l); // String patterns case C2_L_PTSTRING: return c2_match_once_leaf_string(state->atoms, w, leaf.l); default: unreachable(); } } /** * Match a window against a single window condition. * * @return true if matched, false otherwise. */ static bool c2_match_once(const struct c2_state *state, const struct win *w, const c2_condition_node_ptr node) { bool result = false; switch (node.type) { case C2_NODE_TYPE_BRANCH: // Handle a branch (and/or/xor operation) if (!node.b) { return false; } log_verbose("Matching window %#010x (%s) against condition %s", win_id(w), w->name, c2_condition_node_to_str2(node)); switch (node.b->op) { case C2_B_OAND: result = (c2_match_once(state, w, node.b->opr1) && c2_match_once(state, w, node.b->opr2)); break; case C2_B_OOR: result = (c2_match_once(state, w, node.b->opr1) || c2_match_once(state, w, node.b->opr2)); break; case C2_B_OXOR: result = (c2_match_once(state, w, node.b->opr1) != c2_match_once(state, w, node.b->opr2)); break; default: unreachable(); } log_debug("(%#010x): branch: result = %d, pattern = %s", win_id(w), result, c2_condition_node_to_str2(node)); break; case C2_NODE_TYPE_TRUE: return true; case C2_NODE_TYPE_LEAF: // A leaf if (node.l == NULL) { return false; } result = c2_match_once_leaf(state, w, node); log_debug("(%#010x): leaf: result = %d, client = %#010x, pattern = %s", win_id(w), result, win_client_id(w, false), c2_condition_node_to_str2(node)); break; default: unreachable(); } // Postprocess the result if (node.neg) { result = !result; } return result; } c2_condition *c2_new_true(struct list_node *list) { auto ret = ccalloc(1, c2_condition); ret->root = (c2_condition_node_ptr){.type = C2_NODE_TYPE_TRUE}; if (list) { list_insert_after(list, &ret->siblings); } return ret; } /** * Match a window against a condition linked list. * * @param cache a place to cache the last matched condition * @param pdata a place to return the data * @return true if matched, false otherwise. */ bool c2_match(struct c2_state *state, const struct win *w, const struct list_node *conditions, void **pdata) { // Then go through the whole linked list list_foreach(c2_condition, i, conditions, siblings) { if (c2_match_once(state, w, i->root)) { if (pdata) { *pdata = i->data; } return true; } } return false; } /// Match a window against the first condition in a condition linked list. bool c2_match_one(const struct c2_state *state, const struct win *w, const c2_condition *condition, void **pdata) { if (!condition) { return false; } if (c2_match_once(state, w, condition->root)) { if (pdata) { *pdata = condition->data; } return true; } return false; } /// Return user data stored in a condition. void *c2_condition_get_data(const c2_condition *condition) { return condition->data; } void *c2_condition_set_data(c2_condition *condition, void *data) { void *old = condition->data; condition->data = data; return old; } c2_condition *c2_condition_list_entry(struct list_node *list) { return list == NULL ? NULL : list_entry(list, c2_condition, siblings); } c2_condition *c2_condition_list_next(struct list_node *list, c2_condition *condition) { if (condition == NULL) { return NULL; } if (condition->siblings.next == list) { return NULL; } return c2_condition_list_entry(condition->siblings.next); } c2_condition *c2_condition_list_prev(struct list_node *list, c2_condition *condition) { if (condition == NULL) { return NULL; } if (condition->siblings.prev == list) { return NULL; } return c2_condition_list_entry(condition->siblings.prev); } 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); unsigned int external_capacity = 0; char *external_storage = NULL; 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; } if (value->type == C2_PROPERTY_TYPE_STRING) { external_capacity = value->string_capacity; external_storage = value->string; } else if (value->type == C2_PROPERTY_TYPE_NUMBER && value->length > ARR_SIZE(value->numbers)) { external_capacity = value->array_capacity; external_storage = (char *)value->array; } 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; value->type = C2_PROPERTY_TYPE_NONE; } else 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; } if (value->length > external_capacity) { value->string = crealloc(external_storage, value->length); value->string_capacity = value->length; } else { value->string = external_storage; value->string_capacity = external_capacity; } external_capacity = 0; external_storage = NULL; memcpy(value->string, data, len); if (!nul_terminated) { value->string[len] = '\0'; } } else { unsigned step = reply->format / 8; bool is_signed = reply->type == XCB_ATOM_INTEGER; value->length = len / step; 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)) { const unsigned size = value->length * sizeof(*value->array); if (external_capacity < size) { value->array = (int64_t *)crealloc(external_storage, size); value->array_capacity = size; } else { value->array = (int64_t *)external_storage; value->array_capacity = external_capacity; } external_capacity = 0; external_storage = NULL; storage = value->array; } for (uint32_t i = 0; i < value->length; i++) { auto item = (char *)data + (size_t)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); } } } free(external_storage); } 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; } picom-12.5/src/c2.h000066400000000000000000000112001471504570600140170ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2018 Yuxuan Shui #pragma once #include #include #include #include "utils/list.h" typedef struct c2_condition c2_condition; 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 win; struct list_node; typedef void (*c2_userdata_free)(void *); struct c2_condition *c2_parse(struct list_node *list, const char *pattern, void *data); /// 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_condition * c2_parse_with_prefix(struct list_node *list, const char *pattern, void *(*parse_prefix)(const char *input, const char **end, void *), void (*free_value)(void *), void *user_data); void c2_free_condition(c2_condition *lp, c2_userdata_free f); /// 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 win *w, const struct list_node *conditions, void **pdata); bool c2_match_one(const struct c2_state *state, const struct win *w, const c2_condition *condlst, void **pdata); bool c2_list_postprocess(struct c2_state *state, xcb_connection_t *c, struct list_node *list); /// Return user data stored in a condition. void *c2_condition_get_data(const c2_condition *condition); /// Set user data stored in a condition. Return the old user data. void *c2_condition_set_data(c2_condition *condlist, void *data); /// Convert a c2_condition to string. The returned string is only valid until the /// next call to this function, and should not be freed. const char *c2_condition_to_str(const c2_condition *); c2_condition *c2_condition_list_next(struct list_node *list, c2_condition *condition); c2_condition *c2_condition_list_prev(struct list_node *list, c2_condition *condition); c2_condition *c2_condition_list_entry(struct list_node *list); /// Create a new condition list with a single condition that is always true. c2_condition *c2_new_true(struct list_node *list); #define c2_condition_list_foreach(list, i) \ for (c2_condition *i = \ list_is_empty((list)) ? NULL : c2_condition_list_entry((list)->next); \ i; i = c2_condition_list_next(list, i)) #define c2_condition_list_foreach_rev(list, i) \ for (c2_condition *i = \ list_is_empty((list)) ? NULL : c2_condition_list_entry((list)->prev); \ i; i = c2_condition_list_prev(list, i)) #define c2_condition_list_foreach_safe(list, i, n) \ for (c2_condition *i = \ list_is_empty((list)) ? NULL : c2_condition_list_entry((list)->next), \ *n = c2_condition_list_next(list, i); \ i; i = n, n = c2_condition_list_next(list, i)) /** * Destroy a condition list. */ static inline void c2_list_free(struct list_node *list, c2_userdata_free f) { c2_condition_list_foreach_safe(list, i, ni) { c2_free_condition(i, f); } list_init_head(list); } picom-12.5/src/common.h000066400000000000000000000342011471504570600150110ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2018, Yuxuan Shui #pragma once // === Options === // Debug options, enable them using -D in CFLAGS // #define DEBUG_REPAINT 1 // #define DEBUG_EVENTS 1 // #define DEBUG_RESTACK 1 // #define DEBUG_WINMATCH 1 // #define DEBUG_C2 1 // #define DEBUG_GLX_DEBUG_CONTEXT 1 #define MAX_ALPHA (255) // === Includes === // For some special functions #include #include #include #include #include #include #include #include #include #include #include #include #ifdef CONFIG_OPENGL #include "backend/gl/glx.h" #endif // X resource checker #ifdef DEBUG_XRC #include "xrescheck.h" #endif // FIXME This list of includes should get shorter #include "backend/driver.h" #include "config.h" #include "region.h" #include "render.h" #include "utils/statistics.h" #include "wm/defs.h" #include "x.h" // === Constants ===0 #define NS_PER_SEC 1000000000L #define US_PER_SEC 1000000L #define MS_PER_SEC 1000 /// @brief Maximum OpenGL buffer age. #define CGLX_MAX_BUFFER_AGE 5 // Window flags // === Types === typedef struct glx_fbconfig glx_fbconfig_t; struct glx_session; struct atom; struct conv; #ifdef CONFIG_OPENGL #ifdef DEBUG_GLX_DEBUG_CONTEXT typedef GLXContext (*f_glXCreateContextAttribsARB)(Display *dpy, GLXFBConfig config, GLXContext share_context, Bool direct, const int *attrib_list); typedef void (*GLDEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, GLvoid *userParam); typedef void (*f_DebugMessageCallback)(GLDEBUGPROC, void *userParam); #endif typedef struct glx_prog_main { /// GLSL program. GLuint prog; /// Location of uniform "opacity" in window GLSL program. GLint unifm_opacity; /// Location of uniform "invert_color" in blur GLSL program. GLint unifm_invert_color; /// Location of uniform "tex" in window GLSL program. GLint unifm_tex; /// Location of uniform "time" in window GLSL program. GLint unifm_time; } glx_prog_main_t; #define GLX_PROG_MAIN_INIT \ { \ .prog = 0, .unifm_opacity = -1, .unifm_invert_color = -1, \ .unifm_tex = -1, .unifm_time = -1 \ } #else struct glx_prog_main {}; #endif #define PAINT_INIT \ { .pixmap = XCB_NONE, .pict = XCB_NONE } /// Linked list type of atoms. typedef struct _latom { xcb_atom_t atom; 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 === /// ev_io for X connection ev_io xiow; /// Timeout for delayed unredirection. ev_timer unredir_timer; /// 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; /// Signal handler for SIGUSR1 ev_signal usr1_signal; /// Signal handler for SIGINT ev_signal int_signal; // === Backend related === /// backend data backend_t *backend_data; /// backend blur context void *backend_blur_context; /// graphic drivers used enum driver drivers; /// file watch handle void *file_watch_handle; /// libev mainloop struct ev_loop *loop; /// Shaders struct shader_info *shaders; // === Display related === /// X connection struct x_connection c; /// Width of root window. int root_width; /// 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; /// Whether the root tile is filled by us. bool root_tile_fill; /// Picture of the root window background. paint_t root_tile_paint; /// The backend data the root pixmap bound to 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 /// mode. xcb_render_picture_t root_picture; /// A Picture acting as the painting target. xcb_render_picture_t tgt_picture; /// Temporary buffer to paint to before sending to display. paint_t tgt_buffer; /// Window ID of the window we register as a symbol. xcb_window_t reg_win; #ifdef CONFIG_OPENGL /// Pointer to GLX data. struct glx_session *psglx; /// 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; #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; /// 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 === /// Whether there is a pending quest to get the focused window bool pending_focus_check; /// 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; /// 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 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 different kinds of updates so we don't // waste our time. /// Whether there are pending updates, like window creation, etc. bool pending_updates : 1; // === Expose event related === /// Pointer to an array of XRectangle-s of exposed region. /// This is a reuse temporary buffer for handling root expose events. /// This is a dynarr. rect_t *expose_rects; struct wm *wm; struct window_options window_options_default; // === Shadow/dimming related === /// 1x1 black Picture. xcb_render_picture_t black_picture; /// 1x1 Picture of the shadow color. xcb_render_picture_t cshadow_picture; /// 1x1 white Picture. xcb_render_picture_t white_picture; /// Backend shadow context. struct backend_shadow_context *shadow_context; // for shadow precomputation // === Software-optimization-related === /// Nanosecond offset of the first painting. long paint_tm_offset; #ifdef CONFIG_VSYNC_DRM // === DRM VSync related === /// File descriptor of DRI device file. Used for DRM VSync. int drm_fd; #endif // === X extension related === /// Event base number for X Fixes extension. int xfixes_event; /// Error base number for X Fixes extension. int xfixes_error; /// Event base number for X Damage extension. int damage_event; /// Error base number for X Damage extension. int damage_error; /// Event base number for X Render extension. int render_event; /// Error base number for X Render extension. int render_error; /// Event base number for X Composite extension. int composite_event; /// Error base number for X Composite extension. int composite_error; /// Major opcode for X Composite extension. int composite_opcode; /// Whether X Shape extension exists. bool shape_exists; /// Event base number for X Shape extension. int shape_event; /// Error base number for X Shape extension. int shape_error; /// Whether X RandR extension exists. bool randr_exists; /// Event base number for X RandR extension. int randr_event; /// Error base number for X RandR extension. int randr_error; /// Whether X Present extension exists. bool present_exists; /// Whether X GLX extension exists. bool glx_exists; /// Event base number for X GLX extension. int glx_event; /// Error base number for X GLX extension. int glx_error; /// Information about monitors. struct x_monitors monitors; /// Whether X Sync extension exists. bool xsync_exists; /// Event base number for X Sync extension. int xsync_event; /// Error base number for X Sync extension. int xsync_error; /// Whether X Render convolution filter exists. bool xrfilter_convolution_exists; // === Atoms === struct atom *atoms; #ifdef CONFIG_DBUS // === DBus related === struct cdbus_data *dbus_data; #endif int (*vsync_wait)(session_t *); } session_t; /// Enumeration for window event hints. typedef enum { WIN_EVMODE_UNKNOWN, WIN_EVMODE_FRAME, WIN_EVMODE_CLIENT } win_evmode_t; 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); // === Functions === /** * Subtracting two struct timespec values. * * Taken from glibc manual. * * Subtract the `struct timespec' values X and Y, * storing the result in RESULT. * Return 1 if the difference is negative, otherwise 0. */ static inline int timespec_subtract(struct timespec *result, struct timespec *x, struct timespec *y) { /* Perform the carry for the later subtraction by updating y. */ if (x->tv_nsec < y->tv_nsec) { long nsec = (y->tv_nsec - x->tv_nsec) / NS_PER_SEC + 1; y->tv_nsec -= NS_PER_SEC * nsec; y->tv_sec += nsec; } if (x->tv_nsec - y->tv_nsec > NS_PER_SEC) { long nsec = (x->tv_nsec - y->tv_nsec) / NS_PER_SEC; y->tv_nsec += NS_PER_SEC * nsec; y->tv_sec -= nsec; } /* Compute the time remaining to wait. tv_nsec is certainly positive. */ result->tv_sec = x->tv_sec - y->tv_sec; result->tv_nsec = x->tv_nsec - y->tv_nsec; /* Return 1 if result is negative. */ return x->tv_sec < y->tv_sec; } /** * Get current time in struct timeval. */ static inline struct timeval get_time_timeval(void) { struct timeval tv = {0, 0}; gettimeofday(&tv, NULL); // Return a time of all 0 if the call fails return tv; } /** * Get current time in struct timespec. * * Note its starting time is unspecified. */ static inline struct timespec get_time_timespec(void) { struct timespec tm = {0, 0}; clock_gettime(CLOCK_MONOTONIC, &tm); // Return a time of all 0 if the call fails return tm; } /** * Return the painting target window. */ static inline xcb_window_t get_tgt_window(session_t *ps) { return ps->overlay != XCB_NONE ? ps->overlay : ps->c.screen_info->root; } /** * Check if current backend uses GLX. */ static inline bool bkend_use_glx(session_t *ps) { return BKEND_GLX == ps->o.legacy_backend || BKEND_XR_GLX_HYBRID == ps->o.legacy_backend; } /** * Determine if a window has a specific property. * * @param ps current session * @param w window to check * @param atom atom of property to check * @return true if it has the attribute, false otherwise */ static inline bool wid_has_prop(xcb_connection_t *c, xcb_window_t w, xcb_atom_t atom) { auto r = xcb_get_property_reply( c, xcb_get_property(c, 0, w, atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 0), NULL); if (!r) { return false; } auto rtype = r->type; free(r); if (rtype != XCB_NONE) { return true; } return false; } void force_repaint(session_t *ps); /** * Set a bool array of all wintypes to true. */ static inline void wintype_arr_enable(bool arr[]) { wintype_t i; for (i = 0; i < NUM_WINTYPES; ++i) { 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); } } picom-12.5/src/compiler.h000066400000000000000000000071741471504570600153440ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #pragma once #ifdef HAS_STDC_PREDEF_H #include #endif // clang-format off #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)) #define unlikely_if(x) if (unlikely(x)) #ifndef __has_attribute # if __GNUC__ >= 4 # define __has_attribute(x) 1 # else # define __has_attribute(x) 0 # endif #endif #if __has_attribute(const) # define attr_const __attribute__((const)) #else # define attr_const #endif #if __has_attribute(format) # define attr_printf(a, b) __attribute__((format(printf, a, b))) #else # define attr_printf(a, b) #endif #if __has_attribute(pure) # define attr_pure __attribute__((pure)) #else # define attr_pure #endif #if __has_attribute(unused) # define attr_unused __attribute__((unused)) #else # define attr_unused #endif #if __has_attribute(warn_unused_result) # define attr_warn_unused_result __attribute__((warn_unused_result)) #else # define attr_warn_unused_result #endif // An alias for convenience #define must_use attr_warn_unused_result #if __has_attribute(nonnull) # define attr_nonnull(...) __attribute__((nonnull(__VA_ARGS__))) # define attr_nonnull_all __attribute__((nonnull)) #else # define attr_nonnull(...) # define attr_nonnull_all #endif #if __has_attribute(returns_nonnull) # define attr_ret_nonnull __attribute__((returns_nonnull)) #else # define attr_ret_nonnull #endif #if __has_attribute(deprecated) # define attr_deprecated __attribute__((deprecated)) #else # define attr_deprecated #endif #if __has_attribute(malloc) # define attr_malloc __attribute__((malloc)) #else # define attr_malloc #endif #if __has_attribute(fallthrough) # define fallthrough() __attribute__((fallthrough)) #else # 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 # if __has_attribute(noreturn) # define attr_noret __attribute__((noreturn)) # else # define attr_noret # endif #endif #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 # define __has_include(x) 0 #endif #ifndef __has_builtin # define __has_builtin(x) 0 #endif #if !defined(__STDC_NO_THREADS__) && __has_include() # include #elif __STDC_VERSION__ >= 201112L # define thread_local _Thread_local #elif defined(__GNUC__) || defined(__clang__) # define thread_local __thread #else # define thread_local _Pragma("GCC error \"No thread local storage support\"") __error__ #endif #define PICOM_PUBLIC_API __attribute__((visibility("default"))) // clang-format on typedef unsigned long ulong; typedef unsigned int uint; static inline int attr_const popcntul(unsigned long a) { return __builtin_popcountl(a); } /// Get the index of the lowest bit set in a number. The result is undefined if /// `a` is 0. static inline int attr_const index_of_lowest_one(unsigned a) { #if __has_builtin(__builtin_ctz) return __builtin_ctz(a); #else auto lowbit = (a & -a); int r = (lowbit & 0xAAAAAAAA) != 0; r |= ((lowbit & 0xCCCCCCCC) != 0) << 1; r |= ((lowbit & 0xF0F0F0F0) != 0) << 2; r |= ((lowbit & 0xFF00FF00) != 0) << 3; r |= ((lowbit & 0xFFFF0000) != 0) << 4; return r; #endif } picom-12.5/src/config.c000066400000000000000000000505631471504570600147720ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2013 Richard Grenville // Copyright (c) 2018 Yuxuan Shui #include #include #include #include #include #include #include #include #include #include #include #include #include // for xcb_render_fixed_t, XXX #include #include #include "common.h" #include "log.h" #include "utils/dynarr.h" #include "utils/kernel.h" #include "utils/str.h" #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. */ bool parse_long(const char *s, long *dest) { const char *endptr = NULL; long val = strtol(s, (char **)&endptr, 0); if (!endptr || endptr == s) { log_error("Invalid number: %s", s); return false; } while (isspace((unsigned char)*endptr)) { ++endptr; } if (*endptr) { log_error("Trailing characters: %s", s); return false; } *dest = val; return true; } /** * Parse an int number. */ bool parse_int(const char *s, int *dest) { long val; if (!parse_long(s, &val)) { return false; } if (val > INT_MAX || val < INT_MIN) { log_error("Number exceeded int limits: %ld", val); return false; } *dest = (int)val; return true; } /** * Parse a floating-point number in from a string, * also strips the trailing space and comma after the number. * * @param[in] src string to parse * @param[out] dest return the number parsed from the string * @return pointer to the last character parsed */ const char *parse_readnum(const char *src, double *dest) { const char *pc = NULL; double val = strtod_simple(src, &pc); if (!pc || pc == src) { log_error("No number found: %s", src); return src; } while (*pc && (isspace((unsigned char)*pc) || *pc == ',')) { ++pc; } *dest = val; return pc; } int parse_blur_method(const char *src) { if (strcmp(src, "box") == 0) { return BLUR_METHOD_BOX; } if (strcmp(src, "dual_kawase") == 0) { return BLUR_METHOD_DUAL_KAWASE; } 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; } /** * Parse a matrix. * * @param[in] src the blur kernel string * @param[out] endptr return where the end of kernel is in the string * @param[out] hasneg whether the kernel has negative values */ static conv *parse_blur_kern(const char *src, const char **endptr) { int width = 0, height = 0; const char *pc = NULL; // Get matrix width and height double val = 0.0; if (src == (pc = parse_readnum(src, &val))) { goto err1; } src = pc; width = (int)val; if (src == (pc = parse_readnum(src, &val))) { goto err1; } src = pc; height = (int)val; // Validate matrix width and height if (width <= 0 || height <= 0) { log_error("Blue kernel width/height can't be negative."); goto err1; } if (!(width % 2 && height % 2)) { log_error("Blur kernel width/height must be odd."); goto err1; } 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)); // Read elements int skip = height / 2 * width + width / 2; for (int i = 0; i < width * height; ++i) { // Ignore the center element if (i == skip) { matrix->data[i] = 1; continue; } if (src == (pc = parse_readnum(src, &val))) { goto err2; } src = pc; matrix->data[i] = val; } // Detect trailing characters for (; *pc && *pc != ';'; pc++) { if (!isspace((unsigned char)*pc) && *pc != ',') { // TODO(yshui) isspace is locale aware, be careful log_error("Trailing characters in blur kernel string."); goto err2; } } // Jump over spaces after ';' if (*pc == ';') { pc++; while (*pc && isspace((unsigned char)*pc)) { ++pc; } } // Require an end of string if endptr is not provided, otherwise // copy end pointer to endptr if (endptr) { *endptr = pc; } else if (*pc) { log_error("Only one blur kernel expected."); goto err2; } // Fill in width and height matrix->w = width; matrix->h = height; return matrix; err2: free(matrix); err1: return NULL; } /** * Parse a list of convolution kernels. * * @param[in] src string to parse * @param[out] hasneg whether any of the kernels have negative values * @return the kernels */ 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; const char *kern_str; } CONV_KERN_PREDEF[] = { {"3x3box", "3,3,1,1,1,1,1,1,1,1,"}, {"5x5box", "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,"}, {"7x7box", "7,7,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1," "1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,"}, {"3x3gaussian", "3,3,0.243117,0.493069,0.243117,0.493069,0.493069,0.243117,0." "493069,0.243117,"}, {"5x5gaussian", "5,5,0.003493,0.029143,0.059106,0.029143,0.003493,0.029143,0." "243117,0.493069,0.243117,0.029143,0.059106,0.493069,0." "493069,0.059106,0.029143,0.243117,0.493069,0.243117,0." "029143,0.003493,0.029143,0.059106,0.029143,0.003493,"}, {"7x7gaussian", "7,7,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." "000003,0.000102,0.003493,0.029143,0.059106,0.029143,0." "003493,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.003493,0." "029143,0.059106,0.029143,0.003493,0.000102,0.000003,0." "000102,0.000849,0.001723,0.000849,0.000102,0.000003,"}, {"9x9gaussian", "9,9,0.000000,0.000000,0.000001,0.000006,0.000012,0.000006,0.000001,0." "000000,0.000000,0.000000,0.000003,0.000102,0.000849,0.001723,0.000849,0." "000102,0.000003,0.000000,0.000001,0.000102,0.003493,0.029143,0.059106,0." "029143,0.003493,0.000102,0.000001,0.000006,0.000849,0.029143,0.243117,0." "493069,0.243117,0.029143,0.000849,0.000006,0.000012,0.001723,0.059106,0." "493069,0.493069,0.059106,0.001723,0.000012,0.000006,0.000849,0.029143,0." "243117,0.493069,0.243117,0.029143,0.000849,0.000006,0.000001,0.000102,0." "003493,0.029143,0.059106,0.029143,0.003493,0.000102,0.000001,0.000000,0." "000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0.000000,0." "000000,0.000000,0.000001,0.000006,0.000012,0.000006,0.000001,0.000000,0." "000000,"}, {"11x11gaussian", "11,11,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0." "000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000001,0." "000006,0.000012,0.000006,0.000001,0.000000,0.000000,0.000000,0.000000,0." "000000,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0.000003,0." "000000,0.000000,0.000000,0.000001,0.000102,0.003493,0.029143,0.059106,0." "029143,0.003493,0.000102,0.000001,0.000000,0.000000,0.000006,0.000849,0." "029143,0.243117,0.493069,0.243117,0.029143,0.000849,0.000006,0.000000,0." "000000,0.000012,0.001723,0.059106,0.493069,0.493069,0.059106,0.001723,0." "000012,0.000000,0.000000,0.000006,0.000849,0.029143,0.243117,0.493069,0." "243117,0.029143,0.000849,0.000006,0.000000,0.000000,0.000001,0.000102,0." "003493,0.029143,0.059106,0.029143,0.003493,0.000102,0.000001,0.000000,0." "000000,0.000000,0.000003,0.000102,0.000849,0.001723,0.000849,0.000102,0." "000003,0.000000,0.000000,0.000000,0.000000,0.000000,0.000001,0.000006,0." "000012,0.000006,0.000001,0.000000,0.000000,0.000000,0.000000,0.000000,0." "000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0." "000000,"}, }; *count = 0; 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, count); } } int nkernels = 1; for (int i = 0; src[i]; i++) { if (src[i] == ';') { nkernels++; } } struct conv **ret = ccalloc(nkernels, struct conv *); int i = 0; const char *pc = src; // Continue parsing until the end of source string i = 0; while (pc && *pc) { assert(i < nkernels); ret[i] = parse_blur_kern(pc, &pc); if (!ret[i]) { for (int j = 0; j < i; j++) { free(ret[j]); } free(ret); return NULL; } i++; } if (i > 1) { log_warn("You are seeing this message because you are using " "multipass blur. Please report an issue to us so we know " "multipass blur is actually been used. Otherwise it might be " "removed in future releases"); } *count = i; return ret; } void *parse_numeric_prefix(const char *src, const char **end, void *user_data) { int *minmax = user_data; *end = NULL; if (!src) { return NULL; } // Find numeric value char *endptr = NULL; long val = strtol(src, &endptr, 0); if (!endptr || endptr == src) { log_error("No number specified: %s", src); return NULL; } if (val < minmax[0] || val > minmax[1]) { log_error("Number not in range (%d <= n <= %d): %s", minmax[0], minmax[1], src); return NULL; } // Skip over spaces while (*endptr && isspace((unsigned char)*endptr)) { ++endptr; } if (':' != *endptr) { log_error("Number separator (':') not found: %s", src); return NULL; } ++endptr; *end = endptr; 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; } return NULL; } /** * 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// */ char *locate_auxiliary_file(const char *scope, const char *path, const char *include_dir) { if (!path || strlen(path) == 0) { return NULL; } // Filename is absolute path, so try to load from there if (path[0] == '/') { if (access(path, O_RDONLY) == 0) { return realpath(path, NULL); } } // 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; } } // 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; } } // 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 NULL; } 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)}, {"consistent_buffer_age", NULL , offsetof(struct debug_options, consistent_buffer_age)}, }; // 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 (debug_options_entries[i].name[name_len] != '\0') { continue; } 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); } 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); } bool load_plugin(const char *name, const char *include_dir) { scoped_charp path = locate_auxiliary_file("plugins", optarg, include_dir); void *handle = NULL; if (!path) { handle = dlopen(name, RTLD_LAZY); } else { log_debug("Plugin %s resolved to %s", name, path); handle = dlopen(path, RTLD_LAZY); } return handle != NULL; } bool parse_config(options_t *opt, const char *config_file) { // clang-format off *opt = (struct options){ .legacy_backend = BKEND_XRENDER, .use_legacy_backends = false, .glx_no_stencil = false, .mark_wmwin_focused = false, .mark_ovredir_focused = false, .detect_rounded_corners = false, .resize_damage = 0, .unredir_if_possible = false, .unredir_if_possible_blacklist = NULL, .unredir_if_possible_delay = 0, .redirected_force = UNSET, .stoppaint_force = UNSET, .dbus = false, .benchmark = 0, .benchmark_wid = XCB_NONE, .logpath = NULL, .log_level = LOG_LEVEL_WARN, .use_damage = true, .frame_pacing = true, .shadow_red = 0.0, .shadow_green = 0.0, .shadow_blue = 0.0, .shadow_radius = 18, .shadow_offset_x = -15, .shadow_offset_y = -15, .shadow_opacity = .75, .shadow_blacklist = NULL, .shadow_ignore_shaped = false, .crop_shadow_to_monitor = false, .shadow_clip_list = NULL, .corner_radius = 0, .fade_in_step = 0.028, .fade_out_step = 0.03, .fade_delta = 10, .no_fading_openclose = false, .no_fading_destroyed_argb = false, .fade_blacklist = NULL, .inactive_opacity = 1.0, .inactive_opacity_override = false, .active_opacity = 1.0, .frame_opacity = 1.0, .detect_client_opacity = false, .blur_method = BLUR_METHOD_NONE, .blur_radius = 3, .blur_deviation = 0.84089642, .blur_strength = 5, .blur_background_frame = false, .blur_background_fixed = false, .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, .opacity_rules = NULL, .max_brightness = 1.0, .use_ewmh_active_win = false, .focus_blacklist = NULL, .detect_transient = false, .detect_client_leader = false, .no_ewmh_fullscreen = false, .track_leader = false, .rounded_corners_blacklist = NULL, .rules = NULL, }; // clang-format on list_init_head(&opt->included_config_files); list_init_head(&opt->unredir_if_possible_blacklist); list_init_head(&opt->paint_blacklist); list_init_head(&opt->shadow_blacklist); list_init_head(&opt->shadow_clip_list); list_init_head(&opt->fade_blacklist); list_init_head(&opt->blur_background_blacklist); list_init_head(&opt->invert_color_list); list_init_head(&opt->window_shader_fg_rules); list_init_head(&opt->opacity_rules); list_init_head(&opt->rounded_corners_blacklist); list_init_head(&opt->corner_radius_rules); list_init_head(&opt->focus_blacklist); list_init_head(&opt->transparent_clipping_blacklist); list_init_head(&opt->rules); opt->all_scripts = dynarr_new(struct script *, 4); return parse_config_libconfig(opt, config_file); } picom-12.5/src/config.h000066400000000000000000000435101471504570600147710ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2013 Richard Grenville // Copyright (c) 2018 Yuxuan Shui #pragma once /// Common functions and definitions for configuration parsing /// Used for command line arguments and config files #include #include #include #include #include // for xcb_render_fixed_t, XXX #include #include #include #include #include "compiler.h" #include "log.h" #include "utils/kernel.h" #include "utils/list.h" #include "wm/defs.h" typedef struct session session_t; /// @brief Possible backends enum backend { BKEND_XRENDER, BKEND_GLX, BKEND_XR_GLX_HYBRID, BKEND_DUMMY, BKEND_EGL, NUM_BKEND, }; typedef struct win_option_mask { bool shadow : 1; bool fade : 1; bool focus : 1; bool blur_background : 1; bool full_shadow : 1; bool redir_ignore : 1; bool opacity : 1; bool clip_shadow_above : 1; } win_option_mask_t; typedef struct win_option { bool shadow; bool fade; bool focus; bool blur_background; bool full_shadow; bool redir_ignore; double opacity; bool clip_shadow_above; } win_option_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, }; enum animation_trigger { /// When a hidden window is shown ANIMATION_TRIGGER_SHOW = 0, /// When a window is hidden ANIMATION_TRIGGER_HIDE, /// When window opacity is increased ANIMATION_TRIGGER_INCREASE_OPACITY, /// When window opacity is decreased ANIMATION_TRIGGER_DECREASE_OPACITY, /// When a new window opens ANIMATION_TRIGGER_OPEN, /// When a window is closed ANIMATION_TRIGGER_CLOSE, /// When a window's geometry changes ANIMATION_TRIGGER_GEOMETRY, ANIMATION_TRIGGER_INVALID, ANIMATION_TRIGGER_COUNT = ANIMATION_TRIGGER_INVALID, }; static const char *animation_trigger_names[] attr_unused = { [ANIMATION_TRIGGER_SHOW] = "show", [ANIMATION_TRIGGER_HIDE] = "hide", [ANIMATION_TRIGGER_INCREASE_OPACITY] = "increase-opacity", [ANIMATION_TRIGGER_DECREASE_OPACITY] = "decrease-opacity", [ANIMATION_TRIGGER_OPEN] = "open", [ANIMATION_TRIGGER_CLOSE] = "close", [ANIMATION_TRIGGER_GEOMETRY] = "geometry", }; struct script; struct win_script { /// A running animation can be configured to prevent other animations from /// starting. uint64_t suppressions; struct script *script; /// true if this script is generated by us, false if this is a user choice. int output_indices[NUM_OF_WIN_SCRIPT_OUTPUTS]; bool is_generated; }; 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; /// When using damage, replaying an apitrace becomes non-deterministic, because /// the buffer age we got when we rendered will be different from the buffer age /// apitrace gets when it replays. When this option is enabled, we saves the /// contents of each rendered frame, and at the beginning of each render, we /// restore the content of the back buffer based on the buffer age we get, /// ensuring no matter what buffer age apitrace gets during replay, the result /// will be the same. int consistent_buffer_age; }; extern struct debug_options global_debug_options; struct included_config_file { char *path; struct list_node siblings; }; enum window_unredir_option { /// This window should trigger unredirection if it meets certain conditions, and /// it should terminate unredirection otherwise. Termination of unredir is always /// suppressed if there is another window triggering unredirection, this is the /// same for `WINDOW_UNREDIR_TERMINATE` as well. /// /// This is the default choice for windows. WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE, /// This window should trigger unredirection if it meets certain conditions. /// Otherwise it should have no effect on the compositor's redirection status. WINDOW_UNREDIR_WHEN_POSSIBLE, /// This window should always take the compositor out of unredirection, and never /// trigger unredirection. WINDOW_UNREDIR_TERMINATE, /// This window should not cause either redirection or unredirection. WINDOW_UNREDIR_PASSIVE, /// This window always trigger unredirection WINDOW_UNREDIR_FORCED, /// Sentinel value WINDOW_UNREDIR_INVALID, }; struct window_maybe_options { /// Radius of rounded window corners, -1 means not set. int corner_radius; /// Window opacity, NaN means not set. double opacity; /// Window dim level, NaN means not set. double dim; /// The name of the custom fragment shader for this window. NULL means not set. const char *shader; /// Whether transparent clipping is excluded by the rules. enum tristate transparent_clipping; /// Whether a window has shadow. enum tristate shadow; /// Whether to invert window color. enum tristate invert_color; /// Whether to blur window background. enum tristate blur_background; /// Whether this window should fade. enum tristate fade; /// Do not paint shadow over this window. enum tristate clip_shadow_above; /// Whether the window is painted. enum tristate paint; /// Whether this window should be considered for unredirect-if-possible. enum window_unredir_option unredir; /// Whether shadow should be rendered beneath this window. enum tristate full_shadow; /// Window specific animations struct win_script animations[ANIMATION_TRIGGER_COUNT]; }; /// Like `window_maybe_options`, but all fields are guaranteed to be set. struct window_options { double opacity; double dim; const char *shader; unsigned int corner_radius; enum window_unredir_option unredir; bool transparent_clipping; bool shadow; bool invert_color; bool blur_background; bool fade; bool clip_shadow_above; bool paint; bool full_shadow; struct win_script animations[ANIMATION_TRIGGER_COUNT]; }; static inline bool win_options_no_damage(const struct window_options *a, const struct window_options *b) { // Animation changing does not immediately change how window is rendered, so // they don't cause damage; all other options do. return a->opacity == b->opacity && a->dim == b->dim && a->corner_radius == b->corner_radius && a->unredir == b->unredir && a->transparent_clipping == b->transparent_clipping && a->shadow == b->shadow && a->invert_color == b->invert_color && a->blur_background == b->blur_background && a->fade == b->fade && a->clip_shadow_above == b->clip_shadow_above && a->paint == b->paint && a->full_shadow == b->full_shadow && a->shader == b->shader; } /// Structure representing all options. typedef struct options { // === Config === /// Path to the config file char *config_file_path; /// List of config files included by the main config file struct list_node included_config_files; // === Debugging === bool monitor_repaint; bool print_diagnostics; /// Render to a separate window instead of taking over the screen bool debug_mode; /// For picom-inspect only, dump windows in a loop bool inspect_monitor; xcb_window_t inspect_win; // === General === /// Use the legacy backends? bool use_legacy_backends; /// Path to write PID to. char *write_pid_path; /// Name of the backend struct backend_info *backend; /// The backend in use (for legacy backends). int legacy_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; /// Whether to avoid using stencil buffer under GLX backend. Might be /// unsafe. bool glx_no_stencil; /// Whether to avoid rebinding pixmap on window damage. bool glx_no_rebind_pixmap; /// Custom fragment shader for painting windows, as a string. char *glx_fshader_win_str; /// Whether to detect rounded corners. bool detect_rounded_corners; /// Force painting of window content with blending. bool force_win_blend; /// Resize damage for a specific number of pixels. int resize_damage; /// Whether to unredirect all windows if a full-screen opaque window /// is detected. bool unredir_if_possible; /// List of conditions of windows to ignore as a full-screen window /// when determining if a window could be unredirected. struct list_node unredir_if_possible_blacklist; /// Delay before unredirecting screen, in milliseconds. int unredir_if_possible_delay; /// Forced redirection setting through D-Bus. switch_t redirected_force; /// Whether to stop painting. Controlled through D-Bus. switch_t stoppaint_force; /// Whether to enable D-Bus support. bool dbus; /// Path to log file. char *logpath; /// Number of cycles to paint in benchmark mode. 0 for disabled. int benchmark; /// Window to constantly repaint in benchmark mode. 0 for full-screen. xcb_window_t benchmark_wid; /// A list of conditions of windows not to paint. struct list_node paint_blacklist; /// Whether to show all X errors. bool show_all_xerrors; /// Whether to avoid acquiring X Selection. 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 === /// VSync method to use; bool vsync; /// Whether to use glFinish() instead of glFlush() for (possibly) better /// VSync yet probably higher CPU usage. 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. double shadow_red, shadow_green, shadow_blue; int shadow_radius; int shadow_offset_x, shadow_offset_y; double shadow_opacity; /// Shadow blacklist. A linked list of conditions. struct list_node shadow_blacklist; /// Whether bounding-shaped window should be ignored. bool shadow_ignore_shaped; /// 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. struct list_node shadow_clip_list; bool shadow_enable; // === Fading === /// How much to fade in in a single fading step. double fade_in_step; /// How much to fade out in a single fading step. double fade_out_step; /// Fading time delta. In milliseconds. int fade_delta; /// Whether to disable fading on window open/close. bool no_fading_openclose; /// Whether to disable fading on ARGB managed destroyed windows. bool no_fading_destroyed_argb; /// Fading blacklist. A linked list of conditions. struct list_node fade_blacklist; bool fading_enable; // === Opacity === /// Default opacity for inactive windows. /// 32-bit integer with the format of _NET_WM_WINDOW_OPACITY. double inactive_opacity; /// Default opacity for inactive windows. double active_opacity; /// Whether inactive_opacity overrides the opacity set by window /// attributes. bool inactive_opacity_override; /// Frame opacity. Relative to window opacity, also affects shadow /// opacity. double frame_opacity; /// 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 === /// Blur method for background of semi-transparent windows enum blur_method blur_method; // Size of the blur kernel int blur_radius; // Standard deviation for the gaussian blur double blur_deviation; // Strength of the dual_kawase blur int blur_strength; /// Whether to blur background when the window frame is not opaque. /// Implies blur_background. bool blur_background_frame; /// Whether to use fixed blur strength instead of adjusting according /// to window opacity. bool blur_background_fixed; /// Background blur blacklist. A linked list of conditions. struct list_node blur_background_blacklist; /// Blur convolution kernel. 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. struct list_node 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 /// based on window opacity. bool inactive_dim_fixed; /// Conditions of windows to have inverted colors. struct list_node invert_color_list; /// Rules to change window opacity. struct list_node opacity_rules; /// Limit window brightness double max_brightness; // Radius of rounded window corners int corner_radius; /// Rounded corners blacklist. A linked list of conditions. struct list_node rounded_corners_blacklist; /// Rounded corner rules. A linked list of conditions. struct list_node corner_radius_rules; // === Focus related === /// Whether to try to detect WM windows and mark them as focused. bool mark_wmwin_focused; /// Whether to mark override-redirect windows as focused. bool mark_ovredir_focused; /// Whether to use EWMH _NET_ACTIVE_WINDOW to find active window. bool use_ewmh_active_win; /// A list of windows always to be considered focused. struct list_node focus_blacklist; /// Whether to do window grouping with WM_TRANSIENT_FOR. bool detect_transient; /// Whether to do window grouping with WM_CLIENT_LEADER. bool detect_client_leader; // === Calculated === /// Whether we need to track window leaders. bool track_leader; // Don't use EWMH to detect fullscreen applications bool no_ewmh_fullscreen; // 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 struct list_node transparent_clipping_blacklist; bool dithered_present; // === Animation === struct win_script animations[ANIMATION_TRIGGER_COUNT]; /// Array of all the scripts used in `animations`. This is a dynarr. struct script **all_scripts; struct list_node rules; bool has_both_style_of_rules; } options_t; extern const char *const BACKEND_STRS[NUM_BKEND + 1]; bool load_plugin(const char *name, const char *include_dir); 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 *, 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 *); const char *xdg_config_home(void); char **xdg_config_dirs(void); /// Parse a configuration file /// Returns the actually config_file name used, allocated on heap /// Outputs: /// 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 bool parse_config_libconfig(options_t *, const char *config_file); /// Parse a configuration file is that is enabled, also initialize the winopt_mask with /// default values /// Outputs and returns: /// same as parse_config_libconfig bool parse_config(options_t *, const char *config_file); /** * Parse a backend option argument. */ static inline attr_pure int parse_backend(const char *str) { for (int i = 0; BACKEND_STRS[i]; ++i) { if (strcasecmp(str, BACKEND_STRS[i]) == 0) { return i; } } // Keep compatibility with an old revision containing a spelling mistake... if (strcasecmp(str, "xr_glx_hybird") == 0) { log_warn("backend xr_glx_hybird should be xr_glx_hybrid, the misspelt " "version will be removed soon."); return BKEND_XR_GLX_HYBRID; } // cju wants to use dashes if (strcasecmp(str, "xr-glx-hybrid") == 0) { log_warn("backend xr-glx-hybrid should be xr_glx_hybrid, the alternative " "version will be removed soon."); return BKEND_XR_GLX_HYBRID; } return NUM_BKEND; } /** * Parse a VSync option argument. */ static inline bool parse_vsync(const char *str) { if (strcmp(str, "no") == 0 || strcmp(str, "none") == 0 || strcmp(str, "false") == 0 || strcmp(str, "nah") == 0) { return false; } return true; } /// Generate animation script for legacy fading options void generate_fading_config(struct options *opt); static inline void log_warn_both_style_of_rules(const char *option_name) { log_warn("Option \"%s\" is set along with \"rules\". \"rules\" will take " "precedence, and \"%s\" will have no effect.", option_name, option_name); } enum animation_trigger parse_animation_trigger(const char *trigger); // vim: set noet sw=8 ts=8 : picom-12.5/src/config_libconfig.c000066400000000000000000001166111471504570600170030ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2012-2014 Richard Grenville // Copyright (c) 2018 Yuxuan Shui #include #include #include #include #include #include #include #include "backend/backend.h" #include "c2.h" #include "common.h" #include "config.h" #include "log.h" #include "transition/preset.h" #include "transition/script.h" #include "utils/dynarr.h" #include "utils/misc.h" #include "utils/str.h" #include "wm/win.h" #pragma GCC diagnostic error "-Wunused-parameter" /** * Wrapper of libconfig's config_lookup_int. * * So it takes a pointer to bool. */ static inline int lcfg_lookup_bool(const config_t *config, const char *path, bool *value) { int ival; int ret = config_lookup_bool(config, path, &ival); if (ret) { *value = ival; } return ret; } /// Search for config file under a base directory FILE *open_config_file_at(const char *base, char **out_path) { static const char *config_paths[] = {"/picom.conf", "/picom/picom.conf", "/compton.conf", "/compton/compton.conf"}; for (size_t i = 0; i < ARR_SIZE(config_paths); i++) { char *path = mstrjoin(base, config_paths[i]); FILE *ret = fopen(path, "r"); if (ret && out_path) { *out_path = path; } else { free(path); } if (ret) { if (strstr(config_paths[i], "compton")) { log_warn("This compositor has been renamed to \"picom\", " "the old config file paths is deprecated. " "Please replace the \"compton\"s in the path " "with \"picom\""); } return ret; } } return NULL; } /** * Get a file stream of the configuration file to read. * * Follows the XDG specification to search for the configuration file. */ FILE *open_config_file(const char *cpath, char **ppath) { static const char config_filename_legacy[] = "/.compton.conf"; if (cpath) { FILE *ret = fopen(cpath, "r"); if (ret && ppath) { *ppath = strdup(cpath); } return ret; } // First search for config file in user config directory auto config_home = xdg_config_home(); 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); auto ret = fopen(path, "r"); if (ret && ppath) { *ppath = path; } else { free(path); } if (ret) { return ret; } } // Fall back to config file in system config directory auto config_dirs = xdg_config_dirs(); for (int i = 0; config_dirs[i]; i++) { auto ret = open_config_file_at(config_dirs[i], ppath); if (ret) { free(config_dirs); return ret; } } free(config_dirs); return NULL; } /** * Parse a condition list in configuration file. */ bool must_use parse_cfg_condlst(struct list_node *list, const config_t *pcfg, const char *name) { config_setting_t *setting = config_lookup(pcfg, name); 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(list, 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)) { if (!c2_parse(list, config_setting_get_string(setting), NULL)) { return false; } } return true; } /** * Parse a window corner radius rule list in configuration file. */ static inline bool parse_cfg_condlst_with_prefix(struct list_node *list, 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 == 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( list, 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(list, 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, win_option_t *o, win_option_mask_t *mask) { char *str = mstrjoin("wintypes.", member_name); const config_setting_t *setting = config_lookup(cfg, str); free(str); int ival = 0; if (setting) { if (config_setting_lookup_bool(setting, "shadow", &ival)) { o->shadow = ival; mask->shadow = true; } if (config_setting_lookup_bool(setting, "fade", &ival)) { o->fade = ival; mask->fade = true; } if (config_setting_lookup_bool(setting, "focus", &ival)) { o->focus = ival; mask->focus = true; } if (config_setting_lookup_bool(setting, "blur-background", &ival)) { o->blur_background = ival; mask->blur_background = true; } if (config_setting_lookup_bool(setting, "full-shadow", &ival)) { o->full_shadow = ival; mask->full_shadow = true; } if (config_setting_lookup_bool(setting, "redir-ignore", &ival)) { o->redir_ignore = ival; mask->redir_ignore = true; } if (config_setting_lookup_bool(setting, "clip-shadow-above", &ival)) { o->clip_shadow_above = ival; mask->clip_shadow_above = true; } double fval; if (config_setting_lookup_float(setting, "opacity", &fval)) { o->opacity = normalize_d(fval); mask->opacity = true; } } } enum animation_trigger parse_animation_trigger(const char *trigger) { for (unsigned i = 0; i < ANIMATION_TRIGGER_COUNT; i++) { if (strcasecmp(trigger, animation_trigger_names[i]) == 0) { return i; } } return ANIMATION_TRIGGER_INVALID; } /// Compile a script from `setting` into `result`, return false on failure. /// Only the `script` and `output_indices` fields of `result` will be modified. static bool compile_win_script(struct win_script *result, config_setting_t *setting, char **err) { if (config_setting_lookup(setting, "preset")) { return win_script_parse_preset(result, setting); } struct script_output_info outputs[ARR_SIZE(win_script_outputs)]; memcpy(outputs, win_script_outputs, sizeof(win_script_outputs)); struct script_parse_config parse_config = { .context_info = win_script_context_info, .output_info = outputs, }; result->script = script_compile(setting, parse_config, err); if (result->script == NULL) { return false; } for (int i = 0; i < NUM_OF_WIN_SCRIPT_OUTPUTS; i++) { result->output_indices[i] = outputs[i].slot; } return true; } static bool set_animation(struct win_script *animations, const enum animation_trigger *triggers, int number_of_triggers, struct win_script animation, unsigned line) { bool needed = false; for (int i = 0; i < number_of_triggers; i++) { if (triggers[i] == ANIMATION_TRIGGER_INVALID) { log_error("Invalid trigger defined at line %d", line); continue; } if (animations[triggers[i]].script != NULL) { log_error("Duplicate animation defined for trigger %s at line " "%d, it will be ignored.", animation_trigger_names[triggers[i]], line); continue; } animations[triggers[i]] = animation; needed = true; } return needed; } static bool parse_animation_one(struct win_script *animations, struct script ***all_scripts, config_setting_t *setting) { struct win_script result = {}; auto triggers = config_setting_lookup(setting, "triggers"); if (!triggers) { log_error("Missing triggers in animation script, at line %d", config_setting_source_line(setting)); return false; } if (!config_setting_is_list(triggers) && !config_setting_is_array(triggers) && config_setting_get_string(triggers) == NULL) { log_error("The \"triggers\" option must either be a string, a list, or " "an array, but is none of those at line %d", config_setting_source_line(triggers)); return false; } auto number_of_triggers = config_setting_get_string(triggers) == NULL ? config_setting_length(triggers) : 1; if (number_of_triggers > ANIMATION_TRIGGER_COUNT) { log_error("Too many triggers in animation defined at line %d", config_setting_source_line(triggers)); return false; } if (number_of_triggers == 0) { log_error("Trigger list is empty in animation defined at line %d", config_setting_source_line(triggers)); return false; } enum animation_trigger *trigger_types = alloca(sizeof(enum animation_trigger[number_of_triggers])); const char *trigger0 = config_setting_get_string(triggers); if (trigger0 == NULL) { for (int i = 0; i < number_of_triggers; i++) { auto trigger_i = config_setting_get_string_elem(triggers, i); trigger_types[i] = trigger_i == NULL ? ANIMATION_TRIGGER_INVALID : parse_animation_trigger(trigger_i); } } else { trigger_types[0] = parse_animation_trigger(trigger0); } // script parser shouldn't see this. config_setting_remove(setting, "triggers"); auto suppressions_setting = config_setting_lookup(setting, "suppressions"); if (suppressions_setting != NULL) { auto single_suppression = config_setting_get_string(suppressions_setting); if (!config_setting_is_list(suppressions_setting) && !config_setting_is_array(suppressions_setting) && single_suppression == NULL) { log_error("The \"suppressions\" option must either be a string, " "a list, or an array, but is none of those at line %d", config_setting_source_line(suppressions_setting)); return false; } if (single_suppression != NULL) { auto suppression = parse_animation_trigger(single_suppression); if (suppression == ANIMATION_TRIGGER_INVALID) { log_error("Invalid suppression defined at line %d", config_setting_source_line(suppressions_setting)); return false; } result.suppressions = 1 << suppression; } else { auto len = config_setting_length(suppressions_setting); for (int i = 0; i < len; i++) { auto suppression_str = config_setting_get_string_elem(suppressions_setting, i); if (suppression_str == NULL) { log_error( "The \"suppressions\" option must only " "contain strings, but one of them is not at " "line %d", config_setting_source_line(suppressions_setting)); return false; } auto suppression = parse_animation_trigger(suppression_str); if (suppression == ANIMATION_TRIGGER_INVALID) { log_error( "Invalid suppression defined at line %d", config_setting_source_line(suppressions_setting)); return false; } result.suppressions |= 1U << suppression; } } config_setting_remove(setting, "suppressions"); } char *err; if (!compile_win_script(&result, setting, &err)) { log_error("Failed to parse animation script at line %d: %s", config_setting_source_line(setting), err); free(err); return false; } bool needed = set_animation(animations, trigger_types, number_of_triggers, result, config_setting_source_line(setting)); if (!needed) { script_free(result.script); } else { dynarr_push(*all_scripts, result.script); } return true; } /// `out_scripts`: all the script objects created, this is a dynarr. static void parse_animations(struct win_script *animations, config_setting_t *setting, struct script ***out_scripts) { auto number_of_animations = (unsigned)config_setting_length(setting); for (unsigned i = 0; i < number_of_animations; i++) { auto sub = config_setting_get_elem(setting, (unsigned)i); parse_animation_one(animations, out_scripts, sub); } } #define FADING_TEMPLATE_1 \ "opacity = { " \ " duration = %s; " \ " start = \"window-raw-opacity-before\"; " \ " end = \"window-raw-opacity\"; " \ "};" \ "shadow-opacity = \"opacity\";" #define FADING_TEMPLATE_2 \ "blur-opacity = { " \ " duration = %s; " \ " start = %d; end = %d; " \ "};" static bool compile_win_script_from_string(struct win_script *result, const char *input) { config_t tmp_config; config_setting_t *setting; config_init(&tmp_config); config_set_auto_convert(&tmp_config, true); config_read_string(&tmp_config, input); setting = config_root_setting(&tmp_config); // Since we are compiling scripts we generated, it can't fail. char *err = NULL; bool succeeded = compile_win_script(result, setting, &err); config_destroy(&tmp_config); BUG_ON(err != NULL); return succeeded; } void generate_fading_config(struct options *opt) { // We create stand-in animations for fade-in/fade-out if they haven't be // overwritten scoped_charp str = NULL; size_t len = 0; enum animation_trigger trigger[2]; struct script *scripts[4]; unsigned number_of_scripts = 0; int number_of_triggers = 0; double duration = 1.0 / opt->fade_in_step * opt->fade_delta / 1000.0; if (!safe_isinf(duration) && !safe_isnan(duration) && duration > 0) { scoped_charp duration_str = NULL; dtostr(duration, &duration_str); // Fading in from nothing, i.e. `open` and `show` asnprintf(&str, &len, FADING_TEMPLATE_1 FADING_TEMPLATE_2, duration_str, duration_str, 0, 1); struct win_script fade_in1 = {.is_generated = true}; BUG_ON(!compile_win_script_from_string(&fade_in1, str)); if (opt->animations[ANIMATION_TRIGGER_OPEN].script == NULL && !opt->no_fading_openclose) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_OPEN; } if (opt->animations[ANIMATION_TRIGGER_SHOW].script == NULL) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_SHOW; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_in1, 0)) { scripts[number_of_scripts++] = fade_in1.script; } else { script_free(fade_in1.script); } // Fading for opacity change, for these, the blur opacity doesn't change. asnprintf(&str, &len, FADING_TEMPLATE_1, duration_str); struct win_script fade_in2 = {.is_generated = true}; BUG_ON(!compile_win_script_from_string(&fade_in2, str)); number_of_triggers = 0; if (opt->animations[ANIMATION_TRIGGER_INCREASE_OPACITY].script == NULL) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_INCREASE_OPACITY; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_in2, 0)) { scripts[number_of_scripts++] = fade_in2.script; } else { script_free(fade_in2.script); } } else { log_error("Invalid fade-in setting (step: %f, delta: %d), ignoring.", opt->fade_in_step, opt->fade_delta); } duration = 1.0 / opt->fade_out_step * opt->fade_delta / 1000.0; if (!safe_isinf(duration) && !safe_isnan(duration) && duration > 0) { scoped_charp duration_str = NULL; dtostr(duration, &duration_str); // Fading out to nothing, i.e. `hide` and `close` asnprintf(&str, &len, FADING_TEMPLATE_1 FADING_TEMPLATE_2, duration_str, duration_str, 1, 0); struct win_script fade_out1 = {.is_generated = true}; BUG_ON(!compile_win_script_from_string(&fade_out1, str)); number_of_triggers = 0; if (opt->animations[ANIMATION_TRIGGER_CLOSE].script == NULL && !opt->no_fading_openclose) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_CLOSE; } if (opt->animations[ANIMATION_TRIGGER_HIDE].script == NULL) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_HIDE; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_out1, 0)) { scripts[number_of_scripts++] = fade_out1.script; } else { script_free(fade_out1.script); } // Fading for opacity change asnprintf(&str, &len, FADING_TEMPLATE_1, duration_str); struct win_script fade_out2 = {.is_generated = true}; BUG_ON(!compile_win_script_from_string(&fade_out2, str)); number_of_triggers = 0; if (opt->animations[ANIMATION_TRIGGER_DECREASE_OPACITY].script == NULL) { trigger[number_of_triggers++] = ANIMATION_TRIGGER_DECREASE_OPACITY; } if (set_animation(opt->animations, trigger, number_of_triggers, fade_out2, 0)) { scripts[number_of_scripts++] = fade_out2.script; } else { script_free(fade_out2.script); } } else { log_error("Invalid fade-out setting (step: %f, delta: %d), ignoring.", opt->fade_out_step, opt->fade_delta); } log_debug("Generated %d scripts for fading.", number_of_scripts); dynarr_extend_from(opt->all_scripts, scripts, number_of_scripts); } static enum window_unredir_option parse_unredir_option(config_setting_t *setting) { if (config_setting_type(setting) == CONFIG_TYPE_BOOL) { auto bval = config_setting_get_bool(setting); return bval ? WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE : WINDOW_UNREDIR_TERMINATE; } const char *sval = config_setting_get_string(setting); if (!sval) { log_error("Invalid value for \"unredir\" at line %d. It must be a " "boolean or a string.", config_setting_source_line(setting)); return WINDOW_UNREDIR_INVALID; } if (strcmp(sval, "yes") == 0 || strcmp(sval, "true") == 0 || strcmp(sval, "default") == 0 || strcmp(sval, "when-possible-else-terminate") == 0) { return WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE; } if (strcmp(sval, "preferred") == 0 || strcmp(sval, "when-possible") == 0) { return WINDOW_UNREDIR_WHEN_POSSIBLE; } if (strcmp(sval, "no") == 0 || strcmp(sval, "false") == 0 || strcmp(sval, "terminate") == 0) { return WINDOW_UNREDIR_TERMINATE; } if (strcmp(sval, "passive") == 0) { return WINDOW_UNREDIR_PASSIVE; } if (strcmp(sval, "forced") == 0) { return WINDOW_UNREDIR_FORCED; } log_error("Invalid string value for \"unredir\" at line %d. It must be one of " "\"preferred\", \"passive\", or \"force\". Got \"%s\".", config_setting_source_line(setting), sval); return WINDOW_UNREDIR_INVALID; } static const struct { const char *name; ptrdiff_t offset; } all_window_options[] = { {"fade", offsetof(struct window_maybe_options, fade)}, {"paint", offsetof(struct window_maybe_options, paint)}, {"shadow", offsetof(struct window_maybe_options, shadow)}, {"full-shadow", offsetof(struct window_maybe_options, full_shadow)}, {"invert-color", offsetof(struct window_maybe_options, invert_color)}, {"blur-background", offsetof(struct window_maybe_options, blur_background)}, {"clip-shadow-above", offsetof(struct window_maybe_options, clip_shadow_above)}, {"transparent-clipping", offsetof(struct window_maybe_options, transparent_clipping)}, }; static c2_condition * parse_rule(struct list_node *rules, config_setting_t *setting, struct script ***out_scripts) { if (!config_setting_is_group(setting)) { log_error("Invalid rule at line %d. It must be a group.", config_setting_source_line(setting)); return NULL; } int ival; double fval; const char *sval; c2_condition *rule = NULL; if (config_setting_lookup_string(setting, "match", &sval)) { rule = c2_parse(rules, sval, NULL); if (!rule) { log_error("Failed to parse rule at line %d.", config_setting_source_line(setting)); return NULL; } } else { // If no match condition is specified, it matches all windows rule = c2_new_true(rules); } auto wopts = cmalloc(struct window_maybe_options); *wopts = WIN_MAYBE_OPTIONS_DEFAULT; c2_condition_set_data(rule, wopts); for (size_t i = 0; i < ARR_SIZE(all_window_options); i++) { if (config_setting_lookup_bool(setting, all_window_options[i].name, &ival)) { void *ptr = (char *)wopts + all_window_options[i].offset; *(enum tristate *)ptr = tri_from_bool(ival); } } if (config_setting_lookup_float(setting, "opacity", &fval)) { wopts->opacity = normalize_d(fval); } if (config_setting_lookup_float(setting, "dim", &fval)) { wopts->dim = normalize_d(fval); } if (config_setting_lookup_int(setting, "corner-radius", &ival)) { wopts->corner_radius = ival; } auto unredir_setting = config_setting_lookup(setting, "unredir"); if (unredir_setting) { wopts->unredir = parse_unredir_option(unredir_setting); } auto animations = config_setting_lookup(setting, "animations"); if (animations) { parse_animations(wopts->animations, animations, out_scripts); } config_setting_lookup_string(setting, "shader", &wopts->shader); return rule; } static void parse_rules(struct list_node *rules, config_setting_t *setting, struct script ***out_scripts) { if (!config_setting_is_list(setting)) { log_error("Invalid value for \"rules\" at line %d. It must be a list.", config_setting_source_line(setting)); return; } const auto length = (unsigned int)config_setting_length(setting); for (unsigned int i = 0; i < length; i++) { auto sub = config_setting_get_elem(setting, i); parse_rule(rules, sub, out_scripts); } } static const char ** resolve_include(config_t *cfg, const char *include_dir, const char *path, const char **err) { char *result = locate_auxiliary_file("include", path, include_dir); if (result == NULL) { *err = "Failed to locate included file"; return NULL; } struct options *opt = config_get_hook(cfg); auto included = ccalloc(1, struct included_config_file); included->path = strdup(result); list_insert_after(&opt->included_config_files, &included->siblings); log_debug("Resolved include file \"%s\" to \"%s\"", path, result); const char **ret = ccalloc(2, const char *); ret[0] = result; ret[1] = NULL; return ret; } /** * Parse a configuration file from default location. * * Returns if config is successfully parsed. */ bool 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; int ival = 0; bool bval; double dval = 0.0; // libconfig manages string memory itself, so no need to manually free // anything const char *sval = NULL; bool succeeded = false; f = open_config_file(config_file, &path); if (!f) { free(path); if (config_file) { log_fatal("Failed to read configuration file \"%s\".", config_file); return false; } // No config file found, but that's OK. return true; } config_init(&cfg); #ifdef CONFIG_OPTION_ALLOW_OVERRIDES config_set_options(&cfg, CONFIG_OPTION_ALLOW_OVERRIDES); #endif { char *abspath = realpath(path, NULL); char *parent = dirname(abspath); // path2 may be modified if (parent) { config_set_include_dir(&cfg, parent); } config_set_include_func(&cfg, resolve_include); config_set_hook(&cfg, opt); free(abspath); } { int read_result = config_read(&cfg, f); fclose(f); f = NULL; if (read_result == CONFIG_FALSE) { log_fatal("Error when reading configuration file \"%s\", line " "%d: %s", path, config_error_line(&cfg), config_error_text(&cfg)); goto out; } } config_set_auto_convert(&cfg, 1); // --log-level if (config_lookup_string(&cfg, "log-level", &sval)) { 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(opt->log_level); } } // Get options from the configuration file. We don't do range checking // right now. It will be done later // Load user specified plugins at the very beginning, because list of backends // depends on the plugins loaded. auto plugins = config_lookup(&cfg, "plugins"); if (plugins != NULL) { sval = config_setting_get_string(plugins); if (sval) { if (!load_plugin(sval, NULL)) { log_fatal("Failed to load plugin \"%s\".", sval); goto out; } } else if (config_setting_is_array(plugins) || config_setting_is_list(plugins)) { for (int i = 0; i < config_setting_length(plugins); i++) { sval = config_setting_get_string_elem(plugins, i); if (!sval) { log_fatal("Invalid value for \"plugins\" at line " "%d.", config_setting_source_line(plugins)); goto out; } if (!load_plugin(sval, NULL)) { log_fatal("Failed to load plugin \"%s\".", sval); goto out; } } } else { log_fatal("Invalid value for \"plugins\" at line %d.", config_setting_source_line(plugins)); goto out; } } config_setting_t *rules = config_lookup(&cfg, "rules"); if (rules) { parse_rules(&opt->rules, rules, &opt->all_scripts); c2_condition_list_foreach(&opt->rules, i) { auto data = (struct window_maybe_options *)c2_condition_get_data(i); if (data->shader == NULL) { continue; } data->shader = locate_auxiliary_file( "shaders", data->shader, config_get_include_dir(&cfg)); } } // --dbus lcfg_lookup_bool(&cfg, "dbus", &opt->dbus); // -D (fade_delta) 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)) { opt->fade_in_step = normalize_d(dval); } // -O (fade_out_step) 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) config_lookup_float(&cfg, "shadow-opacity", &opt->shadow_opacity); // -l (shadow_offset_x) config_lookup_int(&cfg, "shadow-offset-x", &opt->shadow_offset_x); // -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)) { opt->inactive_opacity = normalize_d(dval); if (!list_is_empty(&opt->rules)) { log_warn_both_style_of_rules("inactive-opacity"); opt->has_both_style_of_rules = true; } } // --active_opacity if (config_lookup_float(&cfg, "active-opacity", &dval)) { opt->active_opacity = normalize_d(dval); if (!list_is_empty(&opt->rules)) { log_warn_both_style_of_rules("active-opacity"); opt->has_both_style_of_rules = true; } } // --corner-radius config_lookup_int(&cfg, "corner-radius", &opt->corner_radius); 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) 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."); opt->wintype_option[WINTYPE_DROPDOWN_MENU].opacity = dval; opt->wintype_option[WINTYPE_POPUP_MENU].opacity = dval; 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)) { opt->fading_enable = ival; } // --no-fading-open-close lcfg_lookup_bool(&cfg, "no-fading-openclose", &opt->no_fading_openclose); // --no-fading-destroyed-argb lcfg_lookup_bool(&cfg, "no-fading-destroyed-argb", &opt->no_fading_destroyed_argb); // --shadow-red config_lookup_float(&cfg, "shadow-red", &opt->shadow_red); // --shadow-green config_lookup_float(&cfg, "shadow-green", &opt->shadow_green); // --shadow-blue config_lookup_float(&cfg, "shadow-blue", &opt->shadow_blue); // --shadow-color if (config_lookup_string(&cfg, "shadow-color", &sval)) { struct color rgb; rgb = hex_to_rgb(sval); opt->shadow_red = rgb.red; opt->shadow_green = rgb.green; opt->shadow_blue = rgb.blue; } // --shadow-exclude-reg 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 out; } // --inactive-opacity-override if (lcfg_lookup_bool(&cfg, "inactive-opacity-override", &opt->inactive_opacity_override) && !list_is_empty(&opt->rules)) { log_warn_both_style_of_rules("inactive-opacity-override"); opt->has_both_style_of_rules = true; } // --inactive-dim if (config_lookup_float(&cfg, "inactive-dim", &opt->inactive_dim) && !list_is_empty(&opt->rules)) { log_warn_both_style_of_rules("inactive-dim"); opt->has_both_style_of_rules = true; } // --mark-wmwin-focused if (lcfg_lookup_bool(&cfg, "mark-wmwin-focused", &opt->mark_wmwin_focused) && !list_is_empty(&opt->rules)) { log_warn_both_style_of_rules("mark-wmwin-focused"); opt->has_both_style_of_rules = true; } // --mark-ovredir-focused if (lcfg_lookup_bool(&cfg, "mark-ovredir-focused", &opt->mark_ovredir_focused) && !list_is_empty(&opt->rules)) { log_warn_both_style_of_rules("mark-ovredir-focused"); opt->has_both_style_of_rules = true; } // --shadow-ignore-shaped if (lcfg_lookup_bool(&cfg, "shadow-ignore-shaped", &opt->shadow_ignore_shaped) && !list_is_empty(&opt->rules)) { log_warn_both_style_of_rules("shadow-ignore-shaped"); opt->has_both_style_of_rules = true; } // --detect-rounded-corners lcfg_lookup_bool(&cfg, "detect-rounded-corners", &opt->detect_rounded_corners); // --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", &ival)) { log_warn("The refresh-rate %s", deprecation_message); } // --vsync if (config_lookup_string(&cfg, "vsync", &sval)) { 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 out; } lcfg_lookup_bool(&cfg, "vsync", &opt->vsync); // --backend if (config_lookup_string(&cfg, "backend", &sval)) { opt->legacy_backend = parse_backend(sval); opt->backend = backend_find(sval); if (opt->legacy_backend >= NUM_BKEND && opt->backend == NULL) { log_fatal("Invalid backend: %s", sval); goto out; } } // --log-file if (config_lookup_string(&cfg, "log-file", &sval)) { if (*sval != '/') { log_warn("The log-file in your configuration file is not an " "absolute path"); } opt->logpath = strdup(sval); } // --sw-opti if (lcfg_lookup_bool(&cfg, "sw-opti", &bval)) { log_error("The sw-opti %s", deprecation_message); goto out; } // --use-ewmh-active-win lcfg_lookup_bool(&cfg, "use-ewmh-active-win", &opt->use_ewmh_active_win); // --unredir-if-possible lcfg_lookup_bool(&cfg, "unredir-if-possible", &opt->unredir_if_possible); // --unredir-if-possible-delay if (config_lookup_int(&cfg, "unredir-if-possible-delay", &ival)) { if (ival < 0) { log_warn("Invalid unredir-if-possible-delay %d", ival); } else { opt->unredir_if_possible_delay = ival; } } // --inactive-dim-fixed lcfg_lookup_bool(&cfg, "inactive-dim-fixed", &opt->inactive_dim_fixed); // --detect-transient lcfg_lookup_bool(&cfg, "detect-transient", &opt->detect_transient); // --detect-client-leader lcfg_lookup_bool(&cfg, "detect-client-leader", &opt->detect_client_leader); // --no-ewmh-fullscreen lcfg_lookup_bool(&cfg, "no-ewmh-fullscreen", &opt->no_ewmh_fullscreen); // --transparent-clipping lcfg_lookup_bool(&cfg, "transparent-clipping", &opt->transparent_clipping); // --dithered_present lcfg_lookup_bool(&cfg, "dithered-present", &opt->dithered_present); if (!list_is_empty(&opt->rules)) { static const char *rule_list[] = { "transparent-clipping-exclude", "shadow-exclude", "clip-shadow-above", "fade-exclude", "focus-exclude", "invert-color-include", "blur-background-exclude", "unredir-if-possible-exclude", "rounded-corners-exclude", "corner-radius-rules", "opacity-rule", "window-shader-fg-rule", "wintypes", }; for (size_t i = 0; i < sizeof(rule_list) / sizeof(rule_list[0]); i++) { if (config_lookup(&cfg, rule_list[i])) { log_warn_both_style_of_rules(rule_list[i]); opt->has_both_style_of_rules = true; } } } else if (!parse_cfg_condlst(&opt->transparent_clipping_blacklist, &cfg, "transparent-clipping-exclude") || !parse_cfg_condlst(&opt->shadow_blacklist, &cfg, "shadow-exclude") || !parse_cfg_condlst(&opt->shadow_clip_list, &cfg, "clip-shadow-above") || !parse_cfg_condlst(&opt->fade_blacklist, &cfg, "fade-exclude") || !parse_cfg_condlst(&opt->focus_blacklist, &cfg, "focus-exclude") || !parse_cfg_condlst(&opt->invert_color_list, &cfg, "invert-color-include") || !parse_cfg_condlst(&opt->blur_background_blacklist, &cfg, "blur-background-exclude") || !parse_cfg_condlst(&opt->unredir_if_possible_blacklist, &cfg, "unredir-if-possible-exclude") || !parse_cfg_condlst(&opt->rounded_corners_blacklist, &cfg, "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 out; } // --blur-method if (config_lookup_string(&cfg, "blur-method", &sval)) { int method = parse_blur_method(sval); if (method >= BLUR_METHOD_INVALID) { log_fatal("Invalid blur method %s", sval); goto out; } opt->blur_method = (enum blur_method)method; } // --blur-size config_lookup_int(&cfg, "blur-size", &opt->blur_radius); // --blur-deviation config_lookup_float(&cfg, "blur-deviation", &opt->blur_deviation); // --blur-strength config_lookup_int(&cfg, "blur-strength", &opt->blur_strength); // --blur-background if (config_lookup_bool(&cfg, "blur-background", &ival) && ival) { if (opt->blur_method == BLUR_METHOD_NONE) { opt->blur_method = BLUR_METHOD_KERNEL; } } // --blur-background-frame lcfg_lookup_bool(&cfg, "blur-background-frame", &opt->blur_background_frame); // --blur-background-fixed 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, &opt->blur_kernel_count); if (!opt->blur_kerns) { log_fatal("Cannot parse \"blur-kern\""); goto out; } } // --resize-damage config_lookup_int(&cfg, "resize-damage", &opt->resize_damage); // --glx-no-stencil lcfg_lookup_bool(&cfg, "glx-no-stencil", &opt->glx_no_stencil); // --glx-no-rebind-pixmap lcfg_lookup_bool(&cfg, "glx-no-rebind-pixmap", &opt->glx_no_rebind_pixmap); lcfg_lookup_bool(&cfg, "force-win-blend", &opt->force_win_blend); // --glx-swap-method 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; } if (strcmp(sval, "undefined") != 0 && val != 0) { // If not undefined, we will use damage and buffer-age to limit // the rendering area. should_remove = false; } log_error("glx-swap-method has been removed, your setting " "\"%s\" should be %s.", sval, !should_remove ? "replaced by `use-damage = true`" : "removed"); goto out; } // --use-damage lcfg_lookup_bool(&cfg, "use-damage", &opt->use_damage); // --max-brightness if (config_lookup_float(&cfg, "max-brightness", &opt->max_brightness) && opt->use_damage && opt->max_brightness < 1) { log_warn("max-brightness requires use-damage = false. Falling back to " "1.0"); 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)) { log_error("glx-use-gpushader4 has been removed, please remove it " "from your config file"); goto out; } // --xrender-sync-fence lcfg_lookup_bool(&cfg, "xrender-sync-fence", &opt->xrender_sync_fence); 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"); } config_setting_t *blur_cfg = config_lookup(&cfg, "blur"); if (blur_cfg) { if (config_setting_lookup_string(blur_cfg, "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 = (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, &opt->blur_kernel_count); if (!opt->blur_kerns) { log_warn("Failed to parse blur kernel: %s", sval); } } config_setting_lookup_float(blur_cfg, "deviation", &opt->blur_deviation); config_setting_lookup_int(blur_cfg, "strength", &opt->blur_strength); } // --write-pid-path if (config_lookup_string(&cfg, "write-pid-path", &sval)) { if (*sval != '/') { log_warn("The write-pid-path in your configuration file is not" " an absolute path"); } opt->write_pid_path = strdup(sval); } // Wintype settings // XXX ! Refactor all the wintype_* arrays into a struct if (list_is_empty(&opt->rules)) { for (wintype_t i = 0; i < NUM_WINTYPES; ++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], &opt->wintype_option_mask[WINTYPE_NOTIFICATION]); } config_setting_t *animations = config_lookup(&cfg, "animations"); if (animations) { parse_animations(opt->animations, animations, &opt->all_scripts); } opt->config_file_path = path; path = NULL; succeeded = true; out: config_destroy(&cfg); free(path); return succeeded; } picom-12.5/src/dbus.c000066400000000000000000001434041471504570600144570ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2018 Yuxuan Shui #include #include #include #include #include #include #include #include #include #include #include #include "backend/backend.h" #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "picom.h" #include "utils/misc.h" #include "utils/str.h" #include "wm/defs.h" #include "wm/win.h" #include "wm/wm.h" #include "dbus.h" struct cdbus_data { /// Mainloop struct ev_loop *loop; /// DBus connection. DBusConnection *dbus_conn; /// DBus service name. char *dbus_service; }; // Window type typedef uint32_t cdbus_window_t; #define CDBUS_TYPE_WINDOW DBUS_TYPE_UINT32 #define CDBUS_TYPE_WINDOW_STR DBUS_TYPE_UINT32_AS_STRING typedef uint32_t cdbus_enum_t; #define CDBUS_TYPE_ENUM DBUS_TYPE_UINT32 #define CDBUS_TYPE_ENUM_STR DBUS_TYPE_UINT32_AS_STRING #define CDBUS_SERVICE_NAME "com.github.chjj.compton" #define CDBUS_INTERFACE_NAME CDBUS_SERVICE_NAME #define CDBUS_OBJECT_NAME "/com/github/chjj/compton" #define CDBUS_ERROR_PREFIX CDBUS_INTERFACE_NAME ".error" #define CDBUS_ERROR_UNKNOWN CDBUS_ERROR_PREFIX ".unknown" #define CDBUS_ERROR_UNKNOWN_S "Well, I don't know what happened. Do you?" #define CDBUS_ERROR_BADMSG CDBUS_ERROR_PREFIX ".bad_message" #define CDBUS_ERROR_BADMSG_S \ "Unrecognized command. Beware compton " \ "cannot make you a sandwich." #define CDBUS_ERROR_BADARG CDBUS_ERROR_PREFIX ".bad_argument" #define CDBUS_ERROR_BADARG_S "Failed to parse argument %d: %s" #define CDBUS_ERROR_BADWIN CDBUS_ERROR_PREFIX ".bad_window" #define CDBUS_ERROR_BADWIN_S "Requested window %#010x not found." #define CDBUS_ERROR_BADTGT CDBUS_ERROR_PREFIX ".bad_target" #define CDBUS_ERROR_BADTGT_S "Target \"%s\" not found." #define CDBUS_ERROR_FORBIDDEN CDBUS_ERROR_PREFIX ".forbidden" #define CDBUS_ERROR_FORBIDDEN_S "Incorrect password, access denied." #define CDBUS_ERROR_CUSTOM CDBUS_ERROR_PREFIX ".custom" #define CDBUS_ERROR_CUSTOM_S "%s" #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); static void cdbus_callback_remove_timeout(DBusTimeout *timeout, void *data); static void cdbus_callback_timeout_toggled(DBusTimeout *timeout, void *data); static dbus_bool_t cdbus_callback_add_watch(DBusWatch *watch, void *data); static void cdbus_callback_remove_watch(DBusWatch *watch, void *data); static void cdbus_callback_watch_toggled(DBusWatch *watch, void *data); /** * Initialize D-Bus connection. */ struct cdbus_data *cdbus_init(session_t *ps, const char *uniq) { auto cd = cmalloc(struct cdbus_data); cd->dbus_service = NULL; DBusError err = {}; // Initialize dbus_error_init(&err); // Connect to D-Bus // 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."); goto fail; } if (!cd->dbus_conn) { log_error("D-Bus connection failed for unknown reason."); goto fail; } // Avoid exiting on disconnect dbus_connection_set_exit_on_disconnect(cd->dbus_conn, false); // Request service name { // Build service name size_t service_len = strlen(CDBUS_SERVICE_NAME) + strlen(uniq) + 2; char *service = ccalloc(service_len, char); snprintf(service, service_len, "%s.%s", CDBUS_SERVICE_NAME, uniq); // Make a valid dbus name by converting non alphanumeric characters to // underscore char *tmp = service + strlen(CDBUS_SERVICE_NAME) + 1; while (*tmp) { if (!isalnum((unsigned char)*tmp)) { *tmp = '_'; } tmp++; } cd->dbus_service = service; // Request for the name int ret = dbus_bus_request_name(cd->dbus_conn, service, DBUS_NAME_FLAG_DO_NOT_QUEUE, &err); if (dbus_error_is_set(&err)) { log_error("Failed to obtain D-Bus name."); goto fail; } 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); goto fail; } } // 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, cd, NULL)) { log_error("Failed to add D-Bus watch functions."); goto fail; } // Add timeout handlers if (!dbus_connection_set_timeout_functions( cd->dbus_conn, cdbus_callback_add_timeout, cdbus_callback_remove_timeout, cdbus_callback_timeout_toggled, cd, NULL)) { log_error("Failed to add D-Bus timeout functions."); goto fail; } // Add match dbus_bus_add_match(cd->dbus_conn, "type='method_call',interface='" CDBUS_INTERFACE_NAME "'", &err); if (dbus_error_is_set(&err)) { log_error("Failed to add D-Bus match."); goto fail; } 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: 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 NULL; } /** * Destroy D-Bus connection. */ void cdbus_destroy(struct cdbus_data *cd) { if (cd->dbus_conn) { // Release DBus name firstly if (cd->dbus_service) { DBusError err = {}; dbus_error_init(&err); dbus_bus_release_name(cd->dbus_conn, cd->dbus_service, &err); if (dbus_error_is_set(&err)) { log_error("Failed to release DBus name (%s).", err.message); dbus_error_free(&err); } free(cd->dbus_service); } // Close and unref the connection dbus_connection_close(cd->dbus_conn); dbus_connection_unref(cd->dbus_conn); } free(cd); } /** @name DBusTimeout handling */ ///@{ typedef struct ev_dbus_timer { ev_timer w; DBusTimeout *t; } ev_dbus_timer; /** * Callback for handling a D-Bus timeout. */ static void cdbus_callback_handle_timeout(EV_P attr_unused, ev_timer *w, int revents attr_unused) { ev_dbus_timer *t = (void *)w; dbus_timeout_handle(t->t); } /** * Callback for adding D-Bus timeout. */ static dbus_bool_t cdbus_callback_add_timeout(DBusTimeout *timeout, void *data) { struct cdbus_data *cd = data; auto t = ccalloc(1, ev_dbus_timer); double i = dbus_timeout_get_interval(timeout) / 1000.0; ev_timer_init(&t->w, cdbus_callback_handle_timeout, i, i); t->t = timeout; dbus_timeout_set_data(timeout, t, NULL); if (dbus_timeout_get_enabled(timeout)) { ev_timer_start(cd->loop, &t->w); } return true; } /** * Callback for removing D-Bus timeout. */ static void cdbus_callback_remove_timeout(DBusTimeout *timeout, void *data) { struct cdbus_data *cd = data; ev_dbus_timer *t = dbus_timeout_get_data(timeout); assert(t); ev_timer_stop(cd->loop, &t->w); free(t); } /** * Callback for toggling a D-Bus timeout. */ static void cdbus_callback_timeout_toggled(DBusTimeout *timeout, void *data) { struct cdbus_data *cd = data; ev_dbus_timer *t = dbus_timeout_get_data(timeout); assert(t); 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(cd->loop, &t->w); } } ///@} /** @name DBusWatch handling */ ///@{ typedef struct ev_dbus_io { ev_io w; struct cdbus_data *cd; DBusWatch *dw; } 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) { flags |= DBUS_WATCH_READABLE; } 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) ; } /** * Determine the poll condition of a DBusWatch. */ 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) { condition |= EV_READ; } if (flags & DBUS_WATCH_WRITABLE) { condition |= EV_WRITE; } return condition; } /** * Callback for adding D-Bus watch. */ static dbus_bool_t cdbus_callback_add_watch(DBusWatch *watch, void *data) { struct cdbus_data *cd = data; auto w = ccalloc(1, ev_dbus_io); w->dw = watch; 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(cd->loop, &w->w); } dbus_watch_set_data(watch, w, NULL); // Always return true return true; } /** * Callback for removing D-Bus watch. */ static void cdbus_callback_remove_watch(DBusWatch *watch, void *data) { struct cdbus_data *cd = data; ev_dbus_io *w = dbus_watch_get_data(watch); ev_io_stop(cd->loop, &w->w); free(w); } /** * Callback for toggling D-Bus watch status. */ static void cdbus_callback_watch_toggled(DBusWatch *watch, void *data) { struct cdbus_data *cd = data; ev_io *w = dbus_watch_get_data(watch); if (dbus_watch_get_enabled(watch)) { ev_io_start(cd->loop, w); } else { ev_io_stop(cd->loop, w); } } ///@} static bool cdbus_append_boolean(DBusMessage *msg, dbus_bool_t val) { return dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &val, DBUS_TYPE_INVALID); } static bool cdbus_append_int32(DBusMessage *msg, int32_t val) { return dbus_message_append_args(msg, DBUS_TYPE_INT32, &val, DBUS_TYPE_INVALID); } static bool cdbus_append_uint32(DBusMessage *msg, uint32_t val) { return dbus_message_append_args(msg, DBUS_TYPE_UINT32, &val, DBUS_TYPE_INVALID); } static bool cdbus_append_double(DBusMessage *msg, double val) { return dbus_message_append_args(msg, DBUS_TYPE_DOUBLE, &val, DBUS_TYPE_INVALID); } 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); } static bool cdbus_append_enum(DBusMessage *msg, cdbus_enum_t val) { return dbus_message_append_args(msg, CDBUS_TYPE_ENUM, &val, DBUS_TYPE_INVALID); } 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); } /// 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_; 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; } if (!dbus_message_iter_append_basic(&it2, CDBUS_TYPE_WINDOW, &val)) { return false; } if (!dbus_message_iter_close_container(&it, &it2)) { return false; } return true; } /// 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; } if (!dbus_message_iter_append_basic(&it2, DBUS_TYPE_BOOLEAN, &val)) { return false; } if (!dbus_message_iter_close_container(&it, &it2)) { return false; } return true; } /// 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 : ""; 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; } if (!dbus_message_iter_append_basic(&it2, DBUS_TYPE_STRING, &str)) { log_error("Failed to append argument."); return false; } if (!dbus_message_iter_close_container(&it, &it2)) { return false; } return true; } /// 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) { 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; } bool failed = false; wm_stack_foreach(ps->wm, cursor) { if (wm_ref_is_zombie(cursor)) { continue; } auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } auto wid = win_id(w); if (!dbus_message_iter_append_basic(&subit, CDBUS_TYPE_WINDOW, &wid)) { failed = true; break; } } if (!dbus_message_iter_close_container(&it, &subit)) { log_error("Failed to close container."); return false; } if (failed) { log_error("Failed to append argument."); return false; } return true; } /** * Get n-th argument of a D-Bus message. * * @param count the position of the argument to get, starting from 0 * @param type libdbus type number of the type * @param pdest pointer to the target * @return true if successful, false otherwise. */ static bool cdbus_msg_get_arg(DBusMessage *msg, int count, const int type, void *pdest) { assert(count >= 0); DBusMessageIter iter = {}; if (!dbus_message_iter_init(msg, &iter)) { log_error("Message has no argument."); return false; } { const int oldcount = count; while (count) { if (!dbus_message_iter_next(&iter)) { log_error("Failed to find argument %d.", oldcount); return false; } --count; } } if (type != dbus_message_iter_get_arg_type(&iter)) { log_error("Argument has incorrect type."); return false; } dbus_message_iter_get_basic(&iter, pdest); return true; } /** @name Message processing */ ///@{ /** * Process a list_win D-Bus request. */ 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 property Get D-Bus request. static DBusHandlerResult cdbus_process_window_property_get(session_t *ps, DBusMessage *msg, cdbus_window_t wid, DBusMessage *reply, DBusError *e) { const char *target = NULL; const char *interface = NULL; 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_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; } 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 cursor = wm_find(ps->wm, wid); if (cursor == NULL) { log_debug("Window %#010x not found.", wid); dbus_set_error(e, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); return DBUS_HANDLER_RESULT_HANDLED; } auto w = wm_ref_deref(cursor); if (w == NULL) { log_debug("Window %#010x is not managed.", wid); dbus_set_error(e, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); return DBUS_HANDLER_RESULT_HANDLED; } #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(name, member, type) append(name, type, w->member) append(Mapped, bool_variant, w->state == WSTATE_MAPPED); append(Id, wid_variant, win_id(w)); append(RawFocused, bool_variant, w->a.map_state == XCB_MAP_STATE_VIEWABLE && w->is_focused); append(ClientWin, wid_variant, win_client_id(w, /*fallback_to_self=*/true)); append(Leader, wid_variant, wm_ref_win_id(wm_ref_leader(w->tree_ref))); append_win_property(Name, name, string_variant); if (strcmp("Type", target) == 0) { DBusMessageIter iter, sub; dbus_message_iter_init_append(reply, &iter); if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "s", &sub)) { return DBUS_HANDLER_RESULT_NEED_MEMORY; } for (int i = 0; i < NUM_WINTYPES; i++) { if ((w->window_types & (1 << i)) == 0) { continue; } if (!dbus_message_iter_append_basic(&sub, DBUS_TYPE_STRING, &WINTYPES[i].name)) { return DBUS_HANDLER_RESULT_NEED_MEMORY; } } if (!dbus_message_iter_close_container(&iter, &sub)) { return DBUS_HANDLER_RESULT_NEED_MEMORY; } return DBUS_HANDLER_RESULT_HANDLED; } if (strcmp("Next", target) == 0) { cdbus_window_t next_id = 0; auto below = wm_ref_below(cursor); if (below != NULL) { next_id = wm_ref_win_id(below); } if (!cdbus_append_wid_variant(reply, next_id)) { return DBUS_HANDLER_RESULT_NEED_MEMORY; } return DBUS_HANDLER_RESULT_HANDLED; } #undef append_win_property #undef append log_debug(CDBUS_ERROR_BADTGT_S, target); dbus_set_error(e, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); return DBUS_HANDLER_RESULT_HANDLED; } 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; } 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; } return DBUS_HANDLER_RESULT_HANDLED; } static inline cdbus_enum_t tristate_to_switch(enum tristate val) { switch (val) { case TRI_FALSE: return OFF; case TRI_TRUE: return ON; default: return UNSET; } } /** * 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 (!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; } auto cursor = wm_find(ps->wm, wid); if (cursor == NULL) { log_debug("Window %#010x not found.", wid); dbus_set_error(err, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); return DBUS_HANDLER_RESULT_HANDLED; } auto w = wm_ref_deref(cursor); if (w == NULL) { log_debug("Window %#010x is not managed.", wid); dbus_set_error(err, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); return DBUS_HANDLER_RESULT_HANDLED; } #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) == 0) { auto below = wm_ref_below(cursor); xcb_window_t next_id = below ? wm_ref_win_id(below) : XCB_NONE; if (!cdbus_append_wid(reply, next_id)) { return DBUS_HANDLER_RESULT_NEED_MEMORY; } return DBUS_HANDLER_RESULT_HANDLED; } auto w_opts = win_options(w); append(id, wid, win_id(w)); append(client_win, wid, win_client_id(w, /*fallback_to_self=*/true)); append(map_state, boolean, w->a.map_state); append(wmwin, boolean, win_is_wmwin(w)); append(focused_raw, boolean, w->a.map_state == XCB_MAP_STATE_VIEWABLE && w->is_focused); 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(fade_force, enum, tristate_to_switch(w->options_override.fade)); append(shadow_force, enum, tristate_to_switch(w->options_override.shadow)); append(invert_color_force, enum, tristate_to_switch(w->options_override.invert_color)); append(opacity_is_set, boolean, !safe_isnan(w->options.opacity)); append(shadow, boolean, w_opts.shadow); append(fade, boolean, w_opts.fade); append(blur_background, boolean, w_opts.blur_background); append(leader, wid, wm_ref_win_id(wm_ref_leader(w->tree_ref))); append_win_property(mode, enum); append_win_property(opacity, double); append_win_property(ever_damaged, boolean); append_win_property(window_types, 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, double); append_win_property(has_opacity_prop, boolean); append_win_property(opacity_prop, uint32); append_win_property(opacity_set, double); append_win_property(frame_opacity, double); #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 DBusHandlerResult cdbus_process_win_set(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { cdbus_window_t wid = XCB_NONE; const char *target = NULL; 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_set\" (%s).", err->message); dbus_set_error_const(err, DBUS_ERROR_INVALID_ARGS, NULL); return DBUS_HANDLER_RESULT_HANDLED; } auto cursor = wm_find(ps->wm, wid); if (cursor == NULL) { log_debug("Window %#010x not found.", wid); dbus_set_error(err, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); return DBUS_HANDLER_RESULT_HANDLED; } auto w = wm_ref_deref(cursor); if (w == NULL) { log_debug("Window %#010x is not managed.", wid); dbus_set_error(err, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); return DBUS_HANDLER_RESULT_HANDLED; } 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; } bool changed = false; if (strcmp("shadow_force", target) == 0) { w->options_override.shadow = val == UNSET ? TRI_UNKNOWN : (val == ON ? TRI_TRUE : TRI_FALSE); changed = true; } else if (strcmp("fade_force", target) == 0) { w->options_override.fade = val == UNSET ? TRI_UNKNOWN : (val == ON ? TRI_TRUE : TRI_FALSE); changed = true; } else if (strcmp("invert_color_force", target) == 0) { w->options_override.invert_color = val == UNSET ? TRI_UNKNOWN : (val == ON ? TRI_TRUE : TRI_FALSE); changed = true; } 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; } if (changed) { add_damage_from_win(ps, w); queue_redraw(ps); } 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 DBusHandlerResult cdbus_process_find_win(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { if (reply == NULL) { return DBUS_HANDLER_RESULT_HANDLED; } 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; if (strcmp("client", target) == 0) { // Find window by client window cdbus_window_t client = XCB_NONE; 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 = wm_ref_win_id(w); } } else if (strcmp("focused", target) == 0) { // Find focused window auto focused_win = wm_focused_win(ps->wm); auto w = wm_ref_deref(focused_win); if (focused_win && w && w->state == WSTATE_MAPPED) { wid = wm_ref_win_id(focused_win); } } 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; } 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 DBusHandlerResult cdbus_process_opts_get(session_t *ps, DBusMessage *msg, DBusMessage *reply, DBusError *err) { if (reply == NULL) { return DBUS_HANDLER_RESULT_HANDLED; } 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; } #define append(tgt, type, ret) \ if (strcmp(#tgt, target) == 0) { \ 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) if (strcmp("backend", target) == 0) { assert(!ps->o.use_legacy_backends || (size_t)ps->o.legacy_backend < ARR_SIZE(BACKEND_STRS)); const char *name = ps->o.use_legacy_backends ? BACKEND_STRS[ps->o.legacy_backend] : backend_name(ps->o.backend); if (reply != NULL && !cdbus_append_string(reply, name)) { return DBUS_HANDLER_RESULT_NEED_MEMORY; } return DBUS_HANDLER_RESULT_HANDLED; } append(version, string, PICOM_FULL_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.legacy_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 append_session_option(glx_no_stencil, boolean); append_session_option(glx_no_rebind_pixmap, boolean); #endif #undef append_session_option #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 opts_set D-Bus request. */ 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)) { 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; \ } if (strcmp("clear_shadow", target) == 0 || strcmp("track_focus", target) == 0) { 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); if (strcmp("unredir_if_possible", target) == 0) { dbus_bool_t val = FALSE; get_msg_arg(BOOLEAN, val); if (ps->o.unredir_if_possible != val) { ps->o.unredir_if_possible = val; queue_redraw(ps); } goto cdbus_process_opts_set_success; } if (strcmp("redirected_force", target) == 0) { cdbus_enum_t val = UNSET; 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; } #undef get_msg_arg log_error(CDBUS_ERROR_BADTGT_S, target); dbus_set_error(err, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, target); return DBUS_HANDLER_RESULT_HANDLED; cdbus_process_opts_set_success: 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 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" " \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"; if (reply != NULL && !cdbus_append_string(reply, str_introspect)) { return DBUS_HANDLER_RESULT_NEED_MEMORY; } return DBUS_HANDLER_RESULT_HANDLED; } ///@} /// Process an D-Bus Introspect request, for /windows. static DBusHandlerResult 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); wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } char *tmp = NULL; if (asprintf(&tmp, "\n", win_id(w)) < 0) { log_fatal("Failed to allocate memory."); return DBUS_HANDLER_RESULT_NEED_MEMORY; } mstrextend(&introspect, tmp); free(tmp); } 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" " \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; 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; } } 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 { 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_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); } 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 if (strcmp(interface, PICOM_WINDOW_INTERFACE) == 0 && strcmp(member, "BlockUnblockAnimation") == 0) { bool block = false; const char *trigger_str = NULL; if (!cdbus_msg_get_arg(msg, 0, DBUS_TYPE_STRING, &trigger_str) || !cdbus_msg_get_arg(msg, 1, DBUS_TYPE_BOOLEAN, &block)) { dbus_set_error_const(&err, DBUS_ERROR_INVALID_ARGS, NULL); goto finished; } auto trigger = parse_animation_trigger(trigger_str); if (trigger == ANIMATION_TRIGGER_INVALID) { dbus_set_error(&err, CDBUS_ERROR_BADTGT, CDBUS_ERROR_BADTGT_S, trigger_str); goto finished; } auto cursor = wm_find(ps->wm, wid); if (cursor == NULL) { dbus_set_error(&err, CDBUS_ERROR_BADWIN, CDBUS_ERROR_BADWIN_S, wid); goto finished; } auto w = wm_ref_deref(cursor); unsigned count = 0; if (w != NULL) { if (block) { w->animation_block[trigger] += 1; } else if (w->animation_block[trigger] > 0) { w->animation_block[trigger] -= 1; } count = w->animation_block[trigger]; } if (reply != NULL && !cdbus_append_uint32(reply, count)) { ret = DBUS_HANDLER_RESULT_NEED_MEMORY; } } 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 (!dbus_connection_send(cd->dbus_conn, msg, NULL)) { log_error("Failed to send D-Bus signal."); dbus_message_unref(msg); return false; } dbus_connection_flush(cd->dbus_conn); dbus_message_unref(msg); return true; } /** @name Core callbacks */ ///@{ 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", win_id(w)); cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinAdded", win_id(w)); } } 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", win_id(w)); cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinDestroyed", win_id(w)); } } 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", win_id(w)); cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinMapped", win_id(w)); } } 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", win_id(w)); cdbus_signal_wid(cd, PICOM_COMPOSITOR_INTERFACE, "WinUnmapped", win_id(w)); } } 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", win_id(w)); } } 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", win_id(w)); } } //!@} picom-12.5/src/dbus.h000066400000000000000000000036531471504570600144650ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2018 Yuxuan Shui #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. */ static inline const char *cdbus_repr_msgtype(DBusMessage *msg) { return dbus_message_type_to_string(dbus_message_get_type(msg)); } /** * Initialize D-Bus connection. */ struct cdbus_data *cdbus_init(session_t *ps, const char *uniq_name); /** * Destroy D-Bus connection. */ void cdbus_destroy(struct cdbus_data *cd); /// Generate dbus win_added signal void cdbus_ev_win_added(struct cdbus_data *cd, struct win *w); /// Generate dbus win_destroyed signal void cdbus_ev_win_destroyed(struct cdbus_data *cd, struct win *w); /// Generate dbus win_mapped signal void cdbus_ev_win_mapped(struct cdbus_data *cd, struct win *w); /// Generate dbus win_unmapped signal void cdbus_ev_win_unmapped(struct cdbus_data *cd, struct win *w); /// Generate dbus win_focusout signal void cdbus_ev_win_focusout(struct cdbus_data *cd, struct win *w); /// Generate dbus win_focusin signal 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 : picom-12.5/src/diagnostic.c000066400000000000000000000040051471504570600156370ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #include #include #include #include "backend/backend.h" #include "backend/driver.h" #include "common.h" #include "config.h" #include "diagnostic.h" #include "picom.h" void print_diagnostics(session_t *ps, const char *config_file, bool compositor_running) { printf("**Version:** " PICOM_FULL_VERSION "\n"); // printf("**CFLAGS:** %s\n", "??"); printf("\n### Extensions:\n\n"); printf("* Shape: %s\n", ps->shape_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"); if (ps->overlay == XCB_NONE) { if (compositor_running) { printf(" (Another compositor is already running)\n"); } else if (session_redirection_mode(ps) != XCB_COMPOSITE_REDIRECT_MANUAL) { printf(" (Not in manual redirection mode)\n"); } else { printf("\n"); } } #ifdef __FAST_MATH__ printf("* Fast Math: Yes\n"); #endif printf("* Config file specified: %s\n", config_file ?: "None"); printf("* Config file used: %s\n", ps->o.config_file_path ?: "None"); if (!list_is_empty(&ps->o.included_config_files)) { printf("* Included config files:\n"); list_foreach(struct included_config_file, i, &ps->o.included_config_files, siblings) { printf(" - %s\n", i->path); } } printf("\n### Drivers (inaccurate):\n\n"); print_drivers(ps->drivers); for (auto i = backend_iter(); i; i = backend_iter_next(i)) { auto backend_data = backend_init(i, ps, session_get_target_window(ps)); if (!backend_data) { printf(" Cannot initialize backend %s\n", backend_name(i)); continue; } if (backend_data->ops.diagnostics) { printf("\n### Backend: %s\n\n", backend_name(i)); backend_data->ops.diagnostics(backend_data); } backend_data->ops.deinit(backend_data); } } // vim: set noet sw=8 ts=8 : picom-12.5/src/diagnostic.h000066400000000000000000000003701471504570600156450ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #pragma once #include typedef struct session session_t; void print_diagnostics(session_t *, const char *config_file, bool compositor_running); picom-12.5/src/err.h000066400000000000000000000016221471504570600143120ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2019 Yuxuan Shui #pragma once #include #include #include "compiler.h" // Functions for error reporting, adopted from Linux // INFO in user space we can probably be more liberal about what pointer we consider // error. e.g. In x86_64 Linux, all addresses with the highest bit set is invalid in user // space. #define MAX_ERRNO 4095 static inline void *must_use ERR_PTR(intptr_t err) { return (void *)err; } static inline intptr_t must_use PTR_ERR(void *ptr) { return (intptr_t)ptr; } static inline bool must_use IS_ERR(void *ptr) { return unlikely((uintptr_t)ptr > (uintptr_t)-MAX_ERRNO); } static inline bool must_use IS_ERR_OR_NULL(void *ptr) { return unlikely(!ptr) || IS_ERR(ptr); } static inline intptr_t must_use PTR_ERR_OR_ZERO(void *ptr) { if (IS_ERR(ptr)) { return PTR_ERR(ptr); } return 0; } picom-12.5/src/event.c000066400000000000000000000656201471504570600146460ustar00rootroot00000000000000// 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" #include "event.h" #include "log.h" #include "picom.h" #include "region.h" #include "utils/dynarr.h" #include "wm/defs.h" #include "wm/wm.h" #include "x.h" /// Event handling with X is complicated. Handling events with other events possibly /// in-flight is no good. Because your internal state won't be up to date. Also, querying /// the server while events are in-flight is not good. Because events later in the queue /// might container information you are querying. Thus those events will cause you to do /// unnecessary updates even when you already have the latest information (remember, you /// 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 /// 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 /// 2. Make sure the event queue is empty before we make any query to the server /// /// Notice (2) has a dependency circle. To handle events, you sometimes need to make /// queries. But to make queries you have to first handle events. /// /// To break that circle, we split all event handling into top and bottom halves. The /// bottom half will just look at the event itself, update as much state as they can /// without making queries, then queue up necessary works need to be done by the top half. /// The top half will do all the other necessary updates. Before entering the top half, we /// grab the server and make sure the event queue is empty. /// /// 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. 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. */ 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->c.screen_info->root == wid) { name = "(Root window)"; } else if (ps->overlay == wid) { name = "(Overlay)"; } else { auto cursor = wm_find(ps->wm, wid); if (!cursor || !wm_ref_deref(cursor)) { cursor = wm_find_by_client(ps->wm, wid); } auto w = cursor ? wm_ref_deref(cursor) : NULL; if (w && w->name) { name = w->name; } } } return name; } static inline xcb_window_t attr_pure ev_window(session_t *ps, xcb_generic_event_t *ev) { switch (ev->response_type) { case XCB_FOCUS_IN: case XCB_FOCUS_OUT: return ((xcb_focus_in_event_t *)ev)->event; case XCB_CREATE_NOTIFY: return ((xcb_create_notify_event_t *)ev)->window; case XCB_CONFIGURE_NOTIFY: return ((xcb_configure_notify_event_t *)ev)->window; case XCB_DESTROY_NOTIFY: return ((xcb_destroy_notify_event_t *)ev)->window; case XCB_MAP_NOTIFY: return ((xcb_map_notify_event_t *)ev)->window; case XCB_UNMAP_NOTIFY: return ((xcb_unmap_notify_event_t *)ev)->window; case XCB_REPARENT_NOTIFY: return ((xcb_reparent_notify_event_t *)ev)->window; case XCB_CIRCULATE_NOTIFY: return ((xcb_circulate_notify_event_t *)ev)->window; case XCB_EXPOSE: return ((xcb_expose_event_t *)ev)->window; case XCB_PROPERTY_NOTIFY: return ((xcb_property_notify_event_t *)ev)->window; case XCB_CLIENT_MESSAGE: return ((xcb_client_message_event_t *)ev)->window; default: if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) { return ((xcb_damage_notify_event_t *)ev)->drawable; } if (ps->shape_exists && ev->response_type == ps->shape_event) { return ((xcb_shape_notify_event_t *)ev)->affected_window; } return 0; } } #define CASESTRRET(s) \ case XCB_##s: return #s; static inline const char *ev_name(session_t *ps, xcb_generic_event_t *ev) { static char buf[128]; switch (XCB_EVENT_RESPONSE_TYPE(ev)) { CASESTRRET(FOCUS_IN); CASESTRRET(FOCUS_OUT); CASESTRRET(CREATE_NOTIFY); CASESTRRET(CONFIGURE_NOTIFY); CASESTRRET(DESTROY_NOTIFY); CASESTRRET(MAP_NOTIFY); CASESTRRET(UNMAP_NOTIFY); CASESTRRET(REPARENT_NOTIFY); CASESTRRET(CIRCULATE_NOTIFY); CASESTRRET(EXPOSE); CASESTRRET(PROPERTY_NOTIFY); CASESTRRET(CLIENT_MESSAGE); } if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) { return "DAMAGE_NOTIFY"; } if (ps->shape_exists && ev->response_type == ps->shape_event) { return "SHAPE_NOTIFY"; } if (ps->xsync_exists) { int o = ev->response_type - ps->xsync_event; switch (o) { CASESTRRET(SYNC_COUNTER_NOTIFY); CASESTRRET(SYNC_ALARM_NOTIFY); } } sprintf(buf, "Event %d", ev->response_type); return buf; } static inline const char *attr_pure ev_focus_mode_name(xcb_focus_in_event_t *ev) { #undef CASESTRRET #define CASESTRRET(s) \ case XCB_NOTIFY_MODE_##s: return #s switch (ev->mode) { CASESTRRET(NORMAL); CASESTRRET(WHILE_GRABBED); CASESTRRET(GRAB); CASESTRRET(UNGRAB); } return "Unknown"; } static inline const char *attr_pure ev_focus_detail_name(xcb_focus_in_event_t *ev) { #undef CASESTRRET #define CASESTRRET(s) \ case XCB_NOTIFY_DETAIL_##s: return #s switch (ev->detail) { CASESTRRET(ANCESTOR); CASESTRRET(VIRTUAL); CASESTRRET(INFERIOR); CASESTRRET(NONLINEAR); CASESTRRET(NONLINEAR_VIRTUAL); CASESTRRET(POINTER); CASESTRRET(POINTER_ROOT); CASESTRRET(NONE); } return "Unknown"; } #undef CASESTRRET struct ev_ewmh_active_win_request { struct x_async_request_base base; session_t *ps; }; /// Update current active window based on EWMH _NET_ACTIVE_WIN. /// /// Does not change anything if we fail to get the attribute or the window /// returned could not be found. static void update_ewmh_active_win(struct x_connection * /*c*/, struct x_async_request_base *req_base, const xcb_raw_generic_event_t *reply_or_error) { auto ps = ((struct ev_ewmh_active_win_request *)req_base)->ps; free(req_base); ps->pending_focus_check = false; if (reply_or_error == NULL) { // Connection shutdown return; } if (reply_or_error->response_type == 0) { log_error("Failed to get _NET_ACTIVE_WINDOW: %s", x_strerror(((xcb_generic_error_t *)reply_or_error))); return; } // Search for the window auto reply = (const xcb_get_property_reply_t *)reply_or_error; if (reply->type == XCB_NONE || xcb_get_property_value_length(reply) < 4) { log_debug("EWMH _NET_ACTIVE_WINDOW not set."); return; } auto wid = *(xcb_window_t *)xcb_get_property_value(reply); log_debug("EWMH _NET_ACTIVE_WINDOW is %#010x", wid); // Mark the window focused. No need to unfocus the previous one. auto cursor = wm_find_by_client(ps->wm, wid); wm_ref_set_focused(ps->wm, cursor); ps->pending_updates = true; log_debug("%#010x (%s) focused.", wid, win_wm_ref_name(cursor)); } struct ev_recheck_focus_request { struct x_async_request_base base; session_t *ps; }; /** * Recheck currently focused window and set its w->focused * to true. * * @param ps current session * @return struct _win of currently focused window, NULL if not found */ static void recheck_focus(struct x_connection * /*c*/, struct x_async_request_base *req_base, const xcb_raw_generic_event_t *reply_or_error) { auto ps = ((struct ev_ewmh_active_win_request *)req_base)->ps; free(req_base); ps->pending_focus_check = false; if (reply_or_error == NULL) { // Connection shutdown return; } // Determine the currently focused window so we can apply appropriate // opacity on it if (reply_or_error->response_type == 0) { // Not able to get input focus means very not good things... auto e = (xcb_generic_error_t *)reply_or_error; log_error_x_error(e, "Failed to get focused window."); return; } auto reply = (const xcb_get_input_focus_reply_t *)reply_or_error; xcb_window_t wid = reply->focus; log_debug("Current focused window is %#010x", wid); if (wid == XCB_NONE || wid == XCB_INPUT_FOCUS_POINTER_ROOT || wid == ps->c.screen_info->root) { // Focus is not on a toplevel. return; } auto cursor = wm_find(ps->wm, wid); assert(cursor != NULL || !wm_is_consistent(ps->wm)); if (cursor != NULL) { cursor = wm_ref_toplevel_of(ps->wm, cursor); assert(cursor != NULL || !wm_is_consistent(ps->wm)); } // And we set the focus state here wm_ref_set_focused(ps->wm, cursor); ps->pending_updates = true; log_debug("%#010x (%s) focused.", wid, win_wm_ref_name(cursor)); } void ev_update_focused(struct session *ps) { if (ps->pending_focus_check) { return; } if (ps->o.use_ewmh_active_win) { auto req = ccalloc(1, struct ev_ewmh_active_win_request); req->base.sequence = xcb_get_property(ps->c.c, 0, ps->c.screen_info->root, ps->atoms->a_NET_ACTIVE_WINDOW, XCB_ATOM_WINDOW, 0, 1) .sequence; req->base.callback = update_ewmh_active_win; req->ps = ps; x_await_request(&ps->c, &req->base); log_debug("Started async request to get _NET_ACTIVE_WINDOW"); } else { auto req = ccalloc(1, struct ev_recheck_focus_request); req->base.sequence = xcb_get_input_focus(ps->c.c).sequence; req->base.callback = recheck_focus; req->ps = ps; x_await_request(&ps->c, &req->base); log_debug("Started async request to recheck focus"); } ps->pending_focus_check = true; } static inline void ev_focus_change(session_t *ps) { if (ps->o.use_ewmh_active_win) { // Not using focus_in/focus_out events. return; } ev_update_focused(ps); } static inline void ev_focus_in(session_t *ps, xcb_focus_in_event_t *ev) { log_debug("{ mode: %s, detail: %s }", ev_focus_mode_name(ev), ev_focus_detail_name(ev)); ev_focus_change(ps); } static inline void ev_focus_out(session_t *ps, xcb_focus_out_event_t *ev) { log_debug("{ mode: %s, detail: %s }", ev_focus_mode_name(ev), ev_focus_detail_name(ev)); ev_focus_change(ps); } static inline void ev_create_notify(session_t *ps, xcb_create_notify_event_t *ev) { auto parent = wm_find(ps->wm, ev->parent); if (parent == NULL) { log_error("Create notify received for window %#010x, but its parent " "window %#010x is not in our tree. Expect malfunction.", ev->window, ev->parent); assert(false); } wm_import_start(ps->wm, &ps->c, ps->atoms, ev->window, parent); } /// Handle configure event of a regular window static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { auto cursor = wm_find(ps->wm, ce->window); if (!cursor) { if (wm_is_consistent(ps->wm)) { log_error("Configure event received for unknown window %#010x", ce->window); assert(false); } return; } wm_stack_move_to_above(ps->wm, cursor, ce->above_sibling); auto w = wm_ref_deref(cursor); if (!w) { log_debug("Window %#010x is unmanaged.", ce->window); return; } bool changed = win_set_pending_geometry(w, win_geometry_from_configure_notify(ce)) | (w->a.override_redirect != ce->override_redirect); w->a.override_redirect = ce->override_redirect; if (w->state == WSTATE_MAPPED) { add_damage_from_win(ps, w); ps->pending_updates |= changed; } } static inline void ev_configure_notify(session_t *ps, xcb_configure_notify_event_t *ev) { log_debug("{ event: %#010x, id: %#010x, above: %#010x, override_redirect: %d }", ev->event, ev->window, ev->above_sibling, ev->override_redirect); if (ps->overlay && ev->window == ps->overlay) { return; } if (ev->window == ps->c.screen_info->root) { configure_root(ps); } else { if (ev->window == ev->event) { return; } configure_win(ps, ev); } } static inline void ev_destroy_notify(session_t *ps, xcb_destroy_notify_event_t *ev) { log_debug("{ event: %#010x, id: %#010x }", ev->event, ev->window); // If we hit an ABA problem, it is possible to get a DestroyNotify event from a // parent for its child, but not from the child for itself. if (ev->event != ev->window) { wm_disconnect(ps->wm, ev->window, ev->event, XCB_NONE); } else { wm_destroy(ps->wm, ev->window); } } static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { if (ev->window == ev->event) { return; } // Unmap overlay window if it got mapped but we are currently not // in redirected state. if (ps->overlay && ev->window == ps->overlay) { if (!ps->redirected) { log_debug("Overlay is mapped while we are not redirected"); auto succeeded = XCB_AWAIT_VOID(xcb_unmap_window, ps->c.c, ps->overlay); if (!succeeded) { log_error("Failed to unmap the overlay window"); } } // We don't track the overlay window, so we can return return; } auto cursor = wm_find(ps->wm, ev->window); if (cursor == NULL) { if (wm_is_consistent(ps->wm)) { log_debug("Map event received for unknown window %#010x, overlay " "is %#010x", ev->window, ps->overlay); assert(false); } return; } auto w = wm_ref_deref(cursor); if (w == NULL) { 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 ps->pending_updates = true; // to update focus } static inline void ev_unmap_notify(session_t *ps, xcb_unmap_notify_event_t *ev) { if (ps->overlay && ev->window == ps->overlay) { return; } if (ev->event == ev->window) { return; } auto cursor = wm_find(ps->wm, ev->window); if (cursor == NULL) { if (wm_is_consistent(ps->wm)) { log_error("Unmap event received for unknown window %#010x", ev->window); assert(false); } return; } auto w = wm_ref_deref(cursor); if (w != NULL) { unmap_win_start(w); } } 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, " "send_event: %#010x", ev->window, ev->parent, ev->override_redirect, ev->event); if (ev->event == ev->window) { return; } if (ev->parent != ev->event) { wm_disconnect(ps->wm, ev->window, ev->event, ev->parent); } else { wm_reparent(ps->wm, &ps->c, ps->atoms, ev->window, ev->parent); } } static inline void ev_circulate_notify(session_t *ps, xcb_circulate_notify_event_t *ev) { if (ev->event == ev->window) { return; } auto cursor = wm_find(ps->wm, ev->window); if (cursor == NULL) { if (wm_is_consistent(ps->wm)) { log_debug("Circulate event received for unknown window %#010x", ev->window); assert(false); } return; } log_debug("Moving window %#010x (%s) to the %s", ev->window, ev_window_name(ps, ev->window), ev->place == PlaceOnTop ? "top" : "bottom"); wm_stack_move_to_end(ps->wm, cursor, ev->place == XCB_PLACE_ON_BOTTOM); auto w = wm_ref_deref(cursor); if (w != NULL) { add_damage_from_win(ps, w); } } static inline void expose_root(session_t *ps, const rect_t *rects, size_t nrects) { region_t region; pixman_region32_init_rects(®ion, rects, (int)nrects); add_damage(ps, ®ion); pixman_region32_fini(®ion); } static inline void ev_expose(session_t *ps, xcb_expose_event_t *ev) { if (ev->window == ps->c.screen_info->root || (ps->overlay && ev->window == ps->overlay)) { dynarr_reserve(ps->expose_rects, ev->count + 1); rect_t new_rect = { .x1 = ev->x, .y1 = ev->y, .x2 = ev->x + ev->width, .y2 = ev->y + ev->height, }; dynarr_push(ps->expose_rects, new_rect); if (ev->count == 0) { expose_root(ps, ps->expose_rects, dynarr_len(ps->expose_rects)); dynarr_clear_pod(ps->expose_rects); } } } static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t *ev) { log_debug("{ atom = %#010x, window = %#010x, state = %d }", ev->atom, ev->window, ev->state); 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.c, xcb_get_atom_name(ps->c.c, ev->atom), NULL); const char *name = "?"; int name_len = 1; if (reply) { name = xcb_get_atom_name_name(reply); name_len = xcb_get_atom_name_name_length(reply); } log_debug("{ atom = %.*s }", name_len, name); free(reply); } if (ps->c.screen_info->root == ev->window) { if (ps->o.use_ewmh_active_win && ps->atoms->a_NET_ACTIVE_WINDOW == ev->atom) { ev_update_focused(ps); } else { // Destroy the root "image" if the wallpaper probably changed if (x_is_root_back_pixmap_atom(ps->atoms, ev->atom)) { root_damaged(ps); } } // Unconcerned about any other properties on root window return; } ps->pending_updates = true; auto cursor = wm_find(ps->wm, ev->window); if (cursor == NULL) { if (wm_is_consistent(ps->wm)) { log_error("Property notify received for unknown window %#010x", ev->window); assert(false); } return; } auto toplevel_cursor = wm_ref_toplevel_of(ps->wm, cursor); if (ev->atom == ps->atoms->aWM_STATE) { log_debug("WM_STATE changed for window %#010x (%s): %s", ev->window, ev_window_name(ps, ev->window), ev->state == XCB_PROPERTY_DELETE ? "deleted" : "set"); wm_set_has_wm_state(ps->wm, cursor, ev->state != XCB_PROPERTY_DELETE); } if (toplevel_cursor == NULL) { assert(!wm_is_consistent(ps->wm)); return; } // We only care if the property is set on the toplevel itself, or on its // client window if it has one. WM_STATE is an exception, it is handled // always because it is what determines if a window is a client window. auto client_cursor = wm_ref_client_of(toplevel_cursor) ?: toplevel_cursor; if (cursor != client_cursor && cursor != toplevel_cursor) { return; } if (ev->atom == ps->atoms->a_NET_WM_BYPASS_COMPOSITOR) { // Unnecessary until we remove the queue_redraw in ev_handle queue_redraw(ps); } auto toplevel = wm_ref_deref(toplevel_cursor); if (toplevel) { win_set_property_stale(toplevel, ev->atom); } if (ev->atom == ps->atoms->a_NET_WM_WINDOW_OPACITY && toplevel != NULL) { // 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? win_set_property_stale(toplevel, 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 = cursor == client_cursor; if (toplevel) { c2_window_state_mark_dirty(ps->c2_state, &toplevel->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(toplevel, WIN_FLAGS_FACTOR_CHANGED); } } } static inline void repair_win(session_t *ps, struct win *w) { // Only mapped window can receive damages 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); log_debug("Window %#010x (%s) has been damaged the first time", win_id(w), w->name); } else { auto cookie = xcb_damage_subtract(ps->c.c, w->damage, XCB_NONE, ps->damage_ring.x_region); if (!ps->o.show_all_xerrors) { x_set_error_action_ignore(&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); } log_trace("Mark window %#010x (%s) as having received damage", win_id(w), w->name); w->ever_damaged = true; w->pixmap_damaged = true; // Why care about damage when screen is unredirected? // We will force full-screen repaint on redirection. if (!ps->redirected) { pixman_region32_fini(&parts); return; } // 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(&without_ignored, &parts, w->reg_ignore); } 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); } static inline void ev_damage_notify(session_t *ps, xcb_damage_notify_event_t *de) { auto cursor = wm_find(ps->wm, de->drawable); if (cursor == NULL) { log_error("Damage notify received for unknown window %#010x", de->drawable); return; } auto w = wm_ref_deref(cursor); if (w != NULL) { repair_win(ps, w); } } static inline void ev_shape_notify(session_t *ps, xcb_shape_notify_event_t *ev) { auto cursor = wm_find(ps->wm, ev->affected_window); if (cursor == NULL) { log_error("Shape notify received for unknown window %#010x", ev->affected_window); return; } auto w = wm_ref_deref(cursor); if (w == NULL || w->a.map_state == XCB_MAP_STATE_UNMAPPED) { return; } /* * Empty bounding_shape may indicated an * unmapped/destroyed window, in which case * seemingly BadRegion errors would be triggered * if we attempt to rebuild border_size */ // Mark the old bounding shape as damaged if (!win_check_flags_any(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { region_t tmp = win_get_bounding_shape_global_by_val(w); add_damage(ps, &tmp); pixman_region32_fini(&tmp); } w->reg_ignore_valid = false; win_set_flags(w, WIN_FLAGS_SIZE_STALE); ps->pending_updates = true; } static inline void ev_selection_clear(session_t *ps, xcb_selection_clear_event_t attr_unused *ev) { // The only selection we own is the _NET_WM_CM_Sn selection. // If we lose that one, we should exit. log_fatal("Another composite manager started and took the _NET_WM_CM_Sn " "selection."); quit(ps); } void ev_handle(session_t *ps, xcb_generic_event_t *ev) { xcb_window_t wid = ev_window(ps, ev); if (ev->response_type != ps->damage_event + XCB_DAMAGE_NOTIFY) { log_debug("event %10.10s serial %#010x window %#010x \"%s\"", ev_name(ps, ev), ev->full_sequence, wid, ev_window_name(ps, wid)); } else { log_trace("event %10.10s serial %#010x window %#010x \"%s\"", ev_name(ps, ev), ev->full_sequence, wid, ev_window_name(ps, wid)); } // Check if a custom XEvent constructor was registered in xlib for this event // type, and call it discarding the constructed XEvent if any. XESetWireToEvent // might be used by libraries to intercept messages from the X server e.g. the // OpenGL lib waiting for DRI2 events. // XXX This exists to workaround compton issue #33, #34, #47 // 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 response_type = XCB_EVENT_RESPONSE_TYPE(ev); auto proc = XESetWireToEvent(ps->c.dpy, response_type, 0); if (proc) { XESetWireToEvent(ps->c.dpy, response_type, proc); XEvent dummy; // Stop Xlib from complaining about lost sequence numbers. // proc might also just be Xlib internal event processing functions, and // because they probably won't see all X replies, they will complain about // missing sequence numbers. // // We only need the low 16 bits 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 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 XCB_DESTROY_NOTIFY: ev_destroy_notify(ps, (xcb_destroy_notify_event_t *)ev); break; 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 XCB_CIRCULATE_NOTIFY: ev_circulate_notify(ps, (xcb_circulate_notify_event_t *)ev); break; 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 XCB_SELECTION_CLEAR: ev_selection_clear(ps, (xcb_selection_clear_event_t *)ev); break; default: if (ps->shape_exists && ev->response_type == ps->shape_event) { ev_shape_notify(ps, (xcb_shape_notify_event_t *)ev); break; } if (ps->randr_exists && ev->response_type == (ps->randr_event + XCB_RANDR_SCREEN_CHANGE_NOTIFY)) { x_update_monitors_async(&ps->c, &ps->monitors); break; } if (ps->damage_event + XCB_DAMAGE_NOTIFY == ev->response_type) { ev_damage_notify(ps, (xcb_damage_notify_event_t *)ev); break; } } } picom-12.5/src/event.h000066400000000000000000000003701471504570600146420ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2019, Yuxuan Shui #pragma once #include #include "common.h" void ev_handle(session_t *ps, xcb_generic_event_t *ev); void ev_update_focused(struct session *ps); picom-12.5/src/fuzzer/000077500000000000000000000000001471504570600146755ustar00rootroot00000000000000picom-12.5/src/fuzzer/c2.c000066400000000000000000000006321471504570600153460ustar00rootroot00000000000000 #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. }picom-12.5/src/inspect.c000066400000000000000000000173041471504570600151660ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2024 Yuxuan Shui #include #include #include #include #include #include #include #include "inspect.h" #include "c2.h" #include "common.h" #include "config.h" #include "log.h" #include "utils/console.h" #include "utils/dynarr.h" #include "utils/str.h" #include "wm/defs.h" #include "wm/win.h" #include "x.h" xcb_window_t inspect_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 { const struct c2_state *state; const struct win *w; bool print_value; }; static bool c2_match_and_log(const struct list_node *list, const struct c2_state *state, const struct win *w, bool print_value) { void *rule_data = NULL; c2_condition_list_foreach((struct list_node *)list, i) { printf(" %s ... ", c2_condition_to_str(i)); bool matched = c2_match_one(state, w, i, rule_data); printf("%s", matched ? "\033[1;32mmatched\033[0m" : "not matched"); if (print_value && matched) { printf("/%lu", (unsigned long)(intptr_t)rule_data); print_value = false; } printf("\n"); } return false; } void inspect_dump_window(const struct c2_state *state, const struct options *opts, const struct win *w) { if (list_is_empty(&opts->rules)) { printf("Checking " BOLD("transparent-clipping-exclude") ":\n"); c2_match_and_log(&opts->transparent_clipping_blacklist, state, w, false); printf("Checking " BOLD("shadow-exclude") ":\n"); c2_match_and_log(&opts->shadow_blacklist, state, w, false); printf("Checking " BOLD("fade-exclude") ":\n"); c2_match_and_log(&opts->fade_blacklist, state, w, false); printf("Checking " BOLD("clip-shadow-above") ":\n"); c2_match_and_log(&opts->shadow_clip_list, state, w, true); printf("Checking " BOLD("focus-exclude") ":\n"); c2_match_and_log(&opts->focus_blacklist, state, w, false); printf("Checking " BOLD("invert-color-include") ":\n"); c2_match_and_log(&opts->invert_color_list, state, w, false); printf("Checking " BOLD("blur-background-exclude") ":\n"); c2_match_and_log(&opts->blur_background_blacklist, state, w, false); printf("Checking " BOLD("unredir-if-possible-exclude") ":\n"); c2_match_and_log(&opts->unredir_if_possible_blacklist, state, w, false); printf("Checking " BOLD("rounded-corners-exclude") ":\n"); c2_match_and_log(&opts->rounded_corners_blacklist, state, w, false); printf("Checking " BOLD("opacity-rule") ":\n"); c2_match_and_log(&opts->opacity_rules, state, w, true); printf("Checking " BOLD("corner-radius-rule") ":\n"); c2_match_and_log(&opts->corner_radius_rules, state, w, true); } 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_types != 0) { for (int i = 0; i < NUM_WINTYPES; i++) { if (w->window_types & (1 << i)) { printf(" window_type = '%s'\n", WINTYPES[i].name); } } } printf(" %sfullscreen\n", w->is_fullscreen ? "" : "! "); if (w->bounding_shaped) { printf(" bounding_shaped\n"); } printf(" border_width = %d\n", w->g.border_width); } void inspect_dump_window_maybe_options(struct window_maybe_options wopts) { bool nothing = true; printf(" Applying:\n"); if (wopts.shadow != TRI_UNKNOWN) { printf(" shadow = %s\n", wopts.shadow == TRI_TRUE ? "true" : "false"); nothing = false; } if (wopts.fade != TRI_UNKNOWN) { printf(" fade = %s\n", wopts.fade == TRI_TRUE ? "true" : "false"); nothing = false; } if (wopts.blur_background != TRI_UNKNOWN) { printf(" blur_background = %s\n", wopts.blur_background == TRI_TRUE ? "true" : "false"); nothing = false; } if (wopts.invert_color != TRI_UNKNOWN) { printf(" invert_color = %s\n", wopts.invert_color == TRI_TRUE ? "true" : "false"); nothing = false; } if (wopts.clip_shadow_above != TRI_UNKNOWN) { printf(" clip_shadow_above = %s\n", wopts.clip_shadow_above == TRI_TRUE ? "true" : "false"); nothing = false; } if (wopts.transparent_clipping != TRI_UNKNOWN) { printf(" transparent_clipping = %s\n", wopts.transparent_clipping == TRI_TRUE ? "true" : "false"); nothing = false; } if (wopts.full_shadow != TRI_UNKNOWN) { printf(" full_shadow = %s\n", wopts.full_shadow == TRI_TRUE ? "true" : "false"); nothing = false; } if (wopts.unredir != WINDOW_UNREDIR_INVALID) { const char *str = NULL; switch (wopts.unredir) { case WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE: str = "true"; break; case WINDOW_UNREDIR_TERMINATE: str = "false"; break; case WINDOW_UNREDIR_FORCED: str = "\"forced\""; break; case WINDOW_UNREDIR_PASSIVE: str = "\"passive\""; break; case WINDOW_UNREDIR_WHEN_POSSIBLE: str = "\"preferred\""; break; default: unreachable(); } printf(" unredir = %s\n", str); nothing = false; } if (!safe_isnan(wopts.opacity)) { printf(" opacity = %f\n", wopts.opacity); nothing = false; } if (!safe_isnan(wopts.dim)) { printf(" dim = %f\n", wopts.dim); nothing = false; } if (wopts.corner_radius >= 0) { printf(" corner_radius = %d\n", wopts.corner_radius); nothing = false; } char **animation_triggers = dynarr_new(char *, 0); for (int i = 0; i < ANIMATION_TRIGGER_COUNT; i++) { if (wopts.animations[i].script != NULL) { char *name = NULL; casprintf(&name, "\"%s\"", animation_trigger_names[i]); dynarr_push(animation_triggers, name); } } if (dynarr_len(animation_triggers) > 0) { char *animation_triggers_str = dynarr_join(animation_triggers, ", "); printf(" animations = { triggers = [%s]; }\n", animation_triggers_str); free(animation_triggers_str); nothing = false; } else { dynarr_free_pod(animation_triggers); } if (nothing) { printf(" (nothing)\n"); } } picom-12.5/src/inspect.h000066400000000000000000000010241471504570600151630ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2024 Yuxuan Shui #pragma once #include #include "wm/win.h" struct x_connection; struct c2_state; struct options; int inspect_main(int argc, char **argv, const char *config_file); xcb_window_t inspect_select_window(struct x_connection *c); void inspect_dump_window(const struct c2_state *state, const struct options *opts, const struct win *w); void inspect_dump_window_maybe_options(struct window_maybe_options wopts); picom-12.5/src/log.c000066400000000000000000000217731471504570600143070ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include #include #include #include #include #ifdef CONFIG_OPENGL #include #endif #include "compiler.h" #include "log.h" #include "utils/console.h" #include "utils/misc.h" thread_local struct log *tls_logger; struct log_target; struct log { struct log_target *head; int log_level; }; struct log_target { const struct log_ops *ops; struct log_target *next; }; struct log_ops { void (*write)(struct log_target *, const char *, size_t); void (*writev)(struct log_target *, const struct iovec *, int vcnt); void (*destroy)(struct log_target *); /// Additional strings to print around the log_level string const char *(*colorize_begin)(enum log_level); const char *(*colorize_end)(enum log_level); }; /// Fallback writev for targets don't implement it static attr_unused void log_default_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { size_t total = 0; for (int i = 0; i < vcnt; i++) { total += vec[i].iov_len; } if (!total) { // Nothing to write return; } char *buf = ccalloc(total, char); total = 0; for (int i = 0; i < vcnt; i++) { memcpy(buf + total, vec[i].iov_base, vec[i].iov_len); total += vec[i].iov_len; } tgt->ops->write(tgt, buf, total); free(buf); } 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"; case LOG_LEVEL_ERROR: return "ERROR"; case LOG_LEVEL_FATAL: return "FATAL ERROR"; default: return "????"; } } int string_to_log_level(const char *str) { if (strcasecmp(str, "TRACE") == 0) { return LOG_LEVEL_TRACE; } if (strcasecmp(str, "VERBOSE") == 0) { return LOG_LEVEL_VERBOSE; } if (strcasecmp(str, "DEBUG") == 0) { return LOG_LEVEL_DEBUG; } if (strcasecmp(str, "INFO") == 0) { return LOG_LEVEL_INFO; } if (strcasecmp(str, "WARN") == 0) { return LOG_LEVEL_WARN; } if (strcasecmp(str, "ERROR") == 0) { return LOG_LEVEL_ERROR; } return LOG_LEVEL_INVALID; } struct log *log_new(void) { auto ret = cmalloc(struct log); ret->log_level = LOG_LEVEL_WARN; ret->head = NULL; return ret; } void log_add_target(struct log *l, struct log_target *tgt) { assert(tgt->ops->writev); tgt->next = l->head; l->head = tgt; } /// 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) { struct log_target *now = l->head, **prev = &l->head; while (now) { if (now == tgt) { *prev = now->next; tgt->ops->destroy(tgt); break; } prev = &now->next; now = now->next; } } /// Destroy a log struct and every log target added to it void log_destroy(struct log *l) { // free all tgt struct log_target *head = l->head; while (head) { auto next = head->next; head->ops->destroy(head); head = next; } free(l); } void log_set_level(struct log *l, int level) { assert(level <= LOG_LEVEL_FATAL && level >= 0); l->log_level = level; } enum log_level log_get_level(const struct log *l) { return l->log_level; } 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) { return; } char *buf = NULL; va_list args; va_start(args, fmt); int blen = vasprintf(&buf, fmt, args); va_end(args); if (blen < 0 || !buf) { free(buf); return; } struct timespec ts; timespec_get(&ts, TIME_UTC); struct tm now; localtime_r(&ts.tv_sec, &now); char time_buf[100]; strftime(time_buf, sizeof time_buf, "%x %T", &now); char *time = NULL; int tlen = asprintf(&time, "%s.%03ld", time_buf, ts.tv_nsec / 1000000); if (tlen < 0 || !time) { free(buf); free(time); return; } const char *log_level_str = log_level_to_string(level); size_t llen = strlen(log_level_str); size_t flen = strlen(func); struct log_target *head = l->head; while (head) { const char *p = "", *s = ""; size_t plen = 0, slen = 0; if (head->ops->colorize_begin) { // construct target specific prefix p = head->ops->colorize_begin(level); plen = strlen(p); if (head->ops->colorize_end) { s = head->ops->colorize_end(level); slen = strlen(s); } } head->ops->writev( head, (struct iovec[]){{.iov_base = "[ ", .iov_len = 2}, {.iov_base = time, .iov_len = (size_t)tlen}, {.iov_base = " ", .iov_len = 1}, {.iov_base = (void *)func, .iov_len = flen}, {.iov_base = " ", .iov_len = 1}, {.iov_base = (void *)p, .iov_len = plen}, {.iov_base = (void *)log_level_str, .iov_len = llen}, {.iov_base = (void *)s, .iov_len = slen}, {.iov_base = " ] ", .iov_len = 3}, {.iov_base = buf, .iov_len = (size_t)blen}, {.iov_base = "\n", .iov_len = 1}}, 11); head = head->next; } free(time); free(buf); } /// A trivial deinitializer that simply frees the memory static attr_unused void logger_trivial_destroy(struct log_target *tgt) { free(tgt); } /// A null log target that does nothing static const struct log_ops null_logger_ops; static struct log_target null_logger_target = { .ops = &null_logger_ops, }; struct log_target *null_logger_new(void) { return &null_logger_target; } static void null_logger_write(struct log_target *tgt attr_unused, const char *str attr_unused, size_t len attr_unused) { } static void null_logger_writev(struct log_target *tgt attr_unused, const struct iovec *vec attr_unused, int vcnt attr_unused) { } static const struct log_ops null_logger_ops = { .write = null_logger_write, .writev = null_logger_writev, }; /// A file based logger that writes to file (or stdout/stderr) struct file_logger { struct log_target tgt; FILE *f; struct log_ops ops; }; static void file_logger_write(struct log_target *tgt, const char *str, size_t len) { auto f = (struct file_logger *)tgt; fwrite(str, 1, len, f->f); } static void file_logger_writev(struct log_target *tgt, const struct iovec *vec, int vcnt) { auto f = (struct file_logger *)tgt; fflush(f->f); ssize_t _ attr_unused = writev(fileno(f->f), vec, vcnt); } static void file_logger_destroy(struct log_target *tgt) { auto f = (struct file_logger *)tgt; fclose(f->f); free(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"); case LOG_LEVEL_ERROR: return ANSI("31;1"); case LOG_LEVEL_FATAL: return ANSI("30;103;1"); default: return ""; } } static const char *terminal_colorize_end(enum log_level level attr_unused) { return ANSI("0"); } #undef PREFIX static const struct log_ops file_logger_ops = { .write = file_logger_write, .writev = file_logger_writev, .destroy = file_logger_destroy, }; struct log_target *file_logger_new(const char *filename) { FILE *f = fopen(filename, "a"); if (!f) { return NULL; } auto ret = cmalloc(struct file_logger); ret->tgt.ops = &ret->ops; ret->f = f; // Always assume a file is not a terminal ret->ops = file_logger_ops; return &ret->tgt; } struct log_target *stderr_logger_new(void) { int fd = dup(STDERR_FILENO); if (fd < 0) { return NULL; } FILE *f = fdopen(fd, "w"); if (!f) { return NULL; } auto ret = cmalloc(struct file_logger); ret->tgt.ops = &ret->ops; ret->f = f; ret->ops = file_logger_ops; if (isatty(fd)) { ret->ops.colorize_begin = terminal_colorize_begin; ret->ops.colorize_end = terminal_colorize_end; } return &ret->tgt; } #ifdef CONFIG_OPENGL 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') { len--; } glStringMarkerGREMEDY((GLsizei)len, str); } static const struct log_ops gl_string_marker_logger_ops = { .write = gl_string_marker_logger_write, .writev = log_default_writev, .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 (!epoxy_has_gl_extension("GL_GREMEDY_string_marker")) { return NULL; } auto ret = cmalloc(struct log_target); ret->ops = &gl_string_marker_logger_ops; return ret; } #else struct log_target *gl_string_marker_logger_new(void) { return NULL; } #endif // vim: set noet sw=8 ts=8: picom-12.5/src/log.h000066400000000000000000000073761471504570600143170ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #pragma once #include #include #include #include "compiler.h" 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, }; #define LOG_UNLIKELY(level, x, ...) \ do { \ if (unlikely(LOG_LEVEL_##level >= log_get_level_tls())) { \ log_printf(tls_logger, LOG_LEVEL_##level, __func__, x, ##__VA_ARGS__); \ } \ } while (0) #define LOG_(level, x, ...) \ do { \ if (level >= log_get_level_tls()) { \ log_printf(tls_logger, level, __func__, x, ##__VA_ARGS__); \ } \ } while (0) #define LOG(level, x, ...) LOG_(LOG_LEVEL_##level, x, ##__VA_ARGS__) #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__) #define log_error(x, ...) LOG(ERROR, x, ##__VA_ARGS__) #define log_fatal(x, ...) LOG(FATAL, x, ##__VA_ARGS__) #define log_error_errno(x, ...) LOG(ERROR, x ": %s", ##__VA_ARGS__, strerror(errno)) struct log; struct log_target; attr_printf(4, 5) void log_printf(struct log *, int level, const char *func, const char *fmt, ...); attr_malloc struct log *log_new(void); /// Destroy a log struct and every log target added to it 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 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); extern thread_local struct log *tls_logger; /// Create a thread local logger static inline void log_init_tls(void) { tls_logger = log_new(); } /// Set thread local logger log level static inline void log_set_level_tls(int level) { assert(tls_logger); log_set_level(tls_logger, level); } static inline attr_nonnull_all void log_add_target_tls(struct log_target *tgt) { assert(tls_logger); log_add_target(tls_logger, tgt); } static inline attr_nonnull_all void log_remove_target_tls(struct log_target *tgt) { assert(tls_logger); log_remove_target(tls_logger, tgt); } static inline attr_pure enum log_level log_get_level_tls(void) { assert(tls_logger); return log_get_level(tls_logger); } static inline void log_deinit_tls(void) { assert(tls_logger); log_destroy(tls_logger); tls_logger = NULL; } attr_malloc struct log_target *stderr_logger_new(void); attr_malloc struct log_target *file_logger_new(const char *file); attr_malloc struct log_target *null_logger_new(void); attr_malloc struct log_target *gl_string_marker_logger_new(void); // vim: set noet sw=8 ts=8: picom-12.5/src/meson.build000066400000000000000000000072041471504570600155150ustar00rootroot00000000000000libev = dependency('libev', required: false) if not libev.found() libev = cc.find_library('ev') endif base_deps = [cc.find_library('m'), libev] srcs = [ files( 'api.c', 'atom.c', 'c2.c', 'config.c', 'config_libconfig.c', 'diagnostic.c', 'event.c', 'inspect.c', 'log.c', 'options.c', 'picom.c', 'render.c', 'vblank.c', 'vsync.c', 'x.c', ), ] picom_inc = include_directories(['.', '../include']) cflags = [] required_xcb_packages = [ 'xcb', 'xcb-composite', 'xcb-damage', '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 = [ 'pixman-1', 'x11', 'x11-xcb', 'xcb-image', 'xcb-renderutil', 'xcb-util', 'threads', ] foreach i : required_packages base_deps += [dependency(i, required: true)] endforeach foreach i : required_xcb_packages base_deps += [dependency(i, version: '>=1.12.0', required: true)] endforeach libconfig_dep = dependency('libconfig', version: '>=1.7', required: false) if not libconfig_dep.found() warning('Trying to clone and build libconfig as a subproject.') cmake = import('cmake') sub_libconfig_opts = cmake.subproject_options() sub_libconfig_opts.add_cmake_defines( { 'BUILD_SHARED_LIBS': false, }, ) sub_libconfig_opts.set_install(false) sub_libconfig = cmake.subproject('libconfig', options: sub_libconfig_opts) base_deps += [sub_libconfig.dependency('config')] else base_deps += [libconfig_dep] endif if not cc.has_header('uthash.h') error('Dependency uthash not found') endif deps = [] if get_option('regex') pcre = dependency('libpcre2-8', required: true) cflags += ['-DCONFIG_REGEX_PCRE'] deps += [pcre] endif if get_option('vsync_drm') cflags += ['-DCONFIG_VSYNC_DRM'] deps += [dependency('libdrm', required: true)] endif if get_option('opengl') 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', 'rtkit.c'] endif if get_option('xrescheck') cflags += ['-DDEBUG_XRC'] srcs += ['xrescheck.c'] endif if get_option('unittest') cflags += ['-DUNIT_TEST'] endif host_system = host_machine.system() if host_system == 'linux' cflags += ['-DHAS_INOTIFY'] elif ( host_system == 'freebsd' or host_system == 'netbsd' or host_system == 'dragonfly' or host_system == 'openbsd' ) cflags += ['-DHAS_KQUEUE'] endif subdir('backend') subdir('wm') subdir('renderer') subdir('transition') subdir('utils') dl_dep = [] if not cc.has_function('dlopen') dl_dep = [cc.find_library('dl', required: true)] endif libtools = static_library( 'libtools', [ 'log.c', 'utils/dynarr.c', 'utils/misc.c', 'utils/str.c', 'transition/curve.c', 'transition/script.c', ], include_directories: picom_inc, dependencies: [test_h_dep], install: false, build_by_default: false, ) picom = executable( 'picom', srcs, c_args: cflags, dependencies: [base_deps, deps, test_h_dep] + dl_dep, install: true, include_directories: picom_inc, export_dynamic: true, gnu_symbol_visibility: 'hidden', ) 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 picom-12.5/src/opengl.c000066400000000000000000001315671471504570600150150ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey #include #include #include #include #include #include #include "backend/gl/gl_common.h" #include "backend/gl/glx.h" #include "common.h" #include "compiler.h" #include "config.h" #include "log.h" #include "region.h" #include "utils/kernel.h" #include "utils/misc.h" #include "utils/str.h" #include "wm/win.h" #include "wm/wm.h" #include "opengl.h" #ifndef GL_TEXTURE_RECTANGLE #define GL_TEXTURE_RECTANGLE 0x84F5 #endif static inline XVisualInfo *get_visualinfo_from_visual(session_t *ps, xcb_visualid_t visual) { XVisualInfo vreq = {.visualid = visual}; int nitems = 0; return XGetVisualInfo(ps->c.dpy, VisualIDMask, &vreq, &nitems); } /** * Initialize OpenGL. */ bool glx_init(session_t *ps, bool need_render) { bool success = false; XVisualInfo *pvis = NULL; // Check for GLX extension if (!ps->glx_exists) { log_error("No GLX extension."); goto glx_init_end; } // Get XVisualInfo 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; } // Ensure the visual is double-buffered if (need_render) { int value = 0; 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->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) { goto glx_init_end; } // Initialize GLX data structure if (!ps->psglx) { static const glx_session_t CGLX_SESSION_DEF = CGLX_SESSION_INIT; ps->psglx = cmalloc(glx_session_t); memcpy(ps->psglx, &CGLX_SESSION_DEF, sizeof(glx_session_t)); // +1 for the zero terminator ps->psglx->blur_passes = ccalloc(ps->o.blur_kernel_count, glx_blur_pass_t); for (int i = 0; i < ps->o.blur_kernel_count; ++i) { glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; ppass->unifm_factor_center = -1; ppass->unifm_offset_x = -1; ppass->unifm_offset_y = -1; } ps->psglx->round_passes = ccalloc(1, glx_round_pass_t); glx_round_pass_t *ppass = ps->psglx->round_passes; ppass->unifm_radius = -1; ppass->unifm_texcoord = -1; ppass->unifm_texsize = -1; ppass->unifm_borderw = -1; ppass->unifm_borderc = -1; ppass->unifm_resolution = -1; ppass->unifm_tex_scr = -1; } glx_session_t *psglx = ps->psglx; if (!psglx->context) { // Get GLX context #ifndef DEBUG_GLX_DEBUG_CONTEXT psglx->context = glXCreateContext(ps->c.dpy, pvis, None, GL_TRUE); #else { GLXFBConfig fbconfig = get_fbconfig_from_visualinfo(ps, pvis); if (!fbconfig) { log_error("Failed to get GLXFBConfig for root visual " "%#lx.", pvis->visualid); goto glx_init_end; } f_glXCreateContextAttribsARB p_glXCreateContextAttribsARB = (f_glXCreateContextAttribsARB)glXGetProcAddress( (const GLubyte *)"glXCreateContextAttribsARB"); if (!p_glXCreateContextAttribsARB) { log_error("Failed to get glXCreateContextAttribsARB()."); goto glx_init_end; } static const int attrib_list[] = { GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_DEBUG_BIT_ARB, None}; psglx->context = p_glXCreateContextAttribsARB( ps->c.dpy, fbconfig, NULL, GL_TRUE, attrib_list); } #endif if (!psglx->context) { log_error("Failed to get GLX context."); goto glx_init_end; } // Attach GLX context if (!glXMakeCurrent(ps->c.dpy, get_tgt_window(ps), psglx->context)) { log_error("Failed to attach GLX context."); goto glx_init_end; } #ifdef DEBUG_GLX_DEBUG_CONTEXT { f_DebugMessageCallback p_DebugMessageCallback = (f_DebugMessageCallback)glXGetProcAddress( (const GLubyte *)"glDebugMessageCallback"); if (!p_DebugMessageCallback) { log_error("Failed to get glDebugMessageCallback(0."); goto glx_init_end; } p_DebugMessageCallback(glx_debug_msg_callback, ps); } #endif } // Ensure we have a stencil buffer. X Fixes does not guarantee rectangles // in regions don't overlap, so we must use stencil buffer to make sure // we don't paint a region for more than one time, I think? if (need_render && !ps->o.glx_no_stencil) { GLint val = 0; glGetIntegerv(GL_STENCIL_BITS, &val); if (!val) { log_error("Target window doesn't have stencil buffer."); goto glx_init_end; } } // Check GL_ARB_texture_non_power_of_two, requires a GLX context and // must precede FBConfig fetching if (need_render) { psglx->has_texture_non_power_of_two = epoxy_has_gl_extension("GL_ARB_texture_non_power_of_two"); } // Render preparations if (need_render) { glx_on_root_change(ps); glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glDisable(GL_BLEND); if (!ps->o.glx_no_stencil) { // Initialize stencil buffer glClear(GL_STENCIL_BUFFER_BIT); glDisable(GL_STENCIL_TEST); glStencilMask(0x1); glStencilFunc(GL_EQUAL, 0x1, 0x1); } // Clear screen glClearColor(0.0F, 0.0F, 0.0F, 1.0F); // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // glXSwapBuffers(ps->c.dpy, get_tgt_window(ps)); } success = true; glx_init_end: XFree(pvis); if (!success) { glx_destroy(ps); } return success; } static void glx_free_prog_main(glx_prog_main_t *pprogram) { if (!pprogram) { return; } if (pprogram->prog) { glDeleteProgram(pprogram->prog); pprogram->prog = 0; } pprogram->unifm_opacity = -1; pprogram->unifm_invert_color = -1; pprogram->unifm_tex = -1; } /** * Destroy GLX related resources. */ void glx_destroy(session_t *ps) { if (!ps->psglx) { return; } // Free all GLX resources of windows wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w != NULL) { free_win_res_glx(ps, w); } } // Free GLSL shaders/programs for (int i = 0; i < ps->o.blur_kernel_count; ++i) { glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; if (ppass->frag_shader) { glDeleteShader(ppass->frag_shader); } if (ppass->prog) { glDeleteProgram(ppass->prog); } } free(ps->psglx->blur_passes); glx_round_pass_t *ppass = ps->psglx->round_passes; if (ppass->frag_shader) { glDeleteShader(ppass->frag_shader); } if (ppass->prog) { glDeleteProgram(ppass->prog); } free(ps->psglx->round_passes); glx_free_prog_main(&ps->glx_prog_win); gl_check_err(); // Destroy GLX context if (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 = (struct glx_fbconfig_info){0}; } /** * Callback to run on root window size change. */ void glx_on_root_change(session_t *ps) { glViewport(0, 0, ps->root_width, ps->root_height); // Initialize matrix, copied from dcompmgr glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, ps->root_width, 0, ps->root_height, -1000.0, 1000.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } /** * Initialize GLX blur filter. */ bool glx_init_blur(session_t *ps) { assert(ps->o.blur_kernel_count > 0); assert(ps->o.blur_kerns); assert(ps->o.blur_kerns[0]); // Allocate PBO if more than one blur kernel is present if (ps->o.blur_kernel_count > 1) { // Try to generate a framebuffer GLuint fbo = 0; glGenFramebuffers(1, &fbo); if (!fbo) { log_error("Failed to generate Framebuffer. Cannot do multi-pass " "blur with GLX" " backend."); return false; } glDeleteFramebuffers(1, &fbo); } { 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"); static const char *FRAG_SHADER_BLUR_PREFIX = "#version 110\n" "%s" "uniform float offset_x;\n" "uniform float offset_y;\n" "uniform float factor_center;\n" "uniform %s tex_scr;\n" "\n" "void main() {\n" " vec4 sum = vec4(0.0, 0.0, 0.0, 0.0);\n"; static const char *FRAG_SHADER_BLUR_ADD = " sum += float(%.7g) * %s(tex_scr, vec2(gl_TexCoord[0].x + offset_x " "* float(%d), gl_TexCoord[0].y + offset_y * float(%d)));\n"; static const char *FRAG_SHADER_BLUR_SUFFIX = " sum += %s(tex_scr, vec2(gl_TexCoord[0].x, gl_TexCoord[0].y)) * " "factor_center;\n" " gl_FragColor = sum / (factor_center + float(%.7g));\n" "}\n"; const bool use_texture_rect = !ps->psglx->has_texture_non_power_of_two; const char *sampler_type = (use_texture_rect ? "sampler2DRect" : "sampler2D"); const char *texture_func = (use_texture_rect ? "texture2DRect" : "texture2D"); const char *shader_add = FRAG_SHADER_BLUR_ADD; char *extension = NULL; if (use_texture_rect) { mstrextend(&extension, "#extension GL_ARB_texture_rectangle : " "require\n"); } if (!extension) { extension = strdup(""); } for (int i = 0; i < ps->o.blur_kernel_count; ++i) { auto kern = ps->o.blur_kerns[i]; glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; // Build shader int width = kern->w, height = kern->h; int nele = width * height - 1; assert(nele >= 0); auto len = strlen(FRAG_SHADER_BLUR_PREFIX) + strlen(sampler_type) + strlen(extension) + (strlen(shader_add) + strlen(texture_func) + 42) * (uint)nele + strlen(FRAG_SHADER_BLUR_SUFFIX) + strlen(texture_func) + 12 + 1; char *shader_str = ccalloc(len, char); char *pc = shader_str; sprintf(pc, FRAG_SHADER_BLUR_PREFIX, extension, sampler_type); pc += strlen(pc); assert(strlen(shader_str) < len); 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) { continue; } double val = kern->data[j * width + k]; if (val == 0) { continue; } sum += val; sprintf(pc, shader_add, val, texture_func, k - width / 2, j - height / 2); pc += strlen(pc); assert(strlen(shader_str) < len); } } sprintf(pc, FRAG_SHADER_BLUR_SUFFIX, texture_func, sum); assert(strlen(shader_str) < len); ppass->frag_shader = gl_create_shader(GL_FRAGMENT_SHADER, shader_str); free(shader_str); if (!ppass->frag_shader) { log_error("Failed to create fragment shader %d.", i); free(extension); free(lc_numeric_old); return false; } // Build program ppass->prog = gl_create_program(&ppass->frag_shader, 1); if (!ppass->prog) { log_error("Failed to create GLSL program."); free(extension); free(lc_numeric_old); return false; } // Get uniform addresses #define P_GET_UNIFM_LOC(name, target) \ { \ ppass->target = glGetUniformLocation(ppass->prog, name); \ if (ppass->target < 0) { \ log_error("Failed to get location of %d-th uniform '" name \ "'. Might be troublesome.", \ i); \ } \ } P_GET_UNIFM_LOC("factor_center", unifm_factor_center); P_GET_UNIFM_LOC("offset_x", unifm_offset_x); P_GET_UNIFM_LOC("offset_y", unifm_offset_y); #undef P_GET_UNIFM_LOC } free(extension); // Restore LC_NUMERIC setlocale(LC_NUMERIC, lc_numeric_old); free(lc_numeric_old); } gl_check_err(); return true; } /** * Initialize GLX rounded corners filter. */ bool glx_init_rounded_corners(session_t *ps) { 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"); static const char *FRAG_SHADER = "#version 110\n" "%s" // extensions "uniform float u_radius;\n" "uniform float u_borderw;\n" "uniform vec4 u_borderc;\n" "uniform vec2 u_texcoord;\n" "uniform vec2 u_texsize;\n" "uniform vec2 u_resolution;\n" "uniform %s tex_scr;\n" // sampler2D | sampler2DRect "\n" "// https://www.shadertoy.com/view/ltS3zW\n" "float RectSDF(vec2 p, vec2 b, float r) {\n" " vec2 d = abs(p) - b + vec2(r);\n" " return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;\n" "}\n\n" "void main()\n" "{\n" " vec2 coord = vec2(u_texcoord.x, " "u_resolution.y-u_texsize.y-u_texcoord.y);\n" " vec4 u_v4WndBgColor = %s(tex_scr, vec2(gl_TexCoord[0].st));\n" " float u_fRadiusPx = u_radius;\n" " float u_fHalfBorderThickness = u_borderw / 2.0;\n" " vec4 u_v4BorderColor = u_borderc;\n" " vec4 u_v4FillColor = vec4(0.0, 0.0, 0.0, 0.0);\n" " vec4 v4FromColor = u_v4BorderColor; //Always the border " "color. If no border, this still should be set\n" " vec4 v4ToColor = u_v4WndBgColor; //Outside color is the " "background texture\n" "\n" " vec2 u_v2HalfShapeSizePx = u_texsize/2.0 - " "vec2(u_fHalfBorderThickness);\n" " vec2 v_v2CenteredPos = (gl_FragCoord.xy - u_texsize.xy / 2.0 - " "coord);\n" "\n" " float fDist = RectSDF(v_v2CenteredPos, u_v2HalfShapeSizePx, " "u_fRadiusPx - u_fHalfBorderThickness);\n" " if (u_fHalfBorderThickness > 0.0) {\n" " if (fDist < 0.0) {\n" " v4ToColor = u_v4FillColor;\n" " }\n" " fDist = abs(fDist) - u_fHalfBorderThickness;\n" " } else {\n" " v4FromColor = u_v4FillColor;\n" " }\n" " float fBlendAmount = smoothstep(-1.0, 1.0, fDist);\n" " vec4 c = mix(v4FromColor, v4ToColor, fBlendAmount);\n" "\n" " // final color\n" " gl_FragColor = c;\n" "\n" "}\n"; const bool use_texture_rect = !ps->psglx->has_texture_non_power_of_two; const char *sampler_type = (use_texture_rect ? "sampler2DRect" : "sampler2D"); const char *texture_func = (use_texture_rect ? "texture2DRect" : "texture2D"); char *extension = NULL; if (use_texture_rect) { mstrextend(&extension, "#extension GL_ARB_texture_rectangle : " "require\n"); } if (!extension) { extension = strdup(""); } bool success = false; // Build rounded corners shader auto ppass = ps->psglx->round_passes; auto len = strlen(FRAG_SHADER) + strlen(extension) + strlen(sampler_type) + strlen(texture_func) + 1; char *shader_str = ccalloc(len, char); sprintf(shader_str, FRAG_SHADER, extension, sampler_type, texture_func); assert(strlen(shader_str) < len); log_debug("Generated rounded corners shader:\n%s\n", shader_str); ppass->frag_shader = gl_create_shader(GL_FRAGMENT_SHADER, shader_str); free(shader_str); if (!ppass->frag_shader) { log_error("Failed to create rounded corners fragment shader."); goto out; } // Build program ppass->prog = gl_create_program(&ppass->frag_shader, 1); if (!ppass->prog) { log_error("Failed to create GLSL program."); goto out; } // Get uniform addresses #define P_GET_UNIFM_LOC(name, target) \ { \ ppass->target = glGetUniformLocation(ppass->prog, name); \ if (ppass->target < 0) { \ log_debug("Failed to get location of rounded corners uniform " \ "'" name "'. Might be troublesome."); \ } \ } P_GET_UNIFM_LOC("u_radius", unifm_radius); P_GET_UNIFM_LOC("u_texcoord", unifm_texcoord); P_GET_UNIFM_LOC("u_texsize", unifm_texsize); P_GET_UNIFM_LOC("u_borderw", unifm_borderw); P_GET_UNIFM_LOC("u_borderc", unifm_borderc); P_GET_UNIFM_LOC("u_resolution", unifm_resolution); P_GET_UNIFM_LOC("tex_scr", unifm_tex_scr); #undef P_GET_UNIFM_LOC success = true; out: free(extension); // Restore LC_NUMERIC setlocale(LC_NUMERIC, lc_numeric_old); free(lc_numeric_old); gl_check_err(); return success; } /** * Load a GLSL main program from shader strings. */ bool glx_load_prog_main(const char *vshader_str, const char *fshader_str, glx_prog_main_t *pprogram) { assert(pprogram); // Build program pprogram->prog = gl_create_program_from_str(vshader_str, fshader_str); if (!pprogram->prog) { log_error("Failed to create GLSL program."); return false; } // Get uniform addresses #define P_GET_UNIFM_LOC(name, target) \ { \ pprogram->target = glGetUniformLocation(pprogram->prog, name); \ if (pprogram->target < 0) { \ log_error("Failed to get location of uniform '" name \ "'. Might be troublesome."); \ } \ } P_GET_UNIFM_LOC("opacity", unifm_opacity); P_GET_UNIFM_LOC("invert_color", unifm_invert_color); P_GET_UNIFM_LOC("tex", unifm_tex); P_GET_UNIFM_LOC("time", unifm_time); #undef P_GET_UNIFM_LOC gl_check_err(); return true; } static inline void glx_copy_region_to_tex(session_t *ps, GLenum tex_tgt, int basex, int basey, int dx, int dy, int width, int height) { if (width > 0 && height > 0) { glCopyTexSubImage2D(tex_tgt, 0, dx - basex, dy - basey, dx, ps->root_height - dy - height, width, height); } } static inline GLuint glx_gen_texture(GLenum tex_tgt, int width, int height) { GLuint tex = 0; glGenTextures(1, &tex); if (!tex) { return 0; } glEnable(tex_tgt); glBindTexture(tex_tgt, tex); glTexParameteri(tex_tgt, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(tex_tgt, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(tex_tgt, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(tex_tgt, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(tex_tgt, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); glBindTexture(tex_tgt, 0); return tex; } /** * Bind an OpenGL texture and fill it with pixel data from back buffer */ bool glx_bind_texture(session_t *ps attr_unused, glx_texture_t **pptex, int x, int y, int width, int height) { if (ps->o.legacy_backend != BKEND_GLX && ps->o.legacy_backend != BKEND_XR_GLX_HYBRID) { return true; } glx_texture_t *ptex = *pptex; // log_trace("Copying xy(%d %d) wh(%d %d) ptex(%p)", x, y, width, height, ptex); // Release texture if parameters are inconsistent if (ptex && ptex->texture && (ptex->width != width || ptex->height != height)) { free_texture(ps, &ptex); } // Allocate structure if (!ptex) { ptex = ccalloc(1, glx_texture_t); *pptex = ptex; ptex->width = width; ptex->height = height; ptex->target = GL_TEXTURE_RECTANGLE; if (ps->psglx->has_texture_non_power_of_two) { ptex->target = GL_TEXTURE_2D; } } // Create texture if (!ptex->texture) { ptex->texture = glx_gen_texture(ptex->target, width, height); } if (!ptex->texture) { log_error("Failed to allocate texture."); return false; } // Read destination pixels into a texture glEnable(ptex->target); glBindTexture(ptex->target, ptex->texture); if (width > 0 && height > 0) { glx_copy_region_to_tex(ps, ptex->target, x, y, x, y, width, height); } // Cleanup glBindTexture(ptex->target, 0); glDisable(ptex->target); gl_check_err(); return true; } /** * Bind a X pixmap to an OpenGL texture. */ 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.legacy_backend != BKEND_GLX && ps->o.legacy_backend != BKEND_XR_GLX_HYBRID) { return true; } if (!pixmap) { log_error("Binding to an empty pixmap %#010x. This can't work.", pixmap); return false; } assert(fbcfg); glx_texture_t *ptex = *pptex; bool need_release = true; // Release pixmap if parameters are inconsistent if (ptex && ptex->texture && ptex->pixmap != pixmap) { glx_release_pixmap(ps, ptex); } // Allocate structure if (!ptex) { static const glx_texture_t GLX_TEX_DEF = { .texture = 0, .glpixmap = 0, .pixmap = 0, .target = 0, .width = 0, .height = 0, .y_inverted = false, }; ptex = cmalloc(glx_texture_t); memcpy(ptex, &GLX_TEX_DEF, sizeof(glx_texture_t)); *pptex = ptex; } // Create GLX pixmap int depth = 0; if (!ptex->glpixmap) { need_release = false; // Retrieve pixmap parameters, if they aren't provided if (!width || !height) { auto r = xcb_get_geometry_reply( 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; } depth = r->depth; width = r->width; height = r->height; free(r); } // Determine texture target, copied from compiz // The assumption we made here is the target never changes based on any // 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) { tex_tgt = GLX_TEXTURE_2D_EXT; } 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)) { tex_tgt = GLX_TEXTURE_RECTANGLE_EXT; } 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)); GLint attrs[] = { GLX_TEXTURE_FORMAT_EXT, fbcfg->texture_fmt, GLX_TEXTURE_TARGET_EXT, (GLint)tex_tgt, 0, }; 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); ptex->width = width; ptex->height = height; ptex->y_inverted = fbcfg->y_inverted; } if (!ptex->glpixmap) { log_error("Failed to allocate GLX pixmap."); return false; } glEnable(ptex->target); // Create texture if (!ptex->texture) { need_release = false; GLuint texture = 0; glGenTextures(1, &texture); glBindTexture(ptex->target, texture); glTexParameteri(ptex->target, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(ptex->target, GL_TEXTURE_MAG_FILTER, GL_NEAREST); if (repeat) { glTexParameteri(ptex->target, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(ptex->target, GL_TEXTURE_WRAP_T, GL_REPEAT); } else { glTexParameteri(ptex->target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(ptex->target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } glBindTexture(ptex->target, 0); ptex->texture = texture; } if (!ptex->texture) { log_error("Failed to allocate texture."); return false; } glBindTexture(ptex->target, ptex->texture); // The specification requires rebinding whenever the content changes... // We can't follow this, too slow. if (need_release) { glXReleaseTexImageEXT(ps->c.dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); } glXBindTexImageEXT(ps->c.dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT, NULL); // Cleanup glBindTexture(ptex->target, 0); glDisable(ptex->target); gl_check_err(); return true; } /** * @brief Release binding of a texture. */ 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->c.dpy, ptex->glpixmap, GLX_FRONT_LEFT_EXT); glBindTexture(ptex->target, 0); } // Free GLX Pixmap if (ptex->glpixmap) { glXDestroyPixmap(ps->c.dpy, ptex->glpixmap); ptex->glpixmap = 0; } gl_check_err(); } /** * Set clipping region on the target window. */ void glx_set_clip(session_t *ps, const region_t *reg) { // Quit if we aren't using stencils if (ps->o.glx_no_stencil) { return; } glDisable(GL_STENCIL_TEST); glDisable(GL_SCISSOR_TEST); if (!reg) { return; } int nrects; const rect_t *rects = pixman_region32_rectangles((region_t *)reg, &nrects); if (nrects == 1) { glEnable(GL_SCISSOR_TEST); glScissor(rects[0].x1, ps->root_height - rects[0].y2, rects[0].x2 - rects[0].x1, rects[0].y2 - rects[0].y1); } gl_check_err(); } #define P_PAINTREG_START(var) \ region_t reg_new; \ int nrects; \ const rect_t *rects; \ assert(width >= 0 && height >= 0); \ pixman_region32_init_rect(®_new, dx, dy, (uint)width, (uint)height); \ pixman_region32_intersect(®_new, ®_new, (region_t *)reg_tgt); \ rects = pixman_region32_rectangles(®_new, &nrects); \ glBegin(GL_QUADS); \ \ for (int ri = 0; ri < nrects; ++ri) { \ rect_t var = rects[ri]; #define P_PAINTREG_END() \ } \ glEnd(); \ \ pixman_region32_fini(®_new); /** * Blur contents in a particular region. * * XXX seems to be way to complex for what it does */ bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, GLfloat factor_center, const region_t *reg_tgt, glx_blur_cache_t *pbc) { assert(ps->psglx->blur_passes[0].prog); const bool more_passes = ps->o.blur_kernel_count > 1; const bool have_scissors = glIsEnabled(GL_SCISSOR_TEST); const bool have_stencil = glIsEnabled(GL_STENCIL_TEST); bool ret = false; // Calculate copy region size glx_blur_cache_t ibc = {.width = 0, .height = 0}; if (!pbc) { pbc = &ibc; } int mdx = dx, mdy = dy, mwidth = width, mheight = height; // log_trace("%d, %d, %d, %d", mdx, mdy, mwidth, mheight); /* if (ps->o.resize_damage > 0) { int inc_x = 0, inc_y = 0; for (int i = 0; i < MAX_BLUR_PASS; ++i) { XFixed *kern = ps->o.blur_kerns[i]; if (!kern) break; inc_x += XFIXED_TO_DOUBLE(kern[0]) / 2; inc_y += XFIXED_TO_DOUBLE(kern[1]) / 2; } inc_x = min2(ps->o.resize_damage, inc_x); inc_y = min2(ps->o.resize_damage, inc_y); mdx = max2(dx - inc_x, 0); mdy = max2(dy - inc_y, 0); int mdx2 = min2(dx + width + inc_x, ps->root_width), mdy2 = min2(dy + height + inc_y, ps->root_height); mwidth = mdx2 - mdx; mheight = mdy2 - mdy; } */ GLenum tex_tgt = GL_TEXTURE_RECTANGLE; 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) { free_glx_bc_resize(ps, pbc); } // Generate FBO and textures if needed 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]) { 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) { glGenFramebuffers(1, &pbc->fbo); } const GLuint fbo = pbc->fbo; if (!tex_scr || (more_passes && !tex_scr2)) { log_error("Failed to allocate texture."); goto glx_blur_dst_end; } if (more_passes && !fbo) { log_error("Failed to allocate framebuffer."); goto glx_blur_dst_end; } // Read destination pixels into a texture glEnable(tex_tgt); glBindTexture(tex_tgt, tex_scr); glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, mdy, mwidth, mheight); /* if (tex_scr2) { glBindTexture(tex_tgt, tex_scr2); glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, mdy, mwidth, dx - mdx); glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, dy + height, mwidth, mdy + mheight - dy - height); glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, mdx, dy, dx - mdx, height); glx_copy_region_to_tex(ps, tex_tgt, mdx, mdy, dx + width, dy, mdx + mwidth - dx - width, height); } */ // Texture scaling factor GLfloat texfac_x = 1.0F, texfac_y = 1.0F; if (tex_tgt == GL_TEXTURE_2D) { texfac_x /= (GLfloat)mwidth; texfac_y /= (GLfloat)mheight; } // Paint it back if (more_passes) { glDisable(GL_STENCIL_TEST); glDisable(GL_SCISSOR_TEST); } bool last_pass = false; for (int i = 0; i < ps->o.blur_kernel_count; ++i) { last_pass = (i == ps->o.blur_kernel_count - 1); const glx_blur_pass_t *ppass = &ps->psglx->blur_passes[i]; assert(ppass->prog); assert(tex_scr); glBindTexture(tex_tgt, tex_scr); if (!last_pass) { glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex_scr2, 0); glDrawBuffer(GL_COLOR_ATTACHMENT0); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { log_error("Framebuffer attachment failed."); goto glx_blur_dst_end; } } else { glBindFramebuffer(GL_FRAMEBUFFER, 0); glDrawBuffer(GL_BACK); if (have_scissors) { glEnable(GL_SCISSOR_TEST); } if (have_stencil) { glEnable(GL_STENCIL_TEST); } } // Color negation for testing... // glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); // glTexEnvf(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_REPLACE); // glTexEnvf(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_ONE_MINUS_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glUseProgram(ppass->prog); if (ppass->unifm_offset_x >= 0) { glUniform1f(ppass->unifm_offset_x, texfac_x); } if (ppass->unifm_offset_y >= 0) { glUniform1f(ppass->unifm_offset_y, texfac_y); } 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; auto ry = (GLfloat)(mheight - (crect.y1 - mdy)) * texfac_y; auto rxe = rx + (GLfloat)(crect.x2 - crect.x1) * texfac_x; auto rye = ry - (GLfloat)(crect.y2 - crect.y1) * texfac_y; auto rdx = (GLfloat)(crect.x1 - mdx); auto rdy = (GLfloat)(mheight - crect.y1 + mdy); if (last_pass) { rdx = (GLfloat)crect.x1; rdy = (GLfloat)(ps->root_height - crect.y1); } auto rdxe = rdx + (GLfloat)(crect.x2 - crect.x1); auto rdye = rdy - (GLfloat)(crect.y2 - crect.y1); // log_trace("%f, %f, %f, %f -> %f, %f, %f, %f", rx, ry, // rxe, rye, rdx, // rdy, rdxe, rdye); glTexCoord2f(rx, ry); glVertex3f(rdx, rdy, z); glTexCoord2f(rxe, ry); glVertex3f(rdxe, rdy, z); glTexCoord2f(rxe, rye); glVertex3f(rdxe, rdye, z); glTexCoord2f(rx, rye); glVertex3f(rdx, rdye, z); } P_PAINTREG_END(); glUseProgram(0); // Swap tex_scr and tex_scr2 { GLuint tmp = tex_scr2; tex_scr2 = tex_scr; tex_scr = tmp; } } ret = true; glx_blur_dst_end: glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindTexture(tex_tgt, 0); glDisable(tex_tgt); if (have_scissors) { glEnable(GL_SCISSOR_TEST); } if (have_stencil) { glEnable(GL_STENCIL_TEST); } if (&ibc == pbc) { free_glx_bc(ps, pbc); } gl_check_err(); return ret; } // 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 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 // glReadPixel is probably the most reliable way. void glx_read_border_pixel(int root_height, int root_width, int x, int y, int width, int height, float *ppixel) { assert(ppixel); // Reset the color so the shader doesn't use it ppixel[0] = ppixel[1] = ppixel[2] = ppixel[3] = -1.0F; // First try bottom left corner past the // circle radius (after the rounded corner ends) auto screen_x = x; auto screen_y = root_height - height - y; // X is out of bounds // move to the right side if (screen_x < 0) { screen_x += width; } // Y is out of bounds // move to to top part if (screen_y < 0) { screen_y += height; } // All corners are out of bounds, give up if (screen_x < 0 || screen_y < 0 || screen_x >= root_width || screen_y >= root_height) { return; } // Invert Y-axis so we can query border color from texture (0,0) glReadPixels(screen_x, screen_y, 1, 1, GL_RGBA, GL_FLOAT, (void *)ppixel); log_trace("xy(%d, %d), glxy(%d %d) wh(%d %d), border_col(%.2f, %.2f, %.2f, %.2f)", x, y, screen_x, screen_y, width, height, (float)ppixel[0], (float)ppixel[1], (float)ppixel[2], (float)ppixel[3]); gl_check_err(); } bool glx_round_corners_dst(session_t *ps, struct win *w, const glx_texture_t *ptex, int dx, int dy, int width, int height, float z, float cr, const region_t *reg_tgt) { assert(ps->psglx->round_passes->prog); bool ret = false; // log_warn("dxy(%d, %d) wh(%d %d) rwh(%d %d) b(%d), f(%d)", // dx, dy, width, height, ps->root_width, ps->root_height, w->g.border_width, // w->focused); int mdx = dx, mdy = dy, mwidth = width, mheight = height; log_trace("%d, %d, %d, %d", mdx, mdy, mwidth, mheight); if (w->g.border_width > 0) { glx_read_border_pixel(ps->root_height, ps->root_width, dx, dy, width, height, &w->border_col[0]); } { const glx_round_pass_t *ppass = ps->psglx->round_passes; assert(ppass->prog); glEnable(GL_BLEND); glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glUseProgram(ppass->prog); // If caller specified a texture use it as source log_trace("ptex: %p wh(%d %d) %d %d", ptex, ptex->width, ptex->height, ptex->target, ptex->texture); glActiveTexture(GL_TEXTURE0); glBindTexture(ptex->target, ptex->texture); if (ppass->unifm_tex_scr >= 0) { glUniform1i(ppass->unifm_tex_scr, (GLint)0); } if (ppass->unifm_radius >= 0) { glUniform1f(ppass->unifm_radius, cr); } if (ppass->unifm_texcoord >= 0) { glUniform2f(ppass->unifm_texcoord, (float)dx, (float)dy); } if (ppass->unifm_texsize >= 0) { glUniform2f(ppass->unifm_texsize, (float)mwidth, (float)mheight); } if (ppass->unifm_borderw >= 0) { // Don't render rounded border if we don't know the border color glUniform1f(ppass->unifm_borderw, w->border_col[0] != -1. ? (GLfloat)w->g.border_width : 0); } if (ppass->unifm_borderc >= 0) { glUniform4f(ppass->unifm_borderc, w->border_col[0], w->border_col[1], w->border_col[2], w->border_col[3]); } if (ppass->unifm_resolution >= 0) { glUniform2f(ppass->unifm_resolution, (float)ps->root_width, (float)ps->root_height); } // Painting { P_PAINTREG_START(crect) { // texture-local coordinates auto rx = (GLfloat)(crect.x1 - dx); auto ry = (GLfloat)(crect.y1 - dy); auto rxe = rx + (GLfloat)(crect.x2 - crect.x1); auto rye = ry + (GLfloat)(crect.y2 - crect.y1); if (GL_TEXTURE_2D == ptex->target) { rx = rx / (GLfloat)width; ry = ry / (GLfloat)height; rxe = rxe / (GLfloat)width; rye = rye / (GLfloat)height; } // coordinates for the texture in the target auto rdx = (GLfloat)crect.x1; auto rdy = (GLfloat)(ps->root_height - crect.y1); auto rdxe = (GLfloat)rdx + (GLfloat)(crect.x2 - crect.x1); auto rdye = (GLfloat)rdy - (GLfloat)(crect.y2 - crect.y1); // Invert Y if needed, this may not work as expected, // though. I don't have such a FBConfig to test with. ry = 1.0F - ry; rye = 1.0F - rye; // log_trace("Rect %d (i:%d): %f, %f, %f, %f -> %f, %f, // %f, %f", ri ,ptex ? ptex->y_inverted : -1, rx, ry, // rxe, // rye, rdx, rdy, rdxe, rdye); glTexCoord2f(rx, ry); glVertex3f(rdx, rdy, z); glTexCoord2f(rxe, ry); glVertex3f(rdxe, rdy, z); glTexCoord2f(rxe, rye); glVertex3f(rdxe, rdye, z); glTexCoord2f(rx, rye); glVertex3f(rdx, rdye, z); } P_PAINTREG_END(); } glUseProgram(0); glDisable(GL_BLEND); } ret = true; glBindTexture(ptex->target, 0); glDisable(ptex->target); glDisable(GL_BLEND); gl_check_err(); return ret; } bool glx_dim_dst(session_t *ps, int dx, int dy, int width, int height, int z, GLfloat factor, const region_t *reg_tgt) { // It's possible to dim in glx_render(), but it would be over-complicated // 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); P_PAINTREG_START(crect) { // XXX what does all of these variables mean? GLint rdx = crect.x1; GLint rdy = ps->root_height - crect.y1; GLint rdxe = rdx + (crect.x2 - crect.x1); GLint rdye = rdy - (crect.y2 - crect.y1); glVertex3i(rdx, rdy, z); glVertex3i(rdxe, rdy, z); glVertex3i(rdxe, rdye, z); glVertex3i(rdx, rdye, z); } P_PAINTREG_END(); glColor4f(0.0F, 0.0F, 0.0F, 0.0F); glDisable(GL_BLEND); gl_check_err(); return true; } /** * @brief Render a region with texture data. */ bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, int dy, int width, int height, int z, double opacity, bool argb, bool neg, const region_t *reg_tgt, const glx_prog_main_t *pprogram) { if (!ptex || !ptex->texture) { log_error("Missing texture."); return false; } const bool has_prog = pprogram && pprogram->prog; bool dual_texture = false; // It's required by legacy versions of OpenGL to enable texture target // before specifying environment. Thanks to madsy for telling me. glEnable(ptex->target); // Enable blending if needed if (opacity < 1.0 || argb) { glEnable(GL_BLEND); // Needed for handling opacity of ARGB texture glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); // This is all weird, but X Render is using premultiplied ARGB format, and // we need to use those things to correct it. Thanks to derhass for help. glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glColor4d(opacity, opacity, opacity, opacity); } if (!has_prog) { // The default, fixed-function path // Color negation if (neg) { // Simple color negation if (!glIsEnabled(GL_BLEND)) { glEnable(GL_COLOR_LOGIC_OP); glLogicOp(GL_COPY_INVERTED); } // ARGB texture color negation else if (argb) { dual_texture = true; // Use two texture stages because the calculation is too // complicated, thanks to madsy for providing code Texture // stage 0 glActiveTexture(GL_TEXTURE0); // Negation for premultiplied color: color = A - C glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_SUBTRACT); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); // Pass texture alpha through glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_REPLACE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); // Texture stage 1 glActiveTexture(GL_TEXTURE1); glEnable(ptex->target); glBindTexture(ptex->target, ptex->texture); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); // Modulation with constant factor glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PRIMARY_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_ALPHA); // Modulation with constant factor glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_PREVIOUS); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PRIMARY_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA); glActiveTexture(GL_TEXTURE0); } // RGB blend color negation else { glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); // Modulation with constant factor glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_RGB, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_ONE_MINUS_SRC_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_RGB, GL_PRIMARY_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR); // Modulation with constant factor glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE0_ALPHA, GL_TEXTURE); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA); glTexEnvi(GL_TEXTURE_ENV, GL_SOURCE1_ALPHA, GL_PRIMARY_COLOR); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA); } } } else { // Programmable path assert(pprogram->prog); glUseProgram(pprogram->prog); struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); if (pprogram->unifm_opacity >= 0) { glUniform1f(pprogram->unifm_opacity, (float)opacity); } if (pprogram->unifm_invert_color >= 0) { glUniform1i(pprogram->unifm_invert_color, neg); } 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); } } // log_trace("Draw: %d, %d, %d, %d -> %d, %d (%d, %d) z %d", x, y, width, height, // dx, dy, ptex->width, ptex->height, z); // Bind texture glBindTexture(ptex->target, ptex->texture); if (dual_texture) { glActiveTexture(GL_TEXTURE1); glBindTexture(ptex->target, ptex->texture); glActiveTexture(GL_TEXTURE0); } // Painting { P_PAINTREG_START(crect) { // texture-local coordinates auto rx = (GLfloat)(crect.x1 - dx + x); auto ry = (GLfloat)(crect.y1 - dy + y); auto rxe = rx + (GLfloat)(crect.x2 - crect.x1); auto rye = ry + (GLfloat)(crect.y2 - crect.y1); // Rectangle textures have [0-w] [0-h] while 2D texture has [0-1] // [0-1] Thanks to amonakov for pointing out! if (GL_TEXTURE_2D == ptex->target) { rx = rx / (GLfloat)ptex->width; ry = ry / (GLfloat)ptex->height; rxe = rxe / (GLfloat)ptex->width; rye = rye / (GLfloat)ptex->height; } // coordinates for the texture in the target GLint rdx = crect.x1; GLint rdy = ps->root_height - crect.y1; GLint rdxe = rdx + (crect.x2 - crect.x1); GLint rdye = rdy - (crect.y2 - crect.y1); // 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; } // log_trace("Rect %d: %f, %f, %f, %f -> %d, %d, %d, %d", ri, rx, // ry, rxe, rye, // rdx, rdy, rdxe, rdye); #define P_TEXCOORD(cx, cy) \ { \ if (dual_texture) { \ glMultiTexCoord2f(GL_TEXTURE0, cx, cy); \ glMultiTexCoord2f(GL_TEXTURE1, cx, cy); \ } else \ glTexCoord2f(cx, cy); \ } P_TEXCOORD(rx, ry); glVertex3i(rdx, rdy, z); P_TEXCOORD(rxe, ry); glVertex3i(rdxe, rdy, z); P_TEXCOORD(rxe, rye); glVertex3i(rdxe, rdye, z); P_TEXCOORD(rx, rye); glVertex3i(rdx, rdye, z); } P_PAINTREG_END(); } // Cleanup glBindTexture(ptex->target, 0); 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); glDisable(ptex->target); if (dual_texture) { glActiveTexture(GL_TEXTURE1); glBindTexture(ptex->target, 0); glDisable(ptex->target); glActiveTexture(GL_TEXTURE0); } if (has_prog) { glUseProgram(0); } gl_check_err(); return true; } /** * Free GLX part of win. */ void free_win_res_glx(session_t *ps, struct win *w) { free_paint_glx(ps, &w->paint); free_paint_glx(ps, &w->shadow_paint); free_glx_bc(ps, &w->glx_blur_cache); free_texture(ps, &w->glx_texture_bg); } picom-12.5/src/opengl.h000066400000000000000000000137411471504570600150130ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey #pragma once #include "common.h" #include "compiler.h" #include "region.h" #include "render.h" #include "wm/win.h" #include #include #include #include #include #include #include typedef struct { /// Fragment shader for blur. GLuint frag_shader; /// GLSL program for blur. GLuint prog; /// Location of uniform "offset_x" in blur GLSL program. GLint unifm_offset_x; /// Location of uniform "offset_y" in blur GLSL program. GLint unifm_offset_y; /// Location of uniform "factor_center" in blur GLSL program. GLint unifm_factor_center; } glx_blur_pass_t; typedef struct { /// Fragment shader for rounded corners. GLuint frag_shader; /// GLSL program for rounded corners. GLuint prog; /// Location of uniform "radius" in rounded-corners GLSL program. GLint unifm_radius; /// Location of uniform "texcoord" in rounded-corners GLSL program. GLint unifm_texcoord; /// Location of uniform "texsize" in rounded-corners GLSL program. GLint unifm_texsize; /// Location of uniform "borderw" in rounded-corners GLSL program. GLint unifm_borderw; /// Location of uniform "borderc" in rounded-corners GLSL program. GLint unifm_borderc; /// Location of uniform "resolution" in rounded-corners GLSL program. GLint unifm_resolution; /// Location of uniform "texture_scr" in rounded-corners GLSL program. GLint unifm_tex_scr; } glx_round_pass_t; /// Structure containing GLX-dependent data for a session. typedef struct glx_session { // === OpenGL related === /// GLX context. GLXContext context; /// Whether we have GL_ARB_texture_non_power_of_two. bool has_texture_non_power_of_two; /// Current GLX Z value. int z; glx_blur_pass_t *blur_passes; glx_round_pass_t *round_passes; } glx_session_t; /// @brief Wrapper of a bound GLX texture. typedef struct _glx_texture { GLuint texture; GLXPixmap glpixmap; xcb_pixmap_t pixmap; GLenum target; int width; int height; bool y_inverted; } glx_texture_t; #define CGLX_SESSION_INIT \ { .context = NULL } bool glx_dim_dst(session_t *ps, int dx, int dy, int width, int height, int z, GLfloat factor, const region_t *reg_tgt); bool glx_render(session_t *ps, const glx_texture_t *ptex, int x, int y, int dx, int dy, int width, int height, int z, double opacity, bool argb, bool neg, const region_t *reg_tgt, const glx_prog_main_t *pprogram); bool glx_init(session_t *ps, bool need_render); void glx_destroy(session_t *ps); void glx_on_root_change(session_t *ps); bool glx_init_blur(session_t *ps); bool glx_init_rounded_corners(session_t *ps); #ifdef CONFIG_OPENGL bool glx_load_prog_main(const char *vshader_str, const char *fshader_str, glx_prog_main_t *pprogram); #endif 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 *); void glx_release_pixmap(session_t *ps, glx_texture_t *ptex); bool glx_bind_texture(session_t *ps, glx_texture_t **pptex, int x, int y, int width, int height); void glx_paint_pre(session_t *ps, region_t *preg) attr_nonnull(1, 2); /** * Check if a texture is bound, or is bound to the given 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); } void glx_set_clip(session_t *ps, const region_t *reg); bool glx_blur_dst(session_t *ps, int dx, int dy, int width, int height, float z, GLfloat factor_center, const region_t *reg_tgt, glx_blur_cache_t *pbc); bool glx_round_corners_dst(session_t *ps, struct win *w, const glx_texture_t *ptex, int dx, int dy, int width, int height, float z, float cr, const region_t *reg_tgt); GLuint glx_create_shader(GLenum shader_type, const char *shader_str); GLuint glx_create_program(const GLuint *const shaders, int nshaders); GLuint glx_create_program_from_str(const char *vert_shader_str, const char *frag_shader_str); unsigned char *glx_take_screenshot(session_t *ps, int *out_length); /** * Check if there's a GLX context. */ static inline bool glx_has_context(session_t *ps) { return ps->psglx && ps->psglx->context; } /** * Ensure we have a GLX context. */ static inline bool ensure_glx_context(session_t *ps) { // Create GLX context if (!glx_has_context(ps)) { glx_init(ps, false); } return glx_has_context(ps); } /** * Free a GLX texture. */ static inline void free_texture_r(session_t *ps attr_unused, GLuint *ptexture) { if (*ptexture) { assert(glx_has_context(ps)); glDeleteTextures(1, ptexture); *ptexture = 0; } } /** * Free a GLX Framebuffer object. */ static inline void free_glx_fbo(GLuint *pfbo) { if (*pfbo) { glDeleteFramebuffers(1, pfbo); *pfbo = 0; } assert(!*pfbo); } /** * Free data in glx_blur_cache_t on resize. */ static inline void free_glx_bc_resize(session_t *ps, glx_blur_cache_t *pbc) { free_texture_r(ps, &pbc->textures[0]); free_texture_r(ps, &pbc->textures[1]); pbc->width = 0; pbc->height = 0; } /** * Free a glx_blur_cache_t */ static inline void free_glx_bc(session_t *ps, glx_blur_cache_t *pbc) { free_glx_fbo(&pbc->fbo); free_glx_bc_resize(ps, pbc); } /** * Free a glx_texture_t. */ static inline void free_texture(session_t *ps, glx_texture_t **pptex) { glx_texture_t *ptex = *pptex; // Quit if there's nothing if (!ptex) { return; } glx_release_pixmap(ps, ptex); free_texture_r(ps, &ptex->texture); // Free structure itself free(ptex); *pptex = NULL; } /** * Free GLX part of paint_t. */ static inline void free_paint_glx(session_t *ps, paint_t *ppaint) { free_texture(ps, &ppaint->ptex); ppaint->fbcfg = (struct glx_fbconfig_info){0}; } /** * Free GLX part of win. */ void free_win_res_glx(session_t *ps, struct win *w); picom-12.5/src/options.c000066400000000000000000001440641471504570600152200ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include #include #include #include #include #include // for xcb_render_fixed_t, XXX #include "backend/backend.h" #include "c2.h" #include "common.h" #include "config.h" #include "log.h" #include "options.h" #include "transition/script.h" #include "utils/dynarr.h" #include "utils/misc.h" #include "utils/str.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; const char *argv0; }; 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 set_rule_flag(const struct picom_option *arg_opt, const struct picom_arg *arg, const char * /*arg_str*/, void *output) { auto opt = (struct options *)output; if (!list_is_empty(&opt->rules)) { log_warn_both_style_of_rules(arg_opt->long_name); opt->has_both_style_of_rules = true; return true; } *(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_rule_float(const struct picom_option *arg_opt, const struct picom_arg *arg, const char *arg_str, void *output) { auto opt = (struct options *)output; if (!list_is_empty(&opt->rules)) { log_warn_both_style_of_rules(arg_opt->long_name); opt->has_both_style_of_rules = true; return true; } return store_float(arg_opt, arg, arg_str, output); } 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 *arg_opt, const struct picom_arg *arg, const char *arg_str, void *output) { const struct picom_rules_parser *parser = arg->user_data; struct options *opt = (struct options *)output; if (!list_is_empty(&opt->rules)) { log_warn_both_style_of_rules(arg_opt->long_name); opt->has_both_style_of_rules = true; return true; } auto rules = (struct list_node *)(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, \ } /// A true or false option that functions like a window rule. Which is superseded by the /// `rules` option. #define ENABLE_RULE(member) \ no_argument, { \ .offset = OFFSET(member), .handler = set_rule_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}, \ } /// A float option that functions like a window rule. Which is superseded by the `rules` /// option. #define FLOAT_RULE(member, min, max) \ required_argument, { \ .offset = OFFSET(member), .handler = store_rule_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; } static bool store_backend(const struct picom_option * /*opt*/, const struct picom_arg * /*arg*/, const char *arg_str, void *output) { struct options *opt = (struct options *)output; opt->legacy_backend = parse_backend(arg_str); opt->backend = backend_find(arg_str); if (opt->legacy_backend == NUM_BKEND && opt->backend == NULL) { log_error("Invalid backend: %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."}, [307] = {"plugins" , IGNORE(required_argument), "Plugins to load. Can be specified multiple times, each time with a single plugin."}, // "Rule-like" options [262] = {"mark-wmwin-focused" , ENABLE_RULE(mark_wmwin_focused) , "Try to detect WM windows and mark them as active."}, [264] = {"mark-ovredir-focused" , ENABLE_RULE(mark_ovredir_focused) , "Mark windows that have no WM frame as active."}, [266] = {"shadow-ignore-shaped" , ENABLE_RULE(shadow_ignore_shaped) , "Do not paint shadows on shaped windows. (Deprecated, use --shadow-exclude " "\'bounding_shaped\' or --shadow-exclude \'bounding_shaped && " "!rounded_corners\' instead.)"}, [260] = {"inactive-opacity-override", ENABLE_RULE(inactive_opacity_override), "Inactive opacity set by -i overrides value of _NET_WM_WINDOW_OPACITY."}, [297] = {"active-opacity" , FLOAT_RULE(active_opacity, 0, 1) , "Default opacity for active windows. (0.0 - 1.0)"}, [261] = {"inactive-dim" , FLOAT_RULE(inactive_dim, 0, 1) , "Dim inactive windows. (0.0 - 1.0, defaults to 0)"}, ['i'] = {"inactive-opacity" , FLOAT_RULE(inactive_opacity, 0, 1) , "Opacity of inactive windows. (0.0 - 1.0)"}, // 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."}, [265] = {"no-fading-openclose" , ENABLE(no_fading_openclose) , "Do not fade on window open/close."}, [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."}, [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(use_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."}, [805] = {"monitor" , ENABLE(inspect_monitor) , "For picom-inspect, run in a loop and dump information every time something " "changed about a window.", "picom-inspect"}, // 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)"}, ['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)."}, [283] = {"blur-background" , FIXED(blur_method, BLUR_METHOD_KERNEL) , "Blur background of semi-transparent / ARGB windows. May impact performance"}, [290] = {"backend" , DO(store_backend) , "Backend. Possible values are: " BACKENDS}, [293] = {"benchmark" , INTEGER(benchmark, 0, INT_MAX) , "Benchmark mode. Repeatedly paint until reaching the specified cycles."}, [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(legacy_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) { FILE *f = (ret ? stderr : stdout); fprintf(f, "picom " PICOM_FULL_VERSION "\n"); 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; } const char *basename = strrchr(argv0, '/') ? strrchr(argv0, '/') + 1 : argv0; 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; } if (picom_options[i].argv0 != NULL && strcmp(picom_options[i].argv0, basename) != 0) { // Hide options that are not for this program. 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; } if (picom_options[i].argv0 != NULL && strcmp(picom_options[i].argv0, basename) != 0) { // Hide options that are not for this program. 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 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(); scoped_charp current_working_dir = getcwd(NULL, 0); int o = 0, longopt_idx = -1; // Pre-parse the command line arguments to check for --config and invalid // switches // Must reset optind to 0 here in case we reread the command line // arguments optind = 1; *config_file = NULL; *exit_code = 0; while (-1 != (o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) { if (o == 256) { *config_file = strdup(optarg); } else if (o == 'h') { usage(argv[0], 0); return true; } else if (o == 'b') { *fork = true; } else if (o == 314) { *all_xerrors = true; } else if (o == 318) { printf(PICOM_FULL_VERSION "\n"); return true; } else if (o == 307) { // --plugin if (!load_plugin(optarg, current_working_dir)) { log_error("Failed to load plugin %s", optarg); goto err; } } else if (o == '?' || o == ':') { usage(argv[0], 1); goto err; } // TODO(yshui) maybe log-level should be handled here. } // Check for abundant positional arguments if (optind < argc) { // log is not initialized here yet fprintf(stderr, "picom doesn't accept positional arguments.\n"); goto err; } return false; err: *exit_code = 1; return true; } static void script_ptr_deinit(struct script **ptr) { if (*ptr) { script_free(*ptr); *ptr = NULL; } } static bool sanitize_options(struct options *opt) { if (opt->use_legacy_backends) { if (opt->legacy_backend == BKEND_EGL) { log_error("The egl backend is not supported with " "--legacy-backends"); return false; } if (opt->monitor_repaint && opt->legacy_backend != BKEND_XRENDER) { log_warn("For legacy backends, --monitor-repaint is only " "implemented for " "xrender."); } if (opt->debug_mode) { log_error("Debug mode does not work with the legacy backends."); return false; } if (opt->transparent_clipping) { log_error("Transparent clipping does not work with the legacy " "backends"); return false; } if (opt->max_brightness < 1.0) { log_warn("--max-brightness is not supported by the legacy " "backends. Falling back to 1.0."); opt->max_brightness = 1.0; } if (opt->blur_method == BLUR_METHOD_DUAL_KAWASE) { log_warn("Dual-kawase blur is not implemented by the legacy " "backends."); opt->blur_method = BLUR_METHOD_NONE; } if (dynarr_len(opt->all_scripts) > 0) { log_warn("Custom animations are not supported by the legacy " "backends. Disabling animations."); for (size_t i = 0; i < ARR_SIZE(opt->animations); i++) { opt->animations[i].script = NULL; } dynarr_clear(opt->all_scripts, script_ptr_deinit); } if (opt->window_shader_fg || !list_is_empty(&opt->window_shader_fg_rules)) { log_warn("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); } if (opt->legacy_backend == BKEND_XRENDER) { bool has_neg = false; 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) { has_neg = true; break; } } if (has_neg) { log_warn("A convolution kernel with negative " "values may not work properly under X " "Render backend."); break; } } } } else { if (opt->backend == NULL) { auto valid_backend_name = backend_find(BACKEND_STRS[opt->legacy_backend]) != NULL; if (!valid_backend_name) { log_error("Backend \"%s\" is only available as part of " "the legacy backends.", BACKEND_STRS[opt->legacy_backend]); } else { // If the backend name is a valid new backend, then // it must not have been specified by the user, because // otherwise opt->backend wouldn't be NULL. log_error("Backend not specified. You must choose one " "explicitly. Valid ones are: "); for (auto i = backend_iter(); i; i = backend_iter_next(i)) { log_error("\t%s", backend_name(i)); } } return false; } if (opt->glx_fshader_win_str) { log_warn("--glx-fshader-win has been replaced by " "\"--window-shader-fg\" for the new backends."); } if (opt->max_brightness < 1.0 && opt->use_damage) { log_warn("--max-brightness requires --no-use-damage. " "Falling back to 1.0."); opt->max_brightness = 1.0; } } if (opt->write_pid_path && *opt->write_pid_path != '/') { log_warn("--write-pid-path is not an absolute path"); } // Sanitize parameters for dual-filter kawase blur if (opt->blur_method == BLUR_METHOD_DUAL_KAWASE) { if (opt->blur_strength <= 0 && opt->blur_radius > 500) { log_warn("Blur radius >500 not supported by dual_kawase " "method, " "capping to 500."); opt->blur_radius = 500; } if (opt->blur_strength > 20) { log_warn("Blur strength >20 not supported by dual_kawase " "method, " "capping to 20."); opt->blur_strength = 20; } } if (opt->resize_damage < 0) { log_warn("Negative --resize-damage will not work correctly."); } if (opt->has_both_style_of_rules) { log_warn("You have set both \"rules\", as well as old-style rule options " "in your configuration. The old-style rule options will have no " "effect. It is recommended that you remove the old-style rule " "options, and use only \"rules\" for all your window rules. If " "you do genuinely need to use the old-style rule options, you " "must not set \"rules\"."); } return true; } /** * Process arguments and configuration files. */ bool get_cfg(options_t *opt, int argc, char *const *argv) { int o = 0, longopt_idx = -1; bool failed = false; optind = 1; const char *basename = strrchr(argv[0], '/') ? strrchr(argv[0], '/') + 1 : argv[0]; while (-1 != (o = getopt_long(argc, argv, shortopts, longopts, &longopt_idx))) { if (o == '?' || o == ':' || picom_options[o].arg.handler == NULL) { usage(argv[0], 1); failed = true; } else if (picom_options[o].argv0 != NULL && strcmp(picom_options[o].argv0, basename) != 0) { log_error("Invalid option %s", argv[optind - 1]); failed = true; } else if (!picom_options[o].arg.handler( &picom_options[o], &picom_options[o].arg, optarg, opt)) { failed = true; } if (failed) { // Parsing this option has failed, break the loop break; } } if (failed) { return false; } log_set_level_tls(opt->log_level); 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 (!sanitize_options(opt)) { return false; } // --blur-background-frame implies --blur-background if (opt->blur_background_frame && opt->blur_method == BLUR_METHOD_NONE) { opt->blur_method = BLUR_METHOD_KERNEL; } // Apply default wintype options that are dependent on global options set_default_winopts(opt); // Determine whether we track window grouping if (opt->detect_transient || opt->detect_client_leader) { opt->track_leader = true; } // 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", &opt->blur_kernel_count); CHECK(opt->blur_kerns); CHECK(opt->blur_kernel_count); } if (opt->fading_enable) { generate_fading_config(opt); } return true; } void options_postprocess_c2_lists(struct c2_state *state, struct x_connection *c, struct options *option) { if (!list_is_empty(&option->rules)) { if (!c2_list_postprocess(state, c->c, &option->rules)) { log_error("Post-processing of rules failed, some of your rules " "might not work"); } return; } 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"); } } static void free_window_maybe_options(void *data) { auto wopts = (struct window_maybe_options *)data; free((void *)wopts->shader); free(wopts); } 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); c2_list_free(&options->rules, free_window_maybe_options); free(options->config_file_path); 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); dynarr_free(options->all_scripts, script_ptr_deinit); memset(options->animations, 0, sizeof(options->animations)); list_foreach_safe(struct included_config_file, i, &options->included_config_files, siblings) { free(i->path); list_remove(&i->siblings); free(i); } } // vim: set noet sw=8 ts=8 : picom-12.5/src/options.h000066400000000000000000000021121471504570600152100ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once /// Parse command line options #include #include // for xcb_render_fixed_t #include #include "compiler.h" #include "config.h" typedef struct session session_t; /// 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); /** * Process arguments and configuration files. * * Parameters: * shadow_enable = Carry overs from parse_config * fading_enable * conv_kern_hasneg * winopt_mask * Returns: * Whether configuration are processed successfully. */ 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: picom-12.5/src/picom.c000066400000000000000000002617611471504570600146400ustar00rootroot00000000000000// SPDX-License-Identifier: MIT /* * picom - a compositor for X11 * * Based on `compton` - Copyright (c) 2011-2013, Christopher Jeffrey * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard * * Copyright (c) 2019-2023, Yuxuan Shui * * See LICENSE-mit for more information. * */ #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 #include #ifdef CONFIG_OPENGL #include "opengl.h" #endif #include "api_internal.h" #include "atom.h" #include "backend/backend.h" #include "c2.h" #include "common.h" #include "compiler.h" #include "config.h" #include "dbus.h" #include "diagnostic.h" #include "event.h" #include "inspect.h" #include "log.h" #include "options.h" #include "picom.h" #include "region.h" #include "render.h" #include "renderer/command_builder.h" #include "renderer/layout.h" #include "renderer/renderer.h" #include "utils/dynarr.h" #include "utils/file_watch.h" #include "utils/kernel.h" #include "utils/list.h" #include "utils/misc.h" #include "utils/statistics.h" #include "utils/str.h" #include "utils/uthash_extra.h" #include "vblank.h" #include "wm/defs.h" #include "wm/wm.h" #include "x.h" /// Get session_t pointer from a pointer to a member of session_t #define session_ptr(ptr, member) \ ({ \ const __typeof__(((session_t *)0)->member) *__mptr = (ptr); \ (session_t *)((char *)__mptr - offsetof(session_t, member)); \ }) static bool must_use redirect_start(session_t *ps); static void unredirect(session_t *ps); // === Global constants === /// Name strings for window types. 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 /// Names of backends. 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 // === Global variables === /// Pointer to current session, as a global variable. Only used by /// xerror(), which could not have a pointer to current session passed in. /// XXX Limit what xerror can access by not having this pointer session_t *ps_g = NULL; void quit(session_t *ps) { ps->quit = true; ev_break(ps->loop, EVBREAK_ALL); } /** * Convert struct timespec to milliseconds. */ static inline int64_t timespec_ms(struct timespec ts) { return (int64_t)ts.tv_sec * 1000 + (int64_t)ts.tv_nsec / 1000000; } 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; } 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; } 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; } void schedule_render(session_t *ps, bool triggered_by_vblank); /// 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; } schedule_render(ps, false); return VBLANK_CALLBACK_DONE; } /// 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; } // 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) { log_verbose("Queue redraw, render_queued: %d, backend_busy: %d", ps->render_queued, ps->backend_busy); if (ps->render_queued) { return; } ps->render_queued = true; schedule_render(ps, false); } /** * Get a region of the screen size. */ static inline void get_screen_region(session_t *ps, region_t *res) { pixman_box32_t b = {.x1 = 0, .y1 = 0, .x2 = ps->root_width, .y2 = ps->root_height}; pixman_region32_fini(res); pixman_region32_init_rects(res, &b, 1); } void add_damage(session_t *ps, const region_t *damage) { // Ignore damage when screen isn't redirected if (!ps->redirected) { return; } if (!damage || ps->damage_ring.count <= 0) { return; } log_trace("Adding damage: "); dump_region(damage); auto cursor = &ps->damage_ring.damages[ps->damage_ring.cursor]; pixman_region32_union(cursor, cursor, (region_t *)damage); } // === Windows === /** * Rebuild cached screen_reg. */ static void rebuild_screen_reg(session_t *ps) { get_screen_region(ps, &ps->screen_reg); } /// Free up all the images and deinit the backend static void destroy_backend(session_t *ps) { wm_stack_foreach_safe(ps->wm, cursor, next_cursor) { auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } // An unmapped window shouldn't have a pixmap, unless it has animation // running. (`w->previous.state != w->state` means there might be // animation but we haven't had a chance to start it because // `win_process_animation_and_state_change` hasn't been called.) // TBH, this assertion is probably too complex than what it's worth. assert(!w->win_image || w->state == WSTATE_MAPPED || w->running_animation_instance != NULL || w->previous.state != w->state); // Wrapping up animation in progress free(w->running_animation_instance); w->running_animation_instance = NULL; if (ps->backend_data) { // Unmapped windows could still have shadow images. // 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) { win_destroy_finish(ps, w); } } 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) { ps->backend_data->ops.release_image(ps->backend_data, ps->root_image); ps->root_image = NULL; } 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( ps->backend_data, ps->backend_blur_context); ps->backend_blur_context = NULL; } ps->backend_data->ops.deinit(ps->backend_data); ps->backend_data = NULL; } } static bool initialize_blur(session_t *ps) { struct kernel_blur_args kargs; struct gaussian_blur_args gargs; struct box_blur_args bargs; struct dual_kawase_blur_args dkargs; void *args = NULL; switch (ps->o.blur_method) { case BLUR_METHOD_BOX: bargs.size = ps->o.blur_radius; args = (void *)&bargs; break; case BLUR_METHOD_KERNEL: kargs.kernel_count = ps->o.blur_kernel_count; kargs.kernels = ps->o.blur_kerns; args = (void *)&kargs; break; case BLUR_METHOD_GAUSSIAN: gargs.size = ps->o.blur_radius; gargs.deviation = ps->o.blur_deviation; args = (void *)&gargs; break; case BLUR_METHOD_DUAL_KAWASE: dkargs.size = ps->o.blur_radius; dkargs.strength = ps->o.blur_strength; args = (void *)&dkargs; break; 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, format, args); return ps->backend_blur_context != NULL; } /// Init the backend and bind all the window pixmap to backend images static bool initialize_backend(session_t *ps) { if (!ps->o.use_legacy_backends) { assert(!ps->backend_data); // Reinitialize win_data ps->backend_data = backend_init(ps->o.backend, ps, session_get_target_window(ps)); api_backend_plugins_invoke(backend_name(ps->o.backend), ps->backend_data); if (!ps->backend_data) { log_fatal("Failed to initialize backend, aborting..."); quit(ps); return false; } if (!initialize_blur(ps)) { log_fatal("Failed to prepare for background blur, aborting..."); goto err; } // Create shaders if (!ps->backend_data->ops.create_shader && ps->shaders) { log_warn("Shaders are not supported by selected backend %s, " "they will be ignored", backend_name(ps->o.backend)); } else { 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 { shader->attributes = 0; if (ps->backend_data->ops.get_shader_attributes) { shader->attributes = ps->backend_data->ops.get_shader_attributes( ps->backend_data, shader->backend_shader); } log_debug("Shader %s has attributes %" PRIu64, shader->key, shader->attributes); } } } wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w != NULL) { assert(w->state != WSTATE_DESTROYED); // We need to reacquire image log_debug("Marking window %#010x (%s) for update after " "redirection", win_id(w), w->name); win_set_flags(w, WIN_FLAGS_PIXMAP_STALE); ps->pending_updates = true; } } 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); if (!ps->renderer) { log_fatal("Failed to create renderer, aborting..."); goto err; } } // 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; } static inline void invalidate_reg_ignore(session_t *ps) { // Invalidate reg_ignore from the top wm_stack_foreach(ps->wm, cursor) { auto top_w = wm_ref_deref(cursor); if (top_w != NULL) { rc_region_unref(&top_w->reg_ignore); top_w->reg_ignore_valid = false; break; } } } /// Handle configure event of the root window void configure_root(session_t *ps) { // 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(); } log_info("Root configuration changed, new geometry: %dx%d", r->width, r->height); bool has_root_change = false; if (ps->redirected) { // On root window changes if (!ps->o.use_legacy_backends) { assert(ps->backend_data); has_root_change = ps->backend_data->ops.root_change != NULL; } else { // Old backend can handle root change has_root_change = true; } if (!has_root_change) { // deinit/reinit backend and free up resources if the backend // cannot handle root change destroy_backend(ps); } free_paint(ps, &ps->tgt_buffer); } ps->root_width = r->width; ps->root_height = r->height; free(r); rebuild_screen_reg(ps); invalidate_reg_ignore(ps); // Whether a window is fullscreen depends on the new screen // size. So we need to refresh the fullscreen state of all // windows. wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w != NULL) { win_update_is_fullscreen(ps, w); } } if (ps->redirected) { for (int i = 0; i < ps->damage_ring.count; i++) { pixman_region32_clear(&ps->damage_ring.damages[i]); } ps->damage_ring.cursor = ps->damage_ring.count - 1; #ifdef CONFIG_OPENGL // GLX root change callback if (BKEND_GLX == ps->o.legacy_backend && ps->o.use_legacy_backends) { glx_on_root_change(ps); } #endif if (has_root_change) { if (ps->backend_data != NULL) { ps->backend_data->ops.root_change(ps->backend_data, ps); } // Old backend's root_change is not a specific function } else { if (!initialize_backend(ps)) { log_fatal("Failed to re-initialize backend after root " "change, aborting..."); ps->quit = true; /* TODO(yshui) only event handlers should request * ev_break, otherwise it's too hard to keep track of what * can break the event loop */ ev_break(ps->loop, EVBREAK_ALL); return; } // Re-acquire the root pixmap. root_damaged(ps); } force_repaint(ps); } } /** * Go through the window stack and calculate some parameters for rendering. * * @return whether the operation succeeded */ static bool paint_preprocess(session_t *ps, bool *animation, struct 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 win *bottom = NULL; *animation = false; *out_bottom = NULL; // 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. wm_stack_foreach_safe(ps->wm, cursor, tmp) { auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } const winmode_t mode_old = w->mode; const bool was_painted = w->to_paint; if (w->running_animation_instance != NULL) { *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->running_animation_instance != NULL) { add_damage_from_win(ps, w); } if (win_has_frame(w)) { w->frame_opacity = ps->o.frame_opacity; } else { w->frame_opacity = 1.0; } // Update window mode w->mode = win_calc_mode(w); // Destroy all reg_ignore above when frame opaque state changes on // SOLID mode if (was_painted && w->mode != mode_old) { w->reg_ignore_valid = false; } } // Opacity will not change, from now on. rc_region_t *last_reg_ignore = rc_region_new(); bool unredir_possible = false; // Track whether it's the highest window to paint bool is_highest = true; bool reg_ignore_valid = true; wm_stack_foreach_safe(ps->wm, cursor, next_cursor) { __label__ skip_window; auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } 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 = win_animatable_get(w, WIN_SCRIPT_OPACITY); const double blur_opacity = win_animatable_get(w, WIN_SCRIPT_BLUR_OPACITY); auto window_options = win_options(w); struct shader_info *fg_shader = NULL; if (window_options.shader != NULL) { HASH_FIND_STR(ps->shaders, window_options.shader, fg_shader); } if (fg_shader != NULL && fg_shader->attributes & SHADER_ATTRIBUTE_ANIMATED) { add_damage_from_win(ps, w); *animation = true; } // Destroy reg_ignore if some window above us invalidated it if (!reg_ignore_valid) { rc_region_unref(&w->reg_ignore); } // log_trace("%d %d %s", w->a.map_state, w->ever_damaged, w->name); log_trace("Checking whether window %#010x (%s) should be painted", win_id(w), 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 || w->state == WSTATE_DESTROYED) && w->running_animation_instance == NULL) { log_trace("|- is unmapped"); to_paint = false; } else if (unlikely(ps->debug_window != XCB_NONE) && (win_id(w) == ps->debug_window || win_client_id(w, /*fallback_to_self=*/false) == 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("|- is positioned outside of the screen"); to_paint = false; } else if (unlikely(window_opacity * MAX_ALPHA < 1 && (!window_options.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 (!window_options.paint) { log_trace("|- is excluded from painting"); to_paint = false; } else if (unlikely((w->flags & WIN_FLAGS_PIXMAP_ERROR) != 0)) { log_trace("|- has image errors"); to_paint = false; } // log_trace("%s %d %d %d", w->name, to_paint, w->opacity, // w->paint_excluded); // Add window to damaged area if its painting status changes // or opacity changes if (to_paint != was_painted) { w->reg_ignore_valid = false; add_damage_from_win(ps, w); } // to_paint will never change after this point if (!to_paint) { log_trace("|- will not be painted"); goto skip_window; } log_trace("|- will be painted"); log_verbose("Window %#010x (%s) will be painted", win_id(w), w->name); // Generate ignore region for painting to reduce GPU load if (!w->reg_ignore) { w->reg_ignore = rc_region_ref(last_reg_ignore); } // If the window is solid, or we enabled clipping for transparent windows, // 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) || window_options.transparent_clipping) { // w->mode == WMODE_SOLID or WMODE_FRAME_TRANS region_t *tmp = rc_region_new(); if (w->mode == WMODE_SOLID) { *tmp = win_get_bounding_shape_global_without_corners_by_val(w); } else { // w->mode == WMODE_FRAME_TRANS win_get_region_noframe_local_without_corners(w, tmp); pixman_region32_intersect(tmp, tmp, &w->bounding_shape); pixman_region32_translate(tmp, w->g.x, w->g.y); } pixman_region32_union(tmp, tmp, last_reg_ignore); rc_region_unref(&last_reg_ignore); last_reg_ignore = tmp; } // (Un)redirect screen // We could definitely unredirect the screen when there's no window to // paint, but this is typically unnecessary, may cause flickering when // fading is enabled, and could create inconsistency when the wallpaper // is not correctly set. if (ps->o.unredir_if_possible && is_highest && w->mode == WMODE_SOLID && !ps->o.force_win_blend && w->is_fullscreen && (window_options.unredir == WINDOW_UNREDIR_WHEN_POSSIBLE || window_options.unredir == WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE)) { unredir_possible = true; } // Unredirect screen if some window is forcing unredir, even when they are // not on the top. if (ps->o.unredir_if_possible && window_options.unredir == WINDOW_UNREDIR_FORCED) { unredir_possible = true; } w->prev_trans = bottom; bottom = w; // If the screen is not redirected check if the window's unredir setting // allows unredirection to be terminated. if (ps->redirected || window_options.unredir == WINDOW_UNREDIR_TERMINATE || window_options.unredir == WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE) { // Setting is_highest to false will stop all windows stacked below // from triggering unredirection. But if `unredir_possible` is // already set, this will not prevent unredirection. is_highest = false; } skip_window: reg_ignore_valid = reg_ignore_valid && w->reg_ignore_valid; w->reg_ignore_valid = true; if (w->state == WSTATE_DESTROYED && w->running_animation_instance == NULL) { // the window should be destroyed because it was destroyed // by X server and now its animations are finished win_destroy_finish(ps, w); w = NULL; } // Avoid setting w->to_paint if w is freed if (w) { w->to_paint = to_paint; } } rc_region_unref(&last_reg_ignore); // If possible, unredirect all windows and stop painting if (ps->o.redirected_force != UNSET) { unredir_possible = !ps->o.redirected_force; } else if (ps->o.unredir_if_possible && is_highest && !ps->redirected) { // `is_highest` being true means there's no window with a unredir setting // that allows unredirection to be terminated. So if screen is not // redirected, keep it that way. // // (might not be the best naming.) unredir_possible = true; } if (unredir_possible) { if (ps->redirected) { if (!ps->o.unredir_if_possible_delay || ps->tmout_unredir_hit) { unredirect(ps); } else if (!ev_is_active(&ps->unredir_timer)) { ev_timer_set( &ps->unredir_timer, (double)ps->o.unredir_if_possible_delay / 1000.0, 0); ev_timer_start(ps->loop, &ps->unredir_timer); } } } else { ev_timer_stop(ps->loop, &ps->unredir_timer); if (!ps->redirected) { if (!redirect_start(ps)) { return false; } } } *out_bottom = bottom; return true; } void root_damaged(session_t *ps) { if (ps->root_tile_paint.pixmap) { free_root_tile(ps); } if (!ps->redirected) { return; } 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->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, visual)); ps->root_image_generation += 1; if (!ps->root_image) { err: log_error("Failed to bind root back pixmap"); } } } // Mark screen damaged force_repaint(ps); } /** * Force a full-screen repaint. */ void force_repaint(session_t *ps) { assert(pixman_region32_not_empty(&ps->screen_reg)); queue_redraw(ps); add_damage(ps, &ps->screen_reg); } /** * Setup window properties, then register us with the compositor selection (_NET_WM_CM_S) * * @return 0 if success, 1 if compositor already running, -1 if error. */ static int register_cm(session_t *ps) { assert(!ps->reg_win); ps->reg_win = x_new_id(&ps->c); auto e = xcb_request_check( 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."); free(e); return -1; } const xcb_atom_t prop_atoms[] = { ps->atoms->aWM_NAME, ps->atoms->a_NET_WM_NAME, ps->atoms->aWM_ICON_NAME, }; const bool prop_is_utf8[] = {false, true, false}; // Set names and classes for (size_t i = 0; i < ARR_SIZE(prop_atoms); i++) { e = xcb_request_check( 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]); free(e); } } const char picom_class[] = "picom\0picom"; e = xcb_request_check( 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); } // Set WM_CLIENT_MACHINE. As per EWMH, because we set _NET_WM_PID, we must also // set WM_CLIENT_MACHINE. { 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.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"); free(e); } } else { log_error_errno("Failed to get hostname"); } free(hostname); } // Set _NET_WM_PID { auto pid = getpid(); 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.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); } // Acquire X Selection _NET_WM_CM_S? if (!ps->o.no_x_selection) { const char register_prop[] = "_NET_WM_CM_S"; xcb_atom_t atom; char *buf = NULL; casprintf(&buf, "%s%d", register_prop, ps->c.screen); 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.c, xcb_get_selection_owner(ps->c.c, atom), NULL); if (reply && reply->owner != XCB_NONE) { // Another compositor already running free(reply); return 1; } free(reply); xcb_set_selection_owner(ps->c.c, ps->reg_win, atom, 0); } return 0; } /** * Write PID to a file. */ static inline bool write_pid(session_t *ps) { if (!ps->o.write_pid_path) { return true; } FILE *f = fopen(ps->o.write_pid_path, "w"); if (unlikely(!f)) { log_error("Failed to write PID to \"%s\".", ps->o.write_pid_path); return false; } fprintf(f, "%ld\n", (long)getpid()); fclose(f); return true; } /** * 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.c, xcb_composite_get_overlay_window(ps->c.c, ps->c.screen_info->root), NULL); if (reply) { ps->overlay = reply->overlay_win; free(reply); } else { ps->overlay = XCB_NONE; } 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.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.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."); return false; } // Listen to Expose events on the overlay 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 // overlay // 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.c, ps->overlay); } else { log_error("Cannot get X Composite overlay window. Falling " "back to painting on root window."); } log_debug("overlay = %#010x", ps->overlay); return true; } 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); auto err = xcb_request_check( 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.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.c, xcb_map_window_checked(ps->c.c, ps->debug_window)); if (err) { goto err_out; } return true; err_out: free(err); return false; } 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->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.use_legacy_backends); return XCB_COMPOSITE_REDIRECT_AUTOMATIC; } if (!ps->o.use_legacy_backends && !backend_can_present(ps->o.backend)) { // if the backend doesn't render anything, we don't need to take over the // screen. return XCB_COMPOSITE_REDIRECT_AUTOMATIC; } return XCB_COMPOSITE_REDIRECT_MANUAL; } /** * Redirect all windows. * * @return whether the operation succeeded or not */ static bool redirect_start(session_t *ps) { assert(!ps->redirected); log_debug("Redirecting the screen."); // 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.c, ps->overlay); } 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; } xcb_aux_sync(ps->c.c); if (!initialize_backend(ps)) { return false; } if (!ps->o.use_legacy_backends) { assert(ps->backend_data); ps->damage_ring.count = ps->backend_data->ops.max_buffer_age(ps->backend_data); ps->layout_manager = layout_manager_new((unsigned)ps->damage_ring.count); } else { ps->damage_ring.count = maximum_buffer_age(ps); } 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->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.use_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 xcb_aux_sync(ps->c.c); ps->redirected = true; ps->first_frame = true; root_damaged(ps); // Repaint the whole screen force_repaint(ps); log_debug("Screen redirected."); return true; } /** * Unredirect all windows. */ static void unredirect(session_t *ps) { assert(ps->redirected); log_debug("Unredirecting the screen."); destroy_backend(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.c, ps->overlay); } // Free the damage ring 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; } // Must call XSync() here xcb_aux_sync(ps->c.c); ps->redirected = false; log_debug("Screen unredirected."); } /// 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_x_events(struct session *ps) { uint32_t latest_completed = ps->c.latest_completed_request; while (true) { if (!x_prepare_for_sleep(&ps->c)) { log_fatal("X connection broke."); exit(1); } // If we send any new requests, we should loop again to flush them. There // is no direct way to do this from xcb. So if we called `ev_handle`, or // if any pending requests were completed, we conservatively loop again. bool needs_flush = false; if (ps->vblank_scheduler) { vblank_handle_x_events(ps->vblank_scheduler); } xcb_generic_event_t *ev; while ((ev = x_poll_for_event(&ps->c, true))) { ev_handle(ps, (xcb_generic_event_t *)ev); needs_flush = true; free(ev); }; if (ps->c.latest_completed_request != latest_completed) { needs_flush = true; latest_completed = ps->c.latest_completed_request; } if (!needs_flush) { break; } } int err = xcb_connection_has_error(ps->c.c); if (err) { log_fatal("X11 server connection broke (error %d)", err); exit(1); } if (wm_has_tree_changes(ps->wm)) { log_debug("Window tree changed, queueing redraw."); ps->pending_updates = true; queue_redraw(ps); } } static void handle_x_events_ev(EV_P attr_unused, ev_prepare *w, int revents attr_unused) { session_t *ps = session_ptr(w, event_check); handle_x_events(ps); } struct new_window_attributes_request { struct x_async_request_base base; struct session *ps; wm_treeid id; }; static void handle_new_window_attributes_reply(struct x_connection * /*c*/, struct x_async_request_base *req_base, const xcb_raw_generic_event_t *reply_or_error) { auto req = (struct new_window_attributes_request *)req_base; auto ps = req->ps; auto id = req->id; auto new_window = wm_find(ps->wm, id.x); free(req); if (reply_or_error == NULL) { // Shutting down return; } if (reply_or_error->response_type == 0) { log_debug("Failed to get window attributes for newly created window " "%#010x", id.x); return; } if (new_window == NULL) { // Highly unlikely. This window is destroyed, then another window is // created with the same window ID before this request completed, and the // latter window isn't in our tree yet. if (wm_is_consistent(ps->wm)) { log_error("Newly created window %#010x is not in the window tree", id.x); assert(false); } return; } auto current_id = wm_ref_treeid(new_window); if (!wm_treeid_eq(current_id, id)) { log_debug("Window %#010x was not the window we queried anymore. Current " "gen %" PRIu64 ", expected %" PRIu64, id.x, current_id.gen, id.gen); return; } auto toplevel = wm_ref_toplevel_of(ps->wm, new_window); if (toplevel != new_window) { log_debug("Newly created window %#010x was moved away from toplevel " "while we were waiting for its attributes", id.x); return; } if (wm_ref_deref(toplevel) != NULL) { // This is possible if a toplevel is reparented away, then reparented to // root so it became a toplevel again. If the GetWindowAttributes request // sent for the first time it became a toplevel wasn't completed for this // whole duration, it will create a managed window object for the // toplevel. But there is another get attributes request sent the // second time it became a toplevel. When we get the reply for the second // request, we will reach here. log_debug("Newly created window %#010x is already managed", id.x); return; } auto w = win_maybe_allocate( ps, toplevel, (const xcb_get_window_attributes_reply_t *)reply_or_error); if (w != NULL && w->a.map_state == XCB_MAP_STATE_VIEWABLE) { win_set_flags(w, WIN_FLAGS_MAPPED); ps->pending_updates = true; } } static void handle_new_windows(session_t *ps) { // Check tree changes first, because later property updates need accurate // client window information struct win *w = NULL; while (true) { auto wm_change = wm_dequeue_change(ps->wm); if (wm_change.type == WM_TREE_CHANGE_NONE) { break; } switch (wm_change.type) { case WM_TREE_CHANGE_TOPLEVEL_NEW:; auto req = ccalloc(1, struct new_window_attributes_request); // We don't directly record the toplevel wm_ref here, because any // number of things could happen before we get the reply. The // window can be reparented, destroyed, then get its window ID // reused, etc. req->id = wm_ref_treeid(wm_change.toplevel); req->ps = ps; req->base.callback = handle_new_window_attributes_reply, req->base.sequence = xcb_get_window_attributes(ps->c.c, req->id.x).sequence; x_await_request(&ps->c, &req->base); break; case WM_TREE_CHANGE_TOPLEVEL_KILLED: w = wm_ref_deref(wm_change.toplevel); if (w != NULL) { win_destroy_start(ps, w); } else { // This window is not managed, no point keeping the zombie // around. wm_reap_zombie(wm_change.toplevel); } break; case WM_TREE_CHANGE_CLIENT: log_debug("Client window for window %#010x changed from " "%#010x to %#010x", wm_ref_win_id(wm_change.toplevel), wm_change.client.old.x, wm_change.client.new_.x); w = wm_ref_deref(wm_change.toplevel); if (w != NULL) { win_set_flags(w, WIN_FLAGS_CLIENT_STALE); } else { log_debug("An unmanaged window %#010x has a new client " "%#010x", wm_ref_win_id(wm_change.toplevel), wm_change.client.new_.x); } ev_update_focused(ps); break; case WM_TREE_CHANGE_TOPLEVEL_RESTACKED: invalidate_reg_ignore(ps); break; default: unreachable(); } } } static void refresh_windows(session_t *ps) { wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } win_process_primary_flags(ps, w); } wm_refresh_leaders(ps->wm); wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } win_process_secondary_flags(ps, w); } } static void refresh_images(session_t *ps) { wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w != NULL) { win_process_image_flags(ps, w); } } } /** * Unredirection timeout callback. */ static void tmout_unredir_callback(EV_P attr_unused, ev_timer *w, int revents attr_unused) { session_t *ps = session_ptr(w, unredir_timer); ps->tmout_unredir_hit = true; queue_redraw(ps); } static void handle_pending_updates(struct session *ps, double delta_t) { // Process new windows, and maybe allocate struct managed_win for them handle_new_windows(ps); if (ps->pending_updates) { log_debug("Delayed handling of events"); // Process window flags. This needs to happen before `refresh_images` // because this might set the pixmap stale window flag. refresh_windows(ps); ps->pending_updates = false; } wm_stack_foreach_safe(ps->wm, cursor, tmp) { auto w = wm_ref_deref(cursor); BUG_ON(w != NULL && w->tree_ref != cursor); // Window might be freed by this function, if it's destroyed and its // animation finished if (w != NULL && win_process_animation_and_state_change(ps, w, delta_t)) { free(w->running_animation_instance); w->running_animation_instance = NULL; w->in_openclose = false; if (w->saved_win_image != NULL) { win_release_saved_win_image(ps->backend_data, w); } if (w->state == WSTATE_UNMAPPED) { unmap_win_finish(ps, w); } else if (w->state == WSTATE_DESTROYED) { win_destroy_finish(ps, w); } } } // Process window flags (stale images) refresh_images(ps); assert(ps->pending_updates == false); } /** * Turn on the program reset flag. * * 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..."); ev_break(EV_A_ EVBREAK_ALL); } static void exit_enable(EV_P attr_unused, ev_signal *w, int revents attr_unused) { session_t *ps = session_ptr(w, int_signal); log_info("picom is quitting..."); quit(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); // Fading step calculation auto now_ms = timespec_ms(now); int64_t delta_ms = 0L; if (ps->fade_time) { assert(now_ms >= ps->fade_time); delta_ms = now_ms - ps->fade_time; } ps->fade_time = now_ms; 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(ps, (double)delta_ms / 1000.0); 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 // won't have pixmap before we rendered it, causing us to crash. // But we will only render them if they are in fading. So we just skip // fading for all windows here. // // Using foreach_safe here since skipping fading can cause window to be // freed if it's destroyed. // // TODO(yshui) I think maybe we don't need this anymore, since now we // immediate acquire pixmap right after `map_win_start`. wm_stack_foreach_safe(ps->wm, cursor, tmp) { auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } free(w->running_animation_instance); w->running_animation_instance = NULL; if (w->state == WSTATE_DESTROYED) { win_destroy_finish(ps, w); } } } if (ps->o.benchmark) { if (ps->o.benchmark_wid) { auto w = wm_find(ps->wm, ps->o.benchmark_wid); if (w == NULL) { log_fatal("Couldn't find specified benchmark window."); exit(1); } w = wm_ref_toplevel_of(ps->wm, w); auto win = w == NULL ? NULL : wm_ref_deref(w); if (win != NULL) { add_damage_from_win(ps, win); } else { force_repaint(ps); } } else { force_repaint(ps); } } /* TODO(yshui) Have a stripped down version of paint_preprocess that is used when * screen is not redirected. its sole purpose should be to decide whether the * screen should be redirected. */ bool animation = false; bool was_redirected = ps->redirected; struct win *bottom = NULL; if (!paint_preprocess(ps, &animation, &bottom)) { log_fatal("Pre-render preparation has failed, exiting..."); exit(1); } ps->tmout_unredir_hit = false; if (!was_redirected && ps->redirected) { // paint_preprocess redirected the screen, which might change the state of // some of the windows (e.g. the window image might become stale). // so we rerun _draw_callback to make sure the rendering decision we make // is up-to-date, and all the new flags got handled. // // TODO(yshui) This is not ideal, we should try to avoid setting window // flags in paint_preprocess. log_debug("Re-run _draw_callback"); return draw_callback_impl(EV_A_ ps, revents); } 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_verbose("Render start, frame %d", paint); if (!ps->o.use_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; if (ps->backend_data->ops.device_status && ps->backend_data->ops.device_status(ps->backend_data) != DEVICE_STATUS_NORMAL) { 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. // // 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); log_info("Resetting picom after device reset"); reset_enable(ps->loop, NULL, 0); return; } 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.crop_shadow_to_monitor ? &ps->monitors : NULL, ps->shaders, &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); } log_verbose("Render end"); ps->first_frame = false; paint++; if (ps->o.benchmark && paint >= ps->o.benchmark) { exit(0); } } // 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; 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. // Queue redraw if animation is running. This should be picked up by next present // event. if (animation) { queue_redraw(ps); } else { ps->fade_time = 0L; } if (ps->vblank_scheduler && ps->backend_busy) { // 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 draw_callback(EV_P_ ev_timer *w, int revents) { session_t *ps = session_ptr(w, 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); // 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); } } static void x_event_callback(EV_P attr_unused, ev_io *w, int revents attr_unused) { // Make sure the X connection is being read from at least once every time // we woke up because of readability of the X connection. // // `handle_x_events` is not guaranteed to read from X, so if we don't do // it here we could dead lock. struct session *ps = session_ptr(w, xiow); auto ev = x_poll_for_event(&ps->c, false); if (ev) { ev_handle(ps, (xcb_generic_event_t *)ev); free(ev); } } static void config_file_change_cb(void *_ps) { auto ps = (struct session *)_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 struct window_options win_options_from_config(const struct options *opts) { struct window_options ret = { .blur_background = opts->blur_method != BLUR_METHOD_NONE, .full_shadow = false, .shadow = opts->shadow_enable, .corner_radius = (unsigned)opts->corner_radius, .transparent_clipping = opts->transparent_clipping, .dim = 0, .fade = opts->fading_enable, .shader = opts->window_shader_fg, .invert_color = false, .paint = true, .clip_shadow_above = false, .unredir = WINDOW_UNREDIR_WHEN_POSSIBLE_ELSE_TERMINATE, .opacity = 1, }; memcpy(ret.animations, opts->animations, sizeof(ret.animations)); return ret; } /** * Initialize a session. * * @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_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, .root_height = 0, .root_width = 0, // .root_damage = XCB_NONE, .overlay = XCB_NONE, .root_tile_fill = false, .root_tile_paint = PAINT_INIT, .tgt_picture = XCB_NONE, .tgt_buffer = PAINT_INIT, .reg_win = XCB_NONE, #ifdef CONFIG_OPENGL .glx_prog_win = GLX_PROG_MAIN_INIT, #endif .redirected = false, .alpha_picts = NULL, .fade_time = 0L, .quit = false, .expose_rects = NULL, .black_picture = XCB_NONE, .cshadow_picture = XCB_NONE, .white_picture = XCB_NONE, .shadow_context = NULL, .last_msc = 0, #ifdef CONFIG_VSYNC_DRM .drm_fd = -1, #endif .xfixes_event = 0, .xfixes_error = 0, .damage_event = 0, .damage_error = 0, .render_event = 0, .render_error = 0, .composite_event = 0, .composite_error = 0, .composite_opcode = 0, .shape_exists = false, .shape_event = 0, .shape_error = 0, .randr_exists = 0, .randr_event = 0, .randr_error = 0, .glx_exists = false, .glx_event = 0, .glx_error = 0, .xrfilter_convolution_exists = false, #ifdef CONFIG_DBUS .dbus_data = NULL, #endif }; auto stderr_logger = stderr_logger_new(); if (stderr_logger) { // stderr logger might fail to create if we are already // daemonized. log_add_target_tls(stderr_logger); } // Allocate a session and copy default values into it session_t *ps = cmalloc(session_t); xcb_generic_error_t *e = NULL; *ps = s_def; ps->loop = EV_DEFAULT; pixman_region32_init(&ps->screen_reg); // 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 x_connection_init(&ps->c, dpy); const xcb_query_extension_reply_t *ext_info; 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); 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); } ps->render_event = ext_info->first_event; ps->render_error = ext_info->first_error; 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); } ps->composite_opcode = ext_info->major_opcode; ps->composite_event = ext_info->first_event; ps->composite_error = ext_info->first_error; { xcb_composite_query_version_reply_t *reply = xcb_composite_query_version_reply( ps->c.c, xcb_composite_query_version(ps->c.c, XCB_COMPOSITE_MAJOR_VERSION, XCB_COMPOSITE_MINOR_VERSION), NULL); if (!reply || (reply->major_version == 0 && reply->minor_version < 2)) { log_fatal("Your X server doesn't have Composite >= 0.2 support, " "we cannot proceed."); exit(1); } free(reply); } 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.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.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.c, xcb_xfixes_query_version(ps->c.c, XCB_XFIXES_MAJOR_VERSION, XCB_XFIXES_MINOR_VERSION) .sequence); 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.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; } // Parse configuration file if (!parse_config(&ps->o, config_file)) { return NULL; } // Parse all of the rest command line options if (!get_cfg(&ps->o, argc, argv)) { log_fatal("Failed to get configuration, usually mean you have specified " "invalid options."); return NULL; } const char *basename = strrchr(argv[0], '/') ? strrchr(argv[0], '/') + 1 : argv[0]; if (strcmp(basename, "picom-inspect") == 0) { ps->o.backend = backend_find("dummy"); ps->o.print_diagnostics = false; ps->o.dbus = false; if (!ps->o.inspect_monitor) { ps->o.inspect_win = inspect_select_window(&ps->c); } } ps->window_options_default = win_options_from_config(&ps->o); 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) { log_info("Switching to log file: %s", ps->o.logpath); if (stderr_logger) { log_remove_target_tls(stderr_logger); stderr_logger = NULL; } log_add_target_tls(l); stderr_logger = NULL; } else { log_error("Failed to setup log file %s, I will keep using stderr", ps->o.logpath); } } if (strstr(argv[0], "compton")) { log_warn("This compositor has been renamed to \"picom\", the \"compton\" " "binary will not be installed in the future."); } ps->atoms = init_atoms(ps->c.c); ps->c2_state = c2_state_new(ps->atoms); // Get needed atoms for c2 condition lists options_postprocess_c2_lists(ps->c2_state, &ps->c, &ps->o); // Load shader source file specified in the shader rules c2_condition_list_foreach(&ps->o.window_shader_fg_rules, i) { if (!load_shader_source(ps, c2_condition_get_data(i))) { 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"); } c2_condition_list_foreach(&ps->o.rules, i) { auto data = (struct window_maybe_options *)c2_condition_get_data(i); if (data->shader == NULL) { continue; } if (load_shader_source(ps, data->shader)) { log_error("Failed to load shader source file for window rules"); } } if (log_get_level_tls() <= LOG_LEVEL_DEBUG) { HASH_ITER2(ps->shaders, shader) { log_debug("Shader %s:", shader->key); log_debug("%s", shader->source); } } if (ps->o.use_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.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.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.c, &xcb_present_id); if (ext_info && ext_info->present) { auto r = xcb_present_query_version_reply( ps->c.c, xcb_present_query_version(ps->c.c, XCB_PRESENT_MAJOR_VERSION, XCB_PRESENT_MINOR_VERSION), NULL); if (r) { ps->present_exists = true; free(r); } } // Query X Sync 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.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))) { ps->xsync_exists = true; free(r); } } ps->sync_fence = XCB_NONE; if (ps->xsync_exists) { ps->sync_fence = x_new_id(&ps->c); e = xcb_request_check( 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. " "xrender-sync-fence will be " "disabled"); ps->o.xrender_sync_fence = false; } ps->sync_fence = XCB_NONE; free(e); } } else if (ps->o.xrender_sync_fence) { log_error("XSync extension not found. No XSync fence sync is " "possible. (xrender-sync-fence can't be enabled)"); ps->o.xrender_sync_fence = false; } // Query X RandR 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; } bool compositor_running = false; if (session_redirection_mode(ps) == XCB_COMPOSITE_REDIRECT_MANUAL) { // We are running in the manual redirection mode, meaning we are running // as a proper compositor. So we need to register us as a compositor, etc. // We are also here when --diagnostics is set. We want to be here because // that gives us more diagnostic information. // Create registration window int ret = register_cm(ps); if (ret == -1) { goto err; } compositor_running = ret == 1; } ps->drivers = detect_driver(ps->c.c, ps->backend_data, ps->c.screen_info->root); apply_driver_workarounds(ps, ps->drivers); if (ps->o.print_diagnostics) { ps->root_width = ps->c.screen_info->width_in_pixels; ps->root_height = ps->c.screen_info->height_in_pixels; print_diagnostics(ps, config_file, compositor_running); exit(0); } if (ps->o.config_file_path) { ps->file_watch_handle = file_watch_init(ps->loop); if (ps->file_watch_handle) { file_watch_add(ps->file_watch_handle, ps->o.config_file_path, config_file_change_cb, ps); list_foreach(struct included_config_file, i, &ps->o.included_config_files, siblings) { file_watch_add(ps->file_watch_handle, i->path, config_file_change_cb, ps); } } } if (bkend_use_glx(ps) && ps->o.use_legacy_backends) { auto gl_logger = gl_string_marker_logger_new(); if (gl_logger) { log_info("Enabling gl string marker"); log_add_target_tls(gl_logger); } } // Monitor screen changes if vsync_sw is enabled and we are using // an auto-detected refresh rate, or when X RandR features are enabled if (ps->randr_exists) { xcb_randr_select_input(ps->c.c, ps->c.screen_info->root, XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE); x_update_monitors_async(&ps->c, &ps->monitors); } { xcb_render_create_picture_value_list_t pa = { .subwindowmode = IncludeInferiors, }; ps->root_picture = x_create_picture_with_visual_and_pixmap( &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->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->c.dpy), EV_READ); ev_io_start(ps->loop, &ps->xiow); ev_init(&ps->unredir_timer, tmout_unredir_callback); ev_init(&ps->draw_timer, draw_callback); // Set up SIGUSR1 signal handler to reset program ev_signal_init(&ps->usr1_signal, reset_enable, SIGUSR1); ev_signal_init(&ps->int_signal, exit_enable, SIGINT); ev_signal_start(ps->loop, &ps->usr1_signal); ev_signal_start(ps->loop, &ps->int_signal); // xcb can read multiple events from the socket when a request with reply is // made. // // Use an ev_prepare to make sure we cannot accidentally forget to handle them // before we go to sleep. // // If we don't drain the queue before goes to sleep (i.e. blocking on socket // input), we will be sleeping with events available in queue. Which might // cause us to block indefinitely because arrival of new events could be // dependent on processing of existing events (e.g. if we don't process damage // event and do damage subtract, new damage event won't be generated). // // So we make use of a ev_prepare handle, which is called right before libev // goes into sleep, to handle all the queued X events. ev_prepare_init(&ps->event_check, handle_x_events_ev); // Make sure nothing can cause xcb to read from the X socket after events are // handled and before we going to sleep. ev_set_priority(&ps->event_check, EV_MINPRI); ev_prepare_start(ps->loop, &ps->event_check); // Initialize DBus. We need to do this early, because add_win might call dbus // functions if (ps->o.dbus) { #ifdef CONFIG_DBUS ps->dbus_data = cdbus_init(ps, DisplayString(ps->c.dpy)); if (!ps->dbus_data) { ps->o.dbus = false; } #else log_fatal("DBus support not compiled in!"); exit(1); #endif } ps->wm = wm_new(); wm_import_start(ps->wm, &ps->c, ps->atoms, ps->c.screen_info->root, NULL); ps->command_builder = command_builder_new(); ps->expose_rects = dynarr_new(rect_t, 0); // wm_complete_import will set event masks on the root window, but its event // mask is missing things we need, so we need to set it again. e = xcb_request_check( 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); goto err; } // Query the size of the root window. We need the size information before any // window can be managed. auto r = XCB_AWAIT(xcb_get_geometry, ps->c.c, ps->c.screen_info->root); if (!r) { log_fatal("Failed to get geometry of the root window"); goto err; } ps->root_width = r->width; ps->root_height = r->height; free(r); rebuild_screen_reg(ps); // Initialize filters, must be preceded by OpenGL context creation if (ps->o.use_legacy_backends && !init_render(ps)) { log_fatal("Failed to initialize the backend"); exit(1); } if (session_redirection_mode(ps) == XCB_COMPOSITE_REDIRECT_MANUAL && compositor_running) { // Don't take the overlay when there is another compositor // running, so we don't disrupt it. // If we are printing diagnostic, we will continue a bit further // to get more diagnostic information, otherwise we will exit. log_fatal("Another composite manager is already running"); goto err; } if (session_redirection_mode(ps) == XCB_COMPOSITE_REDIRECT_MANUAL && !init_overlay(ps)) { goto err; } if (session_redirection_mode(ps) == XCB_COMPOSITE_REDIRECT_AUTOMATIC) { // We are here if we don't really function as a compositor, so we are not // taking over the screen, and we don't need to register as a compositor // If we are in debug mode, we need to create a window for rendering if // the backend supports presenting. // The old backends doesn't have a automatic redirection mode log_info("The compositor is started in automatic redirection mode."); assert(!ps->o.use_legacy_backends); if (backend_can_present(ps->o.backend)) { // If the backend has `present`, we couldn't be in automatic // redirection mode unless we are in debug mode. assert(ps->o.debug_mode); if (!init_debug_window(ps)) { goto err; } } } ps->pending_updates = true; write_pid(ps); if (fork && stderr_logger) { // Remove the stderr logger if we will fork log_remove_target_tls(stderr_logger); } return ps; err: free(ps); return NULL; } /** * Destroy a session. * * Does not close the X connection or free the session_t * structure, though. * * @param ps session to destroy */ static void session_destroy(session_t *ps) { if (ps->redirected) { unredirect(ps); } command_builder_free(ps->command_builder); ps->command_builder = NULL; if (ps->file_watch_handle) { 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.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->dbus_data); ps->dbus_data = NULL; } #endif wm_stack_foreach(ps->wm, cursor) { auto w = wm_ref_deref(cursor); if (w != NULL) { free_win_res(ps, w); } } // Free blacklists 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) { ps->tgt_buffer.pict = XCB_NONE; } 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_paint(ps, &ps->tgt_buffer); pixman_region32_fini(&ps->screen_reg); dynarr_free_pod(ps->expose_rects); 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); } #ifdef CONFIG_VSYNC_DRM // Close file opened for DRM VSync if (ps->drm_fd >= 0) { close(ps->drm_fd); ps->drm_fd = -1; } #endif // Release overlay window if (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.c, ps->sync_fence); ps->sync_fence = XCB_NONE; } // Free reg_win if (ps->reg_win != XCB_NONE) { 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.c, ps->debug_window); ps->debug_window = 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.use_legacy_backends) { // backend is deinitialized in unredirect() assert(ps->backend_data == NULL); } else { deinit_render(ps); } #if CONFIG_OPENGL if (glx_has_context(ps)) { // GLX context created, but not for rendering glx_destroy(ps); } #endif // Flush all events xcb_aux_sync(ps->c.c); ev_io_stop(ps->loop, &ps->xiow); if (ps->o.use_legacy_backends) { free_conv((conv *)ps->shadow_context); } destroy_atoms(ps->atoms); #ifdef DEBUG_XRC // Report about resource leakage xrc_report_xid(); #endif // Stop libev event handlers ev_timer_stop(ps->loop, &ps->unredir_timer); 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); // The X connection could hold references to wm if there are pending async // requests. Therefore the wm must be freed after the X connection. free_x_connection(&ps->c); wm_free(ps->wm); } /** * Do the actual work. * * @param ps current session */ static void session_run(session_t *ps) { if (ps->o.use_realtime_scheduling) { set_rr_scheduling(); } // In benchmark mode, we want draw_timer handler to always be active if (ps->o.benchmark) { 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); } ev_run(ps->loop, 0); } #ifdef CONFIG_FUZZER #define PICOM_MAIN(...) no_main(__VA_ARGS__) #else #define PICOM_MAIN(...) main(__VA_ARGS__) #endif /// Early initialization of logging system. To catch early logs, especially /// from backend entrypoint functions. static void __attribute__((constructor(201))) init_early_logging(void) { log_init_tls(); log_set_level_tls(LOG_LEVEL_WARN); auto stderr_logger = stderr_logger_new(); if (stderr_logger != NULL) { log_add_target_tls(stderr_logger); } } /** * The function that everybody knows. */ int PICOM_MAIN(int argc, char **argv) { // Set locale so window names with special characters are interpreted // correctly setlocale(LC_ALL, ""); parse_debug_options(&global_debug_options); int exit_code; char *config_file = NULL; bool all_xerrors = false, need_fork = false; if (get_early_config(argc, argv, &config_file, &all_xerrors, &need_fork, &exit_code)) { return exit_code; } int pfds[2]; if (need_fork) { if (pipe2(pfds, O_CLOEXEC)) { perror("pipe2"); return 1; } auto pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid > 0) { // We are the parent close(pfds[1]); // We wait for the child to tell us it has finished initialization // by sending us something via the pipe. int tmp; if (read(pfds[0], &tmp, sizeof tmp) <= 0) { // Failed to read, the child has most likely died // We can probably waitpid() here. return 1; } // We are done return 0; } // We are the child close(pfds[0]); } // Main loop bool quit = false; int ret_code = 0; char *pid_file = NULL; do { Display *dpy = XOpenDisplay(NULL); if (!dpy) { log_fatal("Can't open display."); ret_code = 1; break; } XSetEventQueueOwner(dpy, XCBOwnsEventQueue); // Reinit logging system so we don't get leftovers from previous sessions // or early logging. log_deinit_tls(); log_init_tls(); ps_g = session_init(argc, argv, dpy, config_file, all_xerrors, need_fork); if (!ps_g) { log_fatal("Failed to create new session."); ret_code = 1; break; } if (need_fork) { // Finishing up daemonization // Close files if (fclose(stdout) || fclose(stderr) || fclose(stdin)) { log_fatal("Failed to close standard input/output"); ret_code = 1; break; } // Make us the session and process group leader so we don't get // killed when our parent die. setsid(); // Notify the parent that we are done. This might cause the parent // to quit, so only do this after setsid() int tmp = 1; 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; } session_run(ps_g); quit = ps_g->quit; if (quit && ps_g->o.write_pid_path) { pid_file = strdup(ps_g->o.write_pid_path); } session_destroy(ps_g); free(ps_g); ps_g = NULL; if (dpy) { XCloseDisplay(dpy); } } while (!quit); free(config_file); if (pid_file) { log_trace("remove pid file %s", pid_file); unlink(pid_file); free(pid_file); } log_deinit_tls(); 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 picom-12.5/src/picom.h000066400000000000000000000043571471504570600146410ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2018 Yuxuan Shui // Throw everything in here. // !!! DON'T !!! // === Includes === #include #include #include #include #include #include #include "c2.h" #include "common.h" #include "config.h" #include "log.h" // XXX clean up #include "region.h" #include "render.h" #include "wm/win.h" #include "x.h" // == Functions == // TODO(yshui) move static inline functions that are only used in picom.c, into picom.c void add_damage(session_t *ps, const region_t *damage); void circulate_win(session_t *ps, xcb_circulate_notify_event_t *ce); void root_damaged(session_t *ps); void queue_redraw(session_t *ps); void discard_pending(session_t *ps, uint32_t sequence); void configure_root(session_t *ps); void quit(session_t *ps); 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]) { arr[i] = ON; } } } /** * Check if a window ID exists in an array of window IDs. * * @param arr the array of window IDs * @param count amount of elements in the array * @param wid window ID to search for */ static inline bool array_wid_exists(const xcb_window_t *arr, int count, xcb_window_t wid) { while (count--) { if (arr[count] == wid) { return true; } } return false; } /** * 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.c, xcb_get_geometry(ps->c.c, drawable), NULL); if (!r) { log_trace("Drawable %#010x: Failed", drawable); return; } log_trace("Drawable %#010x: x = %u, y = %u, wid = %u, hei = %d, b = %u, d = %u", drawable, r->x, r->y, r->width, r->height, r->border_width, r->depth); free(r); } picom-12.5/src/picom.modulemap000066400000000000000000000066431471504570600163750ustar00rootroot00000000000000// modulemap module compiler { header "compiler.h" } module string_utils { header "string_utils.h" } module dbus { header "dbus.h" } module kernel { header "kernel.h" } module utils { // Has macros expands to calloc/malloc header "utils.h" export libc.stdlib } module region { header "region.h" } module picom { header "picom.h" } module types { header "types.h" } module c2 { header "c2.h" } module render { header "render.h" } module options { header "options.h" } module opengl { header "opengl.h" } module diagnostic { header "diagnostic.h" } module win_defs { header "win_defs.h" } module win { header "win.h" export win_defs } module log { header "log.h" export compiler } module x { header "x.h" } module vsync { header "vsync.h" } module common { header "common.h" } module config { header "config.h" } module xrescheck { header "xrescheck.h" } module cache { header "cache.h" } module backend { module gl { module gl_common { header "backend/gl/gl_common.h" } module glx { header "backend/gl/glx.h" export GL.glx } } module backend { header "backend/backend.h" } module backend_common { header "backend/backend_common.h" } } module xcb [system] { module xcb { header "/usr/include/xcb/xcb.h" export * } module randr { header "/usr/include/xcb/randr.h" export * } module render { header "/usr/include/xcb/render.h" export * } module sync { header "/usr/include/xcb/sync.h" export * } module composite { header "/usr/include/xcb/composite.h" export * } module xfixes { header "/usr/include/xcb/xfixes.h" export * } module damage { header "/usr/include/xcb/damage.h" export * } module xproto { header "/usr/include/xcb/xproto.h" export * } module present { header "/usr/include/xcb/present.h" } module util { module render { header "/usr/include/xcb/xcb_renderutil.h" export * } } } module X11 [system] { module Xlib { header "/usr/include/X11/Xlib.h" export * } module Xutil { header "/usr/include/X11/Xutil.h" export * } } module GL [system] { module glx { header "/usr/include/GL/glx.h" export * } module gl { header "/usr/include/GL/gl.h" export * } } module libc [system] { export * module assert { export * textual header "/usr/include/assert.h" } module string { export * header "/usr/include/string.h" } module ctype { export * header "/usr/include/ctype.h" } module errno { export * header "/usr/include/errno.h" } module fenv { export * header "/usr/include/fenv.h" } module inttypes { export * header "/usr/include/inttypes.h" } module math { export * header "/usr/include/math.h" } module setjmp { export * header "/usr/include/setjmp.h" } module stdio { export * header "/usr/include/stdio.h" } module stdlib [system] { export * header "/usr/include/stdlib.h" } } // glib specific header. In it's own module because it // doesn't exist on some systems with unpatched glib 2.26+ module "xlocale.h" [system] { export * header "/usr/include/xlocale.h" } // System header that we have difficult with merging. module "sys_types.h" [system] { export * header "/usr/include/sys/types.h" } module "signal.h" [system] { export * header "/usr/include/signal.h" } picom-12.5/src/region.h000066400000000000000000000151151471504570600150070ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #pragma once #include #include #include #include #include "log.h" #include "utils/misc.h" typedef struct pixman_region32 pixman_region32_t; typedef struct pixman_box32 pixman_box32_t; typedef pixman_region32_t region_t; typedef pixman_box32_t rect_t; RC_TYPE(region_t, rc_region, pixman_region32_init, pixman_region32_fini, static inline) static inline void region_free(region_t *region) { if (region) { pixman_region32_fini(region); } } #define scoped_region_t cleanup(region_free) region_t static inline void dump_region(const region_t *x) { 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++) { 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){ .x1 = rect->x, .y1 = rect->y, .x2 = rect->x + rect->width, .y2 = rect->y + rect->height, }; } /// Convert an array of xcb rectangles to our rectangle type /// Returning an array that needs to be freed static inline rect_t *from_x_rects(int nrects, const xcb_rectangle_t *rects) { rect_t *ret = ccalloc(nrects, rect_t); for (int i = 0; i < nrects; i++) { ret[i] = from_x_rect(rects + i); } return ret; } /** * Resize a region. */ static inline void _resize_region(const region_t *region, region_t *output, int dx, int dy) { if (!region || !output) { return; } if (!dx && !dy) { if (region != output) { pixman_region32_copy(output, (region_t *)region); } return; } // Loop through all rectangles int nrects; int nnewrects = 0; const rect_t *rects = pixman_region32_rectangles((region_t *)region, &nrects); rect_t *newrects = calloc((size_t)nrects, sizeof(rect_t)); for (int i = 0; i < nrects; i++) { int x1 = rects[i].x1 - dx; int y1 = rects[i].y1 - dy; int x2 = rects[i].x2 + dx; int y2 = rects[i].y2 + dy; int wid = x2 - x1; int hei = y2 - y1; if (wid <= 0 || hei <= 0) { continue; } newrects[nnewrects] = (rect_t){.x1 = x1, .x2 = x2, .y1 = y1, .y2 = y2}; ++nnewrects; } pixman_region32_fini(output); pixman_region32_init_rects(output, newrects, nnewrects); free(newrects); } static inline region_t resize_region(const region_t *region, int dx, int dy) { region_t ret; pixman_region32_init(&ret); _resize_region(region, &ret, dx, dy); return ret; } 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); } /// Scale the `region` by `scale`. The origin of scaling is `origin`. Returns the smallest /// integer region that contains the result. static inline void region_scale(region_t *region, ivec2 origin, vec2 scale) { if (vec2_eq(scale, SCALE_IDENTITY)) { return; } int n; region_t tmp = *region; auto r = pixman_region32_rectangles(&tmp, &n); for (int i = 0; i < n; i++) { r[i].x1 = to_i32_saturated(floor((r[i].x1 - origin.x) * scale.x + origin.x)); r[i].y1 = to_i32_saturated(floor((r[i].y1 - origin.y) * scale.y + origin.y)); r[i].x2 = to_i32_saturated(ceil((r[i].x2 - origin.x) * scale.x + origin.x)); r[i].y2 = to_i32_saturated(ceil((r[i].y2 - origin.y) * scale.y + origin.y)); } /* Manipulating the rectangles could break assumptions made internally * by pixman, so we recreate the region with the rectangles to let * pixman fix them. */ pixman_region32_init_rects(region, r, n); pixman_region32_fini(&tmp); } /// 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); } static inline region_t region_from_box(struct ibox a) { region_t ret; unsigned width = (unsigned)(min2(INT_MAX - a.origin.x, a.size.width)), height = (unsigned)(min2(INT_MAX - a.origin.y, a.size.height)); pixman_region32_init_rect(&ret, a.origin.x, a.origin.y, width, height); return ret; } picom-12.5/src/render.c000066400000000000000000001321401471504570600147740ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include #include #include #include #include #include "common.h" #include "options.h" #ifdef CONFIG_OPENGL #include "backend/gl/glx.h" #include "opengl.h" #ifndef GLX_BACK_BUFFER_AGE_EXT #define GLX_BACK_BUFFER_AGE_EXT 0x20F4 #endif #endif #include "compiler.h" #include "config.h" #include "log.h" #include "region.h" #include "utils/kernel.h" #include "utils/misc.h" #include "vsync.h" #include "wm/win.h" #include "x.h" #include "backend/backend_common.h" #include "render.h" #define XRFILTER_CONVOLUTION "convolution" #define XRFILTER_GAUSSIAN "gaussian" #define XRFILTER_BINOMIAL "binomial" /** * Bind texture in paint_t if we are using GLX backend. */ static inline bool paint_bind_tex(session_t *ps, paint_t *ppaint, int wid, int hei, 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) { return false; } struct glx_fbconfig_info *fbcfg; if (!visual) { assert(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.cfg) { log_error("Failed to find appropriate FBConfig for 32 bit depth"); return false; } fbcfg = &ps->argb_fbconfig; } else { auto m = x_get_visual_info(&ps->c, visual); if (m.visual_depth < 0) { return false; } if (depth && depth != m.visual_depth) { log_error("Mismatching visual depth: %d != %d", depth, m.visual_depth); return false; } if (!ppaint->fbcfg.cfg) { glx_find_fbconfig(&ps->c, m, &ppaint->fbcfg); } if (!ppaint->fbcfg.cfg) { log_error("Failed to find appropriate FBConfig for X pixmap"); return false; } fbcfg = &ppaint->fbcfg; } 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; (void)wid; (void)hei; (void)repeat; (void)depth; (void)visual; (void)force; #endif return true; } /** * Check if current backend uses XRender for rendering. */ static inline bool bkend_use_xrender(session_t *ps) { return BKEND_XRENDER == ps->o.legacy_backend || BKEND_XR_GLX_HYBRID == ps->o.legacy_backend; } int maximum_buffer_age(session_t *ps) { if (bkend_use_glx(ps) && ps->o.use_damage) { return CGLX_MAX_BUFFER_AGE; } return 1; } static int get_buffer_age(session_t *ps) { #ifdef CONFIG_OPENGL if (bkend_use_glx(ps)) { if (!glxext.has_GLX_EXT_buffer_age && ps->o.use_damage) { log_warn("GLX_EXT_buffer_age not supported by your driver," "`use-damage` has to be disabled"); ps->o.use_damage = false; } if (ps->o.use_damage) { unsigned int val; glXQueryDrawable(ps->c.dpy, get_tgt_window(ps), GLX_BACK_BUFFER_AGE_EXT, &val); return (int)val ?: -1; } return -1; } #endif return ps->o.use_damage ? 1 : -1; } /** * Reset filter on a Picture. */ static inline void xrfilter_reset(session_t *ps, xcb_render_picture_t p) { #define FILTER "Nearest" xcb_render_set_picture_filter(ps->c.c, p, strlen(FILTER), FILTER, 0, NULL); #undef FILTER } /// Set the input/output clip region of the target buffer (not the actual target!) static inline void attr_nonnull(1, 2) set_tgt_clip(session_t *ps, region_t *reg) { switch (ps->o.legacy_backend) { case BKEND_XRENDER: case BKEND_XR_GLX_HYBRID: 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; #endif default: assert(false); } } /** * Free paint_t. */ void free_paint(session_t *ps, paint_t *ppaint) { #ifdef CONFIG_OPENGL free_paint_glx(ps, ppaint); #endif 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 make_circle(int cx, int cy, int radius, uint32_t max_ntraps, xcb_render_trapezoid_t traps[]) { uint32_t n = 0, k = 0; int y1, y2; double w; while (k < max_ntraps) { y1 = (int)(-radius * cos(M_PI * k / max_ntraps)); traps[n].top = (cy + y1) * 65536; traps[n].left.p1.y = (cy + y1) * 65536; traps[n].right.p1.y = (cy + y1) * 65536; w = sqrt(radius * radius - y1 * y1) * 65536; traps[n].left.p1.x = (int)((cx * 65536) - w); traps[n].right.p1.x = (int)((cx * 65536) + w); do { k++; y2 = (int)(-radius * cos(M_PI * k / max_ntraps)); } while (y1 == y2); traps[n].bottom = (cy + y2) * 65536; traps[n].left.p2.y = (cy + y2) * 65536; traps[n].right.p2.y = (cy + y2) * 65536; w = sqrt(radius * radius - y2 * y2) * 65536; traps[n].left.p2.x = (int)((cx * 65536) - w); traps[n].right.p2.x = (int)((cx * 65536) + w); n++; } return n; } uint32_t make_rectangle(int x, int y, int wid, int hei, xcb_render_trapezoid_t traps[]) { traps[0].top = y * 65536; traps[0].left.p1.y = y * 65536; traps[0].left.p1.x = x * 65536; traps[0].left.p2.y = (y + hei) * 65536; traps[0].left.p2.x = x * 65536; traps[0].bottom = (y + hei) * 65536; traps[0].right.p1.x = (x + wid) * 65536; traps[0].right.p1.y = y * 65536; traps[0].right.p2.x = (x + wid) * 65536; traps[0].right.p2.y = (y + hei) * 65536; return 1; } uint32_t make_rounded_window_shape(xcb_render_trapezoid_t traps[], uint32_t max_ntraps, int cr, int wid, int hei) { uint32_t n = make_circle(cr, cr, cr, max_ntraps, traps); n += make_circle(wid - cr, cr, cr, max_ntraps, traps + n); n += make_circle(wid - cr, hei - cr, cr, max_ntraps, traps + n); n += make_circle(cr, hei - cr, cr, max_ntraps, traps + n); n += make_rectangle(0, cr, wid, hei - 2 * cr, traps + n); n += make_rectangle(cr, 0, wid - 2 * cr, cr, traps + n); n += make_rectangle(cr, hei - cr, wid - 2 * cr, cr, traps + n); return n; } void render(session_t *ps, int x, int y, int dx, int dy, int wid, int hei, int fullwid, int fullhei, double opacity, bool argb, bool neg, unsigned int cr, xcb_render_picture_t pict, glx_texture_t *ptex, const region_t *reg_paint, const glx_prog_main_t *pprogram, clip_t *clip) { switch (ps->o.legacy_backend) { case BKEND_XRENDER: case BKEND_XR_GLX_HYBRID: { auto alpha_step = (int)(opacity * MAX_ALPHA); xcb_render_picture_t alpha_pict = ps->alpha_picts[alpha_step]; if (alpha_step != 0) { if (cr) { xcb_render_picture_t p_tmp = x_create_picture_with_standard( &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 = { .x = 0, .y = 0, .width = to_u16_checked(fullwid), .height = to_u16_checked(fullhei)}; 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); xcb_render_trapezoid_t traps[4 * max_ntraps + 3]; uint32_t n = make_rounded_window_shape( traps, max_ntraps, (int)cr, fullwid, fullhei); xcb_render_trapezoids( 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.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)); 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, wid, hei, XCB_PICT_STANDARD_ARGB_32, 0, 0); xcb_render_color_t black = { .red = 255, .blue = 255, .green = 255, .alpha = 255}; 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.c, XCB_RENDER_PICT_OP_SRC, p_tmp, black, 1, &rect); if (alpha_pict) { xcb_render_composite( 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.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)); } uint8_t op = ((!argb && !alpha_pict && !clip) ? XCB_RENDER_PICT_OP_SRC : XCB_RENDER_PICT_OP_OVER); xcb_render_composite( 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) { x_free_picture(&ps->c, p_tmp); } } } break; } #ifdef CONFIG_OPENGL case BKEND_GLX: glx_render(ps, ptex, x, y, dx, dy, wid, hei, ps->psglx->z, opacity, argb, neg, reg_paint, pprogram); ps->psglx->z += 1; break; #endif default: assert(0); } #ifndef CONFIG_OPENGL (void)neg; (void)ptex; (void)reg_paint; (void)pprogram; #endif } static inline void paint_region(session_t *ps, const struct win *w, int x, int y, int wid, int hei, double opacity, const region_t *reg_paint, xcb_render_picture_t pict) { const int dx = (w ? w->g.x : 0) + x; const int dy = (w ? w->g.y : 0) + y; const int fullwid = w ? w->widthb : 0; const int fullhei = w ? w->heightb : 0; struct window_options w_opts = {}; if (w) { w_opts = win_options(w); } const bool argb = (w && (win_has_alpha(w) || ps->o.force_win_blend)); const bool neg = (w && w_opts.invert_color); render(ps, x, y, dx, dy, wid, hei, fullwid, fullhei, opacity, argb, neg, w_opts.corner_radius, pict, (w ? w->paint.ptex : ps->root_tile_paint.ptex), reg_paint, #ifdef CONFIG_OPENGL w ? &ps->glx_prog_win : NULL #else NULL #endif , XCB_NONE); } /** * Check whether a paint_t contains enough data. */ 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) { return false; } if (bkend_use_xrender(ps) && !ppaint->pict) { return false; } #ifdef CONFIG_OPENGL if (BKEND_GLX == ps->o.legacy_backend && !glx_tex_bound(ppaint->ptex, XCB_NONE)) { return false; } #endif return true; } /** * Paint a window itself and dim it if asked. */ void paint_one(session_t *ps, struct win *w, const struct window_options *w_opts, const region_t *reg_paint) { // Fetch Pixmap if (!w->paint.pixmap) { w->paint.pixmap = x_new_id(&ps->c); x_set_error_action_ignore(&ps->c, xcb_composite_name_window_pixmap( ps->c.c, win_id(w), w->paint.pixmap)); } xcb_drawable_t draw = w->paint.pixmap; if (!draw) { log_error("Failed to get pixmap from window %#010x (%s), window won't be " "visible", win_id(w), w->name); return; } // XRender: Build picture if (bkend_use_xrender(ps) && !w->paint.pict) { xcb_render_create_picture_value_list_t pa = { .subwindowmode = XCB_SUBWINDOW_MODE_INCLUDE_INFERIORS, }; w->paint.pict = x_create_picture_with_pictfmt_and_pixmap( &ps->c, w->pictfmt->id, draw, XCB_RENDER_CP_SUBWINDOW_MODE, &pa); } // GLX: Build texture // Let glx_bind_pixmap() determine pixmap size, because if the user // is resizing windows, the width and height we get may not be up-to-date, // causing the jittering issue M4he reported in #7. if (!paint_bind_tex(ps, &w->paint, 0, 0, false, 0, w->a.visual, (!ps->o.glx_no_rebind_pixmap && w->pixmap_damaged))) { log_error("Failed to bind texture for window %#010x.", win_id(w)); } w->pixmap_damaged = false; if (!paint_isvalid(ps, &w->paint)) { log_error("Window %#010x is missing painting data.", win_id(w)); return; } const int x = w->g.x; 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 = win_animatable_get(w, WIN_SCRIPT_OPACITY); xcb_render_picture_t pict = w->paint.pict; // Invert window color, if required if (bkend_use_xrender(ps) && w_opts->invert_color) { xcb_render_picture_t newpict = x_create_picture_with_pictfmt( &ps->c, wid, hei, w->pictfmt->id, w->pictfmt->depth, 0, NULL); if (newpict) { // Apply clipping region to save some CPU if (reg_paint) { region_t reg; pixman_region32_init(®); pixman_region32_copy(®, (region_t *)reg_paint); pixman_region32_translate(®, -x, -y); // FIXME XFixesSetPictureClipRegion(ps->dpy, newpict, 0, // 0, reg); pixman_region32_fini(®); } 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.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, window_opacity, reg_paint, pict); } else { // Painting parameters const margin_t extents = win_calc_frame_extents(w); 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 *window_opacity, reg_paint, pict) // Sanitize the margins, in case some broken WM makes // top_width + bottom_width > height in some cases. do { // top int body_height = hei; // ctop = checked top // Make sure top margin is smaller than height int ctop = min2(body_height, t); if (ctop > 0) { COMP_BDR(0, 0, wid, ctop); } body_height -= ctop; 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) { COMP_BDR(0, hei - cbot, wid, cbot); } // Height of window exclude the margin body_height -= cbot; if (body_height <= 0) { break; } // left int body_width = wid; int cleft = min2(body_width, l); if (cleft > 0) { COMP_BDR(0, ctop, cleft, body_height); } body_width -= cleft; if (body_width <= 0) { break; } // right int cright = min2(body_width, r); if (cright > 0) { COMP_BDR(wid - cright, ctop, cright, body_height); } body_width -= cright; if (body_width <= 0) { break; } // body paint_region(ps, w, cleft, ctop, body_width, body_height, window_opacity, reg_paint, pict); } while (0); } #undef COMP_BDR if (pict != w->paint.pict) { x_free_picture(&ps->c, pict); pict = XCB_NONE; } // Dimming the window if needed if (w_opts->dim > 0) { double dim_opacity = w_opts->dim; if (!ps->o.inactive_dim_fixed) { dim_opacity *= window_opacity; } switch (ps->o.legacy_backend) { case BKEND_XRENDER: case BKEND_XR_GLX_HYBRID: { auto cval = (uint16_t)(0xffff * dim_opacity); // Premultiply color xcb_render_color_t color = { .red = 0, .green = 0, .blue = 0, .alpha = cval, }; xcb_rectangle_t rect = { .x = to_i16_checked(x), .y = to_i16_checked(y), .width = wid, .height = hei, }; xcb_render_fill_rectangles(ps->c.c, XCB_RENDER_PICT_OP_OVER, ps->tgt_buffer.pict, color, 1, &rect); } break; #ifdef CONFIG_OPENGL case BKEND_GLX: glx_dim_dst(ps, x, y, wid, hei, (int)(ps->psglx->z - 0.7), (float)dim_opacity, reg_paint); break; #endif default: assert(false); } } } static bool get_root_tile(session_t *ps) { /* if (ps->o.paint_on_overlay) { return ps->root_picture; } */ assert(!ps->root_tile_paint.pixmap); ps->root_tile_fill = false; bool fill = false; xcb_pixmap_t pixmap = x_get_root_back_pixmap(&ps->c, ps->atoms); 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 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 xcb_render_create_picture_value_list_t pa = { .repeat = true, }; ps->root_tile_paint.pict = x_create_picture_with_visual_and_pixmap( &ps->c, visual, pixmap, XCB_RENDER_CP_REPEAT, &pa); // Fill pixmap if needed if (fill) { xcb_render_color_t col; xcb_rectangle_t rect; col.red = col.green = col.blue = 0x8080; col.alpha = 0xffff; rect.x = rect.y = 0; rect.width = rect.height = 1; 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.legacy_backend) { return paint_bind_tex(ps, &ps->root_tile_paint, 0, 0, true, 0, visual, false); } #endif return true; } /** * Paint root window content. */ 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)) { return; } paint_region(ps, NULL, 0, 0, ps->root_width, ps->root_height, 1.0, reg_paint, ps->root_tile_paint.pict); } /** * Generate shadow Picture for a window. */ static bool win_build_shadow(session_t *ps, struct win *w, double opacity) { const int width = w->widthb; const int height = w->heightb; // log_trace("(): building shadow for %s %d %d", w->name, width, height); xcb_image_t *shadow_image = NULL; xcb_pixmap_t shadow_pixmap = XCB_NONE, shadow_pixmap_argb = XCB_NONE; 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, (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, shadow_image->width, shadow_image->height); shadow_pixmap_argb = 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"); goto shadow_picture_err; } shadow_picture = x_create_picture_with_standard_and_pixmap( &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) { goto shadow_picture_err; } gc = x_new_id(&ps->c); xcb_create_gc(ps->c.c, gc, shadow_pixmap, 0, NULL); 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); assert(!w->shadow_paint.pixmap); w->shadow_paint.pixmap = shadow_pixmap_argb; assert(!w->shadow_paint.pict); w->shadow_paint.pict = shadow_picture_argb; xcb_free_gc(ps->c.c, gc); xcb_image_destroy(shadow_image); xcb_free_pixmap(ps->c.c, shadow_pixmap); x_free_picture(&ps->c, shadow_picture); return true; shadow_picture_err: if (shadow_image) { xcb_image_destroy(shadow_image); } 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; } /** * Paint the shadow of a window. */ static inline void win_paint_shadow(session_t *ps, struct win *w, const struct window_options *w_opts, region_t *reg_paint) { // Bind shadow pixmap to GLX texture if needed paint_bind_tex(ps, &w->shadow_paint, 0, 0, false, 32, 0, false); if (!paint_isvalid(ps, &w->shadow_paint)) { log_error("Window %#010x is missing shadow data.", win_id(w)); return; } xcb_render_picture_t td = XCB_NONE; bool should_clip = (w_opts->corner_radius > 0) && (!w_opts->full_shadow); if (should_clip) { if (ps->o.legacy_backend == BKEND_XRENDER || ps->o.legacy_backend == BKEND_XR_GLX_HYBRID) { uint32_t max_ntraps = to_u32_checked(w_opts->corner_radius); xcb_render_trapezoid_t traps[4 * max_ntraps + 3]; uint32_t n = make_rounded_window_shape(traps, max_ntraps, (int)w_opts->corner_radius, w->widthb, w->heightb); td = x_create_picture_with_standard( &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.c, XCB_RENDER_PICT_OP_SRC, td, trans, 1, &rect); auto solid = solid_picture(&ps->c, false, 1, 0, 0, 0); xcb_render_trapezoids( 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); x_free_picture(&ps->c, solid); } else { // Not implemented } } clip_t clip = { .pict = td, .x = -(w->shadow_dx), .y = -(w->shadow_dy), }; double shadow_opacity = win_animatable_get(w, WIN_SCRIPT_SHADOW_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, shadow_opacity, true, false, 0, w->shadow_paint.pict, w->shadow_paint.ptex, reg_paint, NULL, should_clip ? &clip : NULL); if (td) { x_free_picture(&ps->c, td); } } /** * @brief Blur an area on a buffer. * * @param ps current session * @param tgt_buffer a buffer as both source and destination * @param x x pos * @param y y pos * @param wid width * @param hei height * @param blur_kerns blur kernels, ending with a NULL, guaranteed to have at * least one kernel * @param reg_clip a clipping region to be applied on intermediate buffers * * @return true if successful, false otherwise */ static bool xr_blur_dst(session_t *ps, xcb_render_picture_t tgt_buffer, int16_t x, int16_t y, uint16_t wid, uint16_t hei, struct x_convolution_kernel **blur_kerns, int nkernels, const region_t *reg_clip, xcb_render_picture_t rounded) { assert(blur_kerns); assert(blur_kerns[0]); // 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, 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); } xcb_render_picture_t src_pict = tgt_buffer, dst_pict = tmp_picture; for (int i = 0; i < nkernels; ++i) { xcb_render_fixed_t *convolution_blur = blur_kerns[i]->kernel; // `x / 65536.0` converts from X fixed point to double int kwid = (int)((double)convolution_blur[0] / 65536.0), khei = (int)((double)convolution_blur[1] / 65536.0); bool rd_from_tgt = (tgt_buffer == src_pict); // 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( ps->c.c, src_pict, strlen(XRFILTER_CONVOLUTION), XRFILTER_CONVOLUTION, (uint32_t)(kwid * khei + 2), convolution_blur); 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); xrfilter_reset(ps, src_pict); { xcb_render_picture_t tmp = src_pict; src_pict = dst_pict; dst_pict = tmp; } } 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); } x_free_picture(&ps->c, tmp_picture); return true; } /** * Blur the background of a window. */ static inline void win_blur_background(session_t *ps, struct win *w, const struct window_options *w_opts, xcb_render_picture_t tgt_buffer, const region_t *reg_paint) { const int16_t x = w->g.x; const int16_t y = w->g.y; auto const wid = to_u16_checked(w->widthb); auto const hei = to_u16_checked(w->heightb); const int cr = w_opts ? (int)w_opts->corner_radius : 0; const double window_opacity = win_animatable_get(w, WIN_SCRIPT_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 - window_opacity * (1.0 - 1.0 / 9.0); factor_center = pct * 8.0 / (1.1 - pct); } switch (ps->o.legacy_backend) { case BKEND_XRENDER: case BKEND_XR_GLX_HYBRID: { // Normalize blur kernels for (int i = 0; i < ps->o.blur_kernel_count; i++) { // Note: `x * 65536` converts double `x` to a X fixed point // representation. `x / 65536` is the other way. auto kern_src = ps->o.blur_kerns[i]; auto kern_dst = ps->blur_kerns_cache[i]; assert(!kern_dst || (kern_src->w == kern_dst->kernel[0] / 65536 && kern_src->h == kern_dst->kernel[1] / 65536)); // Skip for fixed factor_center if the cache exists already if (ps->o.blur_background_fixed && kern_dst) { continue; } x_create_convolution_kernel(kern_src, factor_center, &ps->blur_kerns_cache[i]); } xcb_render_picture_t td = XCB_NONE; if (cr) { uint32_t max_ntraps = to_u32_checked(cr); xcb_render_trapezoid_t traps[4 * max_ntraps + 3]; uint32_t n = make_rounded_window_shape(traps, max_ntraps, cr, wid, hei); td = x_create_picture_with_standard( &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.c, XCB_RENDER_PICT_OP_SRC, td, trans, 1, &rect); auto solid = solid_picture(&ps->c, false, 1, 0, 0, 0); xcb_render_trapezoids( 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); x_free_picture(&ps->c, solid); } // Minimize the region we try to blur, if the window itself is not // opaque, only the frame is. region_t reg_blur = win_get_bounding_shape_global_by_val(w); if (w->mode == WMODE_FRAME_TRANS && !ps->o.force_win_blend) { region_t reg_noframe; pixman_region32_init(®_noframe); win_get_region_noframe_local(w, ®_noframe); pixman_region32_translate(®_noframe, w->g.x, w->g.y); pixman_region32_subtract(®_blur, ®_blur, ®_noframe); pixman_region32_fini(®_noframe); } // Translate global coordinates to local ones pixman_region32_translate(®_blur, -x, -y); xr_blur_dst(ps, tgt_buffer, x, y, wid, hei, ps->blur_kerns_cache, ps->o.blur_kernel_count, ®_blur, td); if (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, (float)factor_center, reg_paint, &w->glx_blur_cache); break; #endif default: assert(0); } #ifndef CONFIG_OPENGL (void)reg_paint; #endif } /// paint all windows /// region = ?? /// region_real = the damage region void paint_all(session_t *ps, struct win *t) { if (ps->o.xrender_sync_fence || (ps->drivers & DRIVER_NVIDIA)) { 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.c, ps->sync_fence); ps->sync_fence = XCB_NONE; ps->o.xrender_sync_fence = false; ps->xsync_exists = false; } } region_t region; pixman_region32_init(®ion); damage_ring_collect(&ps->damage_ring, &ps->screen_reg, ®ion, get_buffer_age(ps)); if (!pixman_region32_not_empty(®ion)) { return; } #ifdef DEBUG_REPAINT static struct timespec last_paint = {0}; #endif if (ps->o.resize_damage > 0) { resize_region_in_place(®ion, ps->o.resize_damage, ps->o.resize_damage); } // Remove the damaged area out of screen pixman_region32_intersect(®ion, ®ion, &ps->screen_reg); if (!paint_isvalid(ps, &ps->tgt_buffer)) { if (!ps->tgt_buffer.pixmap) { free_paint(ps, &ps->tgt_buffer); ps->tgt_buffer.pixmap = 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" "painting"); exit(1); } } if (BKEND_GLX != ps->o.legacy_backend) { ps->tgt_buffer.pict = x_create_picture_with_visual_and_pixmap( &ps->c, ps->c.screen_info->root_visual, ps->tgt_buffer.pixmap, 0, 0); } } if (BKEND_XRENDER == ps->o.legacy_backend) { 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; } #endif region_t reg_tmp, *reg_paint; pixman_region32_init(®_tmp); if (t) { // Calculate the region upon which the root window is to be // painted based on the ignore region of the lowest window, if // available pixman_region32_subtract(®_tmp, ®ion, t->reg_ignore); reg_paint = ®_tmp; } else { reg_paint = ®ion; } // Region on screen we don't want any shadows on region_t reg_shadow_clip; pixman_region32_init(®_shadow_clip); set_tgt_clip(ps, reg_paint); paint_root(ps, reg_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. // // Whether this is beneficial is to be determined XXX for (auto w = t; w; w = w->prev_trans) { region_t bshape_no_corners = win_get_bounding_shape_global_without_corners_by_val(w); region_t bshape_corners = win_get_bounding_shape_global_by_val(w); auto const w_opts = win_options(w); // Painting shadow if (w_opts.shadow) { // Lazy shadow building 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); if (pixman_region32_not_empty(®_shadow_clip)) { pixman_region32_subtract(®_tmp, ®_tmp, ®_shadow_clip); } // Might be worth while to crop the region to shadow // border assert(w->shadow_width >= 0 && w->shadow_height >= 0); pixman_region32_intersect_rect( ®_tmp, ®_tmp, w->g.x + w->shadow_dx, w->g.y + w->shadow_dy, (uint)w->shadow_width, (uint)w->shadow_height); // Mask out the body of the window from the shadow if // needed Doing it here instead of in make_shadow() for // saving GPU power and handling shaped windows (XXX // unconfirmed) if (!w_opts.full_shadow) { pixman_region32_subtract(®_tmp, ®_tmp, &bshape_no_corners); } if (ps->o.crop_shadow_to_monitor) { auto monitor_index = win_find_monitor(&ps->monitors, w); if (monitor_index >= 0) { // There can be a window where number of monitors // is updated, but the monitor number attached to // the window have not. // // 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->monitors.regions[monitor_index]); } } // Detect if the region is empty before painting if (pixman_region32_not_empty(®_tmp)) { set_tgt_clip(ps, ®_tmp); win_paint_shadow(ps, w, &w_opts, ®_tmp); } } // Only clip shadows above visible windows if (win_animatable_get(w, WIN_SCRIPT_OPACITY) * MAX_ALPHA >= 1) { if (w_opts.clip_shadow_above) { // Add window bounds to shadow-clip region pixman_region32_union(®_shadow_clip, ®_shadow_clip, &bshape_corners); } else { // Remove overlapping window bounds from shadow-clip // region pixman_region32_subtract( ®_shadow_clip, ®_shadow_clip, &bshape_corners); } } // Calculate the paint region based on the reg_ignore of the current // window and its bounding region. // Remember, reg_ignore is the union of all windows above the current // window. pixman_region32_subtract(®_tmp, ®ion, w->reg_ignore); pixman_region32_intersect(®_tmp, ®_tmp, &bshape_corners); pixman_region32_fini(&bshape_corners); pixman_region32_fini(&bshape_no_corners); if (pixman_region32_not_empty(®_tmp)) { set_tgt_clip(ps, ®_tmp); #ifdef CONFIG_OPENGL // If rounded corners backup the region first if (w_opts.corner_radius > 0 && ps->o.legacy_backend == BKEND_GLX) { const int16_t x = w->g.x; const int16_t y = w->g.y; 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 // Blur window background if (w_opts.blur_background && (w->mode == WMODE_TRANS || (ps->o.blur_background_frame && w->mode == WMODE_FRAME_TRANS) || ps->o.force_win_blend)) { win_blur_background(ps, w, &w_opts, ps->tgt_buffer.pict, ®_tmp); } // Painting the window paint_one(ps, w, &w_opts, ®_tmp); #ifdef CONFIG_OPENGL // Rounded corners for XRender is implemented inside render() // Round window corners if (w_opts.corner_radius > 0 && ps->o.legacy_backend == BKEND_GLX) { 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, (float)w_opts.corner_radius, ®_tmp); } #endif } } // Free up all temporary regions pixman_region32_fini(®_tmp); pixman_region32_fini(®_shadow_clip); // Move the head of the damage ring damage_ring_advance(&ps->damage_ring); // Do this as early as possible set_tgt_clip(ps, &ps->screen_reg); if (ps->o.vsync) { // Make sure all previous requests are processed to achieve best // effect xcb_aux_sync(ps->c.c); #ifdef CONFIG_OPENGL if (glx_has_context(ps)) { if (ps->o.vsync_use_glfinish) { glFinish(); } else { glFlush(); } glXWaitX(); } #endif } if (ps->vsync_wait) { ps->vsync_wait(ps); } auto rwidth = to_u16_checked(ps->root_width); auto rheight = to_u16_checked(ps->root_height); switch (ps->o.legacy_backend) { case BKEND_XRENDER: if (ps->o.monitor_repaint) { // Copy the screen content to a new picture, and highlight the // paint region. This is not very efficient, but since it's for // debug only, we don't really care // First we create a new picture, and copy content from the buffer // to it 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, 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.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.c, XCB_RENDER_PICT_OP_SRC, new_pict, XCB_NONE, ps->tgt_picture, 0, 0, 0, 0, 0, 0, rwidth, rheight); 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: xcb_aux_sync(ps->c.c); if (ps->o.vsync_use_glfinish) { glFinish(); } else { glFlush(); } glXWaitX(); assert(ps->tgt_buffer.pixmap); paint_bind_tex(ps, &ps->tgt_buffer, ps->root_width, ps->root_height, 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 { 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->c.dpy, get_tgt_window(ps)); break; #endif default: assert(0); } xcb_aux_sync(ps->c.c); #ifdef CONFIG_OPENGL if (glx_has_context(ps)) { glFlush(); glXWaitX(); } #endif #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 // Free the paint region pixman_region32_fini(®ion); } /** * Query needed X Render / OpenGL filters to check for their existence. */ static bool xr_init_blur(session_t *ps) { // Query filters xcb_render_query_filters_reply_t *pf = xcb_render_query_filters_reply( 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)) { int len = xcb_str_name_length(iter.data); char *name = xcb_str_name(iter.data); // Check for the convolution filter if (strlen(XRFILTER_CONVOLUTION) == len && !memcmp(XRFILTER_CONVOLUTION, name, strlen(XRFILTER_CONVOLUTION))) { ps->xrfilter_convolution_exists = true; } } free(pf); } // Turn features off if any required filter is not present if (!ps->xrfilter_convolution_exists) { log_error("Xrender convolution filter " "unsupported by your X server. " "Background blur is not possible."); return false; } return true; } /** * Pregenerate alpha pictures. */ static bool init_alpha_picts(session_t *ps) { ps->alpha_picts = ccalloc(MAX_ALPHA + 1, xcb_render_picture_t); for (int i = 0; i <= MAX_ALPHA; ++i) { double o = (double)i / MAX_ALPHA; ps->alpha_picts[i] = solid_picture(&ps->c, false, o, 0, 0, 0); if (ps->alpha_picts[i] == XCB_NONE) { return false; } } return true; } bool init_render(session_t *ps) { if (ps->o.legacy_backend == BKEND_DUMMY) { return false; } // Initialize OpenGL as early as possible #ifdef CONFIG_OPENGL glxext_init(ps->c.dpy, ps->c.screen); #endif if (bkend_use_glx(ps)) { #ifdef CONFIG_OPENGL if (!glx_init(ps, true)) { return false; } #else log_error("GLX backend support not compiled in."); return false; #endif } // Initialize VSync if (!vsync_init(ps)) { return false; } // Initialize window GL shader if (BKEND_GLX == ps->o.legacy_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)) { return false; } #else log_error("GLSL supported not compiled in, can't load " "shader."); return false; #endif } if (!init_alpha_picts(ps)) { log_error("Failed to init alpha pictures."); return false; } // Blur filter if (ps->o.blur_method && ps->o.blur_method != BLUR_METHOD_KERNEL) { log_warn("Old backends only support blur method \"kernel\". Your blur " "setting will not be applied"); ps->o.blur_method = BLUR_METHOD_NONE; } if (ps->o.blur_method == BLUR_METHOD_KERNEL) { ps->blur_kerns_cache = ccalloc(ps->o.blur_kernel_count, struct x_convolution_kernel *); bool ret = false; if (ps->o.legacy_backend == BKEND_GLX) { #ifdef CONFIG_OPENGL ret = glx_init_blur(ps); #else assert(false); #endif } else { ret = xr_init_blur(ps); } if (!ret) { return ret; } } 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."); return false; } // Generates another Picture for shadows if the color is modified by // user 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, 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."); return false; } } // Initialize our rounded corners fragment shader if (ps->o.corner_radius > 0 && ps->o.legacy_backend == BKEND_GLX) { #ifdef CONFIG_OPENGL if (!glx_init_rounded_corners(ps)) { log_error("Failed to init rounded corners shader."); return false; } #else assert(false); #endif } return true; } /** * Free root tile related things. */ void free_root_tile(session_t *ps) { 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.c, ps->root_tile_paint.pixmap); ps->root_tile_paint.pixmap = XCB_NONE; } ps->root_tile_paint.pixmap = XCB_NONE; ps->root_tile_fill = false; } void deinit_render(session_t *ps) { // Free alpha_picts 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) { x_free_picture(&ps->c, ps->cshadow_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 ps->root_tile_paint.fbcfg = (struct glx_fbconfig_info){0}; if (bkend_use_glx(ps)) { glx_destroy(ps); } #endif if (ps->o.blur_method != BLUR_METHOD_NONE) { for (int i = 0; i < ps->o.blur_kernel_count; i++) { free(ps->blur_kerns_cache[i]); } free(ps->blur_kerns_cache); } } // vim: set ts=8 sw=8 noet : picom-12.5/src/render.h000066400000000000000000000022301471504570600147750ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #ifdef CONFIG_OPENGL #include "backend/gl/glx.h" #endif #include "region.h" typedef struct _glx_texture glx_texture_t; typedef struct glx_prog_main glx_prog_main_t; typedef struct session session_t; struct win; typedef struct paint { xcb_pixmap_t pixmap; xcb_render_picture_t pict; glx_texture_t *ptex; #ifdef CONFIG_OPENGL struct glx_fbconfig_info fbcfg; #endif } paint_t; typedef struct clip { xcb_render_picture_t pict; int x; int y; } clip_t; void render(session_t *ps, int x, int y, int dx, int dy, int w, int h, int fullw, int fullh, double opacity, bool argb, bool neg, unsigned int cr, xcb_render_picture_t pict, glx_texture_t *ptex, const region_t *reg_paint, const glx_prog_main_t *pprogram, clip_t *clip); void paint_all(session_t *ps, struct win *const t); void free_paint(session_t *ps, paint_t *ppaint); void free_root_tile(session_t *ps); bool init_render(session_t *ps); void deinit_render(session_t *ps); int maximum_buffer_age(session_t *); picom-12.5/src/renderer/000077500000000000000000000000001471504570600151565ustar00rootroot00000000000000picom-12.5/src/renderer/command_builder.c000066400000000000000000000423071471504570600204540ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include "common.h" #include "layout.h" #include "utils/dynarr.h" #include "wm/win.h" #include "command_builder.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_base, const region_t *frame_region, bool inactive_dim_fixed, bool force_blend, double max_brightness, const struct shader_info *shaders) { auto w = layer->win; auto cmd = cmd_base; scoped_region_t crop = region_from_box(layer->crop); auto mode = win_calc_mode_raw(layer->win); int border_width = w->g.border_width; double dim = layer->options.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); } pixman_region32_copy(&cmd->target_mask, &w->bounding_shape); pixman_region32_translate(&cmd->target_mask, layer->window.origin.x, layer->window.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 && !force_blend) { 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 (layer->options.corner_radius > 0) { win_region_remove_corners(w, layer->window.origin, &cmd->opaque_region); } struct shader_info *shader = NULL; if (layer->options.shader != NULL) { HASH_FIND_STR(shaders, layer->options.shader, shader); } float opacity = layer->opacity * (1 - layer->saved_image_blend); if (opacity > (1. - 1. / MAX_ALPHA)) { // Avoid division by a very small number opacity = 1; } float opacity_saved = 0; if (opacity < 1) { opacity_saved = layer->opacity * layer->saved_image_blend / (1 - opacity); } struct backend_blit_args args_base = { .border_width = border_width, .corner_radius = layer->options.corner_radius, .opacity = opacity, .dim = dim, .scale = layer->scale, .effective_size = layer->window.size, .shader = shader != NULL ? shader->backend_shader : NULL, .color_inverted = layer->options.invert_color, .source_mask = NULL, .max_brightness = max_brightness, }; region_scale(&cmd->target_mask, layer->window.origin, layer->scale); region_scale(&cmd->opaque_region, layer->window.origin, layer->scale); pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); pixman_region32_intersect(&cmd->opaque_region, &cmd->opaque_region, &crop); cmd->op = BACKEND_COMMAND_BLIT; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; cmd->origin = layer->window.origin; cmd->blit = args_base; cmd->blit.target_mask = &cmd->target_mask; cmd -= 1; if (layer->saved_image_blend > 0) { pixman_region32_copy(&cmd->target_mask, &cmd[1].target_mask); cmd->opaque_region = cmd[1].opaque_region; pixman_region32_init(&cmd[1].opaque_region); cmd->op = BACKEND_COMMAND_BLIT; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW_SAVED; cmd->origin = layer->window.origin; cmd->blit = args_base; cmd->blit.effective_size = (ivec2){ .width = (int)(layer->window.size.width / w->saved_win_image_scale.width), .height = (int)(layer->window.size.height / w->saved_win_image_scale.height), }; cmd->blit.opacity = opacity_saved; cmd->blit.target_mask = &cmd->target_mask; cmd->blit.scale = vec2_scale(cmd->blit.scale, w->saved_win_image_scale); cmd -= 1; } if (w->frame_opacity == 1 || w->frame_opacity == 0) { return (unsigned)(cmd_base - cmd); } pixman_region32_copy(&cmd->target_mask, frame_region); region_scale(&cmd->target_mask, cmd->origin, layer->scale); pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); pixman_region32_init(&cmd->opaque_region); cmd->op = BACKEND_COMMAND_BLIT; cmd->origin = layer->window.origin; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; cmd->blit = args_base; cmd->blit.target_mask = &cmd->target_mask; cmd->blit.opacity = w->frame_opacity * opacity; cmd -= 1; if (layer->saved_image_blend > 0) { pixman_region32_copy(&cmd->target_mask, &cmd[1].target_mask); pixman_region32_init(&cmd->opaque_region); cmd->op = BACKEND_COMMAND_BLIT; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW_SAVED; cmd->origin = layer->window.origin; cmd->blit = args_base; cmd->blit.effective_size = (ivec2){ .width = (int)(layer->window.size.width / w->saved_win_image_scale.width), .height = (int)(layer->window.size.height / w->saved_win_image_scale.height), }; cmd->blit.opacity = w->frame_opacity * opacity_saved; cmd->blit.target_mask = &cmd->target_mask; cmd->blit.scale = vec2_scale(cmd->blit.scale, w->saved_win_image_scale); cmd -= 1; } return (unsigned)(cmd_base - cmd); } /// 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 x_monitors *monitors, const struct backend_command *end) { auto w = layer->win; if (!layer->options.shadow) { return 0; } auto shadow_size_scaled = ivec2_scale_floor(layer->shadow.size, layer->shadow_scale); 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)shadow_size_scaled.width, (unsigned)shadow_size_scaled.height); log_trace("Calculate shadow for %#010x (%s)", win_id(w), w->name); log_region(TRACE, &cmd->target_mask); if (!layer->options.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 || j->source == BACKEND_COMMAND_SOURCE_WINDOW_SAVED); 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) { auto monitor_index = win_find_monitor(monitors, w); if (monitor_index >= 0) { pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &monitors->regions[monitor_index]); } } log_region(TRACE, &cmd->target_mask); if (layer->options.corner_radius > 0) { cmd->source_mask.corner_radius = layer->options.corner_radius; cmd->source_mask.inverted = true; cmd->source_mask.origin = ivec2_sub(layer->window.origin, layer->shadow.origin); } scoped_region_t crop = region_from_box(layer->crop); pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); cmd->blit = (struct backend_blit_args){ .opacity = layer->shadow_opacity, .max_brightness = 1, .source_mask = layer->options.corner_radius > 0 ? &cmd->source_mask : NULL, .scale = layer->shadow_scale, .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 (!layer->options.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->window.origin.x, layer->window.origin.y); } else if (blur_frame && mode == WMODE_FRAME_TRANS) { pixman_region32_copy(&cmd->target_mask, frame_region); } else { return 0; } region_scale(&cmd->target_mask, layer->window.origin, layer->scale); scoped_region_t crop = region_from_box(layer->crop); pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); cmd->op = BACKEND_COMMAND_BLUR; cmd->origin = (ivec2){}; if (layer->options.corner_radius > 0) { cmd->source_mask.origin = layer->window.origin; cmd->source_mask.corner_radius = layer->options.corner_radius; cmd->source_mask.inverted = false; } cmd->blur = (struct backend_blur_args){ .opacity = layer->blur_opacity, .target_mask = &cmd->target_mask, .source_mask = layer->options.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 (dynarr_is_empty(layout->layers)) { 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 = &dynarr_last(layout->layers); // `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->options.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->window.origin.x, layer->window.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->options.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, const struct x_monitors *monitors, const struct shader_info *shaders) { unsigned ncmds = 1; dynarr_foreach(layout->layers, layer) { auto mode = win_calc_mode_raw(layer->win); if (layer->options.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->options.shadow) { ncmds += 1; } unsigned n_cmds_for_window_body = 1; if (layer->win->frame_opacity < 1 && layer->win->frame_opacity > 0) { // Needs to draw the frame separately n_cmds_for_window_body += 1; } if (layer->saved_image_blend > 0) { n_cmds_for_window_body *= 2; } ncmds += n_cmds_for_window_body; // window body } auto list = command_builder_command_list_new(cb, ncmds); layout->commands = list->commands; auto cmd = &layout->commands[ncmds - 1]; dynarr_foreach_rev(layout->layers, layer) { auto last = cmd; auto frame_region = win_get_region_frame_local_by_val(layer->win); pixman_region32_translate(&frame_region, layer->window.origin.x, layer->window.origin.y); // Add window body cmd -= commands_for_window_body(layer, cmd, &frame_region, inactive_dim_fixed, force_blend, max_brightness, shaders); // Add shadow cmd -= command_for_shadow(layer, cmd, 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); } picom-12.5/src/renderer/command_builder.h000066400000000000000000000023761471504570600204630ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include struct command_builder; struct backend_command; struct layout; struct x_monitors; struct win_option; struct shader_info; 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, const struct x_monitors *monitors, const struct shader_info *shaders); picom-12.5/src/renderer/damage.c000066400000000000000000000361641471504570600165520ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include "layout.h" #include "region.h" #include "utils/dynarr.h" #include "wm/win.h" #include "damage.h" /// 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 (!ibox_eq(past_layer->window, curr_layer->window)) { // Window moved or size changed return false; } // TODO(yshui) consider window body and shadow separately. if (!vec2_eq(past_layer->scale, curr_layer->scale) || !vec2_eq(past_layer->shadow_scale, curr_layer->shadow_scale)) { // Window or shadow scale changed return false; } if (!ibox_eq(past_layer->shadow, curr_layer->shadow)) { // Shadow moved or size changed return false; } if (past_layer->saved_image_blend != curr_layer->saved_image_blend) { // The amount of blending with the saved image 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); region_scale(scratch_region, cmd2->origin, cmd2->blit.scale); 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); dynarr_foreach(layout->layers, layer) { log_trace("\t%#010x %dx%d+%dx%d (prev %d, next %d)", layer->key.x, layer->window.size.width, layer->window.size.height, layer->window.origin.x, layer->window.origin.y, layer->prev_rank, layer->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 < dynarr_len(past_layout->layers)) { 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 < dynarr_len(curr_layout->layers)) { 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 >= dynarr_len(past_layout->layers) || curr_layer_rank >= dynarr_len(curr_layout->layers)) { // No more matching layers left. assert(past_layer_rank >= dynarr_len(past_layout->layers) && curr_layer_rank >= dynarr_len(curr_layout->layers)); break; } assert(wm_treeid_eq(past_layer->key, curr_layer->key)); log_trace("%#010x == %#010x %s", past_layer->key.x, curr_layer->key.x, 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); } } } picom-12.5/src/renderer/damage.h000066400000000000000000000037241471504570600165530ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include 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); picom-12.5/src/renderer/layout.c000066400000000000000000000235551471504570600166510ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include "command_builder.h" #include "common.h" #include "region.h" #include "utils/dynarr.h" #include "utils/list.h" #include "utils/misc.h" #include "wm/win.h" #include "wm/wm.h" #include "layout.h" struct layer_index { UT_hash_handle hh; wm_treeid 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 win *w, ivec2 size) { bool to_paint = false; auto w_opts = win_options(w); if (!w->ever_damaged || !w_opts.paint) { goto out; } if (w->win_image == NULL) { goto out; } out_layer->options = w_opts; out_layer->scale = (vec2){ .x = win_animatable_get(w, WIN_SCRIPT_SCALE_X), .y = win_animatable_get(w, WIN_SCRIPT_SCALE_Y), }; out_layer->window.origin = vec2_as((vec2){.x = w->g.x + win_animatable_get(w, WIN_SCRIPT_OFFSET_X), .y = w->g.y + win_animatable_get(w, WIN_SCRIPT_OFFSET_Y)}); out_layer->window.size = vec2_as((vec2){.width = w->widthb, .height = w->heightb}); out_layer->crop.origin = vec2_as((vec2){ .x = win_animatable_get(w, WIN_SCRIPT_CROP_X), .y = win_animatable_get(w, WIN_SCRIPT_CROP_Y), }); out_layer->crop.size = vec2_as((vec2){ .x = win_animatable_get(w, WIN_SCRIPT_CROP_WIDTH), .y = win_animatable_get(w, WIN_SCRIPT_CROP_HEIGHT), }); if (w_opts.shadow) { out_layer->shadow_scale = (vec2){ .x = win_animatable_get(w, WIN_SCRIPT_SHADOW_SCALE_X), .y = win_animatable_get(w, WIN_SCRIPT_SHADOW_SCALE_Y), }; out_layer->shadow.origin = vec2_as((vec2){.x = w->g.x + w->shadow_dx + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_X), .y = w->g.y + w->shadow_dy + win_animatable_get(w, WIN_SCRIPT_SHADOW_OFFSET_Y)}); out_layer->shadow.size = vec2_as((vec2){.width = w->shadow_width, .height = w->shadow_height}); } else { out_layer->shadow.origin = (ivec2){}; out_layer->shadow.size = (ivec2){}; out_layer->shadow_scale = SCALE_IDENTITY; } struct ibox window_scaled = { .origin = out_layer->window.origin, .size = ivec2_scale_floor(out_layer->window.size, out_layer->scale), }; struct ibox screen = {.origin = {0, 0}, .size = size}; if (!ibox_overlap(window_scaled, screen) || !ibox_overlap(out_layer->crop, screen)) { goto out; } out_layer->opacity = (float)win_animatable_get(w, WIN_SCRIPT_OPACITY); out_layer->blur_opacity = (float)win_animatable_get(w, WIN_SCRIPT_BLUR_OPACITY); out_layer->shadow_opacity = (float)(win_animatable_get(w, WIN_SCRIPT_SHADOW_OPACITY) * w->shadow_opacity * w->frame_opacity); if (out_layer->opacity == 0 && out_layer->blur_opacity == 0) { goto out; } out_layer->saved_image_blend = (float)win_animatable_get(w, WIN_SCRIPT_SAVED_IMAGE_BLEND); if (w->saved_win_image == NULL) { out_layer->saved_image_blend = 0; } pixman_region32_copy(&out_layer->damaged, &w->damaged); pixman_region32_translate(&out_layer->damaged, out_layer->window.origin.x, out_layer->window.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->next_rank = -1; out_layer->prev_rank = -1; out_layer->key = wm_ref_treeid(w->tree_ref); out_layer->win = w; to_paint = true; out: pixman_region32_clear(&w->damaged); return to_paint; } static void layer_deinit(struct layer *layer) { pixman_region32_fini(&layer->damaged); } static void layer_init(struct layer *layer) { pixman_region32_init(&layer->damaged); } static void layout_deinit(struct layout *layout) { dynarr_free(layout->layers, layer_deinit); 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){}; planner->layouts[i].layers = dynarr_new(struct layer, 5); } 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; layout->size = size; unsigned rank = 0; struct layer_index *index, *next_index; wm_stack_foreach_rev(wm, cursor) { auto w = wm_ref_deref(cursor); if (w == NULL) { continue; } dynarr_resize(layout->layers, rank + 1, layer_init, layer_deinit); if (!layer_from_window(&layout->layers[rank], (struct 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++; } dynarr_truncate(layout->layers, rank, layer_deinit); // 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. dynarr_foreach(layout->layers, layer) { if (layer->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 = layer->key; index->index = to_u32_checked(layer - layout->layers); 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; } picom-12.5/src/renderer/layout.h000066400000000000000000000101541471504570600166450ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include #include "config.h" #include "region.h" #include "wm/wm.h" /// A layer to be rendered in a render layout struct layer { /// Window that will be rendered in this layer wm_treeid key; /// The window, this is only valid for the current layout. Once /// a frame has passed, windows could have been freed. struct win *win; struct window_options options; /// Damaged region of this layer, in screen coordinates region_t damaged; /// Window rectangle in screen coordinates, before it's scaled. struct ibox window; /// Shadow rectangle in screen coordinates, before it's scaled. struct ibox shadow; /// Scale of the window. The origin of scaling is the top left corner of the /// window. vec2 scale; /// Scale of the shadow. The origin of scaling is the top left corner of the /// shadow. vec2 shadow_scale; /// 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 much the image of this window should be blended with the saved image float saved_image_blend; /// Crop the content of this layer to this box, in screen coordinates. struct ibox crop; /// 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; // TODO(yshui) make opaqueness/blur finer grained maybe? to support // things like blur-background-frame // region_t opaque_region; // region_t blur_region; }; /// 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; /// Layers as a flat array, from bottom to top in stack order. This is a dynarr. 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); picom-12.5/src/renderer/meson.build000066400000000000000000000001151471504570600173150ustar00rootroot00000000000000srcs += [ files('command_builder.c', 'damage.c', 'layout.c', 'renderer.c') ] picom-12.5/src/renderer/renderer.c000066400000000000000000000556431471504570600171450ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include "renderer.h" #include #include #include "backend/backend.h" #include "backend/backend_common.h" #include "command_builder.h" #include "damage.h" #include "layout.h" #include "picom.h" #include "utils/dynarr.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; /// Copy of the entire back buffer image_handle *back_buffer_copy; /// 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; /// A dynarr of region_t for storing culled masks region_t *culled_masks; }; 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); } dynarr_free(r->culled_masks, pixman_region32_fini); 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(backend) + 1; renderer->culled_masks = dynarr_new(region_t, 0); 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->back_buffer_copy) { for (int i = 0; i < r->max_buffer_age; i++) { backend->ops.release_image(backend, r->back_buffer_copy[i]); } free(r->back_buffer_copy); r->back_buffer_copy = NULL; } 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 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, unsigned 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, .scale = SCALE_IDENTITY, .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, .scale = SCALE_IDENTITY, }; 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 win *w) { if (backend->ops.quirks(backend) & BACKEND_QUIRK_SLOW_BLUR) { xcb_pixmap_t shadow = XCB_NONE; if (!build_shadow(backend->c, r->shadow_color.alpha, w->widthb, w->heightb, (void *)r->shadow_kernel, r->shadow_pixel, &shadow)) { 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, win_options(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)", win_id(layer->win), win_client_id(layer->win, false), 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; } else if (cmd->source == BACKEND_COMMAND_SOURCE_WINDOW_SAVED) { assert(w->saved_win_image); cmd->blit.source_image = w->saved_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_images_ready(struct renderer *r, struct backend_base *backend, bool monitor_repaint) { if (monitor_repaint) { 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]); } } } if (global_debug_options.consistent_buffer_age && !r->back_buffer_copy) { r->back_buffer_copy = ccalloc(r->max_buffer_age, image_handle); for (int i = 0; i < r->max_buffer_age; i++) { r->back_buffer_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->back_buffer_copy[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, const struct x_monitors *monitors, const struct shader_info *shaders, 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. x_set_error_action_abort( 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; } renderer_ensure_images_ready(r, backend, monitor_repaint); command_builder_build(cb, layout, force_blend, blur_frame, inactive_dim_fixed, max_brightness, monitors, shaders); 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)", win_id(layer->win), win_client_id(layer->win, false), 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 && global_debug_options.consistent_buffer_age && buffer_age < r->max_buffer_age) { int past_frame = (r->frame_index + r->max_buffer_age - buffer_age) % r->max_buffer_age; region_t region; pixman_region32_init_rect(®ion, 0, 0, (unsigned)r->canvas_size.width, (unsigned)r->canvas_size.height); backend->ops.copy_area(backend, (ivec2){}, backend->ops.back_buffer(backend), r->back_buffer_copy[past_frame], ®ion); pixman_region32_fini(®ion); } if (buffer_age > 0 && (unsigned)buffer_age <= layout_manager_max_buffer_age(lm)) { layout_manager_damage(lm, (unsigned)buffer_age, blur_size, &damage_region); } dynarr_resize(r->culled_masks, layout->number_of_commands, pixman_region32_init, pixman_region32_fini); 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) { x_set_error_action_abort( 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); x_set_error_action_abort( 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, .scale = SCALE_IDENTITY, }; log_trace("Blit for monitor repaint"); backend->ops.blit(backend, (ivec2){}, r->back_image, &blit); } backend->ops.copy_area_quantize(backend, (ivec2){}, backend->ops.back_buffer(backend), r->back_image, &damage_region); if (global_debug_options.consistent_buffer_age) { region_t region; pixman_region32_init_rect(®ion, 0, 0, (unsigned)r->canvas_size.width, (unsigned)r->canvas_size.height); backend->ops.copy_area(backend, (ivec2){}, r->back_buffer_copy[r->frame_index], backend->ops.back_buffer(backend), ®ion); pixman_region32_fini(®ion); } if (backend->ops.present && !backend->ops.present(backend)) { log_warn("Failed to present the frame"); } // "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; } picom-12.5/src/renderer/renderer.h000066400000000000000000000022761471504570600171440ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include struct renderer; struct layout_manager; struct backend_base; struct command_builder; struct shader_info; 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, const struct x_monitors *monitors, const struct shader_info *shaders, uint64_t *after_damage_us); picom-12.5/src/rtkit.c000066400000000000000000000137451471504570600146630ustar00rootroot00000000000000// 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/misc.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; } picom-12.5/src/rtkit.h000066400000000000000000000005571471504570600146650ustar00rootroot00000000000000// 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 picom-12.5/src/transition/000077500000000000000000000000001471504570600155425ustar00rootroot00000000000000picom-12.5/src/transition/curve.c000066400000000000000000000153041471504570600170350ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include "compiler.h" #include "utils/misc.h" #include "utils/str.h" #include "curve.h" static double curve_sample_linear(const struct curve *this attr_unused, double progress) { return progress; } static char *curve_linear_to_c(const struct curve * /*this*/) { return strdup("{.type = CURVE_LINEAR},"); } // Cubic bezier interpolator. // // Stolen from servo: // https://searchfox.org/mozilla-central/rev/5da2d56d12/servo/components/style/bezier.rs static inline double cubic_bezier_sample_x(const struct curve_cubic_bezier *self, double t) { return ((self->ax * t + self->bx) * t + self->cx) * t; } static inline double cubic_bezier_sample_y(const struct curve_cubic_bezier *self, double t) { return ((self->ay * t + self->by) * t + self->cy) * t; } static inline double cubic_bezier_sample_derivative_x(const struct curve_cubic_bezier *self, double t) { return (3.0 * self->ax * t + 2.0 * self->bx) * t + self->cx; } // Solve for the `t` in cubic bezier function that corresponds to `x` static inline double cubic_bezier_solve_x(const struct curve_cubic_bezier *this, double x) { static const int NEWTON_METHOD_ITERATIONS = 8; double t = x; // Fast path: try Newton's method. for (int i = 0; i < NEWTON_METHOD_ITERATIONS; i++) { double x2 = cubic_bezier_sample_x(this, t); if (fabs(x2 - x) < 1e-7) { return t; } double dx = cubic_bezier_sample_derivative_x(this, t); if (fabs(dx) < 1e-6) { break; } t -= (x2 - x) / dx; } // Slow path: Use bisection. double low = 0.0, high = 1.0; t = x; while (high - low > 1e-7) { double x2 = cubic_bezier_sample_x(this, t); if (fabs(x2 - x) < 1e-7) { return t; } if (x > x2) { low = t; } else { high = t; } t = (high - low) / 2.0 + low; } return t; } static double curve_sample_cubic_bezier(const struct curve_cubic_bezier *curve, double progress) { assert(progress >= 0 && progress <= 1); if (progress == 0 || progress == 1) { return progress; } double t = cubic_bezier_solve_x(curve, progress); return cubic_bezier_sample_y(curve, t); } static char *curve_cubic_bezier_to_c(const struct curve_cubic_bezier *curve) { char *buf = NULL; casprintf(&buf, "{.type = CURVE_CUBIC_BEZIER, .bezier = { .ax = %a, .bx = %a, " ".cx = %a, .ay = %a, .by = %a, .cy = %a }},", curve->ax, curve->bx, curve->cx, curve->ay, curve->by, curve->cy); return buf; } static double curve_sample_step(const struct curve_step *this, double progress) { double y_steps = this->steps - 1 + this->jump_end + this->jump_start, x_steps = this->steps; if (progress == 1) { return 1; } if (progress == 0) { return this->jump_start ? 1 / y_steps : 0; } double scaled = progress * x_steps; double quantized = this->jump_start ? ceil(scaled) : floor(scaled); return quantized / y_steps; } static char *curve_step_to_c(const struct curve_step *this) { char *buf = NULL; casprintf(&buf, "{.type = CURVE_STEP, .step = { .steps = %d, .jump_start = %s, " ".jump_end = %s }},", this->steps, this->jump_start ? "true" : "false", this->jump_end ? "true" : "false"); return buf; } struct curve parse_linear(const char *str, const char **end, char **err) { *end = str; *err = NULL; return CURVE_LINEAR_INIT; } struct curve parse_steps(const char *input_str, const char **out_end, char **err) { const char *str = input_str; *err = NULL; if (*str != '(') { casprintf(err, "Invalid steps %s.", str); return CURVE_INVALID_INIT; } str += 1; str = skip_space(str); char *end; auto steps = strtol(str, &end, 10); if (end == str || steps > INT_MAX) { casprintf(err, "Invalid step count at \"%s\".", str); return CURVE_INVALID_INIT; } str = skip_space(end); if (*str != ',') { casprintf(err, "Invalid steps argument list \"%s\".", input_str); return CURVE_INVALID_INIT; } str = skip_space(str + 1); bool jump_start = starts_with(str, "jump-start", true) || starts_with(str, "jump-both", true); bool jump_end = starts_with(str, "jump-end", true) || starts_with(str, "jump-both", true); if (!jump_start && !jump_end && !starts_with(str, "jump-none", true)) { casprintf(err, "Invalid jump setting for steps \"%s\".", str); return CURVE_INVALID_INIT; } str += jump_start ? (jump_end ? 9 : 10) : (jump_end ? 8 : 9); str = skip_space(str); if (*str != ')') { casprintf(err, "Invalid steps argument list \"%s\".", input_str); return CURVE_INVALID_INIT; } *out_end = str + 1; return curve_new_step((int)steps, jump_start, jump_end); } struct curve parse_cubic_bezier(const char *input_str, const char **out_end, char **err) { double numbers[4]; const char *str = input_str; if (*str != '(') { casprintf(err, "Invalid cubic-bazier %s.", str); return CURVE_INVALID_INIT; } str += 1; for (int i = 0; i < 4; i++) { str = skip_space(str); const char *end = NULL; numbers[i] = strtod_simple(str, &end); if (end == str) { casprintf(err, "Invalid number %s.", str); return CURVE_INVALID_INIT; } str = skip_space(end); const char expected = i == 3 ? ')' : ','; if (*str != expected) { casprintf(err, "Invalid cubic-bazier argument list %s.", input_str); return CURVE_INVALID_INIT; } str += 1; } *out_end = str; return curve_new_cubic_bezier(numbers[0], numbers[1], numbers[2], numbers[3]); } typedef struct curve (*curve_parser)(const char *str, const char **end, char **err); static const struct { curve_parser parse; const char *name; } curve_parsers[] = { {parse_cubic_bezier, "cubic-bezier"}, {parse_linear, "linear"}, {parse_steps, "steps"}, }; struct curve curve_parse(const char *str, const char **end, char **err) { str = skip_space(str); for (size_t i = 0; i < ARR_SIZE(curve_parsers); i++) { auto name_len = strlen(curve_parsers[i].name); if (strncasecmp(str, curve_parsers[i].name, name_len) == 0) { return curve_parsers[i].parse(str + name_len, end, err); } } casprintf(err, "Unknown curve type \"%s\".", str); return CURVE_INVALID_INIT; } double curve_sample(const struct curve *curve, double progress) { switch (curve->type) { case CURVE_LINEAR: return curve_sample_linear(curve, progress); case CURVE_STEP: return curve_sample_step(&curve->step, progress); case CURVE_CUBIC_BEZIER: return curve_sample_cubic_bezier(&curve->bezier, progress); case CURVE_INVALID: default: unreachable(); } } char *curve_to_c(const struct curve *curve) { switch (curve->type) { case CURVE_LINEAR: return curve_linear_to_c(curve); case CURVE_STEP: return curve_step_to_c(&curve->step); case CURVE_CUBIC_BEZIER: return curve_cubic_bezier_to_c(&curve->bezier); case CURVE_INVALID: default: unreachable(); } } picom-12.5/src/transition/curve.h000066400000000000000000000026301471504570600170400ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include enum curve_type { CURVE_LINEAR, CURVE_CUBIC_BEZIER, CURVE_STEP, CURVE_INVALID, }; struct curve { enum curve_type type; union { struct curve_cubic_bezier { double ax, bx, cx; double ay, by, cy; } bezier; struct curve_step { int steps; bool jump_start, jump_end; } step; }; }; static const struct curve CURVE_LINEAR_INIT = {.type = CURVE_LINEAR}; static const struct curve CURVE_INVALID_INIT = {.type = CURVE_INVALID}; static inline struct curve curve_new_cubic_bezier(double x1, double y1, double x2, double y2) { double cx = 3. * x1; double bx = 3. * (x2 - x1) - cx; double cy = 3. * y1; double by = 3. * (y2 - y1) - cy; return (struct curve){ .type = CURVE_CUBIC_BEZIER, .bezier = {.ax = 1. - cx - bx, .bx = bx, .cx = cx, .ay = 1. - cy - by, .by = by, .cy = cy}, }; } static inline struct curve curve_new_step(int steps, bool jump_start, bool jump_end) { assert(steps > 0); return (struct curve){ .type = CURVE_STEP, .step = {.steps = steps, .jump_start = jump_start, .jump_end = jump_end}, }; } struct curve curve_parse(const char *str, const char **end, char **err); /// Calculate the value of the curve at `progress`. double curve_sample(const struct curve *curve, double progress); char *curve_to_c(const struct curve *curve); picom-12.5/src/transition/generated/000077500000000000000000000000001471504570600175005ustar00rootroot00000000000000picom-12.5/src/transition/generated/script_templates.c000066400000000000000000001664771471504570600232530ustar00rootroot00000000000000// This file is generated by tools/animgen.c from data/animation_presets.conf // This file is included in git repository for convenience only. // DO NOT EDIT THIS FILE! #include #include "../curve.h" #include "../script.h" #include "../script_internal.h" #include "config.h" #include "utils/misc.h" static struct script *script_template__disappear(int *output_slots) { static const struct instruction instrs[] = { {.type = INST_BRANCH_ONCE, .rel = 59}, {.type = INST_LOAD, .slot = 14}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 11}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 15}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_LINEAR}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 0}, {.type = INST_LOAD, .slot = 0}, {.type = INST_STORE, .slot = 1}, {.type = INST_LOAD, .slot = 0}, {.type = INST_STORE, .slot = 2}, {.type = INST_LOAD, .slot = 17}, {.type = INST_LOAD, .slot = 16}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 11}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 18}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = -0x1.4cccccccccccep-1, .bx = 0x1.051eb851eb852p+0, .cx = 0x1.428f5c28f5c2ap-1, .ay = -0x1.47ae147ae148p-6, .by = 0x1.eb851eb851eb8p-1, .cy = 0x1.eb851eb851ebap-5}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 16}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 5}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD, .slot = 5}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_IMM, .imm = 0x1p+1}, {.type = INST_OP, .op = OP_DIV}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 3}, {.type = INST_LOAD, .slot = 5}, {.type = INST_STORE, .slot = 6}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD, .slot = 6}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_IMM, .imm = 0x1p+1}, {.type = INST_OP, .op = OP_DIV}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 4}, {.type = INST_LOAD, .slot = 5}, {.type = INST_STORE, .slot = 7}, {.type = INST_LOAD, .slot = 6}, {.type = INST_STORE, .slot = 8}, {.type = INST_LOAD, .slot = 3}, {.type = INST_STORE, .slot = 9}, {.type = INST_LOAD, .slot = 4}, {.type = INST_STORE, .slot = 10}, {.type = INST_BRANCH_ONCE, .rel = 15}, {.type = INST_HALT}, {.type = INST_LOAD_CTX, .ctx = 64}, {.type = INST_STORE_OVER_NAN, .slot = 13}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 15}, {.type = INST_LOAD_CTX, .ctx = 72}, {.type = INST_STORE, .slot = 14}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_STORE_OVER_NAN, .slot = 16}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 18}, {.type = INST_LOAD_CTX, .ctx = 1073741828}, {.type = INST_STORE, .slot = 17}, {.type = INST_BRANCH, .rel = -70}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE, .slot = 12}, {.type = INST_LOAD, .slot = 15}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 12}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 12}, {.type = INST_LOAD, .slot = 18}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 12}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 12}, {.type = INST_HALT}, }; struct script *ret = malloc(offsetof(struct script, instrs) + sizeof(instrs)); ret->len = ARR_SIZE(instrs); ret->elapsed_slot = 11; ret->n_slots = 19; ret->stack_size = 3; ret->vars = NULL; ret->overrides = NULL; memcpy(ret->instrs, instrs, sizeof(instrs)); { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("opacity"), .slot = 0, .index = 0}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("blur-opacity"), .slot = 1, .index = 1}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-opacity"), .slot = 2, .index = 2}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-x"), .slot = 3, .index = 3}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-y"), .slot = 4, .index = 4}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("scale-x"), .slot = 5, .index = 5}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("scale-y"), .slot = 6, .index = 6}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-scale-x"), .slot = 7, .index = 7}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-scale-y"), .slot = 8, .index = 8}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-x"), .slot = 9, .index = 9}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-y"), .slot = 10, .index = 10}; HASH_ADD_STR(ret->vars, name, var); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("opacity"), .slot = 13}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("scale-x"), .slot = 16}; HASH_ADD_STR(ret->overrides, name, override); } output_slots[0] = 3; output_slots[1] = 4; output_slots[2] = 9; output_slots[3] = 10; output_slots[4] = 0; output_slots[5] = 1; output_slots[6] = 2; output_slots[7] = 5; output_slots[8] = 6; output_slots[9] = 7; output_slots[10] = 8; output_slots[11] = -1; output_slots[12] = -1; output_slots[13] = -1; output_slots[14] = -1; output_slots[15] = -1; return ret; } static bool win_script_preset__disappear(struct win_script *output, config_setting_t *setting) { output->script = script_template__disappear(output->output_indices); double knob_duration = 0x1.999999999999ap-3; config_setting_lookup_float(setting, "duration", &knob_duration); double knob_scale = 0x1.e666666666666p-1; config_setting_lookup_float(setting, "scale", &knob_scale); struct script_specialization_context spec[] = { {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 0, .value = knob_duration}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 4, .value = knob_scale}, }; script_specialize(output->script, spec, ARR_SIZE(spec)); return true; } static struct script *script_template__appear(int *output_slots) { static const struct instruction instrs[] = { {.type = INST_BRANCH_ONCE, .rel = 59}, {.type = INST_LOAD, .slot = 14}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 11}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 15}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_LINEAR}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 0}, {.type = INST_LOAD, .slot = 0}, {.type = INST_STORE, .slot = 1}, {.type = INST_LOAD, .slot = 0}, {.type = INST_STORE, .slot = 2}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD, .slot = 16}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 11}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 17}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = -0x1.4cccccccccccep-1, .bx = 0x1.dc28f5c28f5c3p-1, .cx = 0x1.70a3d70a3d70bp-1, .ay = -0x1.47ae147ae14p-6, .by = -0x1.cccccccccccd4p-1, .cy = 0x1.eb851eb851ebap+0}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 16}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 5}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD, .slot = 5}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_IMM, .imm = 0x1p+1}, {.type = INST_OP, .op = OP_DIV}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 3}, {.type = INST_LOAD, .slot = 5}, {.type = INST_STORE, .slot = 6}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD, .slot = 6}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_IMM, .imm = 0x1p+1}, {.type = INST_OP, .op = OP_DIV}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 4}, {.type = INST_LOAD, .slot = 5}, {.type = INST_STORE, .slot = 7}, {.type = INST_LOAD, .slot = 6}, {.type = INST_STORE, .slot = 8}, {.type = INST_LOAD, .slot = 3}, {.type = INST_STORE, .slot = 9}, {.type = INST_LOAD, .slot = 4}, {.type = INST_STORE, .slot = 10}, {.type = INST_BRANCH_ONCE, .rel = 13}, {.type = INST_HALT}, {.type = INST_LOAD_CTX, .ctx = 64}, {.type = INST_STORE_OVER_NAN, .slot = 13}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 15}, {.type = INST_LOAD_CTX, .ctx = 72}, {.type = INST_STORE, .slot = 14}, {.type = INST_LOAD_CTX, .ctx = 1073741828}, {.type = INST_STORE_OVER_NAN, .slot = 16}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 17}, {.type = INST_BRANCH, .rel = -68}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE, .slot = 12}, {.type = INST_LOAD, .slot = 15}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 12}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 12}, {.type = INST_LOAD, .slot = 17}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 12}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 12}, {.type = INST_HALT}, }; struct script *ret = malloc(offsetof(struct script, instrs) + sizeof(instrs)); ret->len = ARR_SIZE(instrs); ret->elapsed_slot = 11; ret->n_slots = 18; ret->stack_size = 3; ret->vars = NULL; ret->overrides = NULL; memcpy(ret->instrs, instrs, sizeof(instrs)); { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("opacity"), .slot = 0, .index = 0}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("blur-opacity"), .slot = 1, .index = 1}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-opacity"), .slot = 2, .index = 2}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-x"), .slot = 3, .index = 3}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-y"), .slot = 4, .index = 4}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("scale-x"), .slot = 5, .index = 5}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("scale-y"), .slot = 6, .index = 6}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-scale-x"), .slot = 7, .index = 7}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-scale-y"), .slot = 8, .index = 8}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-x"), .slot = 9, .index = 9}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-y"), .slot = 10, .index = 10}; HASH_ADD_STR(ret->vars, name, var); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("opacity"), .slot = 13}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("scale-x"), .slot = 16}; HASH_ADD_STR(ret->overrides, name, override); } output_slots[0] = 3; output_slots[1] = 4; output_slots[2] = 9; output_slots[3] = 10; output_slots[4] = 0; output_slots[5] = 1; output_slots[6] = 2; output_slots[7] = 5; output_slots[8] = 6; output_slots[9] = 7; output_slots[10] = 8; output_slots[11] = -1; output_slots[12] = -1; output_slots[13] = -1; output_slots[14] = -1; output_slots[15] = -1; return ret; } static bool win_script_preset__appear(struct win_script *output, config_setting_t *setting) { output->script = script_template__appear(output->output_indices); double knob_duration = 0x1.999999999999ap-3; config_setting_lookup_float(setting, "duration", &knob_duration); double knob_scale = 0x1.e666666666666p-1; config_setting_lookup_float(setting, "scale", &knob_scale); struct script_specialization_context spec[] = { {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 0, .value = knob_duration}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 4, .value = knob_scale}, }; script_specialize(output->script, spec, ARR_SIZE(spec)); return true; } static struct script *script_template__slide_out(int *output_slots) { static const struct instruction instrs[] = { {.type = INST_BRANCH_ONCE, .rel = 55}, {.type = INST_LOAD, .slot = 15}, {.type = INST_LOAD, .slot = 14}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 12}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 16}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = -0x1.4cccccccccccep-1, .bx = 0x1.051eb851eb852p+0, .cx = 0x1.428f5c28f5c2ap-1, .ay = -0x1.47ae147ae148p-6, .by = 0x1.eb851eb851eb8p-1, .cy = 0x1.eb851eb851ebap-5}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 14}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 0}, {.type = INST_LOAD, .slot = 0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 1}, {.type = INST_LOAD, .slot = 0}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 2}, {.type = INST_LOAD, .slot = 1}, {.type = INST_STORE, .slot = 3}, {.type = INST_LOAD, .slot = 2}, {.type = INST_STORE, .slot = 4}, {.type = INST_LOAD_CTX, .ctx = 0}, {.type = INST_STORE, .slot = 5}, {.type = INST_LOAD_CTX, .ctx = 8}, {.type = INST_STORE, .slot = 6}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_STORE, .slot = 7}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_STORE, .slot = 8}, {.type = INST_LOAD, .slot = 18}, {.type = INST_LOAD, .slot = 17}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 12}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 19}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_LINEAR}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 17}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 9}, {.type = INST_LOAD, .slot = 9}, {.type = INST_STORE, .slot = 10}, {.type = INST_LOAD, .slot = 9}, {.type = INST_STORE, .slot = 11}, {.type = INST_BRANCH_ONCE, .rel = 21}, {.type = INST_HALT}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE_OVER_NAN, .slot = 14}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 16}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_LOAD_CTX, .ctx = 1073741828}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_LOAD_CTX, .ctx = 1073741832}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 15}, {.type = INST_LOAD_CTX, .ctx = 64}, {.type = INST_STORE_OVER_NAN, .slot = 17}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 19}, {.type = INST_LOAD_CTX, .ctx = 64}, {.type = INST_STORE, .slot = 18}, {.type = INST_BRANCH, .rel = -72}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE, .slot = 13}, {.type = INST_LOAD, .slot = 16}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 13}, {.type = INST_LOAD, .slot = 19}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 13}, {.type = INST_HALT}, }; struct script *ret = malloc(offsetof(struct script, instrs) + sizeof(instrs)); ret->len = ARR_SIZE(instrs); ret->elapsed_slot = 12; ret->n_slots = 20; ret->stack_size = 3; ret->vars = NULL; ret->overrides = NULL; memcpy(ret->instrs, instrs, sizeof(instrs)); { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("v-timing"), .slot = 0, .index = 0}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-x"), .slot = 1, .index = 1}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-y"), .slot = 2, .index = 2}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-x"), .slot = 3, .index = 3}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-y"), .slot = 4, .index = 4}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-x"), .slot = 5, .index = 5}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-y"), .slot = 6, .index = 6}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-width"), .slot = 7, .index = 7}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-height"), .slot = 8, .index = 8}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("opacity"), .slot = 9, .index = 9}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("blur-opacity"), .slot = 10, .index = 10}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-opacity"), .slot = 11, .index = 11}; HASH_ADD_STR(ret->vars, name, var); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("v-timing"), .slot = 14}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("opacity"), .slot = 17}; HASH_ADD_STR(ret->overrides, name, override); } output_slots[0] = 1; output_slots[1] = 2; output_slots[2] = 3; output_slots[3] = 4; output_slots[4] = 9; output_slots[5] = 10; output_slots[6] = 11; output_slots[7] = -1; output_slots[8] = -1; output_slots[9] = -1; output_slots[10] = -1; output_slots[11] = 5; output_slots[12] = 6; output_slots[13] = 7; output_slots[14] = 8; output_slots[15] = -1; return ret; } static bool win_script_preset__slide_out(struct win_script *output, config_setting_t *setting) { output->script = script_template__slide_out(output->output_indices); double knob_duration = 0x1.999999999999ap-3; config_setting_lookup_float(setting, "duration", &knob_duration); const char *knob_direction = "left"; config_setting_lookup_string(setting, "direction", &knob_direction); double placeholder1_direction; double placeholder2_direction; double placeholder3_direction; if (strcmp(knob_direction, "up") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = -0x1p+0; placeholder3_direction = 0x0p+0; } else if (strcmp(knob_direction, "down") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = 0x1p+0; placeholder3_direction = 0x0p+0; } else if (strcmp(knob_direction, "left") == 0) { placeholder1_direction = -0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; } else if (strcmp(knob_direction, "right") == 0) { placeholder1_direction = 0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; } else { log_error("Invalid choice \"%s\" for option \"direction\". Line %d.", knob_direction, config_setting_source_line( config_setting_get_member(setting, "direction"))); log_error(" Valid ones are: \"up\", \"down\", \"left\", \"right\""); script_free(output->script); output->script = NULL; return false; } struct script_specialization_context spec[] = { {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 0, .value = knob_duration}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 4, .value = placeholder1_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 8, .value = placeholder2_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 12, .value = placeholder3_direction}, }; script_specialize(output->script, spec, ARR_SIZE(spec)); return true; } static struct script *script_template__slide_in(int *output_slots) { static const struct instruction instrs[] = { {.type = INST_BRANCH_ONCE, .rel = 38}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_LOAD, .slot = 11}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 9}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 12}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = -0x1.4cccccccccccep-1, .bx = 0x1.dc28f5c28f5c3p-1, .cx = 0x1.70a3d70a3d70bp-1, .ay = -0x1.47ae147ae14p-6, .by = -0x1.cccccccccccd4p-1, .cy = 0x1.eb851eb851ebap+0}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 11}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 0}, {.type = INST_LOAD, .slot = 0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 1}, {.type = INST_LOAD, .slot = 0}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 2}, {.type = INST_LOAD, .slot = 1}, {.type = INST_STORE, .slot = 3}, {.type = INST_LOAD, .slot = 2}, {.type = INST_STORE, .slot = 4}, {.type = INST_LOAD_CTX, .ctx = 0}, {.type = INST_STORE, .slot = 5}, {.type = INST_LOAD_CTX, .ctx = 8}, {.type = INST_STORE, .slot = 6}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_STORE, .slot = 7}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_STORE, .slot = 8}, {.type = INST_BRANCH_ONCE, .rel = 13}, {.type = INST_HALT}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_LOAD_CTX, .ctx = 1073741828}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_LOAD_CTX, .ctx = 1073741832}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE_OVER_NAN, .slot = 11}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 12}, {.type = INST_BRANCH, .rel = -47}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE, .slot = 10}, {.type = INST_LOAD, .slot = 12}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 10}, {.type = INST_HALT}, }; struct script *ret = malloc(offsetof(struct script, instrs) + sizeof(instrs)); ret->len = ARR_SIZE(instrs); ret->elapsed_slot = 9; ret->n_slots = 13; ret->stack_size = 3; ret->vars = NULL; ret->overrides = NULL; memcpy(ret->instrs, instrs, sizeof(instrs)); { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("v-timing"), .slot = 0, .index = 0}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-x"), .slot = 1, .index = 1}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-y"), .slot = 2, .index = 2}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-x"), .slot = 3, .index = 3}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-y"), .slot = 4, .index = 4}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-x"), .slot = 5, .index = 5}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-y"), .slot = 6, .index = 6}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-width"), .slot = 7, .index = 7}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("crop-height"), .slot = 8, .index = 8}; HASH_ADD_STR(ret->vars, name, var); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("v-timing"), .slot = 11}; HASH_ADD_STR(ret->overrides, name, override); } output_slots[0] = 1; output_slots[1] = 2; output_slots[2] = 3; output_slots[3] = 4; output_slots[4] = -1; output_slots[5] = -1; output_slots[6] = -1; output_slots[7] = -1; output_slots[8] = -1; output_slots[9] = -1; output_slots[10] = -1; output_slots[11] = 5; output_slots[12] = 6; output_slots[13] = 7; output_slots[14] = 8; output_slots[15] = -1; return ret; } static bool win_script_preset__slide_in(struct win_script *output, config_setting_t *setting) { output->script = script_template__slide_in(output->output_indices); double knob_duration = 0x1.999999999999ap-3; config_setting_lookup_float(setting, "duration", &knob_duration); const char *knob_direction = "left"; config_setting_lookup_string(setting, "direction", &knob_direction); double placeholder1_direction; double placeholder2_direction; double placeholder3_direction; if (strcmp(knob_direction, "up") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = -0x1p+0; placeholder3_direction = 0x0p+0; } else if (strcmp(knob_direction, "down") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = 0x1p+0; placeholder3_direction = 0x0p+0; } else if (strcmp(knob_direction, "left") == 0) { placeholder1_direction = -0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; } else if (strcmp(knob_direction, "right") == 0) { placeholder1_direction = 0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; } else { log_error("Invalid choice \"%s\" for option \"direction\". Line %d.", knob_direction, config_setting_source_line( config_setting_get_member(setting, "direction"))); log_error(" Valid ones are: \"up\", \"down\", \"left\", \"right\""); script_free(output->script); output->script = NULL; return false; } struct script_specialization_context spec[] = { {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 0, .value = knob_duration}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 4, .value = placeholder1_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 8, .value = placeholder2_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 12, .value = placeholder3_direction}, }; script_specialize(output->script, spec, ARR_SIZE(spec)); return true; } static struct script *script_template__fly_out(int *output_slots) { static const struct instruction instrs[] = { {.type = INST_BRANCH_ONCE, .rel = 47}, {.type = INST_LOAD, .slot = 11}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 8}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 12}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = -0x1.d70a3d70a3d75p-1, .bx = 0x1.c51eb851eb854p+0, .cx = 0x1.3333333333334p-3, .ay = 0x1.2666666666666p+0, .by = -0x1.3333333333334p-3, .cy = 0x0p+0}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 0}, {.type = INST_LOAD, .slot = 0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 1}, {.type = INST_LOAD, .slot = 0}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 2}, {.type = INST_LOAD, .slot = 1}, {.type = INST_STORE, .slot = 3}, {.type = INST_LOAD, .slot = 2}, {.type = INST_STORE, .slot = 4}, {.type = INST_LOAD, .slot = 14}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 8}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 15}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_LINEAR}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 5}, {.type = INST_LOAD, .slot = 5}, {.type = INST_STORE, .slot = 6}, {.type = INST_LOAD, .slot = 5}, {.type = INST_STORE, .slot = 7}, {.type = INST_BRANCH_ONCE, .rel = 29}, {.type = INST_HALT}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE_OVER_NAN, .slot = 10}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 12}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_LOAD_CTX, .ctx = 1073741832}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD_CTX, .ctx = 8}, {.type = INST_LOAD_CTX, .ctx = 1073741844}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_LOAD_CTX, .ctx = 1073741828}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD_CTX, .ctx = 0}, {.type = INST_LOAD_CTX, .ctx = 1073741840}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 11}, {.type = INST_LOAD_CTX, .ctx = 64}, {.type = INST_STORE_OVER_NAN, .slot = 13}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 15}, {.type = INST_LOAD_CTX, .ctx = 64}, {.type = INST_STORE, .slot = 14}, {.type = INST_BRANCH, .rel = -72}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE, .slot = 9}, {.type = INST_LOAD, .slot = 12}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 9}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 9}, {.type = INST_LOAD, .slot = 15}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 9}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 9}, {.type = INST_HALT}, }; struct script *ret = malloc(offsetof(struct script, instrs) + sizeof(instrs)); ret->len = ARR_SIZE(instrs); ret->elapsed_slot = 8; ret->n_slots = 16; ret->stack_size = 3; ret->vars = NULL; ret->overrides = NULL; memcpy(ret->instrs, instrs, sizeof(instrs)); { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("v-timing"), .slot = 0, .index = 0}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-x"), .slot = 1, .index = 1}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-y"), .slot = 2, .index = 2}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-x"), .slot = 3, .index = 3}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-y"), .slot = 4, .index = 4}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("opacity"), .slot = 5, .index = 5}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-opacity"), .slot = 6, .index = 6}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("blur-opacity"), .slot = 7, .index = 7}; HASH_ADD_STR(ret->vars, name, var); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("v-timing"), .slot = 10}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("opacity"), .slot = 13}; HASH_ADD_STR(ret->overrides, name, override); } output_slots[0] = 1; output_slots[1] = 2; output_slots[2] = 3; output_slots[3] = 4; output_slots[4] = 5; output_slots[5] = 7; output_slots[6] = 6; output_slots[7] = -1; output_slots[8] = -1; output_slots[9] = -1; output_slots[10] = -1; output_slots[11] = -1; output_slots[12] = -1; output_slots[13] = -1; output_slots[14] = -1; output_slots[15] = -1; return ret; } static bool win_script_preset__fly_out(struct win_script *output, config_setting_t *setting) { output->script = script_template__fly_out(output->output_indices); double knob_duration = 0x1.999999999999ap-3; config_setting_lookup_float(setting, "duration", &knob_duration); const char *knob_direction = "up"; config_setting_lookup_string(setting, "direction", &knob_direction); double placeholder1_direction; double placeholder2_direction; double placeholder3_direction; double placeholder4_direction; double placeholder5_direction; if (strcmp(knob_direction, "up") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = -0x1p+0; placeholder3_direction = 0x0p+0; placeholder4_direction = 0x0p+0; placeholder5_direction = -0x1p+0; } else if (strcmp(knob_direction, "down") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = 0x1p+0; placeholder3_direction = 0x0p+0; placeholder4_direction = 0x0p+0; placeholder5_direction = 0x0p+0; } else if (strcmp(knob_direction, "left") == 0) { placeholder1_direction = -0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; placeholder4_direction = -0x1p+0; placeholder5_direction = 0x0p+0; } else if (strcmp(knob_direction, "right") == 0) { placeholder1_direction = 0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; placeholder4_direction = 0x0p+0; placeholder5_direction = 0x0p+0; } else { log_error("Invalid choice \"%s\" for option \"direction\". Line %d.", knob_direction, config_setting_source_line( config_setting_get_member(setting, "direction"))); log_error(" Valid ones are: \"up\", \"down\", \"left\", \"right\""); script_free(output->script); output->script = NULL; return false; } struct script_specialization_context spec[] = { {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 0, .value = knob_duration}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 4, .value = placeholder1_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 8, .value = placeholder2_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 12, .value = placeholder3_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 16, .value = placeholder4_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 20, .value = placeholder5_direction}, }; script_specialize(output->script, spec, ARR_SIZE(spec)); return true; } static struct script *script_template__fly_in(int *output_slots) { static const struct instruction instrs[] = { {.type = INST_BRANCH_ONCE, .rel = 30}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_LOAD, .slot = 7}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 5}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 8}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = -0x1.0f5c28f5c28f8p-1, .bx = 0x1.051eb851eb853p+0, .cx = 0x1.051eb851eb852p-1, .ay = -0x1.47ae147ae146p-4, .by = -0x1.dc28f5c28f5ccp-1, .cy = 0x1.0147ae147ae16p+1}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 7}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 0}, {.type = INST_LOAD, .slot = 0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 1}, {.type = INST_LOAD, .slot = 0}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD_CTX, .ctx = 1073741836}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_STORE, .slot = 2}, {.type = INST_LOAD, .slot = 1}, {.type = INST_STORE, .slot = 3}, {.type = INST_LOAD, .slot = 2}, {.type = INST_STORE, .slot = 4}, {.type = INST_BRANCH_ONCE, .rel = 21}, {.type = INST_HALT}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_LOAD_CTX, .ctx = 1073741832}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD_CTX, .ctx = 8}, {.type = INST_LOAD_CTX, .ctx = 1073741844}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_LOAD_CTX, .ctx = 1073741828}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD_CTX, .ctx = 0}, {.type = INST_LOAD_CTX, .ctx = 1073741840}, {.type = INST_OP, .op = OP_MUL}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE_OVER_NAN, .slot = 7}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 8}, {.type = INST_BRANCH, .rel = -47}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE, .slot = 6}, {.type = INST_LOAD, .slot = 8}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 6}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 6}, {.type = INST_HALT}, }; struct script *ret = malloc(offsetof(struct script, instrs) + sizeof(instrs)); ret->len = ARR_SIZE(instrs); ret->elapsed_slot = 5; ret->n_slots = 9; ret->stack_size = 3; ret->vars = NULL; ret->overrides = NULL; memcpy(ret->instrs, instrs, sizeof(instrs)); { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("v-timing"), .slot = 0, .index = 0}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-x"), .slot = 1, .index = 1}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-y"), .slot = 2, .index = 2}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-x"), .slot = 3, .index = 3}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-y"), .slot = 4, .index = 4}; HASH_ADD_STR(ret->vars, name, var); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("v-timing"), .slot = 7}; HASH_ADD_STR(ret->overrides, name, override); } output_slots[0] = 1; output_slots[1] = 2; output_slots[2] = 3; output_slots[3] = 4; output_slots[4] = -1; output_slots[5] = -1; output_slots[6] = -1; output_slots[7] = -1; output_slots[8] = -1; output_slots[9] = -1; output_slots[10] = -1; output_slots[11] = -1; output_slots[12] = -1; output_slots[13] = -1; output_slots[14] = -1; output_slots[15] = -1; return ret; } static bool win_script_preset__fly_in(struct win_script *output, config_setting_t *setting) { output->script = script_template__fly_in(output->output_indices); double knob_duration = 0x1.999999999999ap-3; config_setting_lookup_float(setting, "duration", &knob_duration); const char *knob_direction = "up"; config_setting_lookup_string(setting, "direction", &knob_direction); double placeholder1_direction; double placeholder2_direction; double placeholder3_direction; double placeholder4_direction; double placeholder5_direction; if (strcmp(knob_direction, "up") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = -0x1p+0; placeholder3_direction = 0x0p+0; placeholder4_direction = 0x0p+0; placeholder5_direction = -0x1p+0; } else if (strcmp(knob_direction, "down") == 0) { placeholder1_direction = 0x0p+0; placeholder2_direction = 0x1p+0; placeholder3_direction = 0x0p+0; placeholder4_direction = 0x0p+0; placeholder5_direction = 0x0p+0; } else if (strcmp(knob_direction, "left") == 0) { placeholder1_direction = -0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; placeholder4_direction = -0x1p+0; placeholder5_direction = 0x0p+0; } else if (strcmp(knob_direction, "right") == 0) { placeholder1_direction = 0x1p+0; placeholder2_direction = 0x0p+0; placeholder3_direction = 0x1p+0; placeholder4_direction = 0x0p+0; placeholder5_direction = 0x0p+0; } else { log_error("Invalid choice \"%s\" for option \"direction\". Line %d.", knob_direction, config_setting_source_line( config_setting_get_member(setting, "direction"))); log_error(" Valid ones are: \"up\", \"down\", \"left\", \"right\""); script_free(output->script); output->script = NULL; return false; } struct script_specialization_context spec[] = { {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 0, .value = knob_duration}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 4, .value = placeholder1_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 8, .value = placeholder2_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 12, .value = placeholder3_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 16, .value = placeholder4_direction}, {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 20, .value = placeholder5_direction}, }; script_specialize(output->script, spec, ARR_SIZE(spec)); return true; } static struct script *script_template__geometry_change(int *output_slots) { static const struct instruction instrs[] = { {.type = INST_BRANCH_ONCE, .rel = 76}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD, .slot = 11}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 9}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 12}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = 0x1.35c28f5c28f5cp+0, .bx = -0x1.ae147ae147ae2p-2, .cx = 0x1.ae147ae147ae2p-3, .ay = -0x1.999999999996p-5, .by = -0x1.cccccccccccd4p-1, .cy = 0x1.f333333333335p+0}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 11}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 0}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 9}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 14}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = 0x1.35c28f5c28f5cp+0, .bx = -0x1.ae147ae147ae2p-2, .cx = 0x1.ae147ae147ae2p-3, .ay = -0x1.999999999996p-5, .by = -0x1.cccccccccccd4p-1, .cy = 0x1.f333333333335p+0}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 13}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 1}, {.type = INST_LOAD, .slot = 0}, {.type = INST_STORE, .slot = 2}, {.type = INST_LOAD, .slot = 1}, {.type = INST_STORE, .slot = 3}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_LOAD, .slot = 15}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 9}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 16}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = 0x1.35c28f5c28f5cp+0, .bx = -0x1.ae147ae147ae2p-2, .cx = 0x1.ae147ae147ae2p-3, .ay = -0x1.999999999996p-5, .by = -0x1.cccccccccccd4p-1, .cy = 0x1.f333333333335p+0}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 15}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 4}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_LOAD, .slot = 17}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 9}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 18}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_CUBIC_BEZIER, .bezier = {.ax = 0x1.35c28f5c28f5cp+0, .bx = -0x1.ae147ae147ae2p-2, .cx = 0x1.ae147ae147ae2p-3, .ay = -0x1.999999999996p-5, .by = -0x1.cccccccccccd4p-1, .cy = 0x1.f333333333335p+0}}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 17}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 5}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_LOAD, .slot = 19}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 9}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_LOAD, .slot = 20}, {.type = INST_OP, .op = OP_DIV}, { .type = INST_CURVE, .curve = {.type = CURVE_LINEAR}, }, {.type = INST_OP, .op = OP_MUL}, {.type = INST_LOAD, .slot = 19}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_STORE, .slot = 6}, {.type = INST_LOAD, .slot = 4}, {.type = INST_STORE, .slot = 7}, {.type = INST_LOAD, .slot = 5}, {.type = INST_STORE, .slot = 8}, {.type = INST_BRANCH_ONCE, .rel = 31}, {.type = INST_HALT}, {.type = INST_LOAD_CTX, .ctx = 48}, {.type = INST_LOAD_CTX, .ctx = 16}, {.type = INST_OP, .op = OP_DIV}, {.type = INST_STORE_OVER_NAN, .slot = 11}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 12}, {.type = INST_LOAD_CTX, .ctx = 56}, {.type = INST_LOAD_CTX, .ctx = 24}, {.type = INST_OP, .op = OP_DIV}, {.type = INST_STORE_OVER_NAN, .slot = 13}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 14}, {.type = INST_LOAD_CTX, .ctx = 32}, {.type = INST_LOAD_CTX, .ctx = 0}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_STORE_OVER_NAN, .slot = 15}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 16}, {.type = INST_LOAD_CTX, .ctx = 40}, {.type = INST_LOAD_CTX, .ctx = 8}, {.type = INST_OP, .op = OP_SUB}, {.type = INST_STORE_OVER_NAN, .slot = 17}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 18}, {.type = INST_IMM, .imm = 0x1p+0}, {.type = INST_STORE_OVER_NAN, .slot = 19}, {.type = INST_LOAD_CTX, .ctx = 1073741824}, {.type = INST_STORE, .slot = 20}, {.type = INST_BRANCH, .rel = -103}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_STORE, .slot = 10}, {.type = INST_LOAD, .slot = 12}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 10}, {.type = INST_LOAD, .slot = 14}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 10}, {.type = INST_LOAD, .slot = 16}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 10}, {.type = INST_LOAD, .slot = 18}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 10}, {.type = INST_LOAD, .slot = 20}, {.type = INST_IMM, .imm = 0x0p+0}, {.type = INST_OP, .op = OP_ADD}, {.type = INST_LOAD, .slot = 10}, {.type = INST_OP, .op = OP_MAX}, {.type = INST_STORE, .slot = 10}, {.type = INST_HALT}, }; struct script *ret = malloc(offsetof(struct script, instrs) + sizeof(instrs)); ret->len = ARR_SIZE(instrs); ret->elapsed_slot = 9; ret->n_slots = 21; ret->stack_size = 3; ret->vars = NULL; ret->overrides = NULL; memcpy(ret->instrs, instrs, sizeof(instrs)); { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("scale-x"), .slot = 0, .index = 0}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("scale-y"), .slot = 1, .index = 1}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-scale-x"), .slot = 2, .index = 2}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-scale-y"), .slot = 3, .index = 3}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-x"), .slot = 4, .index = 4}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("offset-y"), .slot = 5, .index = 5}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("saved-image-blend"), .slot = 6, .index = 6}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-x"), .slot = 7, .index = 7}; HASH_ADD_STR(ret->vars, name, var); } { struct variable_allocation *var = malloc(sizeof(*var)); *var = (struct variable_allocation){ .name = strdup("shadow-offset-y"), .slot = 8, .index = 8}; HASH_ADD_STR(ret->vars, name, var); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("scale-x"), .slot = 11}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("scale-y"), .slot = 13}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("offset-x"), .slot = 15}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("offset-y"), .slot = 17}; HASH_ADD_STR(ret->overrides, name, override); } { struct overridable_slot *override = malloc(sizeof(*override)); *override = (struct overridable_slot){.name = strdup("saved-image-blend"), .slot = 19}; HASH_ADD_STR(ret->overrides, name, override); } output_slots[0] = 4; output_slots[1] = 5; output_slots[2] = 7; output_slots[3] = 8; output_slots[4] = -1; output_slots[5] = -1; output_slots[6] = -1; output_slots[7] = 0; output_slots[8] = 1; output_slots[9] = 2; output_slots[10] = 3; output_slots[11] = -1; output_slots[12] = -1; output_slots[13] = -1; output_slots[14] = -1; output_slots[15] = 6; return ret; } static bool win_script_preset__geometry_change(struct win_script *output, config_setting_t *setting) { output->script = script_template__geometry_change(output->output_indices); double knob_duration = 0x1.999999999999ap-2; config_setting_lookup_float(setting, "duration", &knob_duration); struct script_specialization_context spec[] = { {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + 0, .value = knob_duration}, }; script_specialize(output->script, spec, ARR_SIZE(spec)); return true; } struct { const char *name; bool (*func)(struct win_script *output, config_setting_t *setting); } win_script_presets[] = { {"disappear", win_script_preset__disappear}, {"appear", win_script_preset__appear}, {"slide-out", win_script_preset__slide_out}, {"slide-in", win_script_preset__slide_in}, {"fly-out", win_script_preset__fly_out}, {"fly-in", win_script_preset__fly_in}, {"geometry-change", win_script_preset__geometry_change}, {NULL, NULL}, }; picom-12.5/src/transition/meson.build000066400000000000000000000001231471504570600177000ustar00rootroot00000000000000srcs += [files('generated/script_templates.c', 'curve.c', 'preset.c', 'script.c')] picom-12.5/src/transition/preset.c000066400000000000000000000015241471504570600172120ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include "config.h" #include "preset.h" #include "script.h" extern struct { const char *name; bool (*func)(struct win_script *output, config_setting_t *setting); } win_script_presets[]; bool win_script_parse_preset(struct win_script *output, config_setting_t *setting) { const char *preset = NULL; if (!config_setting_lookup_string(setting, "preset", &preset)) { log_error("Missing preset name in script"); return false; } for (unsigned i = 0; win_script_presets[i].name; i++) { if (strcmp(preset, win_script_presets[i].name) == 0) { log_debug("Using animation preset: %s", preset); return win_script_presets[i].func(output, setting); } } log_error("Unknown preset: %s", preset); return false; } picom-12.5/src/transition/preset.h000066400000000000000000000005171471504570600172200ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include typedef struct config_setting_t config_setting_t; struct win_script; /// Parse a animation preset definition into a win_script. bool win_script_parse_preset(struct win_script *output, config_setting_t *setting); picom-12.5/src/transition/script.c000066400000000000000000001257571471504570600172330ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include "utils/dynarr.h" #include "utils/list.h" #include "utils/str.h" #include "utils/uthash_extra.h" #include "curve.h" #include "script.h" #include "script_internal.h" #define X(x) [x] = #x, static const char *op_names[] = {OPERATORS}; #undef X struct fragment { struct list_node siblings; /// If `once` is true, this is the succeeding fragment if /// this fragment is executed. struct fragment *once_next; /// The succeeding fragment of this fragment. If `once` is true, this is the /// succeeding fragment if this fragment is NOT executed. struct fragment *next; /// Number of instructions unsigned ninstrs; /// The address of the first instruction of this fragment in compiled script. /// For `once` fragments, this is the address of the branch instruction. unsigned addr; /// Whether code is generated for this fragment. 0 = not emitted, 1 = emitted, 2 = /// in queue bool emitted; struct instruction instrs[]; }; /// Represent a variable during compilation. Contains a sequence of fragments, and /// dependency of this variable. struct compilation_stack { struct fragment *entry_point; struct fragment **exit; unsigned index; /// Number of dependencies unsigned ndeps; /// Whether the fragments loads from execution context bool need_context; /// Dependencies unsigned deps[]; }; struct script_compile_context { struct script_context_info_internal *context_info; struct variable_allocation *vars; struct overridable_slot *overrides; /// The memory slot for storing the elapsed time. /// The next slot after this is used for storing the total duration of the script. unsigned elapsed_slot; unsigned allocated_slots; unsigned max_stack; const char *current_variable_name; int *compiled; struct list_node all_fragments; struct fragment **tail, *head; /// Fragments that can be executed once at the beginning of the execution. /// For example, storing imms into memory slots. struct fragment **once_tail; /// Fragments that should be executed once at the end of the first execution. struct fragment **once_end_tail, *once_end_head; }; static const char operators[] = "+-*/^"; static const enum op operator_types[] = {OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_EXP}; static const int operator_pre[] = {0, 0, 1, 1, 2}; static void log_instruction_(enum log_level level, const char *func, unsigned index, const struct instruction *inst) { if (log_get_level_tls() > level) { return; } #define logv(fmt, ...) log_printf(tls_logger, level, func, "%u: " fmt, index, __VA_ARGS__) switch (inst->type) { case INST_IMM: logv("imm %f", inst->imm); break; case INST_BRANCH: logv("br %d", inst->rel); break; case INST_BRANCH_ONCE: logv("br_once %d", inst->rel); break; case INST_HALT: log_printf(tls_logger, level, func, "%u: halt", index); break; case INST_CURVE: log_printf(tls_logger, level, func, "%u: curve", index); break; case INST_OP: logv("op %s", op_names[inst->op]); break; case INST_LOAD: logv("load %u", inst->slot); break; case INST_STORE: logv("store %u", inst->slot); break; case INST_STORE_OVER_NAN: logv("store/nan %u", inst->slot); break; case INST_LOAD_CTX: logv("load_ctx *(%ld)", inst->ctx); break; } #undef logv } #define log_instruction(level, i, inst) \ log_instruction_(LOG_LEVEL_##level, __func__, i, &(inst)) char *instruction_to_c(struct instruction i) { char *buf = NULL; switch (i.type) { case INST_IMM: casprintf(&buf, "{.type = INST_IMM, .imm = %a},", i.imm); break; case INST_BRANCH: casprintf(&buf, "{.type = INST_BRANCH, .rel = %d},", i.rel); break; case INST_BRANCH_ONCE: casprintf(&buf, "{.type = INST_BRANCH_ONCE, .rel = %d},", i.rel); break; case INST_HALT: casprintf(&buf, "{.type = INST_HALT},"); break; case INST_CURVE:; char *curve = curve_to_c(&i.curve); casprintf(&buf, "{.type = INST_CURVE, .curve = %s},", curve); free(curve); break; case INST_OP: casprintf(&buf, "{.type = INST_OP, .op = %s},", op_names[i.op]); break; case INST_LOAD: casprintf(&buf, "{.type = INST_LOAD, .slot = %u},", i.slot); break; case INST_STORE: casprintf(&buf, "{.type = INST_STORE, .slot = %u},", i.slot); break; case INST_STORE_OVER_NAN: casprintf(&buf, "{.type = INST_STORE_OVER_NAN, .slot = %u},", i.slot); break; case INST_LOAD_CTX: casprintf(&buf, "{.type = INST_LOAD_CTX, .ctx = %ld},", i.ctx); break; } return buf; } static char parse_op(const char *input_str, const char **end, char **err) { char *op = strchr(operators, input_str[0]); *err = NULL; if (op != NULL) { *end = input_str + 1; return input_str[0]; } casprintf(err, "Expected one of \"%s\", got '%c'.", operators, input_str[0]); *end = input_str; return 0; } static enum op char_to_op(char ch) { char *op = strchr(operators, ch); BUG_ON(op == NULL); return operator_types[op - operators]; } struct script_context_info_internal { UT_hash_handle hh; struct script_context_info info; }; struct expression_parser_context { char *op_stack; struct compilation_stack *entry; size_t len; unsigned op_top, operand_top; bool need_context; }; /// Parse a number or a variable. Variable can optionally be prefixed with a minus sign. static void parse_raw_operand(struct expression_parser_context *ctx, const char *str, const char **end, struct script_compile_context *script_ctx, char **err) { double number = strtod_simple(str, end); auto expr = ctx->entry->entry_point; *err = NULL; if (*end != str) { expr->instrs[expr->ninstrs++] = (struct instruction){ .type = INST_IMM, .imm = number, }; return; } bool neg = false; bool succeeded = false; if (**end == '-') { neg = true; str = skip_space(str + 1); *end = str; } while (**end) { if (isalnum(**end) || **end == '-' || **end == '_') { succeeded = true; (*end)++; } else { break; } } if (!succeeded) { casprintf(err, "Expected a number or a variable name, got \"%s\".", str); *end = str; return; } struct variable_allocation *var = NULL; struct script_context_info_internal *exe_ctx = NULL; HASH_FIND(hh, script_ctx->vars, str, (unsigned long)(*end - str), var); HASH_FIND(hh, script_ctx->context_info, str, (unsigned long)(*end - str), exe_ctx); if (var != NULL) { expr->instrs[expr->ninstrs++] = (struct instruction){ .type = INST_LOAD, .slot = var->slot, }; ctx->entry->deps[ctx->entry->ndeps++] = var->index; } else if (exe_ctx != NULL) { expr->instrs[expr->ninstrs++] = (struct instruction){ .type = INST_LOAD_CTX, .ctx = exe_ctx->info.offset, }; ctx->need_context = true; } else { casprintf(err, "variable name \"%.*s\" is not defined", (int)(*end - str), str); *end = str; return; } if (neg) { expr->instrs[expr->ninstrs++] = (struct instruction){ .type = INST_OP, .op = OP_NEG, }; } } static inline double op_eval(double l, enum op op, double r) { switch (op) { case OP_ADD: return l + r; case OP_SUB: return l - r; case OP_DIV: return l / r; case OP_MUL: return l * r; case OP_EXP: return pow(l, r); case OP_NEG: return -l; case OP_MAX: return max2(l, r); } unreachable(); } static bool pop_op(const char *input_str, struct expression_parser_context *ctx, char **err) { if (ctx->operand_top < 2) { casprintf(err, "Missing operand for operator %c, in expression %s", ctx->op_stack[ctx->op_top - 1], input_str); return false; } auto f = ctx->entry->entry_point; // Both operands are immediates, do constant propagation. if (f->instrs[f->ninstrs - 1].type == INST_IMM && f->instrs[f->ninstrs - 2].type == INST_IMM) { double imm = op_eval(f->instrs[f->ninstrs - 1].imm, char_to_op(ctx->op_stack[ctx->op_top]), f->instrs[f->ninstrs - 2].imm); ctx->operand_top -= 1; f->instrs[f->ninstrs - 2].imm = imm; f->ninstrs -= 1; ctx->op_top -= 1; return true; } f->instrs[f->ninstrs].type = INST_OP; f->instrs[f->ninstrs].op = char_to_op(ctx->op_stack[ctx->op_top - 1]); f->ninstrs += 1; ctx->op_top -= 1; ctx->operand_top -= 1; return true; } /// Parse an operand surrounded by some parenthesis: /// `(((((var))` or `(((var` or `var)))` static bool parse_operand_or_paren(struct expression_parser_context *ctx, const char *input_str, const char **end, struct script_compile_context *script_ctx, char **err) { const char *str = input_str; while (*str == '(') { str = skip_space(str + 1); ctx->op_stack[ctx->op_top++] = '('; } parse_raw_operand(ctx, str, end, script_ctx, err); if (str == *end) { return false; } str = skip_space(*end); ctx->operand_top += 1; if (ctx->operand_top > script_ctx->max_stack) { script_ctx->max_stack = ctx->operand_top; } while (*str == ')') { while (ctx->op_top > 0 && ctx->op_stack[ctx->op_top - 1] != '(') { if (!pop_op(str, ctx, err)) { return false; } } if (ctx->op_top == 0) { casprintf(err, "Unmatched ')' in expression \"%s\"", input_str); return false; } ctx->op_top -= 1; str = skip_space(str + 1); } *end = str; return true; } static struct fragment *fragment_new(struct script_compile_context *ctx, unsigned ninstrs) { struct fragment *fragment = calloc( 1, sizeof(struct fragment) + sizeof(struct instruction[max2(1, ninstrs)])); allocchk(fragment); list_insert_after(&ctx->all_fragments, &fragment->siblings); return fragment; } /// Precedence based expression parser. Prepend fragments to `stack_entry`, or allocate a /// new one if `stack_entry` is NULL. static bool expression_compile(struct compilation_stack **stack_entry, const char *input_str, struct script_compile_context *script_ctx, unsigned slot, bool allow_override, char **err) { const char *str = skip_space(input_str); const size_t len = strlen(str); BUG_ON(len > UINT_MAX); if (len == 0) { return false; } // At most each character in `input_str` could map to an individual instruction auto fragment = fragment_new(script_ctx, (unsigned)len + 1); if (!*stack_entry) { *stack_entry = calloc(1, sizeof(struct compilation_stack) + sizeof(unsigned[len])); (*stack_entry)->exit = &fragment->next; } else { fragment->next = (*stack_entry)->entry_point; } (*stack_entry)->entry_point = fragment; struct expression_parser_context ctx = { .op_stack = ccalloc(len, char), .entry = *stack_entry, .op_top = 0, .operand_top = 0, }; const char *end = NULL; bool succeeded = false; if (!parse_operand_or_paren(&ctx, str, &end, script_ctx, err)) { goto end; } str = end; while (*str) { str = skip_space(str); char new_op = parse_op(str, &end, err); if (str == end) { goto end; } str = skip_space(end); int pre = operator_pre[char_to_op(new_op)]; while (ctx.op_top > 0 && ctx.op_stack[ctx.op_top - 1] != '(' && pre <= operator_pre[char_to_op(ctx.op_stack[ctx.op_top - 1])]) { if (!pop_op(input_str, &ctx, err)) { goto end; } } ctx.op_stack[ctx.op_top++] = new_op; if (!parse_operand_or_paren(&ctx, str, &end, script_ctx, err)) { goto end; } str = end; } while (ctx.op_top != 0) { if (!pop_op(input_str, &ctx, err)) { goto end; } } if (ctx.operand_top != 1) { casprintf(err, "excessive operand on stack %s", input_str); goto end; } succeeded = true; // Save the value of the expression // For overridable variables, we use store/nan, so caller can "pre-fill" the // variable to override it. fragment->instrs[fragment->ninstrs].type = allow_override ? INST_STORE_OVER_NAN : INST_STORE; fragment->instrs[fragment->ninstrs++].slot = slot; (*stack_entry)->need_context = ctx.need_context; end: free(ctx.op_stack); if (!succeeded) { free(*stack_entry); *stack_entry = NULL; } return succeeded; } static struct compilation_stack * make_imm_stack_entry(struct script_compile_context *ctx, double imm, unsigned slot, bool allow_override) { auto fragment = fragment_new(ctx, 2); fragment->ninstrs = 2; fragment->instrs[0].type = INST_IMM; fragment->instrs[0].imm = imm; fragment->instrs[1].type = allow_override ? INST_STORE_OVER_NAN : INST_STORE; fragment->instrs[1].slot = slot; *ctx->once_tail = fragment; ctx->once_tail = &fragment->next; // Insert an empty fragment for the stack entry fragment = fragment_new(ctx, 0); struct compilation_stack *entry = ccalloc(1, struct compilation_stack); allocchk(entry); entry->entry_point = fragment; entry->exit = &fragment->next; return entry; } static void compilation_stack_cleanup(struct compilation_stack **stack_entry) { free(*stack_entry); *stack_entry = NULL; } static bool transition_compile(struct compilation_stack **stack_entry, config_setting_t *setting, struct script_compile_context *ctx, unsigned slot, char **out_err) { int boolean = 0; double number = 0; struct curve curve; bool reset = false; char *err = NULL; const char *str = NULL; if (config_setting_lookup_string(setting, "curve", &str)) { curve = curve_parse(str, &str, &err); if (curve.type == CURVE_INVALID) { casprintf(out_err, "Cannot parse curve at line %d: %s", config_setting_source_line(setting), err); free(err); return false; } } else if (config_setting_lookup(setting, "curve") != NULL) { casprintf(out_err, "Invalid curve definition at line %d. `curve` must be a string.", config_setting_source_line(setting)); return false; } else { curve = CURVE_LINEAR_INIT; } if (config_setting_lookup_bool(setting, "reset", &boolean)) { reset = boolean; } BUG_ON(ctx->allocated_slots > UINT_MAX - 1); // The start value must take a slot, because it's overridable. auto start_slot = ctx->allocated_slots; ctx->allocated_slots += 1; if (!reset) { auto override = ccalloc(1, struct overridable_slot); override->name = strdup(ctx->current_variable_name); override->slot = start_slot; HASH_ADD_STR(ctx->overrides, name, override); } cleanup(compilation_stack_cleanup) struct compilation_stack *start = NULL, *end = NULL; if (config_setting_lookup_float(setting, "start", &number)) { start = make_imm_stack_entry(ctx, number, start_slot, true); } else if (!config_setting_lookup_string(setting, "start", &str)) { casprintf(out_err, "Transition definition does not contain a start value or " "expression. Line %d.", config_setting_source_line(setting)); return false; } else if (!expression_compile(&start, str, ctx, start_slot, !reset, &err)) { casprintf(out_err, "transition has an invalid start expression: %s Line %d.", err, config_setting_source_line(setting)); free(err); return false; } // 0 = end, 1 = duration, 2 = delay struct instruction load_parameters[3]; if (config_setting_lookup_float(setting, "end", &number)) { load_parameters[0] = (struct instruction){ .type = INST_IMM, .imm = number, }; } else if (!config_setting_lookup_string(setting, "end", &str)) { casprintf(out_err, "Transition definition does not contain a end value or " "expression. Line %d.", config_setting_source_line(setting)); return false; } else { BUG_ON(ctx->allocated_slots > UINT_MAX - 1); auto end_slot = ctx->allocated_slots++; if (!expression_compile(&end, str, ctx, end_slot, false, &err)) { casprintf(out_err, "Transition has an invalid end expression: %s. Line %d", err, config_setting_source_line(setting)); free(err); return false; } load_parameters[0] = (struct instruction){ .type = INST_LOAD, .slot = end_slot, }; } if (config_setting_lookup_float(setting, "duration", &number)) { if (number == 0) { casprintf(out_err, "Duration must be greater than 0. Line %d.", config_setting_source_line(setting)); return false; } load_parameters[1] = (struct instruction){ .type = INST_IMM, .imm = number, }; } else if (!config_setting_lookup_string(setting, "duration", &str)) { casprintf(out_err, "Transition definition does not contain a duration value or " "expression. Line %d.", config_setting_source_line(setting)); return false; } else { BUG_ON(ctx->allocated_slots > UINT_MAX - 1); auto duration_slot = ctx->allocated_slots++; if (!expression_compile(&end, str, ctx, duration_slot, false, &err)) { casprintf(out_err, "Transition has an invalid duration expression: %s. " "Line %d", err, config_setting_source_line(setting)); free(err); return false; } load_parameters[1] = (struct instruction){ .type = INST_LOAD, .slot = duration_slot, }; } if (config_setting_lookup_float(setting, "delay", &number)) { load_parameters[2] = (struct instruction){ .type = INST_IMM, .imm = number, }; } else if (!config_setting_lookup_string(setting, "delay", &str)) { load_parameters[2] = (struct instruction){ .type = INST_IMM, .imm = 0, }; } else { BUG_ON(ctx->allocated_slots > UINT_MAX - 1); auto delay_slot = ctx->allocated_slots++; if (!expression_compile(&end, str, ctx, delay_slot, false, &err)) { casprintf(out_err, "Transition has an invalid delay expression: %s. Line %d", err, config_setting_source_line(setting)); free(err); return false; } load_parameters[2] = (struct instruction){ .type = INST_LOAD, .slot = delay_slot, }; } // clang-format off struct instruction instrs[] = { load_parameters[0], {.type = INST_LOAD, .slot = start_slot}, {.type = INST_OP, .op = OP_SUB}, // v0 = end - start {.type = INST_LOAD, .slot = ctx->elapsed_slot}, load_parameters[2], {.type = INST_OP, .op = OP_SUB}, // v1 = elapsed - delay load_parameters[1], {.type = INST_OP, .op = OP_DIV}, // v2 = v1 / duration {.type = INST_CURVE, .curve = curve}, // v3 = curve(v2) {.type = INST_OP, .op = OP_MUL}, // v4 = v0 * v3 {.type = INST_LOAD, .slot = start_slot}, {.type = INST_OP, .op = OP_ADD}, // v5 = v4 + start {.type = INST_STORE, .slot = slot}, // memory[slot] = v5 }; // Instructs for calculating the total duration of the transition struct instruction total_duration_instrs[] = { load_parameters[1], load_parameters[2], {.type = INST_OP, .op = OP_ADD}, // v0 = duration + delay {.type = INST_LOAD, .slot = ctx->elapsed_slot + 1}, {.type = INST_OP, .op = OP_MAX}, // v1 = max(v0, total_duration) {.type = INST_STORE, .slot = ctx->elapsed_slot + 1}, }; // clang-format on if (ctx->max_stack < 3) { // The list of instructions above needs 3 stack slots ctx->max_stack = 3; } struct fragment *fragment = fragment_new(ctx, ARR_SIZE(instrs)); memcpy(fragment->instrs, instrs, sizeof(instrs)); fragment->ninstrs = ARR_SIZE(instrs); *stack_entry = calloc( 1, sizeof(struct compilation_stack) + sizeof(unsigned[max2(1, start->ndeps)])); allocchk(*stack_entry); struct fragment **next = &(*stack_entry)->entry_point; // Dependencies of the start value is the real dependencies of this transition // variable. (*stack_entry)->ndeps = start->ndeps; // If start value has dependencies, we calculate it like this: // if (first_evaluation) mem[start_slot] = ; // Otherwise, it's put into the "evaluation once" block. if (start->ndeps > 0) { memcpy((*stack_entry)->deps, start->deps, sizeof(unsigned[start->ndeps])); auto branch = fragment_new(ctx, 0); *next = branch; branch->once_next = start->entry_point; auto phi = fragment_new(ctx, 0); *start->exit = phi; branch->next = phi; next = &phi->next; } else { *ctx->once_tail = start->entry_point; ctx->once_tail = start->exit; } // The `end` block includes `end`, `duration`, and `delay` values. if (end != NULL && end->ndeps > 0) { // If we get here, the end/duration/delay values are not static, luckily // we can still just calculate it at the end of the first evaluation, // since at that point nothing can depends on a transition's end value. // However, for the calculation of this transition curve, we don't yet // have the these values, so we do this: `mem[output_slot] = // mem[start_slot]`, instead of compute it normally. *ctx->once_end_tail = end->entry_point; ctx->once_end_tail = end->exit; const struct instruction load_store_instrs[] = { {.type = INST_LOAD, .slot = start_slot}, {.type = INST_STORE, .slot = slot}, }; auto load_store = fragment_new(ctx, ARR_SIZE(load_store_instrs)); load_store->ninstrs = ARR_SIZE(load_store_instrs); memcpy(load_store->instrs, load_store_instrs, sizeof(load_store_instrs)); auto branch = fragment_new(ctx, 0); *next = branch; branch->once_next = load_store; branch->next = fragment; auto phi = fragment_new(ctx, 0); load_store->next = phi; fragment->next = phi; (*stack_entry)->exit = &phi->next; } else { if (end != NULL) { // The end value has no dependencies, so it only needs to be // evaluated once at the start of the first evaluation. And // therefore we can evaluate the curve like normal even for the // first evaluation. *ctx->once_tail = end->entry_point; ctx->once_tail = end->exit; } *next = fragment; (*stack_entry)->exit = &fragment->next; } // This must happen _after_ the `end` block. struct fragment *total_duration_fragment = fragment_new(ctx, ARR_SIZE(instrs)); memcpy(total_duration_fragment->instrs, total_duration_instrs, sizeof(total_duration_instrs)); total_duration_fragment->ninstrs = ARR_SIZE(total_duration_instrs); *ctx->once_end_tail = total_duration_fragment; ctx->once_end_tail = &total_duration_fragment->next; return true; } static void instruction_deinit(struct instruction * /*instr*/) { } static void fragment_free(struct fragment *frag) { list_remove(&frag->siblings); for (unsigned i = 0; i < frag->ninstrs; i++) { instruction_deinit(&frag->instrs[i]); } free(frag); } #define free_hash_table(head) \ do { \ typeof(head) i, ni; \ HASH_ITER(hh, head, i, ni) { \ HASH_DEL(head, i); \ free(i->name); \ free(i); \ } \ } while (0) void script_free(struct script *script) { for (unsigned i = 0; i < script->len; i++) { instruction_deinit(&script->instrs[i]); } free_hash_table(script->vars); free_hash_table(script->overrides); free(script); } static bool script_compile_one(struct compilation_stack **stack_entry, config_setting_t *var, struct script_compile_context *ctx, char **err) { ctx->current_variable_name = config_setting_name(var); struct variable_allocation *alloc = NULL; HASH_FIND_STR(ctx->vars, ctx->current_variable_name, alloc); BUG_ON(!alloc); if (config_setting_is_number(var)) { *stack_entry = make_imm_stack_entry(ctx, config_setting_get_float(var), alloc->slot, false); return true; } const char *str = config_setting_get_string(var); if (str != NULL) { char *tmp_err = NULL; bool succeeded = expression_compile(stack_entry, str, ctx, alloc->slot, false, &tmp_err); if (!succeeded) { casprintf(err, "Failed to parse expression at line %d. %s", config_setting_source_line(var), tmp_err); free(tmp_err); } return succeeded; } if (!config_setting_is_group(var)) { casprintf(err, "Invalid variable \"%s\", it must be either a number, a " "string, " "or a config group defining a transition.", config_setting_name(var)); return false; } return transition_compile(stack_entry, var, ctx, alloc->slot, err); } static void report_cycle(struct compilation_stack **stack, unsigned top, unsigned index, config_setting_t *setting, char **err) { unsigned start = top - 1; while (stack[start]->index != index) { start -= 1; } auto last_var = config_setting_get_elem(setting, index); auto last_name = config_setting_name(last_var); auto len = (size_t)(top - start) * 4 /* " -> " */ + strlen(last_name); for (unsigned i = start; i < top; i++) { auto var = config_setting_get_elem(setting, stack[i]->index); len += strlen(config_setting_name(var)); } auto buf = ccalloc(len + 1, char); auto pos = buf; for (unsigned i = start; i < top; i++) { auto var = config_setting_get_elem(setting, stack[i]->index); auto name = config_setting_name(var); strcpy(pos, name); pos += strlen(name); strcpy(pos, " -> "); pos += 4; } strcpy(pos, last_name); casprintf(err, "Cyclic references detected in animation script defined at line %d: %s", config_setting_source_line(setting), buf); free(buf); } static bool script_compile_one_recursive(struct compilation_stack **stack, config_setting_t *setting, unsigned index, struct script_compile_context *ctx, char **err) { unsigned stack_top = 1; if (!script_compile_one(&stack[0], config_setting_get_elem(setting, index), ctx, err)) { return false; } stack[0]->index = index; ctx->compiled[index] = 2; while (stack_top) { auto stack_entry = stack[stack_top - 1]; while (stack_entry->ndeps) { auto dep = stack_entry->deps[--stack_entry->ndeps]; if (ctx->compiled[dep] == 1) { continue; } if (ctx->compiled[dep] == 2) { report_cycle(stack, stack_top, dep, setting, err); goto out; } auto dep_setting = config_setting_get_elem(setting, dep); if (!script_compile_one(&stack[stack_top], dep_setting, ctx, err)) { goto out; } stack[stack_top]->index = dep; ctx->compiled[dep] = 2; stack_top += 1; goto next; } // Top of the stack has all of its dependencies resolved, we can emit // its fragment. *ctx->tail = stack_entry->entry_point; ctx->tail = stack_entry->exit; ctx->compiled[stack_entry->index] = 1; stack[--stack_top] = NULL; free(stack_entry); next:; } out: for (unsigned i = 0; i < stack_top; i++) { free(stack[i]); } return stack_top == 0; } static void prune_fragments(struct list_node *head) { bool changed = true; while (changed) { changed = false; list_foreach(struct fragment, i, head, siblings) { if (i->once_next == i->next && i->next != NULL) { i->once_next = NULL; changed = true; } } list_foreach(struct fragment, i, head, siblings) { struct fragment *non_empty = i->next; while (non_empty && non_empty->ninstrs == 0 && non_empty->once_next == NULL) { non_empty = non_empty->next; } changed |= (non_empty != i->next); i->next = non_empty; non_empty = i->once_next; while (non_empty && non_empty->ninstrs == 0 && non_empty->once_next == NULL) { non_empty = non_empty->next; } changed |= (non_empty != i->once_next); i->once_next = non_empty; } } } static struct script *script_codegen(struct list_node *all_fragments, struct fragment *head) { unsigned nfragments = 0; list_foreach(struct fragment, i, all_fragments, siblings) { nfragments += 1; } auto queue = ccalloc(nfragments, struct fragment *); unsigned pos = 0, h = 0, t = 1; queue[0] = head; head->emitted = true; // First, layout the fragments in the output while (h != t) { auto curr = queue[h]; while (curr) { curr->addr = pos; curr->emitted = true; pos += curr->ninstrs; if (curr->once_next) { pos += 1; // For branch_once if (!curr->once_next->emitted) { queue[t++] = curr->once_next; curr->once_next->emitted = true; } } if ((curr->next && curr->next->emitted) || !curr->next) { pos += 1; // For branch or halt break; } curr = curr->next; } h += 1; } struct script *script = calloc(1, sizeof(struct script) + sizeof(struct instruction[pos])); script->len = pos; free(queue); list_foreach(const struct fragment, i, all_fragments, siblings) { if (i->ninstrs) { memcpy(&script->instrs[i->addr], i->instrs, sizeof(struct instruction[i->ninstrs])); } auto ninstrs = i->ninstrs; if (i->once_next) { script->instrs[i->addr + ninstrs].type = INST_BRANCH_ONCE; script->instrs[i->addr + ninstrs].rel = (int)i->once_next->addr - (int)(i->addr + ninstrs); ninstrs += 1; } if (i->next && i->next->addr != i->addr + ninstrs) { script->instrs[i->addr + ninstrs].type = INST_BRANCH; script->instrs[i->addr + ninstrs].rel = (int)i->next->addr - (int)(i->addr + ninstrs); } else if (!i->next) { script->instrs[i->addr + ninstrs].type = INST_HALT; } } return script; } static void script_compile_context_init(struct script_compile_context *ctx, config_setting_t *setting) { list_init_head(&ctx->all_fragments); const uint32_t n = to_u32_checked(config_setting_length(setting)); ctx->compiled = ccalloc(n, int); for (uint32_t i = 0; i < n; i++) { auto var = config_setting_get_elem(setting, i); const char *var_name = config_setting_name(var); auto alloc = ccalloc(1, struct variable_allocation); alloc->name = strdup(var_name); alloc->index = i; alloc->slot = i; HASH_ADD_STR(ctx->vars, name, alloc); } ctx->allocated_slots = n + 2; ctx->elapsed_slot = n; auto head = fragment_new(ctx, 0); ctx->head = head; ctx->once_tail = &head->once_next; ctx->tail = &head->next; ctx->once_end_head = fragment_new(ctx, 2); ctx->once_end_head->instrs[0] = (struct instruction){ .type = INST_IMM, .imm = 0, }; ctx->once_end_head->instrs[1] = (struct instruction){ .type = INST_STORE, .slot = ctx->elapsed_slot + 1, }; ctx->once_end_head->ninstrs = 2; ctx->once_end_tail = &ctx->once_end_head->next; ctx->max_stack = 1; } unsigned script_elapsed_slot(const struct script *script) { return script->elapsed_slot; } unsigned script_total_duration_slot(const struct script *script) { return script->elapsed_slot + 1; } struct script * script_compile(config_setting_t *setting, struct script_parse_config cfg, char **out_err) { if (!config_setting_is_group(setting)) { casprintf(out_err, "Script setting must be a group"); return NULL; } struct script_context_info_internal *context_table = NULL; if (cfg.context_info) { for (unsigned i = 0; cfg.context_info[i].name; i++) { struct script_context_info_internal *new_ctx = ccalloc(1, struct script_context_info_internal); new_ctx->info = cfg.context_info[i]; HASH_ADD_STR(context_table, info.name, new_ctx); } } struct script_compile_context ctx = {}; script_compile_context_init(&ctx, setting); ctx.context_info = context_table; const uint32_t n = to_u32_checked(config_setting_length(setting)); auto stack = ccalloc(n, struct compilation_stack *); for (uint32_t i = 0; i < n; i++) { if (ctx.compiled[i]) { continue; } if (!script_compile_one_recursive(stack, setting, i, &ctx, out_err)) { break; } } { struct script_context_info_internal *info, *next_info; HASH_ITER(hh, context_table, info, next_info) { HASH_DEL(context_table, info); free(info); } } for (int i = 0; cfg.output_info && cfg.output_info[i].name; i++) { struct variable_allocation *alloc = NULL; HASH_FIND_STR(ctx.vars, cfg.output_info[i].name, alloc); if (alloc) { cfg.output_info[i].slot = to_int_checked(alloc->slot); } else { cfg.output_info[i].slot = -1; } } bool succeeded = true; for (unsigned i = 0; i < n; i++) { if (ctx.compiled[i] != 1) { succeeded = false; break; } } free(stack); free(ctx.compiled); if (!succeeded) { free_hash_table(ctx.vars); free_hash_table(ctx.overrides); list_foreach_safe(struct fragment, i, &ctx.all_fragments, siblings) { fragment_free(i); } return NULL; } // Connect everything together if (ctx.once_end_head) { auto once_end = fragment_new(&ctx, 0); *ctx.tail = once_end; once_end->once_next = ctx.once_end_head; } *ctx.once_tail = ctx.head->next; prune_fragments(&ctx.all_fragments); auto script = script_codegen(&ctx.all_fragments, ctx.head); script->vars = ctx.vars; script->overrides = ctx.overrides; script->elapsed_slot = ctx.elapsed_slot; script->n_slots = ctx.allocated_slots; script->stack_size = ctx.max_stack; log_debug("Compiled script at line %d, total instructions: %d, " "slots: %d, stack size: %d, memory[%u] = total duration, memory[%u] = " "elapsed", config_setting_source_line(setting), script->len, script->n_slots, script->stack_size, script->elapsed_slot + 1, script->elapsed_slot); if (log_get_level_tls() <= LOG_LEVEL_DEBUG) { log_debug("Output mapping:"); HASH_ITER2(ctx.vars, var) { log_debug(" %s -> %d", var->name, var->slot); } } if (log_get_level_tls() <= LOG_LEVEL_TRACE) { for (unsigned i = 0; i < script->len; i++) { log_instruction(TRACE, i, script->instrs[i]); } } list_foreach_safe(struct fragment, i, &ctx.all_fragments, siblings) { free(i); } return script; } char *script_to_c(const struct script *script, const struct script_output_info *outputs) { char **buf = dynarr_new(char *, script->len * 40); char *tmp = NULL; casprintf(&tmp, "{\n" " static const struct instruction instrs[] = {\n"); dynarr_push(buf, tmp); for (unsigned i = 0; i < script->len; i++) { dynarr_push(buf, instruction_to_c(script->instrs[i])); } casprintf(&tmp, " };\n struct script *ret = \n" " malloc(offsetof(struct script, instrs) + sizeof(instrs));\n" " ret->len = ARR_SIZE(instrs); ret->elapsed_slot = %u;" " ret->n_slots = %u; ret->stack_size = %u;\n" " ret->vars = NULL; ret->overrides = NULL;\n" " memcpy(ret->instrs, instrs, sizeof(instrs));\n", script->elapsed_slot, script->n_slots, script->stack_size); dynarr_push(buf, tmp); struct variable_allocation *var, *next_var; HASH_ITER(hh, script->vars, var, next_var) { char *var_str = NULL; casprintf(&var_str, " {\n" " struct variable_allocation *var = \n" " malloc(sizeof(*var));\n" " *var = (struct variable_allocation){\n" " .name = strdup(\"%s\"), .slot = %u, .index = %u\n" " };\n" " HASH_ADD_STR(ret->vars, name, var);\n" " }\n", var->name, var->slot, var->index); dynarr_push(buf, var_str); } struct overridable_slot *override, *next_override; HASH_ITER(hh, script->overrides, override, next_override) { char *override_str = NULL; casprintf(&override_str, " {\n" " struct overridable_slot *override = \n" " malloc(sizeof(*override));\n" " *override = (struct overridable_slot){\n" " .name = strdup(\"%s\"), .slot = %u\n" " };\n" " HASH_ADD_STR(ret->overrides, name, override);\n" " }\n", override->name, override->slot); dynarr_push(buf, override_str); } for (size_t i = 0; outputs && outputs[i].name; i++) { struct variable_allocation *alloc = NULL; HASH_FIND_STR(script->vars, outputs[i].name, alloc); if (alloc) { casprintf(&tmp, " output_slots[%zu] = %u;\n", i, alloc->slot); } else { casprintf(&tmp, " output_slots[%zu] = -1;\n", i); } dynarr_push(buf, tmp); } casprintf(&tmp, " return ret;\n}\n"); dynarr_push(buf, tmp); return dynarr_join(buf, ""); } void script_specialize(struct script *script, const struct script_specialization_context *spec, unsigned n_context) { for (unsigned i = 0; i < script->len; i++) { if (script->instrs[i].type != INST_LOAD_CTX) { continue; } for (unsigned j = 0; j < n_context; j++) { if (script->instrs[i].ctx == spec[j].offset) { script->instrs[i].type = INST_IMM; script->instrs[i].imm = spec[j].value; break; } } } } struct script_instance *script_instance_new(const struct script *script) { // allocate no space for the variable length array is UB. unsigned memory_size = max2(1, script->n_slots + script->stack_size); struct script_instance *instance = calloc(1, sizeof(struct script_instance) + sizeof(double[memory_size])); allocchk(instance); instance->script = script; for (unsigned i = 0; i < script->n_slots; i++) { instance->memory[i] = NAN; } instance->memory[script->elapsed_slot] = 0; return instance; } void script_instance_resume_from(struct script_instance *old, struct script_instance *new_) { // todo: proper steer logic struct overridable_slot *i, *next; HASH_ITER(hh, new_->script->overrides, i, next) { struct variable_allocation *src_alloc = NULL; HASH_FIND_STR(old->script->vars, i->name, src_alloc); if (!src_alloc) { continue; } new_->memory[i->slot] = old->memory[src_alloc->slot]; } } enum script_evaluation_result script_instance_evaluate(struct script_instance *instance, void *context) { auto script = instance->script; auto stack = (double *)&instance->memory[script->n_slots]; unsigned top = 0; double l, r; bool do_branch_once = instance->memory[script->elapsed_slot] == 0; for (auto i = script->instrs;; i++) { switch (i->type) { case INST_IMM: stack[top++] = i->imm; break; case INST_LOAD: stack[top++] = instance->memory[i->slot]; break; case INST_LOAD_CTX: stack[top++] = *(double *)(context + i->ctx); break; case INST_STORE: BUG_ON(top < 1); instance->memory[i->slot] = stack[--top]; break; case INST_STORE_OVER_NAN: BUG_ON(top < 1); top -= 1; if (safe_isnan(instance->memory[i->slot])) { instance->memory[i->slot] = stack[top]; } break; case INST_BRANCH: i += i->rel - 1; break; case INST_BRANCH_ONCE: if (do_branch_once) { i += i->rel - 1; } break; case INST_HALT: return SCRIPT_EVAL_OK; case INST_OP: if (i->op == OP_NEG) { BUG_ON(top < 1); l = stack[top - 1]; stack[top - 1] = -l; } else { BUG_ON(top < 2); l = stack[top - 2]; r = stack[top - 1]; stack[top - 2] = op_eval(l, i->op, r); top -= 1; } break; case INST_CURVE: BUG_ON(top < 1); l = stack[top - 1]; l = min2(max2(0, l), 1); stack[top - 1] = curve_sample(&i->curve, l); break; } if (top && safe_isnan(stack[top - 1])) { return SCRIPT_EVAL_ERROR_NAN; } if (top && safe_isinf(stack[top - 1])) { return SCRIPT_EVAL_ERROR_INF; } } unreachable(); } #ifdef UNIT_TEST static inline void script_compile_str(struct test_case_metadata *metadata, const char *str, struct script_output_info *outputs, char **err, struct script **out) { config_t cfg; config_init(&cfg); config_set_auto_convert(&cfg, 1); int ret = config_read_string(&cfg, str); TEST_EQUAL(ret, CONFIG_TRUE); config_setting_t *setting = config_root_setting(&cfg); TEST_NOTEQUAL(setting, NULL); *out = script_compile(setting, (struct script_parse_config){.output_info = outputs}, err); config_destroy(&cfg); } TEST_CASE(scripts_1) { static const char *str = "\ a = 10; \ b = \"a * 2\";\ c = \"(b - 1) * (a+1)\";\ d = \"- e - 1\"; \ e : { \ curve = \"cubic-bezier(0.5,0.5, 0.5, 0.5)\"; \ duration = \"a\"; \ delay = 0.5; \ start = 10; \ end = \"2 * c\"; \ }; \ f : { \ curve = \"cubic-bezier(0.1,0.2, 0.3, 0.4)\"; \ duration = 10; \ delay = 0.5; \ start = \"e + 1\"; \ end = \"f - 1\"; \ }; \ neg = \"-a\"; \ timing1 : { \ duration = 10; \ start = 1; \ end = 0; \ };\ timing2 : { \ curve = \"steps(1, jump-start)\"; \ duration = 10; \ start = 1; \ end = 0; \ };"; struct script_output_info outputs[] = {{"a"}, {"b"}, {"c"}, {"d"}, {"e"}, {NULL}}; char *err = NULL; struct script *script = NULL; script_compile_str(metadata, str, outputs, &err, &script); if (err) { log_error("err: %s\n", err); free(err); } TEST_NOTEQUAL(script, NULL); TEST_EQUAL(err, NULL); if (script) { struct variable_allocation *c; HASH_FIND_STR(script->vars, "c", c); TEST_NOTEQUAL(c, NULL); struct script_instance *instance = script_instance_new(script); auto result = script_instance_evaluate(instance, NULL); TEST_EQUAL(result, SCRIPT_EVAL_OK); TEST_EQUAL(instance->memory[script->elapsed_slot + 1], 10.5); TEST_EQUAL(instance->memory[outputs[0].slot], 10); TEST_EQUAL(instance->memory[outputs[1].slot], 20); TEST_EQUAL(instance->memory[outputs[2].slot], 209); TEST_EQUAL(instance->memory[outputs[3].slot], -11); TEST_EQUAL(instance->memory[outputs[4].slot], 10); TEST_TRUE(!script_instance_is_finished(instance)); instance->memory[instance->script->elapsed_slot] += 5.5; result = script_instance_evaluate(instance, NULL); TEST_EQUAL(result, SCRIPT_EVAL_OK); TEST_EQUAL(instance->memory[outputs[4].slot], 214); instance->memory[instance->script->elapsed_slot] += 5.5; result = script_instance_evaluate(instance, NULL); TEST_EQUAL(result, SCRIPT_EVAL_OK); TEST_EQUAL(instance->memory[outputs[0].slot], 10); TEST_EQUAL(instance->memory[outputs[1].slot], 20); TEST_EQUAL(instance->memory[outputs[2].slot], 209); TEST_EQUAL(instance->memory[outputs[3].slot], -419); TEST_EQUAL(instance->memory[outputs[4].slot], 418); TEST_TRUE(script_instance_is_finished(instance)); free(instance); script_free(script); } } TEST_CASE(scripts_report_cycle) { static const char *str = "\ a = \"c\"; \ b = \"a * 2\";\ c = \"b + 1\";"; char *err = NULL; struct script *script = NULL; script_compile_str(metadata, str, NULL, &err, &script); TEST_EQUAL(script, NULL); TEST_NOTEQUAL(err, NULL); TEST_STREQUAL(err, "Cyclic references detected in animation script defined at " "line 0: a -> c -> b -> a"); free(err); } TEST_CASE(script_errors) { static const char *cases[][2] = { {"a = \"1 @ 2 \";", "Failed to parse expression at line 1. Expected one of " "\"+-*/^\", got '@'."}, {"a = { curve = \"asdf\";};", "Cannot parse curve at line 1: Unknown curve " "type \"asdf\"."}, {"a = { curve = \"steps(a)\";};", "Cannot parse curve at line 1: Invalid " "step count at \"a)\"."}, {"a = { curve = \"steps(1)\";};", "Cannot parse curve at line 1: Invalid " "steps argument list \"(1)\"."}, {"a = \"1 + +\";", "Failed to parse expression at line 1. Expected a number " "or a variable name, got \"+\"."}, {"a = \"1)\";", "Failed to parse expression at line 1. Unmatched ')' in " "expression \"1)\""}, {"a = {};", "Transition definition does not contain a start value or " "expression. Line 1."}, {"a = { duration = 0; start = 0; end = 0; };", "Duration must be greater " "than 0. Line 1."}, }; char *err = NULL; struct script *script = NULL; for (size_t i = 0; i < ARR_SIZE(cases); i++) { script_compile_str(metadata, cases[i][0], NULL, &err, &script); TEST_EQUAL(script, NULL); TEST_NOTEQUAL(err, NULL); TEST_STREQUAL(err, cases[i][1]); free(err); err = NULL; } } #endif picom-12.5/src/transition/script.h000066400000000000000000000074711471504570600172300ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include #include struct script_context_info { const char *name; ptrdiff_t offset; }; struct script_specialization_context { ptrdiff_t offset; double value; }; struct script_output_info { const char *name; /// Slot for this variable, -1 if this variable doesn't exist. int slot; }; struct script_parse_config { const struct script_context_info *context_info; /// Set the output variables of this script, also used to receive the slot number /// for those variables. struct script_output_info *output_info; }; struct script; struct script_instance { const struct script *script; double memory[]; }; enum script_evaluation_result { /// +/-inf in results SCRIPT_EVAL_ERROR_INF, /// NaN in results SCRIPT_EVAL_ERROR_NAN, /// OK SCRIPT_EVAL_OK, }; typedef struct config_setting_t config_setting_t; #define SCRIPT_CTX_PLACEHOLDER_BASE (0x40000000) struct script * script_compile(config_setting_t *setting, struct script_parse_config cfg, char **out_err); void script_free(struct script *script); enum script_evaluation_result script_instance_evaluate(struct script_instance *instance, void *context); /// Resume the script instance from another script instance that's currently running. /// The script doesn't have to be the same. For resumable (explained later) transitions, /// if matching variables exist in the `old` script, their starting point will be /// overridden with the current value of matching variables from `old`. A resumable /// transition is a transition that will "resume" from wherever its current value is. /// Imagine a window flying off the screen, for some reason you decided to bring it back /// when it's just halfway cross. It would be jarring if the window jumps, so you would /// want it to fly back in from where it currently is, instead from out of the screen. /// This resuming behavior can be turned off by setting `reset = true;` in the transition /// configuration, in which case the user defined `start` value will always be used. void script_instance_resume_from(struct script_instance *old, struct script_instance *new_); struct script_instance *script_instance_new(const struct script *script); /// Get the total duration slot of a script. unsigned script_total_duration_slot(const struct script *script); unsigned script_elapsed_slot(const struct script *script); /// Specialize a script instance with a context. During evaluation of the resulting /// script, what would have been read from the context will be replaced with the hardcoded /// value in the specialization context. void script_specialize(struct script *instance, const struct script_specialization_context *context, unsigned n_context); /// Check if a script instance has finished. The script instance must have been evaluated /// at least once. static inline bool script_instance_is_finished(const struct script_instance *instance) { return instance->memory[script_elapsed_slot(instance->script)] >= instance->memory[script_total_duration_slot(instance->script)]; } /// Generate code for a C function that will return a script identical to `script` when /// called. The generated function will take a `int *output_slots` parameter, which it /// will fill in, based on `outputs` passed to this function. Specifically, the generated /// function will fill in `output_slots[i]` with the slot number of the output variable /// named `outputs[i].name`. The generated function will return a pointer to the script. /// This function only generates the function body, you need to provide the function /// signature and the function name yourself. char *script_to_c(const struct script *script, const struct script_output_info *outputs); picom-12.5/src/transition/script_internal.h000066400000000000000000000064761471504570600211300ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include "curve.h" #define OPERATORS \ X(OP_ADD) \ X(OP_SUB) \ X(OP_MUL) \ X(OP_DIV) \ /* Exponent */ \ X(OP_EXP) \ /* Negation */ \ X(OP_NEG) \ X(OP_MAX) #define X(x) x, enum op { OPERATORS }; #undef X enum instruction_type { /// Push an immediate value to the top of the stack INST_IMM = 0, /// Pop two values from the top of the stack, apply operator, /// and push the result to the top of the stack INST_OP, /// Load a memory slot and push its value to the top of the stack. INST_LOAD, /// Load from evaluation context and push the value to the top of the stack. INST_LOAD_CTX, /// Pop one value from the top of the stack, and store it into a memory slot. INST_STORE, /// Pop one value from the top of the stack, if the memory slot contains NaN, /// store it into the memory slot; otherwise discard the value. INST_STORE_OVER_NAN, /// Pop a value from the top of the stack, clamp its value to [0, 1], then /// evaluate a curve at that point, push the result to the top of the stack. INST_CURVE, /// Jump to the branch target only when the script is evaluated for the first /// time. Used to perform initialization and such. INST_BRANCH_ONCE, /// Unconditional branch INST_BRANCH, INST_HALT, }; /// Store metadata about where the result of a variable is stored struct variable_allocation { UT_hash_handle hh; char *name; unsigned index; /// The memory slot for variable named `name` unsigned slot; }; struct instruction { enum instruction_type type; union { double imm; enum op op; /// Memory slot for load and store unsigned slot; /// Context offset for load_ctx ptrdiff_t ctx; /// Relative PC change for branching int rel; /// The curve struct curve curve; }; }; /// When interrupting an already executing script and starting a new script, /// we might want to inherit some of the existing values of variables as starting points, /// i.e. we want to "resume" animation for the current state. This is configurable, and /// can be disabled by enabling the `reset` property on a transition. This struct store /// where the `start` variables of those "resumable" transition variables, which can be /// overridden at the start of execution for this use case. struct overridable_slot { UT_hash_handle hh; char *name; unsigned slot; }; struct script { unsigned len; unsigned n_slots; /// The memory slot for storing the elapsed time. /// The next slot after this is used for storing the total duration of the script. unsigned elapsed_slot; unsigned stack_size; struct variable_allocation *vars; struct overridable_slot *overrides; struct instruction instrs[]; }; picom-12.5/src/utils/000077500000000000000000000000001471504570600145105ustar00rootroot00000000000000picom-12.5/src/utils/cache.c000066400000000000000000000023351471504570600157220ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include "cache.h" #include "misc.h" 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; } 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 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); HASH_ADD_KEYPTR(hh, c->entries, (*value)->key, keylen, *value); return 1; } 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); if (free_fn) { free_fn(c, e); } } 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_impl(c, e, free_fn); } } picom-12.5/src/utils/cache.h000066400000000000000000000033201471504570600157220ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #define cache_entry(ptr, type, member) container_of(ptr, type, member) struct cache; struct cache_handle; /// 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); struct cache { struct cache_handle *entries; }; static const struct cache CACHE_INIT = {NULL}; struct cache_handle { char *key; UT_hash_handle hh; }; /// 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); picom-12.5/src/utils/console.h000066400000000000000000000002761471504570600163300ustar00rootroot00000000000000#pragma once /// Generate ANSI escape code #define ANSI(x) "\033[" x "m" /// Create a string that will print `str` in bold when output to terminal #define BOLD(str) "\033[1m" str "\033[0m" picom-12.5/src/utils/dynarr.c000066400000000000000000000010551471504570600161540ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include "dynarr.h" char *dynarr_join(char **arr, const char *sep) { size_t total_len = 0; dynarr_foreach(arr, i) { total_len += strlen(*i); } char *ret = malloc(total_len + strlen(sep) * (dynarr_len(arr) - 1) + 1); size_t pos = 0; allocchk(ret); dynarr_foreach(arr, i) { if (i != arr) { strcpy(ret + pos, sep); pos += strlen(sep); } strcpy(ret + pos, *i); pos += strlen(*i); free(*i); } dynarr_free_pod(arr); ret[pos] = '\0'; return ret; } picom-12.5/src/utils/dynarr.h000066400000000000000000000227611471504570600161700ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include "misc.h" /// Dynamic array implementation, design is similar to sds [0]. /// /// Note this is not very type safe, be sure to annotate wherever you use this. And the /// array won't have a fixed address in memory. /// /// [0]: https://github.com/antirez/sds struct dynarr_header { size_t len; size_t cap; }; static inline void *dynarr_new_impl(size_t size, size_t nelem) { struct dynarr_header *ret = calloc(1, sizeof(struct dynarr_header) + size * nelem); allocchk(ret); ret->len = 0; ret->cap = nelem; return ret + 1; } static inline void *dynarr_reserve_impl(size_t size, void *arr, size_t nelem) { struct dynarr_header *hdr = (struct dynarr_header *)arr - 1; if (hdr->len + nelem > hdr->cap) { hdr->cap = max2(max2(1, hdr->cap * 2), hdr->len + nelem); auto new_hdr = realloc(hdr, sizeof(struct dynarr_header) + hdr->cap * size); allocchk(new_hdr); hdr = new_hdr; } return hdr + 1; } static inline void *dynarr_shrink_to_fit_impl(size_t size, void *arr) { struct dynarr_header *hdr = (struct dynarr_header *)arr - 1; if (hdr->len < hdr->cap) { hdr->cap = hdr->len; auto new_hdr = realloc(hdr, sizeof(struct dynarr_header) + hdr->cap * size); allocchk(new_hdr); hdr = new_hdr; } return hdr + 1; } static inline void dynarr_remove_impl(size_t size, void *arr, size_t idx) { struct dynarr_header *hdr = (struct dynarr_header *)arr - 1; BUG_ON(idx >= hdr->len); memmove((char *)arr + idx * size, (char *)arr + (idx + 1) * size, (hdr->len - idx - 1) * size); hdr->len--; } static inline void dynarr_remove_swap_impl(size_t size, void *arr, size_t idx) { struct dynarr_header *hdr = (struct dynarr_header *)arr - 1; BUG_ON(idx >= hdr->len); hdr->len--; if (idx != hdr->len) { memcpy((char *)arr + idx * size, (char *)arr + hdr->len * size, size); } } /// Create a new dynamic array with capacity `cap` for type `type`. #define dynarr_new(type, cap) ((type *)dynarr_new_impl(sizeof(type), cap)) /// Free a dynamic array, destructing each element with `dtor`. #define dynarr_free(arr, dtor) \ do { \ dynarr_clear(arr, dtor); \ free((struct dynarr_header *)(arr) - 1); \ (arr) = NULL; \ } while (0) #define dynarr_free_pod(arr) dynarr_free(arr, (void (*)(typeof(arr)))NULL) /// Expand the capacity of the array so it can hold at least `nelem` more elements #define dynarr_reserve(arr, nelem) \ ((arr) = dynarr_reserve_impl(sizeof(typeof(*(arr))), (void *)(arr), (nelem))) /// Resize the array to `newlen`. If `newlen` is greater than the current length, the /// new elements will be initialized with `init`. If `newlen` is less than the current /// length, the excess elements will be destructed with `dtor`. #define dynarr_resize(arr, newlen, init, dtor) \ do { \ BUG_ON((arr) == NULL); \ dynarr_reserve((arr), (newlen) - dynarr_len(arr)); \ if ((init) != NULL) { \ for (size_t i = dynarr_len(arr); i < (newlen); i++) { \ (init)((arr) + i); \ } \ } \ if ((dtor) != NULL) { \ for (size_t i = (newlen); i < dynarr_len(arr); i++) { \ (dtor)((arr) + i); \ } \ } \ dynarr_len(arr) = (newlen); \ } while (0) /// Resize the array to `newlen`, for types that can be trivially constructed and /// destructed. #define dynarr_resize_pod(arr, newlen) \ dynarr_resize(arr, newlen, (void (*)(typeof(arr)))NULL, (void (*)(typeof(arr)))NULL) /// Shrink the capacity of the array to its length. #define dynarr_shrink_to_fit(arr) \ ((arr) = dynarr_shrink_to_fit_impl(sizeof(typeof(*(arr))), (void *)(arr))) /// Push an element to the end of the array #define dynarr_push(arr, item) \ do { \ dynarr_reserve(arr, 1); \ (arr)[dynarr_len(arr)++] = (item); \ } while (0) /// Pop an element from the end of the array #define dynarr_pop(arr) ((arr)[--dynarr_len(arr)]) /// Remove an element from the array by shifting the rest of the array forward. #define dynarr_remove(arr, idx) \ dynarr_remove_impl(sizeof(typeof(*(arr))), (void *)(arr), idx) /// Remove an element from the array by swapping it with the last element. #define dynarr_remove_swap(arr, idx) \ dynarr_remove_swap_impl(sizeof(typeof(*(arr))), (void *)(arr), idx) /// Return the length of the array #define dynarr_len(arr) (((struct dynarr_header *)(arr) - 1)->len) /// Return the capacity of the array #define dynarr_cap(arr) (((struct dynarr_header *)(arr) - 1)->cap) /// Return the last element of the array #define dynarr_last(arr) ((arr)[dynarr_len(arr) - 1]) /// Return the pointer just past the last element of the array #define dynarr_end(arr) ((arr) + dynarr_len(arr)) /// Return whether the array is empty #define dynarr_is_empty(arr) (dynarr_len(arr) == 0) /// Reduce the length of the array to `n`, destructing each element with `dtor`. If `n` /// is greater than the current length, this does nothing. #define dynarr_truncate(arr, n, dtor) \ do { \ if ((dtor) != NULL) { \ for (size_t i = n; i < dynarr_len(arr); i++) { \ (dtor)((arr) + i); \ } \ } \ dynarr_len(arr) = n; \ } while (0) #define dynarr_truncate_pod(arr, n) dynarr_truncate(arr, n, (void (*)(typeof(arr)))NULL) /// Clear the array, destructing each element with `dtor`. #define dynarr_clear(arr, dtor) dynarr_truncate(arr, 0, dtor) #define dynarr_clear_pod(arr) dynarr_truncate_pod(arr, 0) /// Extend the array by copying `n` elements from `other` #define dynarr_extend_from(arr, other, n) \ do { \ if ((n) > 0) { \ dynarr_reserve(arr, n); \ memcpy(dynarr_end(arr), other, sizeof(typeof(*(arr))[(n)])); \ dynarr_len(arr) += (n); \ } \ } while (0) /// Extend the array by `n` elements, initializing them with `init` #define dynarr_extend_with(arr, init, n) \ do { \ if ((n) > 0) { \ dynarr_resize(arr, dynarr_len(arr) + (n), init, \ (void (*)(typeof(arr)))NULL); \ } \ } while (0) #define dynarr_foreach(arr, i) for (typeof(arr)(i) = (arr); (i) < dynarr_end(arr); (i)++) #define dynarr_foreach_rev(arr, i) \ for (typeof(arr)(i) = dynarr_end(arr) - 1; (i) >= (arr); (i)--) /// Find the index of the first appearance of an element in the array by using trivial /// comparison, returns -1 if not found. #define dynarr_find_pod(arr, needle) \ ({ \ ptrdiff_t dynarr_find_ret = -1; \ dynarr_foreach(arr, dynarr_find_i) { \ if (*dynarr_find_i == (needle)) { \ dynarr_find_ret = dynarr_find_i - (arr); \ break; \ } \ } \ dynarr_find_ret; \ }) /// Concatenate a dynarr of strings into a single string, separated by `sep`. The array /// will be freed by this function. char *dynarr_join(char **arr, const char *sep); picom-12.5/src/utils/file_watch.c000066400000000000000000000105611471504570600167640ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #ifdef HAS_INOTIFY #include #elif HAS_KQUEUE // clang-format off #include // clang-format on #include #undef EV_ERROR // Avoid clashing with libev's EV_ERROR #include // For O_RDONLY #include // For struct timespec #include // For open #endif #include #include #include "file_watch.h" #include "log.h" #include "misc.h" struct watched_file { int wd; void *ud; file_watch_cb_t cb; UT_hash_handle hh; }; struct file_watch_registry { struct ev_io w; struct watched_file *reg; }; static void file_watch_ev_cb(EV_P attr_unused, struct ev_io *w, int revent attr_unused) { auto fwr = (struct file_watch_registry *)w; while (true) { int wd = -1; #ifdef HAS_INOTIFY struct inotify_event inotify_event; auto ret = read(w->fd, &inotify_event, sizeof(struct inotify_event)); if (ret < 0) { if (errno != EAGAIN) { log_error_errno("Failed to read from inotify fd"); } break; } wd = inotify_event.wd; #elif HAS_KQUEUE struct kevent ev; struct timespec timeout = {0}; int ret = kevent(fwr->w.fd, NULL, 0, &ev, 1, &timeout); if (ret <= 0) { if (ret < 0) { log_error_errno("Failed to get kevent"); } break; } wd = (int)ev.ident; #else assert(false); #endif struct watched_file *wf = NULL; HASH_FIND_INT(fwr->reg, &wd, wf); if (!wf) { log_warn("Got notification for a file I didn't watch."); continue; } wf->cb(wf->ud); } } void *file_watch_init(EV_P) { log_debug("Starting watching for file changes"); int fd = -1; #ifdef HAS_INOTIFY fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); if (fd < 0) { log_error_errno("inotify_init1 failed"); return NULL; } #elif HAS_KQUEUE fd = kqueue(); if (fd < 0) { log_error_errno("Failed to create kqueue"); return NULL; } #else log_info("No file watching support found on the host system."); return NULL; #endif auto fwr = ccalloc(1, struct file_watch_registry); ev_io_init(&fwr->w, file_watch_ev_cb, fd, EV_READ); ev_io_start(EV_A_ & fwr->w); return fwr; } void file_watch_destroy(EV_P_ void *_fwr) { log_debug("Stopping watching for file changes"); auto fwr = (struct file_watch_registry *)_fwr; struct watched_file *i, *tmp; HASH_ITER(hh, fwr->reg, i, tmp) { HASH_DEL(fwr->reg, i); #ifdef HAS_KQUEUE // kqueue watch descriptors are file descriptors of // the files we are watching, so we need to close // them close(i->wd); #endif free(i); } ev_io_stop(EV_A_ & fwr->w); close(fwr->w.fd); free(fwr); } bool file_watch_add(void *_fwr, const char *filename, file_watch_cb_t cb, void *ud) { log_debug("Adding \"%s\" to watched files", filename); auto fwr = (struct file_watch_registry *)_fwr; int wd = -1; struct stat statbuf; int ret = stat(filename, &statbuf); if (ret < 0) { log_error_errno("Failed to retrieve information about file \"%s\"", filename); return false; } if (!S_ISREG(statbuf.st_mode)) { log_info("\"%s\" is not a regular file, not watching it.", filename); return false; } #ifdef HAS_INOTIFY wd = inotify_add_watch(fwr->w.fd, filename, IN_CLOSE_WRITE | IN_MOVE_SELF | IN_DELETE_SELF); if (wd < 0) { log_error_errno("Failed to watch file \"%s\"", filename); return false; } #elif HAS_KQUEUE wd = open(filename, O_RDONLY); if (wd < 0) { log_error_errno("Cannot open file \"%s\" for watching", filename); return false; } uint32_t fflags = NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE | NOTE_ATTRIB; // NOTE_CLOSE_WRITE is relatively new, so we cannot just use it #ifdef NOTE_CLOSE_WRITE fflags |= NOTE_CLOSE_WRITE; #else // NOTE_WRITE will receive notification more frequent than necessary, so is less // preferable fflags |= NOTE_WRITE; #endif struct kevent ev = { .ident = (unsigned int)wd, // the wd < 0 case is checked above .filter = EVFILT_VNODE, .flags = EV_ADD | EV_CLEAR, .fflags = fflags, .data = 0, .udata = NULL, }; if (kevent(fwr->w.fd, &ev, 1, NULL, 0, NULL) < 0) { log_error_errno("Failed to register kevent"); close(wd); return false; } #else assert(false); #endif // HAS_KQUEUE auto w = ccalloc(1, struct watched_file); w->wd = wd; w->cb = cb; w->ud = ud; HASH_ADD_INT(fwr->reg, wd, w); return true; } picom-12.5/src/utils/file_watch.h000066400000000000000000000004741471504570600167730ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include typedef void (*file_watch_cb_t)(void *); void *file_watch_init(EV_P); bool file_watch_add(void *, const char *, file_watch_cb_t, void *); void file_watch_destroy(EV_P_ void *); picom-12.5/src/utils/kernel.c000066400000000000000000000113001471504570600161270ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include "compiler.h" #include "kernel.h" #include "misc.h" /// Sum a region convolution kernel. Region is defined by a width x height rectangle whose /// top left corner is at (x, y) double sum_kernel(const conv *map, int x, int y, int width, int height) { double ret = 0; // Compute sum of values which are "in range" int xstart = normalize_i_range(x, 0, map->w), xend = normalize_i_range(width + x, 0, map->w); int ystart = normalize_i_range(y, 0, map->h), yend = normalize_i_range(height + y, 0, map->h); assert(yend >= ystart && xend >= xstart); int d = map->w; if (map->rsum) { // See sum_kernel_preprocess double v1 = xstart ? map->rsum[(yend - 1) * d + xstart - 1] : 0; double v2 = ystart ? map->rsum[(ystart - 1) * d + xend - 1] : 0; double v3 = (xstart && ystart) ? map->rsum[(ystart - 1) * d + xstart - 1] : 0; return map->rsum[(yend - 1) * d + xend - 1] - v1 - v2 + v3; } for (int yi = ystart; yi < yend; yi++) { for (int xi = xstart; xi < xend; xi++) { ret += map->data[yi * d + xi]; } } return ret; } double sum_kernel_normalized(const conv *map, int x, int y, int width, int height) { double ret = sum_kernel(map, x, y, width, height); if (ret < 0) { ret = 0; } if (ret > 1) { ret = 1; } return ret; } 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) { return 1; } return exp(-0.5 * (x * x + y * y) / (r * r)) / (2 * M_PI * r * r); } conv *gaussian_kernel(double r, int size) { conv *c; int center = size / 2; double t; assert(size % 2 == 1); c = cvalloc(sizeof(conv) + (size_t)(size * size) * sizeof(double)); c->w = c->h = size; c->rsum = NULL; t = 0.0; for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { double g = gaussian(r, x - center, y - center); t += g; c->data[y * size + x] = g; } } for (int y = 0; y < size; y++) { for (int x = 0; x < size; x++) { c->data[y * size + x] /= t; } } return c; } /// 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 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; } double l = 0, r = size * 2; while (r - l > 1e-2) { double mid = (l + r) / 2.0; double vmid = estimate_first_row_sum(size, mid); if (vmid > row_limit) { r = mid; } else { l = mid; } } return (l + r) / 2.0; } /// 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, so the transition from shadow to the background is smooth. /// /// @param[in] shadow_radius the radius of the shadow conv *gaussian_kernel_autodetect_deviation(double shadow_radius) { assert(shadow_radius >= 0); 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, 0.5 / 256.0); return gaussian_kernel(std, size); } /// 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 void sum_kernel_preprocess(conv *map) { if (map->rsum) { free(map->rsum); } auto sum = map->rsum = ccalloc(map->w * map->h, double); sum[0] = map->data[0]; for (int x = 1; x < map->w; x++) { sum[x] = sum[x - 1] + map->data[x]; } const int d = map->w; for (int y = 1; y < map->h; y++) { sum[y * d] = sum[(y - 1) * d] + map->data[y * d]; for (int x = 1; x < map->w; x++) { double tmp = sum[(y - 1) * d + x] + sum[y * d + x - 1] - sum[(y - 1) * d + x - 1]; sum[y * d + x] = tmp + map->data[y * d + x]; } } } // vim: set noet sw=8 ts=8 : picom-12.5/src/utils/kernel.h000066400000000000000000000026111471504570600161410ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include "compiler.h" /// Code for generating convolution kernels typedef struct conv { int w, h; double *rsum; double data[]; } conv; /// Calculate the sum of a rectangle part of the convolution kernel /// the rectangle is defined by top left (x, y), and a size (width x height) double attr_pure sum_kernel(const conv *map, int x, int y, int width, int height); double attr_pure sum_kernel_normalized(const conv *map, int x, int y, int width, int height); /// Create a kernel with gaussian distribution with standard deviation `r`, and size /// `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(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 void sum_kernel_preprocess(conv *map); static inline void free_conv(conv *k) { free(k->rsum); free(k); } picom-12.5/src/utils/list.h000066400000000000000000000073561471504570600156470ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include struct list_node { struct list_node *next, *prev; }; #define list_entry(ptr, type, node) container_of(ptr, type, node) #define list_next_entry(ptr, node) list_entry((ptr)->node.next, __typeof__(*(ptr)), node) #define list_prev_entry(ptr, node) list_entry((ptr)->node.prev, __typeof__(*(ptr)), node) /// Insert a new node between two adjacent nodes in the list static inline void __list_insert_between(struct list_node *prev, struct list_node *next, struct list_node *new_) { new_->prev = prev; new_->next = next; next->prev = new_; prev->next = new_; } /// Insert a new node after `curr` static inline void list_insert_after(struct list_node *curr, struct list_node *new_) { __list_insert_between(curr, curr->next, new_); } /// Insert a new node before `curr` static inline void list_insert_before(struct list_node *curr, struct list_node *new_) { __list_insert_between(curr->prev, curr, new_); } /// Link two nodes in the list, so `next` becomes the successor node of `prev` static inline void __list_link(struct list_node *prev, struct list_node *next) { next->prev = prev; prev->next = next; } /// Remove a node from the list static inline void list_remove(struct list_node *to_remove) { __list_link(to_remove->prev, to_remove->next); to_remove->prev = (void *)-1; to_remove->next = (void *)-2; } /// Move `to_move` so that it's before `new_next` static inline void list_move_before(struct list_node *to_move, struct list_node *new_next) { list_remove(to_move); list_insert_before(new_next, to_move); } /// Move `to_move` so that it's after `new_prev` static inline void list_move_after(struct list_node *to_move, struct list_node *new_prev) { list_remove(to_move); list_insert_after(new_prev, to_move); } /// Initialize a list node that's intended to be the head node static inline void list_init_head(struct list_node *head) { head->next = head->prev = head; } /// Replace list node `old` with `n` static inline void list_replace(struct list_node *old, struct list_node *n) { __list_insert_between(old->prev, old->next, n); old->prev = (void *)-1; old->next = (void *)-2; } /// Return true if head is the only node in the list. Under usual circumstances this means /// the list is empty static inline bool list_is_empty(const struct list_node *head) { return head->prev == head; } /// Splice a list of nodes from `from` to into the beginning of list `to`. static inline void list_splice(struct list_node *from, struct list_node *to) { if (list_is_empty(from)) { return; } __list_link(from->prev, to->next); __list_link(to, from->next); list_init_head(from); } /// Return true if `to_check` is the first node in list headed by `head` static inline bool list_node_is_first(const struct list_node *head, const struct list_node *to_check) { return head->next == to_check; } /// Return true if `to_check` is the last node in list headed by `head` static inline bool list_node_is_last(const struct list_node *head, const struct list_node *to_check) { return head->prev == to_check; } #define list_foreach(type, i, head, member) \ for (type *i = list_entry((head)->next, type, member); &i->member != (head); \ i = list_next_entry(i, member)) /// Like list_for_each, but it's safe to remove the current list node from the list #define list_foreach_safe(type, i, head, member) \ for (type *i = list_entry((head)->next, type, member), \ *__tmp = list_next_entry(i, member); \ &i->member != (head); i = __tmp, __tmp = list_next_entry(i, member)) picom-12.5/src/utils/meson.build000066400000000000000000000001741471504570600166540ustar00rootroot00000000000000srcs += [ files( 'cache.c', 'dynarr.c', 'file_watch.c', 'kernel.c', 'misc.c', 'statistics.c', 'str.c', ), ] picom-12.5/src/utils/meta.h000066400000000000000000000105651471504570600156160ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2019, Yuxuan Shui #pragma once /// Macro metaprogramming #define _APPLY1(a, ...) a(__VA_ARGS__) #define _APPLY2(a, ...) a(__VA_ARGS__) #define _APPLY3(a, ...) a(__VA_ARGS__) #define _APPLY4(a, ...) a(__VA_ARGS__) #define RIOTA1(x) x #define RIOTA2(x) RIOTA1(x##1), RIOTA1(x##0) #define RIOTA4(x) RIOTA2(x##1), RIOTA2(x##0) #define RIOTA8(x) RIOTA4(x##1), RIOTA4(x##0) #define RIOTA16(x) RIOTA8(x##1), RIOTA8(x##0) /// Generate a list containing 31, 30, ..., 0, in binary #define RIOTA32(x) RIOTA16(x##1), RIOTA16(x##0) #define CONCAT2(a, b) a##b #define CONCAT1(a, b) CONCAT2(a, b) #define CONCAT(a, b) CONCAT1(a, b) #define _ARGS_HEAD(head, ...) head #define _ARGS_SKIP4(_1, _2, _3, _4, ...) __VA_ARGS__ #define _ARGS_SKIP8(...) _APPLY1(_ARGS_SKIP4, _ARGS_SKIP4(__VA_ARGS__)) #define _ARGS_SKIP16(...) _APPLY2(_ARGS_SKIP8, _ARGS_SKIP8(__VA_ARGS__)) #define _ARGS_SKIP32(...) _APPLY3(_ARGS_SKIP16, _ARGS_SKIP16(__VA_ARGS__)) /// Return the 33rd argument #define _ARG33(...) _APPLY4(_ARGS_HEAD, _ARGS_SKIP32(__VA_ARGS__)) /// Return the number of arguments passed in binary, handles at most 31 elements #define VA_ARGS_LENGTH(...) _ARG33(0, ##__VA_ARGS__, RIOTA32(0)) // clang-format off #define LIST_APPLY_000000(fn, sep, ...) #define LIST_APPLY_000001(fn, sep, x, ...) fn(x) #define LIST_APPLY_000010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000001(fn, sep, __VA_ARGS__) #define LIST_APPLY_000011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000010(fn, sep, __VA_ARGS__) #define LIST_APPLY_000100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000011(fn, sep, __VA_ARGS__) #define LIST_APPLY_000101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000100(fn, sep, __VA_ARGS__) #define LIST_APPLY_000110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000101(fn, sep, __VA_ARGS__) #define LIST_APPLY_000111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000110(fn, sep, __VA_ARGS__) #define LIST_APPLY_001000(fn, sep, x, ...) fn(x) sep() LIST_APPLY_000111(fn, sep, __VA_ARGS__) #define LIST_APPLY_001001(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001000(fn, sep, __VA_ARGS__) #define LIST_APPLY_001010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001001(fn, sep, __VA_ARGS__) #define LIST_APPLY_001011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001010(fn, sep, __VA_ARGS__) #define LIST_APPLY_001100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001011(fn, sep, __VA_ARGS__) #define LIST_APPLY_001101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001100(fn, sep, __VA_ARGS__) #define LIST_APPLY_001110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001101(fn, sep, __VA_ARGS__) #define LIST_APPLY_001111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001110(fn, sep, __VA_ARGS__) #define LIST_APPLY_010000(fn, sep, x, ...) fn(x) sep() LIST_APPLY_001111(fn, sep, __VA_ARGS__) #define LIST_APPLY_010001(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010000(fn, sep, __VA_ARGS__) #define LIST_APPLY_010010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010001(fn, sep, __VA_ARGS__) #define LIST_APPLY_010011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010010(fn, sep, __VA_ARGS__) #define LIST_APPLY_010100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010011(fn, sep, __VA_ARGS__) #define LIST_APPLY_010101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010100(fn, sep, __VA_ARGS__) #define LIST_APPLY_010110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010101(fn, sep, __VA_ARGS__) #define LIST_APPLY_010111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010110(fn, sep, __VA_ARGS__) #define LIST_APPLY_011000(fn, sep, x, ...) fn(x) sep() LIST_APPLY_010111(fn, sep, __VA_ARGS__) #define LIST_APPLY_011001(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011000(fn, sep, __VA_ARGS__) #define LIST_APPLY_011010(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011001(fn, sep, __VA_ARGS__) #define LIST_APPLY_011011(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011010(fn, sep, __VA_ARGS__) #define LIST_APPLY_011100(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011011(fn, sep, __VA_ARGS__) #define LIST_APPLY_011101(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011100(fn, sep, __VA_ARGS__) #define LIST_APPLY_011110(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011101(fn, sep, __VA_ARGS__) #define LIST_APPLY_011111(fn, sep, x, ...) fn(x) sep() LIST_APPLY_011110(fn, sep, __VA_ARGS__) #define LIST_APPLY_(N, fn, sep, ...) CONCAT(LIST_APPLY_, N)(fn, sep, __VA_ARGS__) #define LIST_APPLY(fn, sep, ...) LIST_APPLY_(VA_ARGS_LENGTH(__VA_ARGS__), fn, sep, __VA_ARGS__) // clang-format on #define SEP_COMMA() , #define SEP_COLON() ; #define SEP_NONE() picom-12.5/src/utils/misc.c000066400000000000000000000062751471504570600156210ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include #include "compiler.h" #include "misc.h" #include "rtkit.h" #include "str.h" /// Report allocation failure without allocating memory void report_allocation_failure(const char *func, const char *file, unsigned int line) { // Since memory allocation failed, we try to print this error message without any // memory allocation. Since logging framework allocates memory (and might even // have not been initialized yet), so we can't use it. char buf[11]; int llen = uitostr(line, buf); const char msg1[] = " has failed to allocate memory, "; const char msg2[] = ". Aborting...\n"; const struct iovec v[] = { {.iov_base = (void *)func, .iov_len = strlen(func)}, {.iov_base = "()", .iov_len = 2}, {.iov_base = (void *)msg1, .iov_len = sizeof(msg1) - 1}, {.iov_base = "at ", .iov_len = 3}, {.iov_base = (void *)file, .iov_len = strlen(file)}, {.iov_base = ":", .iov_len = 1}, {.iov_base = buf, .iov_len = (size_t)llen}, {.iov_base = (void *)msg2, .iov_len = sizeof(msg2) - 1}, }; ssize_t _ attr_unused = writev(STDERR_FILENO, v, ARR_SIZE(v)); abort(); 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) { n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; n++; return n; } // 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]; } /// 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 : picom-12.5/src/utils/misc.h000066400000000000000000000421011471504570600156120ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #pragma once #include #include #include #include #include #include #include #include #include #include #include #include "compiler.h" #include "log.h" #define ARR_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) #ifdef __FAST_MATH__ #warning Use of -ffast-math can cause rendering error or artifacts, \ therefore it is not recommended. #endif #ifdef __clang__ __attribute__((optnone)) #else __attribute__((optimize("-fno-fast-math"))) #endif static inline bool safe_isnan(double a) { return __builtin_isnan(a); } #ifdef __clang__ __attribute__((optnone)) #else __attribute__((optimize("-fno-fast-math"))) #endif static inline bool safe_isinf(double a) { return __builtin_isinf(a); } /// Same as assert(false), but make sure we abort _even in release builds_. /// Silence compiler warning caused by release builds making some code paths reachable. #define BUG() \ do { \ 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) /// Abort the program if `expr` is NULL. This is NOT disabled in release builds. #define BUG_ON_NULL(expr) BUG_ON((expr) == NULL); #define CHECK_EXPR(...) ((void)0) /// Same as assert, but evaluates the expression even in release builds #define CHECK(expr) \ do { \ auto _ = (expr); \ /* make sure the original expression appears in the assertion message */ \ assert((CHECK_EXPR(expr), _)); \ (void)_; \ } while (0) /// Asserts that var is within [lower, upper]. Silence compiler warning about expressions /// being always true or false. #define ASSERT_IN_RANGE(var, lower, upper) \ do { \ auto __assert_in_range_tmp attr_unused = (var); \ _Pragma("GCC diagnostic push"); \ _Pragma("GCC diagnostic ignored \"-Wtype-limits\""); \ assert(__assert_in_range_tmp >= lower); \ assert(__assert_in_range_tmp <= upper); \ _Pragma("GCC diagnostic pop"); \ } while (0) /// Asserts that var >= lower. Silence compiler warning about expressions /// being always true or false. #define ASSERT_GEQ(var, lower) \ do { \ auto __tmp attr_unused = (var); \ _Pragma("GCC diagnostic push"); \ _Pragma("GCC diagnostic ignored \"-Wtype-limits\""); \ assert(__tmp >= lower); \ _Pragma("GCC diagnostic pop"); \ } while (0) // Some macros for checked cast // Note these macros are not complete, as in, they won't work for every integer types. But // they are good enough for our use cases. #define to_int_checked(val) \ ({ \ int64_t __to_tmp = (val); \ ASSERT_IN_RANGE(__to_tmp, INT_MIN, INT_MAX); \ (int)__to_tmp; \ }) #define to_char_checked(val) \ ({ \ int64_t __to_tmp = (val); \ ASSERT_IN_RANGE(__to_tmp, CHAR_MIN, CHAR_MAX); \ (char)__to_tmp; \ }) #define to_u16_checked(val) \ ({ \ auto __to_tmp = (val); \ ASSERT_IN_RANGE(__to_tmp, 0, UINT16_MAX); \ (uint16_t) __to_tmp; \ }) #define to_i16_checked(val) \ ({ \ int64_t __to_tmp = (val); \ ASSERT_IN_RANGE(__to_tmp, INT16_MIN, INT16_MAX); \ (int16_t) __to_tmp; \ }) #define to_u32_checked(val) \ ({ \ 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; \ }) static inline uint16_t u64_to_u16_saturated(uint64_t val) { if (val > UINT16_MAX) { return UINT16_MAX; } return (uint16_t)val; } static inline uint16_t double_to_u16_saturated(double val) { BUG_ON(safe_isnan(val)); if (val < 0) { return 0; } if (val > UINT16_MAX) { return UINT16_MAX; } return (uint16_t)val; } static inline uint16_t i64_to_u16_saturated(int64_t val) { if (val < 0) { return 0; } if (val > UINT16_MAX) { return UINT16_MAX; } return (uint16_t)val; } #define to_u16_saturated(val) \ _Generic((val), \ double: double_to_u16_saturated, \ float: double_to_u16_saturated, \ uint64_t: u64_to_u16_saturated, \ default: i64_to_u16_saturated)((val)) static inline int32_t double_to_i32_saturated(double val) { BUG_ON(safe_isnan(val)); if (val < INT32_MIN) { return INT32_MIN; } if (val > INT32_MAX) { return INT32_MAX; } return (int32_t)val; } static inline int32_t u64_to_i32_saturated(uint64_t val) { if (val > INT32_MAX) { return INT32_MAX; } return (int32_t)val; } static inline int32_t i64_to_i32_saturated(int64_t val) { if (val < INT32_MIN) { return INT32_MIN; } if (val > INT32_MAX) { return INT32_MAX; } return (int32_t)val; } #define to_i32_saturated(val) \ _Generic((val), \ double: double_to_i32_saturated, \ float: double_to_i32_saturated, \ uint64_t: u64_to_i32_saturated, \ default: i64_to_i32_saturated)((val)) /* 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. * * @param i int value to normalize * @param min minimal value * @param max maximum value * @return normalized value */ static inline int attr_const attr_unused normalize_i_range(int i, int min, int max) { if (i > max) { return max; } 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) /** * Normalize a double value to a specific range. * * @param d double value to normalize * @param min minimal value * @param max maximum value * @return normalized value */ static inline double attr_const normalize_d_range(double d, double min, double max) { if (d > max) { return max; } if (d < min) { return min; } return d; } /** * Normalize a double value to 0.\ 0 - 1.\ 0. * * @param d double value to normalize * @return normalized value */ static inline double attr_const attr_unused normalize_d(double d) { return normalize_d_range(d, 0.0, 1.0); } /** * Convert a hex RGB string to RGB */ static inline struct color hex_to_rgb(const char *hex) { struct color rgb; // Ignore the # in front of the string const char *sane_hex = hex + 1; int hex_color = (int)strtol(sane_hex, NULL, 16); rgb.red = (float)(hex_color >> 16) / 256; rgb.green = (float)((hex_color & 0x00ff00) >> 8) / 256; rgb.blue = (float)(hex_color & 0x0000ff) / 256; return rgb; } attr_noret void report_allocation_failure(const char *func, const char *file, unsigned int line); /** * @brief Quit if the passed-in pointer is empty. */ static inline void * allocchk_(const char *func_name, const char *file, unsigned int line, void *ptr) { if (unlikely(!ptr)) { report_allocation_failure(func_name, file, line); } return ptr; } /// @brief Wrapper of allocchk_(). #define allocchk(ptr) allocchk_(__func__, __FILE__, __LINE__, ptr) /// @brief Wrapper of malloc(). #define cmalloc(type) ((type *)allocchk(malloc(sizeof(type)))) /// @brief Wrapper of malloc() that takes a size #define cvalloc(size) allocchk(malloc(size)) /// @brief Wrapper of calloc(). #define ccalloc(nmemb, type) \ ({ \ auto tmp = (nmemb); \ ASSERT_GEQ(tmp, 0); \ ((type *)allocchk(calloc((size_t)tmp, sizeof(type)))); \ }) /// @brief Wrapper of realloc(). #define crealloc(ptr, nmemb) \ ({ \ auto tmp = (nmemb); \ ASSERT_GEQ(tmp, 0); \ ((__typeof__(ptr))allocchk(realloc((ptr), (size_t)tmp * sizeof(*(ptr))))); \ }) /// RC_TYPE generates a reference counted type from `type` /// /// parameters: /// name = the generated type will be called `name`_t. /// ctor = the constructor of `type`, will be called when /// a value of `type` is created. should take one /// argument of `type *`. /// dtor = the destructor. will be called when all reference /// is gone. has same signature as ctor /// Q = function qualifier. this is the qualifier that /// will be put before generated functions // /// functions generated: /// `name`_new: create a new reference counted object of `type` /// `name`_ref: increment the reference counter, return a /// reference to the object /// `name`_unref: decrement the reference counter. take a `type **` /// because it needs to nullify the reference. #define RC_TYPE(type, name, ctor, dtor, Q) \ typedef struct { \ type inner; \ int ref_count; \ } name##_internal_t; \ typedef type name##_t; \ Q type *name##_new(void) { \ name##_internal_t *ret = cmalloc(name##_internal_t); \ ctor((type *)ret); \ ret->ref_count = 1; \ return (type *)ret; \ } \ Q type *name##_ref(type *a) { \ __auto_type b = (name##_internal_t *)a; \ b->ref_count++; \ return a; \ } \ Q void name##_unref(type **a) { \ __auto_type b = (name##_internal_t *)*a; \ if (!b) \ return; \ b->ref_count--; \ if (!b->ref_count) { \ dtor((type *)b); \ free(b); \ } \ *a = NULL; \ } /// Generate prototypes for functions generated by RC_TYPE #define RC_TYPE_PROTO(type, name) \ typedef type name##_t; \ type *name##_new(void); \ 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 /// ref: https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 /// int next_power_of_two(int n); // Find the k-th smallest element in an array. int quickselect(int *elems, int nelem, int k); 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 : picom-12.5/src/utils/statistics.c000066400000000000000000000214651471504570600170560ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui /// Rendering statistics /// /// Tracks how long it takes to render a frame, for measuring performance, and for pacing /// the frames. #include #include "log.h" #include "misc.h" #include "statistics.h" 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 } 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--; } } 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); } picom-12.5/src/utils/statistics.h000066400000000000000000000106421471504570600170560ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include "compiler.h" #define NTIERS (3) 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); } /// 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); 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); picom-12.5/src/utils/str.c000066400000000000000000000100221471504570600154570ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include "str.h" #pragma GCC diagnostic push // gcc warns about legitimate strncpy in mstrjoin and mstrextend // strncpy(str, src1, len1) intentional truncates the null byte from src1. // strncpy(str+len1, src2, len2) uses bound depends on the source argument, // but str is allocated with len1+len2+1, so this strncpy can't overflow #pragma GCC diagnostic ignored "-Wpragmas" #pragma GCC diagnostic ignored "-Wstringop-truncation" #pragma GCC diagnostic ignored "-Wstringop-overflow" /** * Allocate the space and join two strings. */ char *mstrjoin(const char *src1, const char *src2) { auto len1 = strlen(src1); auto len2 = strlen(src2); auto len = len1 + len2 + 1; auto str = ccalloc(len, char); strncpy(str, src1, len1); strncpy(str + len1, src2, len2); str[len - 1] = '\0'; return str; } TEST_CASE(mstrjoin) { char *str = mstrjoin("asdf", "qwer"); TEST_STREQUAL(str, "asdfqwer"); free(str); str = mstrjoin("", "qwer"); TEST_STREQUAL(str, "qwer"); free(str); str = mstrjoin("asdf", ""); TEST_STREQUAL(str, "asdf"); free(str); } /** * Concatenate a string on heap with another string. */ void mstrextend(char **psrc1, const char *src2) { if (!*psrc1) { *psrc1 = strdup(src2); return; } auto len1 = strlen(*psrc1); auto len2 = strlen(src2); auto len = len1 + len2 + 1; *psrc1 = crealloc(*psrc1, len); strncpy(*psrc1 + len1, src2, len2); (*psrc1)[len - 1] = '\0'; } TEST_CASE(mstrextend) { char *str1 = NULL; mstrextend(&str1, "asdf"); TEST_STREQUAL(str1, "asdf"); mstrextend(&str1, "asd"); TEST_STREQUAL(str1, "asdfasd"); mstrextend(&str1, ""); TEST_STREQUAL(str1, "asdfasd"); free(str1); } #pragma GCC diagnostic pop /// 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++; } else if (*src == '+') { src++; } double ret = 0; while (*src >= '0' && *src <= '9') { ret = ret * 10 + (*src - '0'); succeeded = true; src++; } if (*src == '.') { double frac = 0, mult = 0.1; src++; while (*src >= '0' && *src <= '9') { frac += mult * (*src - '0'); mult *= 0.1; succeeded = true; src++; } ret += frac; } if (succeeded) { *end = src; return ret * neg; } return NAN; } TEST_CASE(strtod_simple) { const char *end; double result = strtod_simple("1.0", &end); TEST_EQUAL(result, 1); TEST_EQUAL(*end, '\0'); result = strtod_simple("-1.0", &end); TEST_EQUAL(result, -1); TEST_EQUAL(*end, '\0'); 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; } picom-12.5/src/utils/str.h000066400000000000000000000052531471504570600154760ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #include #include #include #include #include #include #include #include "misc.h" #define mstrncmp(s1, s2) strncmp((s1), (s2), strlen(s1)) 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 **); static inline int uitostr(unsigned int n, char *buf) { int ret = 0; unsigned int tmp = n; while (tmp > 0) { tmp /= 10; ret++; } if (ret == 0) { ret = 1; } int pos = ret; while (pos--) { buf[pos] = (char)(n % 10 + '0'); n /= 10; } return ret; } /// Like `asprintf`, but it aborts the program if memory allocation fails. static inline size_t __attribute__((format(printf, 2, 3))) casprintf(char **strp, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int ret = vasprintf(strp, fmt, ap); va_end(ap); BUG_ON(ret < 0); return (size_t)ret; } /// Convert a double into a string. Avoid using *printf functions to print floating points /// directly because they are locale dependent. static inline void dtostr(double n, char **buf) { BUG_ON(safe_isnan(n)); BUG_ON(safe_isinf(n)); if (fabs(n) > 1e9) { // The number is so big that it's not meaningful to keep decimal places. casprintf(buf, "%.0f", n); return; } if (n > 0) { casprintf(buf, "%.0f.%03d", floor(n), (int)(fmod(n, 1) * 1000)); } else { casprintf(buf, "-%.0f.%03d", floor(-n), (int)(fmod(-n, 1) * 1000)); } } static inline const char *skip_space_const(const char *src) { if (!src) { return NULL; } while (*src && isspace((unsigned char)*src)) { src++; } return src; } static inline char *skip_space_mut(char *src) { if (!src) { return NULL; } 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) 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, ...) __attribute__((format(printf, 3, 4))); picom-12.5/src/utils/uthash_extra.h000066400000000000000000000005321471504570600173600ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once #define HASH_ITER2(head, el) \ for (__typeof__(head) el = (head), __tmp = el != NULL ? el->hh.next : NULL; \ el != NULL; el = __tmp, __tmp = el != NULL ? el->hh.next : NULL) picom-12.5/src/vblank.c000066400000000000000000000447041471504570600150020ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #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 "log.h" #include "utils/dynarr.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; /// List of scheduled vblank callbacks, this is a dynarr 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, vblank_requested; unsigned int last_msc; /// Number of synthesized vblank event. Currently these are being inserted when /// the driver reports duplicate msc to us. /// /// This is because the NVIDIA driver has been observed to recover from a /// duplicated msc loop by us rendering a new frame. So we insert a fake vblank /// event so the render loop can progress, but doing so makes our msc desync from /// the driver's msc. An hypothetical sequence of events go like this: /// /// - driver -> msc 1, we report msc 1 /// - driver -> msc 1 (duplicate msc! if we don't render a new frame, we'll /// be stuck here forever) /// - we report msc 2 /// - new frame rendered, driver recover /// - driver -> msc 2, but we already reported msc 2, so we have to report msc 3 unsigned int vblank_inserted; /// Protects `running`, and `vblank_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); unsigned int last_msc = 0; pthread_mutex_lock(&self->vblank_requested_mtx); while (self->running) { if (!self->vblank_requested) { pthread_cond_wait(&self->vblank_requested_cnd, &self->vblank_requested_mtx); continue; } pthread_mutex_unlock(&self->vblank_requested_mtx); 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)); pthread_mutex_lock(&self->vblank_requested_mtx); self->vblank_requested = false; ev_async_send(self->base.loop, &self->notify); } 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: log_deinit_tls(); 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; } assert(!base->vblank_event_requested); log_verbose("Requesting vblank event for msc %d", self->current_msc + 1); pthread_mutex_lock(&self->vblank_requested_mtx); self->vblank_requested = true; pthread_cond_signal(&self->vblank_requested_cnd); pthread_mutex_unlock(&self->vblank_requested_mtx); base->vblank_event_requested = true; 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->vblank_requested = false; 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) + sched->vblank_inserted; auto ust = atomic_load(&sched->current_ust); if (sched->last_msc >= msc) { // NVIDIA spams us with duplicate vblank events after a suspend/resume // cycle, or when the monitor turns off. // Fake a vblank event in this case. See comments on `vblank_inserted` // for more details. enum log_level level = sched->vblank_inserted == 0 ? LOG_LEVEL_WARN : LOG_LEVEL_DEBUG; LOG_(level, "Duplicate vblank event found with msc %d. Possible NVIDIA bug? " "Number of duplicates so far: %d", msc, sched->vblank_inserted); sched->last_msc++; sched->vblank_inserted++; struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); ust = (uint64_t)(now.tv_sec * 1000000 + now.tv_nsec / 1000); } else { sched->last_msc = msc; } auto event = (struct vblank_event){ .msc = sched->last_msc, .ust = ust, }; sched->base.vblank_event_requested = false; 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); x_set_error_action_abort(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); x_set_error_action_abort(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); struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); auto now_us = (uint64_t)now.tv_sec * 1000000UL + (uint64_t)now.tv_nsec / 1000; // 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 ". Trying to recover, reporting a fake vblank.", cne->msc, cne->ust); self->last_ust = now_us; self->last_msc += 1; } else { self->last_ust = cne->ust; self->last_msc = cne->msc; } 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) { // Schedule a new vblank event if there are no callbacks currently scheduled. if (dynarr_len(self->callbacks) == 0 && self->wind_down == 0 && !vblank_scheduler_schedule_internal(self)) { return false; } struct vblank_closure closure = { .fn = vblank_callback, .user_data = user_data, }; dynarr_push(self->callbacks, closure); 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 = dynarr_len(self->callbacks), 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 == dynarr_len(self->callbacks) && "callbacks should not be added " "when callbacks are being " "invoked."); dynarr_len(self->callbacks) = write_head; if (write_head || 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); } dynarr_free_pod(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; self->callbacks = dynarr_new(struct vblank_closure, 1); 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; } picom-12.5/src/vblank.h000066400000000000000000000030201471504570600147710ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #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); picom-12.5/src/vsync.c000066400000000000000000000111211471504570600146520ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui /// Function pointers to init VSync modes. #include "common.h" #include "log.h" #ifdef CONFIG_OPENGL #include "backend/gl/glx.h" #include "opengl.h" #endif #ifdef CONFIG_VSYNC_DRM #include #include #include #include #include #include #endif #include "config.h" #include "vsync.h" #ifdef CONFIG_VSYNC_DRM /** * Wait for next VSync, DRM method. * * Stolen from: * https://github.com/MythTV/mythtv/blob/master/mythtv/libs/libmythtv/vsync.cpp */ static int vsync_drm_wait(session_t *ps) { int ret = -1; drm_wait_vblank_t vbl; vbl.request.type = _DRM_VBLANK_RELATIVE, vbl.request.sequence = 1; do { ret = ioctl(ps->drm_fd, DRM_IOCTL_WAIT_VBLANK, &vbl); vbl.request.type &= ~(uint)_DRM_VBLANK_RELATIVE; } while (ret && errno == EINTR); if (ret) log_error("VBlank ioctl did not work, unimplemented in this drmver?"); return ret; } /** * Initialize DRM VSync. * * @return true for success, false otherwise */ static bool vsync_drm_init(session_t *ps) { // Should we always open card0? if (ps->drm_fd < 0 && (ps->drm_fd = open("/dev/dri/card0", O_RDWR)) < 0) { log_error("Failed to open device."); return false; } if (vsync_drm_wait(ps)) return false; return true; } #endif #ifdef CONFIG_OPENGL /** * Initialize OpenGL VSync. * * Stolen from: * http://git.tuxfamily.org/?p=ccm/cairocompmgr.git;a=commitdiff;h=efa4ceb97da501e8630ca7f12c99b1dce853c73e * Possible original source: http://www.inb.uni-luebeck.de/~boehme/xvideo_sync.html * * @return true for success, false otherwise */ static bool vsync_opengl_init(session_t *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)) { 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) { return glXSwapIntervalMESA((uint)interval) == 0; } if (glxext.has_GLX_SGI_swap_control) { return glXSwapIntervalSGI(interval) == 0; } if (glxext.has_GLX_EXT_swap_control) { GLXDrawable d = glXGetCurrentDrawable(); if (d == None) { // We don't have a context?? return false; } glXSwapIntervalEXT(ps->c.dpy, glXGetCurrentDrawable(), interval); return true; } return false; } static bool vsync_opengl_swc_init(session_t *ps) { if (!bkend_use_glx(ps)) { log_error("OpenGL swap control requires the GLX backend."); return false; } if (!vsync_opengl_swc_swap_interval(ps, 1)) { log_error("Failed to load a swap control extension."); return false; } return true; } /** * Wait for next VSync, OpenGL method. */ static int vsync_opengl_wait(session_t *ps attr_unused) { unsigned vblank_count = 0; glXGetVideoSyncSGI(&vblank_count); glXWaitVideoSyncSGI(2, (vblank_count + 1) % 2, &vblank_count); return 0; } /** * Wait for next VSync, OpenGL OML method. * * https://mail.gnome.org/archives/clutter-list/2012-November/msg00031.html */ static int vsync_opengl_oml_wait(session_t *ps) { int64_t ust = 0, msc = 0, sbc = 0; 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 /** * Initialize current VSync method. */ bool vsync_init(session_t *ps) { #ifdef CONFIG_OPENGL if (bkend_use_glx(ps)) { // Mesa turns on swap control by default, undo that vsync_opengl_swc_swap_interval(ps, 0); } #endif #ifdef CONFIG_VSYNC_DRM log_warn("The DRM vsync method is deprecated, please don't enable it."); #endif if (!ps->o.vsync) { return true; } #ifdef CONFIG_OPENGL if (bkend_use_glx(ps)) { if (!vsync_opengl_swc_init(ps)) { return false; } ps->vsync_wait = NULL; // glXSwapBuffers will automatically wait // for vsync, we don't need to do anything. return true; } #endif // Oh no, we are not using glx backend. // Throwing things at wall. #ifdef CONFIG_OPENGL if (vsync_opengl_oml_init(ps)) { log_info("Using the opengl-oml vsync method"); ps->vsync_wait = vsync_opengl_oml_wait; return true; } if (vsync_opengl_init(ps)) { log_info("Using the opengl vsync method"); ps->vsync_wait = vsync_opengl_wait; return true; } #endif #ifdef CONFIG_VSYNC_DRM if (vsync_drm_init(ps)) { log_info("Using the drm vsync method"); ps->vsync_wait = vsync_drm_wait; return true; } #endif log_error("No supported vsync method found for this backend"); return false; } picom-12.5/src/vsync.h000066400000000000000000000002571471504570600146670ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include typedef struct session session_t; bool vsync_init(session_t *ps); picom-12.5/src/wm/000077500000000000000000000000001471504570600137735ustar00rootroot00000000000000picom-12.5/src/wm/defs.h000066400000000000000000000063731471504570600150760ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #pragma once typedef enum { WINTYPE_UNKNOWN = 0, WINTYPE_DESKTOP, WINTYPE_DOCK, WINTYPE_TOOLBAR, WINTYPE_MENU, WINTYPE_UTILITY, WINTYPE_SPLASH, WINTYPE_DIALOG, WINTYPE_NORMAL, WINTYPE_DROPDOWN_MENU, WINTYPE_POPUP_MENU, WINTYPE_TOOLTIP, WINTYPE_NOTIFICATION, WINTYPE_COMBO, WINTYPE_DND, NUM_WINTYPES } wintype_t; /// Enumeration type of window painting mode. typedef enum { WMODE_TRANS, // The window body is (potentially) transparent WMODE_FRAME_TRANS, // The window body is opaque, but the frame is not WMODE_SOLID, // The window is opaque including the frame } winmode_t; /// The state of a window from Xserver's perspective typedef enum { /// 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; #define NUM_OF_WSTATES (WSTATE_MAPPED + 1) enum win_flags { // 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 WIN_FLAGS_PIXMAP_STALE = 1, /// there was an error binding the window pixmap WIN_FLAGS_PIXMAP_ERROR = 4, /// Window is damaged, and should be added to the damage region /// (only used by the legacy backends, remove) WIN_FLAGS_DAMAGED = 8, /// 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 WIN_FLAGS_MAPPED = 64, /// this window has properties which needs to be updated WIN_FLAGS_PROPERTY_STALE = 128, // TODO(yshui) _maybe_ split SIZE_STALE into SIZE_STALE and SHAPE_STALE /// this window has an unhandled size/shape change WIN_FLAGS_SIZE_STALE = 256, /// this window has an unhandled position (i.e. x and y) change WIN_FLAGS_POSITION_STALE = 512, /// need better name for this, is set when some aspects of the window changed WIN_FLAGS_FACTOR_CHANGED = 1024, }; enum win_script_output { /// Additional X offset of the window. WIN_SCRIPT_OFFSET_X = 0, /// Additional Y offset of the window. WIN_SCRIPT_OFFSET_Y, /// Additional X offset of the shadow. WIN_SCRIPT_SHADOW_OFFSET_X, /// Additional Y offset of the shadow. WIN_SCRIPT_SHADOW_OFFSET_Y, /// Opacity of the window. WIN_SCRIPT_OPACITY, /// Opacity of the blurred background of the window. WIN_SCRIPT_BLUR_OPACITY, /// Opacity of the shadow. WIN_SCRIPT_SHADOW_OPACITY, /// Horizontal scale WIN_SCRIPT_SCALE_X, /// Vertical scale WIN_SCRIPT_SCALE_Y, /// Horizontal scale of the shadow WIN_SCRIPT_SHADOW_SCALE_X, /// Vertical scale of the shadow WIN_SCRIPT_SHADOW_SCALE_Y, /// X coordinate of the origin of the crop box WIN_SCRIPT_CROP_X, /// Y coordinate of the origin of the crop box WIN_SCRIPT_CROP_Y, /// Width of the crop box WIN_SCRIPT_CROP_WIDTH, /// Height of the crop box WIN_SCRIPT_CROP_HEIGHT, /// How much to blend in the saved window image WIN_SCRIPT_SAVED_IMAGE_BLEND, NUM_OF_WIN_SCRIPT_OUTPUTS, }; picom-12.5/src/wm/meson.build000066400000000000000000000000551471504570600161350ustar00rootroot00000000000000srcs += [ files('win.c', 'wm.c', 'tree.c') ] picom-12.5/src/wm/tree.c000066400000000000000000000444511471504570600151060ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui /// In my ideal world, the compositor shouldn't be concerned with the X window tree. It /// should only need to care about the toplevel windows. However, because we support /// window rules based on window properties, which can be set on any descendant of a /// toplevel, we need to keep track of the entire window tree. /// /// For descendants of a toplevel window, what we actually care about is what's called a /// "client" window. A client window is a window with the `WM_STATE` property set, in /// theory and descendants of a toplevel can gain/lose this property at any time. So we /// setup a minimal structure for every single window to keep track of this. And once /// a window becomes a client window, it will have our full attention and have all of its /// information stored in the toplevel `struct managed_win`. #include #include #include "log.h" #include "utils/list.h" #include "utils/misc.h" #include "wm.h" #include "wm_internal.h" struct wm_tree_change_list { struct wm_tree_change item; struct list_node siblings; }; void wm_tree_reap_zombie(struct wm_tree_node *zombie) { BUG_ON(!zombie->is_zombie); list_remove(&zombie->siblings); free(zombie); } /// Enqueue a tree change. static void wm_tree_enqueue_change(struct wm_tree *tree, struct wm_tree_change change) { struct wm_tree_change_list *change_list; if (!list_is_empty(&tree->free_changes)) { change_list = list_entry(tree->free_changes.next, struct wm_tree_change_list, siblings); list_remove(&change_list->siblings); } else { change_list = cmalloc(struct wm_tree_change_list); } change_list->item = change; list_insert_before(&tree->changes, &change_list->siblings); } /// Enqueue a `WM_TREE_CHANGE_TOPLEVEL_KILLED` change for a toplevel window. If there are /// any `WM_TREE_CHANGE_TOPLEVEL_NEW` changes in the queue for the same toplevel, they /// will be cancelled out. /// /// @return true if this change is cancelled out by a previous change, false otherwise. static bool wm_tree_enqueue_toplevel_killed(struct wm_tree *tree, wm_treeid toplevel, struct wm_tree_node *zombie) { // A gone toplevel will cancel out a previous // `WM_TREE_CHANGE_TOPLEVEL_NEW` change in the queue. bool found = false; list_foreach_safe(struct wm_tree_change_list, i, &tree->changes, siblings) { if (!wm_treeid_eq(i->item.toplevel, toplevel)) { continue; } if (i->item.type == WM_TREE_CHANGE_TOPLEVEL_NEW) { list_remove(&i->siblings); list_insert_after(&tree->free_changes, &i->siblings); found = true; } else if (found) { // We also need to delete all other changes // related to this toplevel in between the new and // gone changes. list_remove(&i->siblings); list_insert_after(&tree->free_changes, &i->siblings); } else if (i->item.type == WM_TREE_CHANGE_CLIENT) { // Need to update client changes, so they points to the // zombie instead of the old toplevel node, since the old // toplevel node could be freed before tree changes are // processed. i->item.client.toplevel = zombie; } } if (found) { wm_tree_reap_zombie(zombie); return true; } wm_tree_enqueue_change(tree, (struct wm_tree_change){ .toplevel = toplevel, .type = WM_TREE_CHANGE_TOPLEVEL_KILLED, .killed = zombie, }); return false; } static void wm_tree_enqueue_client_change(struct wm_tree *tree, struct wm_tree_node *toplevel, wm_treeid old_client, wm_treeid new_client) { // A client change can coalesce with a previous client change. list_foreach_safe(struct wm_tree_change_list, i, &tree->changes, siblings) { if (!wm_treeid_eq(i->item.toplevel, toplevel->id) || i->item.type != WM_TREE_CHANGE_CLIENT) { continue; } if (!wm_treeid_eq(i->item.client.new_, old_client)) { log_warn("Inconsistent client change for toplevel " "%#010x. Missing changes from %#010x to %#010x. " "Possible bug.", toplevel->id.x, i->item.client.new_.x, old_client.x); } i->item.client.new_ = new_client; if (wm_treeid_eq(i->item.client.old, new_client)) { list_remove(&i->siblings); list_insert_after(&tree->free_changes, &i->siblings); } return; } wm_tree_enqueue_change(tree, (struct wm_tree_change){ .toplevel = toplevel->id, .type = WM_TREE_CHANGE_CLIENT, .client = { .toplevel = toplevel, .old = old_client, .new_ = new_client, }, }); } static void wm_tree_enqueue_toplevel_new(struct wm_tree *tree, struct wm_tree_node *toplevel) { // We don't let a `WM_TREE_CHANGE_TOPLEVEL_NEW` cancel out a previous // `WM_TREE_CHANGE_TOPLEVEL_KILLED`, because the new toplevel would be a different // window reusing the same ID. So we need to go through the proper destruction // process for the previous toplevel. Changes are not commutative (naturally). wm_tree_enqueue_change(tree, (struct wm_tree_change){ .toplevel = toplevel->id, .type = WM_TREE_CHANGE_TOPLEVEL_NEW, .new_ = toplevel, }); } static void wm_tree_enqueue_toplevel_restacked(struct wm_tree *tree) { list_foreach(struct wm_tree_change_list, i, &tree->changes, siblings) { if (i->item.type == WM_TREE_CHANGE_TOPLEVEL_RESTACKED || i->item.type == WM_TREE_CHANGE_TOPLEVEL_NEW || i->item.type == WM_TREE_CHANGE_TOPLEVEL_KILLED) { // Only need to keep one // `WM_TREE_CHANGE_TOPLEVEL_RESTACKED` change, and order // doesn't matter. return; } } wm_tree_enqueue_change(tree, (struct wm_tree_change){ .type = WM_TREE_CHANGE_TOPLEVEL_RESTACKED, }); } /// Dequeue the oldest change from the change queue. If the queue is empty, a change with /// `toplevel` set to `XCB_NONE` will be returned. struct wm_tree_change wm_tree_dequeue_change(struct wm_tree *tree) { if (list_is_empty(&tree->changes)) { return (struct wm_tree_change){.type = WM_TREE_CHANGE_NONE}; } auto change = list_entry(tree->changes.next, struct wm_tree_change_list, siblings); list_remove(&change->siblings); list_insert_after(&tree->free_changes, &change->siblings); return change->item; } /// Return the next node in the subtree after `node` in a pre-order traversal. Returns /// NULL if `node` is the last node in the traversal. struct wm_tree_node *wm_tree_next(struct wm_tree_node *node, struct wm_tree_node *subroot) { if (node == NULL) { return NULL; } if (!list_is_empty(&node->children)) { // Descend if there are children return list_entry(node->children.next, struct wm_tree_node, siblings); } while (node != subroot && node->siblings.next == &node->parent->children) { // If the current node has no more children, go back to the // parent. node = node->parent; } if (node == subroot) { // We've gone past the topmost node for our search, stop. return NULL; } return list_entry(node->siblings.next, struct wm_tree_node, siblings); } /// Find a client window under a toplevel window. If there are multiple windows with /// `WM_STATE` set under the toplevel window, we will return an arbitrary one. struct wm_tree_node *attr_pure wm_tree_find_client(struct wm_tree_node *subroot) { if (subroot->has_wm_state) { log_debug("Toplevel %#010x has WM_STATE set, weird. Using itself as its " "client window.", subroot->id.x); return subroot; } BUG_ON(subroot->parent == NULL); // Trying to find client window on the // root window for (auto curr = subroot; curr != NULL; curr = wm_tree_next(curr, subroot)) { if (curr->has_wm_state) { return curr; } } return NULL; } struct wm_tree_node *wm_tree_find(const struct wm_tree *tree, xcb_window_t id) { struct wm_tree_node *node = NULL; HASH_FIND_INT(tree->nodes, &id, node); return node; } struct wm_tree_node * wm_tree_find_toplevel_for(const struct wm_tree *tree, struct wm_tree_node *node) { BUG_ON_NULL(node); BUG_ON_NULL(node->parent); // Trying to find toplevel for the root // window struct wm_tree_node *toplevel; for (auto curr = node; curr->parent != NULL; curr = curr->parent) { toplevel = curr; } return toplevel->parent == tree->root ? toplevel : NULL; } /// Change whether a tree node has the `WM_STATE` property set. /// `destroyed` indicate whether `node` is about to be destroyed, in which case, the `old` /// field of the change event will be set to NULL. void wm_tree_set_wm_state(struct wm_tree *tree, struct wm_tree_node *node, bool has_wm_state) { BUG_ON(node == NULL); if (node->has_wm_state == has_wm_state) { log_debug("WM_STATE unchanged call (window %#010x, WM_STATE %d).", node->id.x, has_wm_state); return; } node->has_wm_state = has_wm_state; BUG_ON(node->parent == NULL); // Trying to set WM_STATE on the root window struct wm_tree_node *toplevel = wm_tree_find_toplevel_for(tree, node); if (toplevel == NULL) { return; } if (toplevel == node) { log_debug("Setting WM_STATE on a toplevel window %#010x, weird.", node->id.x); } if (!has_wm_state && toplevel->client_window == node) { auto new_client = wm_tree_find_client(toplevel); toplevel->client_window = new_client; wm_tree_enqueue_client_change( tree, toplevel, node->id, new_client != NULL ? new_client->id : WM_TREEID_NONE); } else if (has_wm_state && toplevel->client_window == NULL) { toplevel->client_window = node; wm_tree_enqueue_client_change(tree, toplevel, WM_TREEID_NONE, node->id); } else if (has_wm_state) { // If the toplevel window already has a client window, we won't // try to usurp it. log_debug("Toplevel window %#010x already has a client window " "%#010x, ignoring new client window %#010x. I don't " "like your window manager.", toplevel->id.x, toplevel->client_window->id.x, node->id.x); } } struct wm_tree_node *wm_tree_new_window(struct wm_tree *tree, xcb_window_t id) { auto node = ccalloc(1, struct wm_tree_node); node->id.x = id; node->id.gen = tree->gen++; node->has_wm_state = false; node->receiving_events = false; node->is_zombie = false; node->visited = false; node->leader = id; list_init_head(&node->children); return node; } void wm_tree_add_window(struct wm_tree *tree, struct wm_tree_node *node) { HASH_ADD_INT(tree->nodes, id.x, node); } static void wm_tree_refresh_client_and_queue_change(struct wm_tree *tree, struct wm_tree_node *toplevel) { BUG_ON_NULL(toplevel); BUG_ON_NULL(toplevel->parent); BUG_ON(toplevel->parent->parent != NULL); auto new_client = wm_tree_find_client(toplevel); if (new_client != toplevel->client_window) { wm_treeid old_client_id = WM_TREEID_NONE, new_client_id = WM_TREEID_NONE; if (toplevel->client_window != NULL) { old_client_id = toplevel->client_window->id; } if (new_client != NULL) { new_client_id = new_client->id; } log_debug("Toplevel window %#010x had client window %#010x, now has " "%#010x.", toplevel->id.x, old_client_id.x, new_client_id.x); toplevel->client_window = new_client; wm_tree_enqueue_client_change(tree, toplevel, old_client_id, new_client_id); } } struct wm_tree_node *wm_tree_detach(struct wm_tree *tree, struct wm_tree_node *subroot) { BUG_ON(subroot == NULL); BUG_ON(subroot->parent == NULL); // Trying to detach the root window?! auto toplevel = wm_tree_find_toplevel_for(tree, subroot); struct wm_tree_node *zombie = NULL; if (toplevel != subroot) { list_remove(&subroot->siblings); if (toplevel != NULL) { wm_tree_refresh_client_and_queue_change(tree, toplevel); } } else { // Detached a toplevel, create a zombie for it log_debug("Detaching toplevel window %#010x.", subroot->id.x); zombie = ccalloc(1, struct wm_tree_node); zombie->parent = subroot->parent; zombie->id = subroot->id; zombie->is_zombie = true; list_init_head(&zombie->children); list_replace(&subroot->siblings, &zombie->siblings); if (wm_tree_enqueue_toplevel_killed(tree, subroot->id, zombie)) { zombie = NULL; } // Gen bump must happen after enqueuing the change, because otherwise the // kill change won't cancel out a previous new change because the IDs will // be different. subroot->id.gen = tree->gen++; subroot->client_window = NULL; } subroot->parent = NULL; return zombie; } void wm_tree_attach(struct wm_tree *tree, struct wm_tree_node *child, struct wm_tree_node *parent) { BUG_ON(child->parent != NULL); // Trying to attach a window that's already // attached child->parent = parent; if (parent == NULL) { BUG_ON(tree->root != NULL); // Trying to create a second root // window tree->root = child; return; } list_insert_after(&parent->children, &child->siblings); auto toplevel = wm_tree_find_toplevel_for(tree, child); if (child == toplevel) { wm_tree_enqueue_toplevel_new(tree, child); } if (toplevel != NULL) { wm_tree_refresh_client_and_queue_change(tree, toplevel); } } void wm_tree_move_to_end(struct wm_tree *tree, struct wm_tree_node *node, bool to_bottom) { BUG_ON(node == NULL); BUG_ON(node->parent == NULL); // Trying to move the root window if ((node->parent->children.next == &node->siblings && !to_bottom) || (node->parent->children.prev == &node->siblings && to_bottom)) { // Already at the target position return; } list_remove(&node->siblings); if (to_bottom) { list_insert_before(&node->parent->children, &node->siblings); } else { list_insert_after(&node->parent->children, &node->siblings); } if (node->parent == tree->root) { wm_tree_enqueue_toplevel_restacked(tree); } } /// Move `node` to above `other` in their parent's child window stack. void wm_tree_move_to_above(struct wm_tree *tree, struct wm_tree_node *node, struct wm_tree_node *other) { BUG_ON(node == NULL); BUG_ON(node->parent == NULL); // Trying to move the root window BUG_ON(other == NULL); BUG_ON(node->parent != other->parent); if (node->siblings.next == &other->siblings) { // Already above `other` return; } list_remove(&node->siblings); list_insert_before(&other->siblings, &node->siblings); if (node->parent == tree->root) { wm_tree_enqueue_toplevel_restacked(tree); } } void wm_tree_clear(struct wm_tree *tree) { struct wm_tree_node *cur, *tmp; HASH_ITER(hh, tree->nodes, cur, tmp) { HASH_DEL(tree->nodes, cur); free(cur); } list_foreach_safe(struct wm_tree_change_list, i, &tree->changes, siblings) { list_remove(&i->siblings); free(i); } list_foreach_safe(struct wm_tree_change_list, i, &tree->free_changes, siblings) { list_remove(&i->siblings); free(i); } } TEST_CASE(tree_manipulation) { struct wm_tree tree; wm_tree_init(&tree); wm_tree_add_window(&tree, wm_tree_new_window(&tree, 1)); auto root = wm_tree_find(&tree, 1); TEST_NOTEQUAL(root, NULL); TEST_EQUAL(root->parent, NULL); tree.root = root; auto change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.type, WM_TREE_CHANGE_NONE); auto node2 = wm_tree_new_window(&tree, 2); wm_tree_add_window(&tree, node2); wm_tree_attach(&tree, node2, root); TEST_NOTEQUAL(node2, NULL); TEST_EQUAL(node2, wm_tree_find(&tree, 2)); TEST_EQUAL(node2->parent, root); change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.toplevel.x, 2); TEST_EQUAL(change.type, WM_TREE_CHANGE_TOPLEVEL_NEW); TEST_TRUE(wm_treeid_eq(node2->id, change.toplevel)); auto node3 = wm_tree_new_window(&tree, 3); wm_tree_add_window(&tree, node3); wm_tree_attach(&tree, node3, root); change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.toplevel.x, 3); TEST_EQUAL(change.type, WM_TREE_CHANGE_TOPLEVEL_NEW); auto zombie = wm_tree_detach(&tree, node2); wm_tree_attach(&tree, node2, node3); TEST_EQUAL(node2->parent, node3); TEST_EQUAL(node3->children.next, &node2->siblings); // node2 is now a child of node3, so it's no longer a toplevel change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.toplevel.x, 2); TEST_EQUAL(change.type, WM_TREE_CHANGE_TOPLEVEL_KILLED); TEST_EQUAL(change.killed, zombie); wm_tree_reap_zombie(change.killed); wm_tree_set_wm_state(&tree, node2, true); change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.toplevel.x, 3); TEST_EQUAL(change.type, WM_TREE_CHANGE_CLIENT); TEST_TRUE(wm_treeid_eq(change.client.old, WM_TREEID_NONE)); TEST_EQUAL(change.client.new_.x, 2); auto node4 = wm_tree_new_window(&tree, 4); wm_tree_add_window(&tree, node4); wm_tree_attach(&tree, node4, node3); change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.type, WM_TREE_CHANGE_NONE); wm_tree_set_wm_state(&tree, node4, true); change = wm_tree_dequeue_change(&tree); // node3 already has node2 as its client window, so the new one should be ignored. TEST_EQUAL(change.type, WM_TREE_CHANGE_NONE); TEST_EQUAL(wm_tree_detach(&tree, node2), NULL); HASH_DEL(tree.nodes, node2); free(node2); change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.toplevel.x, 3); TEST_EQUAL(change.type, WM_TREE_CHANGE_CLIENT); TEST_EQUAL(change.client.old.x, 2); TEST_EQUAL(change.client.new_.x, 4); // Test window ID reuse TEST_EQUAL(wm_tree_detach(&tree, node4), NULL); HASH_DEL(tree.nodes, node4); free(node4); node4 = wm_tree_new_window(&tree, 4); wm_tree_add_window(&tree, node4); wm_tree_attach(&tree, node4, node3); wm_tree_set_wm_state(&tree, node4, true); change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.toplevel.x, 3); TEST_EQUAL(change.type, WM_TREE_CHANGE_CLIENT); TEST_EQUAL(change.client.old.x, 4); TEST_EQUAL(change.client.new_.x, 4); auto node5 = wm_tree_new_window(&tree, 5); wm_tree_add_window(&tree, node5); wm_tree_attach(&tree, node5, root); TEST_EQUAL(wm_tree_detach(&tree, node5), NULL); HASH_DEL(tree.nodes, node5); free(node5); change = wm_tree_dequeue_change(&tree); TEST_EQUAL(change.type, WM_TREE_CHANGE_NONE); // Changes cancelled out wm_tree_clear(&tree); } picom-12.5/src/wm/win.c000066400000000000000000002262761471504570600147530ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2013 Richard Grenville // Copyright (c) 2018 Yuxuan Shui #include #include #include #include #include #include #include #include #include #include #include #include #include #include "atom.h" #include "c2.h" #include "common.h" #include "compiler.h" #include "config.h" #include "dbus.h" #include "inspect.h" #include "log.h" #include "picom.h" #include "region.h" #include "render.h" #include "utils/console.h" #include "utils/misc.h" #include "x.h" #include "defs.h" #include "wm.h" #include "win.h" #define OPAQUE (0xffffffff) static const int ROUNDED_PIXELS = 1; static const double ROUNDED_PERCENT = 0.05; // 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. static void win_update_prop_shadow_raw(struct x_connection *c, struct atom *atoms, struct win *w); static bool win_update_prop_shadow(struct x_connection *c, struct atom *atoms, struct win *w); /** * Update leader of a window. */ 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); /// 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 /// the returned region. /// Function signature has to be (win *, region_t *) #define gen_without_corners(fun) \ void fun##_without_corners(const struct win *w, region_t *res) { \ fun(w, res); \ win_region_remove_corners_local(w, res); \ } /// Generate a "return by value" function, from a function that returns the /// region via a region_t pointer argument. /// Function signature has to be (win *) #define gen_by_val(fun) \ region_t fun##_by_val(const struct win *w) { \ region_t ret; \ pixman_region32_init(&ret); \ fun(w, &ret); \ return ret; \ } /** * Update focused state of a window. */ static bool win_is_focused(session_t *ps, struct win *w) { bool is_wmwin = win_is_wmwin(w); if (w->a.map_state == XCB_MAP_STATE_VIEWABLE && (w->is_focused || w->is_group_focused)) { return true; } // Use wintype_focus, and treat WM windows and override-redirected // windows specially if (ps->o.wintype_option[index_of_lowest_one(w->window_types)].focus || (ps->o.mark_wmwin_focused && is_wmwin) || (ps->o.mark_ovredir_focused && wm_ref_client_of(w->tree_ref) == NULL && !is_wmwin) || (w->a.map_state == XCB_MAP_STATE_VIEWABLE && c2_match(ps->c2_state, w, &ps->o.focus_blacklist, NULL))) { return true; } return false; } struct group_callback_data { struct session *ps; xcb_window_t leader; }; /** * Get a rectangular region a window occupies, excluding shadow. */ static void win_get_region_local(const struct win *w, region_t *res) { assert(w->widthb >= 0 && w->heightb >= 0); pixman_region32_fini(res); pixman_region32_init_rect(res, 0, 0, (uint)w->widthb, (uint)w->heightb); } /** * Get a rectangular region a window occupies, excluding frame and shadow. */ void win_get_region_noframe_local(const struct win *w, region_t *res) { const margin_t extents = win_calc_frame_extents(w); int x = extents.left; int y = extents.top; int width = max2(w->widthb - (extents.left + extents.right), 0); int height = max2(w->heightb - (extents.top + extents.bottom), 0); pixman_region32_fini(res); if (width > 0 && height > 0) { pixman_region32_init_rect(res, x, y, (uint)width, (uint)height); } else { pixman_region32_init(res); } } gen_without_corners(win_get_region_noframe_local); void win_get_region_frame_local(const struct win *w, region_t *res) { const margin_t extents = win_calc_frame_extents(w); auto outer_width = w->widthb; auto outer_height = w->heightb; pixman_region32_fini(res); pixman_region32_init_rects( res, (rect_t[]){ // top {.x1 = 0, .y1 = 0, .x2 = outer_width, .y2 = extents.top}, // bottom {.x1 = 0, .y1 = outer_height - extents.bottom, .x2 = outer_width, .y2 = outer_height}, // left {.x1 = 0, .y1 = 0, .x2 = extents.left, .y2 = outer_height}, // right {.x1 = outer_width - extents.right, .y1 = 0, .x2 = outer_width, .y2 = outer_height}, }, 4); // limit the frame region to inside the window region_t reg_win; pixman_region32_init_rects(®_win, (rect_t[]){{0, 0, outer_width, outer_height}}, 1); pixman_region32_intersect(res, ®_win, res); pixman_region32_fini(®_win); } gen_by_val(win_get_region_frame_local); /** * Add a window to damaged area. * * @param ps current session * @param w struct _win element representing the window */ void add_damage_from_win(session_t *ps, const struct win *w) { // XXX there was a cached extents region, investigate // if that's better // TODO(yshui) use the bounding shape when the window is shaped, otherwise the // damage would be excessive region_t extents; pixman_region32_init(&extents); win_extents(w, &extents); add_damage(ps, &extents); pixman_region32_fini(&extents); } /// Release the images attached to this window static inline void win_release_pixmap(backend_t *base, struct win *w) { log_debug("Releasing pixmap of window %#010x (%s)", win_id(w), w->name); if (w->win_image) { xcb_pixmap_t pixmap = XCB_NONE; pixmap = base->ops.release_image(base, w->win_image); w->win_image = NULL; if (pixmap != XCB_NONE) { xcb_free_pixmap(base->c->c, pixmap); } } } static inline void win_release_shadow(backend_t *base, struct win *w) { log_debug("Releasing shadow of window %#010x (%s)", win_id(w), w->name); if (w->shadow_image) { xcb_pixmap_t pixmap = XCB_NONE; pixmap = base->ops.release_image(base, w->shadow_image); w->shadow_image = NULL; if (pixmap != XCB_NONE) { xcb_free_pixmap(base->c->c, pixmap); } } } static inline void win_release_mask(backend_t *base, struct 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); } } } void win_release_saved_win_image(backend_t *base, struct win *w) { if (w->saved_win_image) { xcb_pixmap_t pixmap = XCB_NONE; pixmap = base->ops.release_image(base, w->saved_win_image); w->saved_win_image = NULL; if (pixmap != XCB_NONE) { xcb_free_pixmap(base->c->c, pixmap); } } } void win_release_images(struct backend_base *backend, struct 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. assert(w->win_image == NULL || !win_check_flags_all(w, WIN_FLAGS_PIXMAP_STALE)); win_release_pixmap(backend, w); win_release_shadow(backend, w); win_release_mask(backend, w); win_release_saved_win_image(backend, w); } /// Returns true if the `prop` property is stale, as well as clears the stale /// flag. static bool win_fetch_and_unset_property_stale(struct win *w, xcb_atom_t prop); /// 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 win *w); /** * Reread opacity property of a window. */ bool win_update_opacity_prop(struct x_connection *c, struct atom *atoms, struct win *w, bool detect_client_opacity) { bool old_has_opacity_prop = w->has_opacity_prop; auto old_opacity = w->opacity_prop; // get frame opacity first w->has_opacity_prop = wid_get_opacity_prop(c, atoms, win_id(w), OPAQUE, &w->opacity_prop); if (!w->has_opacity_prop && detect_client_opacity) { // didn't find opacity prop on the frame, try to get client opacity auto client_win = wm_ref_client_of(w->tree_ref); if (client_win != NULL) { w->has_opacity_prop = wid_get_opacity_prop( c, atoms, wm_ref_win_id(client_win), OPAQUE, &w->opacity_prop); } } if (w->has_opacity_prop) { return !old_has_opacity_prop || w->opacity_prop != old_opacity; } return old_has_opacity_prop; } // 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 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)) { 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->c, ps->atoms, w, ps->o.detect_client_opacity)) { win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); } if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_FRAME_EXTENTS)) { auto client_win = win_client_id(w, /*fallback_to_self=*/false); win_update_frame_extents(&ps->c, ps->atoms, 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->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->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->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)) { 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 (ps->o.track_leader && (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_fetch_and_unset_property_stale(w, XCB_ATOM_WM_HINTS))) { auto client_win = win_client_id(w, /*fallback_to_self=*/true); auto new_leader = win_get_leader_property(&ps->c, ps->atoms, client_win, ps->o.detect_transient, ps->o.detect_client_leader); wm_ref_set_leader(ps->wm, w->tree_ref, new_leader); } win_clear_all_properties_stale(w); } /// Handle primary flags. These flags are set as direct results of raw X11 window data /// changes. void win_process_primary_flags(session_t *ps, struct win *w) { log_trace("Processing flags for window %#010x (%s), was rendered: %d, flags: " "%#" PRIx64, win_id(w), w->name, w->to_paint, w->flags); if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { win_map_start(ps, w); win_clear_flags(w, WIN_FLAGS_MAPPED); } if (w->state != WSTATE_MAPPED) { // Window is not mapped, so we ignore all its changes until it's mapped // again. return; } if (win_check_flags_all(w, WIN_FLAGS_CLIENT_STALE)) { win_on_client_update(ps, w); win_clear_flags(w, WIN_FLAGS_CLIENT_STALE); } if (win_check_flags_any(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { // 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->previous.g = w->g; 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(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); 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_FLAGS_DAMAGED); 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)) { win_clear_flags(w, WIN_FLAGS_POSITION_STALE); win_set_flags(w, WIN_FLAGS_DAMAGED); } } if (win_check_flags_all(w, WIN_FLAGS_PROPERTY_STALE)) { win_update_properties(ps, w); win_clear_flags(w, WIN_FLAGS_PROPERTY_STALE); } } /// Handle secondary flags. These flags are set during the processing of primary flags. /// Flags are separated into primaries and secondaries because processing of secondary /// flags must happen after primary flags of ALL windows are processed, to make sure some /// global states (e.g. active window group) are consistent because they will be used in /// the processing of secondary flags. void win_process_secondary_flags(session_t *ps, struct win *w) { if (w->state != WSTATE_MAPPED) { return; } // Handle window focus change. Set appropriate flags if focused states of // this window changed in the wm tree. bool new_focused = wm_focused_win(ps->wm) == w->tree_ref; bool new_group_focused = wm_focused_leader(ps->wm) == wm_ref_leader(w->tree_ref); if (new_focused != w->is_focused) { log_debug("Window %#010x (%s) focus state changed from %d to %d", win_id(w), w->name, w->is_focused, new_focused); w->is_focused = new_focused; win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); // Send D-Bus signal if (ps->o.dbus) { if (new_focused) { cdbus_ev_win_focusin(session_get_cdbus(ps), w); } else { cdbus_ev_win_focusout(session_get_cdbus(ps), w); } } } if (new_group_focused != w->is_group_focused) { log_debug("Window %#010x (%s) group focus state changed from %d to %d", win_id(w), w->name, w->is_group_focused, new_group_focused); w->is_group_focused = new_group_focused; win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); } if (w->flags == 0) { return; } auto old_options = win_options(w); region_t extents; pixman_region32_init(&extents); // Save old window extents. If window goes from having a shadow to not // having a shadow, we need to add the old, having-shadow extents to // damage. win_extents(w, &extents); // 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); } if (win_check_flags_all(w, WIN_FLAGS_DAMAGED)) { // Add damage, has to be done last so the window has the latest geometry // information. add_damage_from_win(ps, w); win_clear_flags(w, WIN_FLAGS_DAMAGED); } auto new_options = win_options(w); if (win_options_no_damage(&old_options, &new_options)) { pixman_region32_fini(&extents); return; } add_damage_from_win(ps, w); // Only for legacy backends if (new_options.shadow != old_options.shadow && !new_options.shadow) { add_damage(ps, &extents); win_release_shadow(ps->backend_data, w); } pixman_region32_fini(&extents); } void win_process_image_flags(session_t *ps, struct win *w) { // Assert that the MAPPED flag is already handled. assert(!win_check_flags_all(w, WIN_FLAGS_MAPPED)); if (w->state != WSTATE_MAPPED) { // Flags of invisible windows are processed when they are mapped return; } if (!win_check_flags_any(w, WIN_FLAGS_PIXMAP_STALE) || win_check_flags_all(w, WIN_FLAGS_PIXMAP_ERROR) || // We don't need to do anything here for legacy backends ps->backend_data == NULL) { win_clear_flags(w, WIN_FLAGS_PIXMAP_STALE); return; } // Image needs to be updated, update it. win_clear_flags(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. auto pixmap = x_new_id(&ps->c); auto e = xcb_request_check( ps->c.c, xcb_composite_name_window_pixmap_checked(ps->c.c, win_id(w), pixmap)); if (e != NULL) { log_debug("Failed to get named pixmap for window %#010x(%s): %s. " "Retaining its current window image", win_id(w), w->name, x_strerror(e)); free(e); return; } log_debug("New named pixmap for %#010x (%s) : %#010x", win_id(w), w->name, pixmap); // Must release images first, otherwise breaks NVIDIA driver win_release_pixmap(ps->backend_data, w); w->win_image = ps->backend_data->ops.bind_pixmap( ps->backend_data, pixmap, x_get_visual_info(&ps->c, w->a.visual)); if (!w->win_image) { log_error("Failed to bind pixmap"); xcb_free_pixmap(ps->c.c, pixmap); win_set_flags(w, WIN_FLAGS_PIXMAP_ERROR); } } /** * Check if a window has rounded corners. * XXX This is really dumb */ static bool attr_pure win_has_rounded_corners(const struct win *w) { if (!w->bounding_shaped) { return false; } // Quit if border_size() returns XCB_NONE if (!pixman_region32_not_empty((region_t *)&w->bounding_shape)) { return false; } // Determine the minimum width/height of a rectangle that could mark // a window as having rounded corners auto minwidth = (uint16_t)max2(w->widthb * (1 - ROUNDED_PERCENT), w->widthb - ROUNDED_PIXELS); auto minheight = (uint16_t)max2(w->heightb * (1 - ROUNDED_PERCENT), w->heightb - ROUNDED_PIXELS); // Get the rectangles in the bounding region int nrects = 0; const rect_t *rects = pixman_region32_rectangles((region_t *)&w->bounding_shape, &nrects); // Look for a rectangle large enough for this window be considered // having rounded corners for (int i = 0; i < nrects; ++i) { if (rects[i].x2 - rects[i].x1 >= minwidth && rects[i].y2 - rects[i].y1 >= minheight) { return true; } } return false; } int win_update_name(struct x_connection *c, struct atom *atoms, struct win *w) { char **strlst = NULL; int nstr = 0; auto client_win = win_client_id(w, /*fallback_to_self=*/true); if (!(wid_get_text_prop(c, atoms, client_win, atoms->a_NET_WM_NAME, &strlst, &nstr))) { log_debug("(%#010x): _NET_WM_NAME unset, falling back to WM_NAME.", client_win); if (!wid_get_text_prop(c, atoms, client_win, atoms->aWM_NAME, &strlst, &nstr)) { log_debug("Unsetting window name for %#010x", client_win); free(w->name); w->name = NULL; return -1; } } int ret = 0; if (!w->name || strcmp(w->name, strlst[0]) != 0) { ret = 1; free(w->name); w->name = strdup(strlst[0]); } free(strlst); log_debug("(%#010x): client = %#010x, name = \"%s\", ret = %d", win_id(w), client_win, w->name, ret); return ret; } int win_update_role(struct x_connection *c, struct atom *atoms, struct win *w) { char **strlst = NULL; int nstr = 0; auto client_win = win_client_id(w, /*fallback_to_self=*/true); if (!wid_get_text_prop(c, atoms, client_win, atoms->aWM_WINDOW_ROLE, &strlst, &nstr)) { return -1; } int ret = 0; if (!w->role || strcmp(w->role, strlst[0]) != 0) { ret = 1; free(w->role); w->role = strdup(strlst[0]); } free(strlst); log_trace("(%#010x): client = %#010x, role = \"%s\", ret = %d", win_id(w), client_win, w->role, ret); return ret; } /** * Check if a window is 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 bounding_shaped; } static uint32_t wid_get_prop_window_types(struct x_connection *c, struct atom *atoms, xcb_window_t wid) { winprop_t prop = x_get_prop(c, wid, atoms->a_NET_WM_WINDOW_TYPE, 32L, XCB_ATOM_ATOM, 32); static_assert(NUM_WINTYPES <= 32, "too many window types"); uint32_t ret = 0; for (unsigned i = 0; i < prop.nitems; ++i) { for (wintype_t j = 1; j < NUM_WINTYPES; ++j) { if (get_atom_with_nul(atoms, WINTYPES[j].atom, c->c) == prop.atom[i]) { ret |= (1 << j); break; } } } free_winprop(&prop); return ret; } // XXX should distinguish between frame has alpha and window body has alpha bool win_has_alpha(const struct win *w) { return w->pictfmt && w->pictfmt->type == XCB_RENDER_PICT_TYPE_DIRECT && w->pictfmt->direct.alpha_mask; } bool win_client_has_alpha(const struct win *w) { return w->client_pictfmt && w->client_pictfmt->type == XCB_RENDER_PICT_TYPE_DIRECT && w->client_pictfmt->direct.alpha_mask; } winmode_t win_calc_mode_raw(const struct win *w) { if (win_has_alpha(w)) { if (wm_ref_client_of(w->tree_ref) == NULL) { // 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 if (win_client_has_alpha(w)) { // The client window also has alpha, the entire window is // transparent 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. return WMODE_FRAME_TRANS; } // 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)) { return WMODE_FRAME_TRANS; } // log_trace("Window %#010x(%s) is solid", w->client_win, w->name); return WMODE_SOLID; } winmode_t win_calc_mode(const struct win *w) { if (win_animatable_get(w, WIN_SCRIPT_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 * * @param ps current session * @param w struct _win object representing the window * * @return target opacity */ static double win_calc_opacity_target(session_t *ps, const struct win *w, bool focused) { double opacity = 1; if (w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYED) { // be consistent return 0; } // Try obeying opacity property and window type opacity firstly auto window_type = index_of_lowest_one(w->window_types); if (w->has_opacity_prop) { opacity = ((double)w->opacity_prop) / OPAQUE; } else if (!safe_isnan(w->options.opacity)) { opacity = w->options.opacity; } else if (!safe_isnan(ps->o.wintype_option[window_type].opacity)) { opacity = ps->o.wintype_option[window_type].opacity; } else { // Respect active_opacity only when the window is physically // focused if (w->is_focused) { opacity = ps->o.active_opacity; } else if (!focused) { // Respect inactive_opacity in some cases opacity = ps->o.inactive_opacity; } } // respect inactive override if (ps->o.inactive_opacity_override && !focused) { opacity = ps->o.inactive_opacity; } return opacity; } /// Finish the unmapping of a window (e.g. after fading has finished). /// Doesn't free `w` void unmap_win_finish(session_t *ps, struct win *w) { w->reg_ignore_valid = false; // 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. 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 if (w->state != WSTATE_DESTROYED) { win_clear_flags(w, WIN_FLAGS_PIXMAP_ERROR); } assert(w->running_animation_instance == NULL); } /** * Determine whether a window is to be dimmed. */ static void win_update_dim(session_t *ps, struct win *w, bool focused) { // Make sure we do nothing if the window is unmapped / being destroyed if (w->state == WSTATE_UNMAPPED) { return; } if (ps->o.inactive_dim > 0 && !focused) { w->options.dim = ps->o.inactive_dim; } else { w->options.dim = 0; } } /** * 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(struct x_connection *c, struct atom *atoms, struct win *w) { winprop_t prop = x_get_prop(c, win_id(w), atoms->a_COMPTON_SHADOW, 1, XCB_ATOM_CARDINAL, 32); if (!prop.nitems) { w->prop_shadow = -1; } else { w->prop_shadow = *prop.c32; } free_winprop(&prop); } /** * Determine if a window should have shadow, and update things depending * on shadow state. */ static void win_determine_shadow(session_t *ps, struct win *w) { log_debug("Determining shadow of window %#010x (%s)", win_id(w), w->name); w->options.shadow = TRI_UNKNOWN; if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { return; } if (!ps->o.wintype_option[index_of_lowest_one(w->window_types)].shadow) { log_debug("Shadow disabled by wintypes"); w->options.shadow = TRI_FALSE; } else if (c2_match(ps->c2_state, w, &ps->o.shadow_blacklist, NULL)) { log_debug("Shadow disabled by shadow-exclude"); w->options.shadow = TRI_FALSE; } else if (ps->o.shadow_ignore_shaped && w->bounding_shaped && !w->rounded_corners) { log_debug("Shadow disabled by shadow-ignore-shaped"); w->options.shadow = TRI_FALSE; } else if (w->prop_shadow == 0) { log_debug("Shadow disabled by shadow property"); w->options.shadow = TRI_FALSE; } } /** * Reread _COMPTON_SHADOW property from a window and update related * things. */ static bool win_update_prop_shadow(struct x_connection *c, struct atom *atoms, struct 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; } /** * Update window EWMH fullscreen state. */ bool win_update_prop_fullscreen(struct x_connection *c, const struct atom *atoms, struct win *w) { auto prop = x_get_prop(c, win_client_id(w, /*fallback_to_self=*/true), 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 win *w) { bool should_crop = (ps->o.wintype_option[index_of_lowest_one(w->window_types)].clip_shadow_above || c2_match(ps->c2_state, w, &ps->o.shadow_clip_list, NULL)); w->options.clip_shadow_above = should_crop ? TRI_TRUE : TRI_UNKNOWN; } /** * Determine if a window should have color inverted. */ static void win_determine_invert_color(session_t *ps, struct win *w) { w->options.invert_color = TRI_UNKNOWN; if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { return; } if (c2_match(ps->c2_state, w, &ps->o.invert_color_list, NULL)) { w->options.invert_color = TRI_TRUE; } } /** * Determine if a window should have background blurred. */ static void win_determine_blur_background(session_t *ps, struct win *w) { log_debug("Determining blur-background of window %#010x (%s)", win_id(w), w->name); w->options.blur_background = TRI_UNKNOWN; if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { return; } bool blur_background_new = ps->o.blur_method != BLUR_METHOD_NONE; if (blur_background_new) { if (!ps->o.wintype_option[index_of_lowest_one(w->window_types)].blur_background) { log_debug("Blur background disabled by wintypes"); w->options.blur_background = TRI_FALSE; } else if (c2_match(ps->c2_state, w, &ps->o.blur_background_blacklist, NULL)) { log_debug("Blur background disabled by blur-background-exclude"); w->options.blur_background = TRI_FALSE; } } } /** * Determine if a window should have rounded corners. */ static void win_determine_rounded_corners(session_t *ps, struct win *w) { void *radius_override = NULL; bool blacklisted = c2_match(ps->c2_state, w, &ps->o.rounded_corners_blacklist, NULL); if (blacklisted) { w->options.corner_radius = 0; return; } bool matched = c2_match(ps->c2_state, w, &ps->o.corner_radius_rules, &radius_override); if (matched) { log_debug("Window %#010x (%s) matched corner rule! %d", win_id(w), w->name, (int)(long)radius_override); } // Don't round full screen windows & excluded windows, // unless we find a corner override in corner_radius_rules if (!matched && w->is_fullscreen) { w->options.corner_radius = 0; log_debug("Not rounding corners for window %#010x", win_id(w)); } else { if (matched) { w->options.corner_radius = (int)(long)radius_override; } else { w->options.corner_radius = -1; } log_debug("Rounding corners for window %#010x", win_id(w)); // Initialize the border color to an invalid value w->border_col[0] = w->border_col[1] = w->border_col[2] = w->border_col[3] = -1.0F; } } /** * Determine custom window shader to use for a window. */ static void win_determine_fg_shader(session_t *ps, struct win *w) { if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { return; } w->options.shader = NULL; void *val = NULL; if (c2_match(ps->c2_state, w, &ps->o.window_shader_fg_rules, &val)) { w->options.shader = val; } } /** * Update window opacity according to opacity rules. */ void win_update_opacity_rule(session_t *ps, struct win *w) { if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { return; } double opacity = NAN; void *val = NULL; if (c2_match(ps->c2_state, w, &ps->o.opacity_rules, &val)) { opacity = ((double)(long)val) / 100.0; } w->options.opacity = opacity; } static bool win_update_rule(struct session *ps, struct win *w, const c2_condition *rule, bool inspect) { void *pdata = NULL; if (inspect) { printf(" %s ... ", c2_condition_to_str(rule)); } bool matched = c2_match_one(ps->c2_state, w, rule, &pdata); if (inspect) { printf("%s\n", matched ? ANSI("1;32") "matched\033[0m" : "not matched"); } if (!matched) { return false; } auto wopts_next = (struct window_maybe_options *)pdata; if (inspect) { inspect_dump_window_maybe_options(*wopts_next); } w->options = win_maybe_options_fold(*wopts_next, w->options); return false; } /** * Function to be called on window data changes. * * TODO(yshui) need better name */ void win_on_factor_change(session_t *ps, struct win *w) { auto wid = win_client_id(w, /*fallback_to_self=*/true); bool inspect = (ps->o.inspect_win != XCB_NONE && win_id(w) == ps->o.inspect_win) || ps->o.inspect_monitor; log_debug("Window %#010x, client %#010x (%s) factor change", win_id(w), wid, w->name); c2_window_state_update(ps->c2_state, &w->c2_state, ps->c.c, wid, win_id(w)); // Focus and is_fullscreen needs to be updated first, as other rules might depend // on the focused state of the window win_update_is_fullscreen(ps, w); if (ps->o.inspect_monitor) { printf("Window %#010x (Client %#010x):\n======\n\n", win_id(w), win_client_id(w, /*fallback_to_self=*/true)); } assert(w->window_types != 0); if (list_is_empty(&ps->o.rules)) { bool focused = win_is_focused(ps, w); auto window_type = index_of_lowest_one(w->window_types); // Universal rules take precedence over wintype_option and // other exclusion/inclusion lists. And it also supersedes // some of the "override" options. 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); win_update_opacity_rule(ps, w); win_update_dim(ps, w, focused); w->mode = win_calc_mode(w); log_debug("Window mode changed to %d", w->mode); win_update_opacity_rule(ps, w); w->opacity = win_calc_opacity_target(ps, w, focused); w->options.paint = TRI_UNKNOWN; w->options.unredir = WINDOW_UNREDIR_INVALID; w->options.fade = TRI_UNKNOWN; w->options.transparent_clipping = TRI_UNKNOWN; if (w->a.map_state == XCB_MAP_STATE_VIEWABLE && c2_match(ps->c2_state, w, &ps->o.paint_blacklist, NULL)) { w->options.paint = TRI_FALSE; } if (w->a.map_state == XCB_MAP_STATE_VIEWABLE && c2_match(ps->c2_state, w, &ps->o.unredir_if_possible_blacklist, NULL)) { if (ps->o.wintype_option[window_type].redir_ignore) { w->options.unredir = WINDOW_UNREDIR_PASSIVE; } else { w->options.unredir = WINDOW_UNREDIR_TERMINATE; } } else if (win_is_bypassing_compositor(ps, w)) { // Here we deviate from EWMH a bit. EWMH says we must not // unredirect the screen if the window requesting bypassing would // look different after unredirecting. Instead we always follow // the request. w->options.unredir = WINDOW_UNREDIR_FORCED; } else if (ps->o.wintype_option[window_type].redir_ignore) { w->options.unredir = WINDOW_UNREDIR_WHEN_POSSIBLE; } if (c2_match(ps->c2_state, w, &ps->o.fade_blacklist, NULL)) { w->options.fade = TRI_FALSE; } if (c2_match(ps->c2_state, w, &ps->o.transparent_clipping_blacklist, NULL)) { w->options.transparent_clipping = TRI_FALSE; } w->options.full_shadow = tri_from_bool(ps->o.wintype_option[window_type].full_shadow); } else { w->options = WIN_MAYBE_OPTIONS_DEFAULT; assert(w->state == WSTATE_MAPPED); if (inspect) { printf("Checking " BOLD("window rules") ":\n"); } c2_condition_list_foreach_rev(&ps->o.rules, i) { win_update_rule(ps, w, i, inspect); } if (safe_isnan(w->options.opacity) && w->has_opacity_prop) { w->options.opacity = ((double)w->opacity_prop) / OPAQUE; } if (w->options.unredir == WINDOW_UNREDIR_INVALID && win_is_bypassing_compositor(ps, w)) { // If `unredir` is not set by a rule, we follow the bypassing // compositor property. w->options.unredir = WINDOW_UNREDIR_FORCED; } w->opacity = win_options(w).opacity; } w->mode = win_calc_mode(w); log_debug("Window mode changed to %d", w->mode); w->reg_ignore_valid = false; if (ps->debug_window != XCB_NONE && (win_id(w) == ps->debug_window || (win_client_id(w, /*fallback_to_self=*/false) == ps->debug_window))) { w->options.paint = TRI_FALSE; } if (inspect) { inspect_dump_window(ps->c2_state, &ps->o, w); printf("\n"); if (!ps->o.inspect_monitor) { quit(ps); } } } /** * Update cache data in struct _win that depends on window size. */ void win_on_win_size_change(struct 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", win_id(w), 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 = 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. */ bool win_update_wintype(struct x_connection *c, struct atom *atoms, struct win *w) { const uint32_t wtypes_old = w->window_types; auto wid = win_client_id(w, /*fallback_to_self=*/true); // Detect window type here w->window_types = wid_get_prop_window_types(c, atoms, wid); // 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 (w->window_types == 0) { if (w->a.override_redirect || !wid_has_prop(c->c, wid, atoms->aWM_TRANSIENT_FOR)) { w->window_types = (1 << WINTYPE_NORMAL); } else { w->window_types = (1 << WINTYPE_DIALOG); } } log_debug("Window (%#010x) has type %#x", win_id(w), w->window_types); return w->window_types != wtypes_old; } /** * Update window after its client window changed. * * @param ps current session * @param w struct _win of the parent window */ void win_on_client_update(session_t *ps, struct win *w) { // If the window isn't mapped yet, stop here, as the function will be // called in map_win() if (w->a.map_state != XCB_MAP_STATE_VIEWABLE) { return; } win_update_wintype(&ps->c, ps->atoms, w); xcb_window_t client_win_id = win_client_id(w, /*fallback_to_self=*/true); // Get frame widths. The window is in damaged area already. win_update_frame_extents(&ps->c, ps->atoms, w, client_win_id, ps->o.frame_opacity); // Get window group if (ps->o.track_leader) { auto new_leader = win_get_leader_property(&ps->c, ps->atoms, client_win_id, ps->o.detect_transient, ps->o.detect_client_leader); wm_ref_set_leader(ps->wm, w->tree_ref, new_leader); } // Get window name and class if we are tracking them 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_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); auto r = XCB_AWAIT(xcb_get_window_attributes, ps->c.c, client_win_id); if (!r) { return; } w->client_pictfmt = x_get_pictform_for_visual(&ps->c, r->visual); free(r); } #ifdef CONFIG_OPENGL void free_win_res_glx(session_t *ps, struct win *w); #else static inline void free_win_res_glx(session_t * /*ps*/, struct win * /*w*/) { } #endif /** * Free all resources in a struct _win. */ void free_win_res(session_t *ps, struct win *w) { // No need to call backend release_image here because // finish_unmap_win should've done that for us. // XXX unless we are called by session_destroy // assert(w->win_data == NULL); free_win_res_glx(ps, w); free_paint(ps, &w->paint); free_paint(ps, &w->shadow_paint); // 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 x_set_error_action_ignore(&ps->c, xcb_damage_destroy(ps->c.c, w->damage)); rc_region_unref(&w->reg_ignore); free(w->name); free(w->class_instance); free(w->class_general); free(w->role); free(w->stale_props); w->stale_props = NULL; w->stale_props_capacity = 0; c2_window_state_destroy(ps->c2_state, &w->c2_state); } /// Query the Xorg for information about window `win`, and assign a window to `cursor` if /// this window should be managed. struct win *win_maybe_allocate(session_t *ps, struct wm_ref *cursor, const xcb_get_window_attributes_reply_t *attrs) { static const struct win win_def = { .frame_opacity = 1.0, .in_openclose = true, // set to false after first map is done, // true here because window is just created .flags = 0, // updated by // property/attributes/etc // change .mode = WMODE_TRANS, .opacity_prop = OPAQUE, .opacity_set = 1, .frame_extents = MARGIN_INIT, .prop_shadow = -1, .paint = PAINT_INIT, .shadow_paint = PAINT_INIT, }; // Reject overlay window if (wm_ref_win_id(cursor) == ps->overlay) { // Would anyone reparent windows to the overlay window? Doing this // just in case. return NULL; } xcb_window_t wid = wm_ref_win_id(cursor); log_debug("Managing window %#010x", wid); if (attrs->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. return NULL; } if (attrs->_class == XCB_WINDOW_CLASS_INPUT_ONLY) { // No need to manage this window, but we still keep it on the // window stack return NULL; } // Allocate and initialize the new win structure auto new = cmalloc(struct win); // Fill structure // We only need to initialize the part that are not initialized // by map_win *new = win_def; new->a = *attrs; new->shadow_opacity = ps->o.shadow_opacity; pixman_region32_init(&new->bounding_shape); xcb_generic_error_t *e; auto g = xcb_get_geometry_reply(ps->c.c, xcb_get_geometry(ps->c.c, wid), &e); if (!g) { log_debug("Failed to get geometry of window %#010x: %s", wid, x_strerror(e)); free(e); free(new); return NULL; } new->pending_g = (struct win_geometry){ .x = g->x, .y = g->y, .width = g->width, .height = g->height, .border_width = g->border_width, }; free(g); // Create Damage for window (if not Input Only) new->damage = x_new_id(&ps->c); e = xcb_request_check( ps->c.c, xcb_damage_create_checked(ps->c.c, new->damage, wid, XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY)); if (e) { log_debug("Failed to create damage for window %#010x: %s", wid, x_strerror(e)); free(e); free(new); return NULL; } // Set window event mask uint32_t frame_event_mask = XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_STRUCTURE_NOTIFY; if (!ps->o.use_ewmh_active_win) { frame_event_mask |= XCB_EVENT_MASK_FOCUS_CHANGE; } x_set_error_action_ignore( &ps->c, xcb_change_window_attributes(ps->c.c, wid, XCB_CW_EVENT_MASK, (const uint32_t[]){frame_event_mask})); // Get notification when the shape of a window changes if (ps->shape_exists) { x_set_error_action_ignore(&ps->c, xcb_shape_select_input(ps->c.c, wid, 1)); } new->pictfmt = x_get_pictform_for_visual(&ps->c, new->a.visual); new->client_pictfmt = NULL; new->tree_ref = cursor; new->options = WIN_MAYBE_OPTIONS_DEFAULT; new->options_override = WIN_MAYBE_OPTIONS_DEFAULT; new->options_default = &ps->window_options_default; // 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_SIZE_STALE | WIN_FLAGS_POSITION_STALE | WIN_FLAGS_PROPERTY_STALE | WIN_FLAGS_FACTOR_CHANGED); xcb_atom_t init_stale_props[] = { ps->atoms->a_NET_WM_WINDOW_TYPE, ps->atoms->a_NET_WM_WINDOW_OPACITY, ps->atoms->a_NET_FRAME_EXTENTS, ps->atoms->aWM_NAME, 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); wm_ref_set(cursor, new); return new; } /** * Update leader of a window. */ 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; bool exists = false; // Read the leader properties if (detect_transient) { leader = wid_get_prop_window(c, wid, atoms->aWM_TRANSIENT_FOR, &exists); log_debug("Leader via WM_TRANSIENT_FOR of window %#010x: %#010x", wid, leader); if (exists && (leader == c->screen_info->root || leader == XCB_NONE)) { // If WM_TRANSIENT_FOR is set to NONE or the root window, use the // window group leader. // // Ref: // https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html#idm44981516332096 auto prop = x_get_prop(c, wid, XCB_ATOM_WM_HINTS, INT_MAX, XCB_ATOM_WM_HINTS, 32); if (prop.nitems >= 9) { // 9-th member is window_group leader = prop.c32[8]; log_debug("Leader via WM_HINTS of window %#010x: %#010x", wid, leader); } else { leader = XCB_NONE; } free_winprop(&prop); } } if (detect_client_leader && leader == XCB_NONE) { leader = wid_get_prop_window(c, wid, atoms->aWM_CLIENT_LEADER, &exists); log_debug("Leader via WM_CLIENT_LEADER of window %#010x: %#010x", wid, leader); } log_debug("window %#010x: leader %#010x", wid, leader); return leader; } /** * Retrieve the WM_CLASS of a window and update its * win structure. */ bool win_update_class(struct x_connection *c, struct atom *atoms, struct win *w) { char **strlst = NULL; int nstr = 0; auto client_win = win_client_id(w, /*fallback_to_self=*/true); // Free and reset old strings free(w->class_instance); free(w->class_general); w->class_instance = NULL; w->class_general = NULL; // Retrieve the property string list if (!wid_get_text_prop(c, atoms, client_win, atoms->aWM_CLASS, &strlst, &nstr)) { return false; } // Copy the strings if successful w->class_instance = strdup(strlst[0]); if (nstr > 1) { w->class_general = strdup(strlst[1]); } free(strlst); log_trace("(%#010x): client = %#010x, instance = \"%s\", general = \"%s\"", win_id(w), client_win, w->class_instance, w->class_general); return true; } /** * Get a rectangular region a window (and possibly its shadow) occupies. * * Note w->shadow and shadow geometry must be correct before calling this * function. */ void win_extents(const struct win *w, region_t *res) { pixman_region32_clear(res); if (w->state != WSTATE_MAPPED) { return; } pixman_region32_union_rect(res, res, w->g.x, w->g.y, (uint)w->widthb, (uint)w->heightb); if (win_options(w).shadow) { assert(w->shadow_width >= 0 && w->shadow_height >= 0); pixman_region32_union_rect(res, res, w->g.x + w->shadow_dx, w->g.y + w->shadow_dy, (uint)w->shadow_width, (uint)w->shadow_height); } } gen_by_val(win_extents); /** * Update the out-dated bounding shape of a window. * * Mark the window shape as updated */ void win_update_bounding_shape(struct x_connection *c, struct 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, win_id(w)); } // 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) { /* * if window doesn't exist anymore, this will generate an error * as well as not generate a region. */ xcb_shape_get_rectangles_reply_t *r = xcb_shape_get_rectangles_reply( c->c, xcb_shape_get_rectangles(c->c, win_id(w), XCB_SHAPE_SK_BOUNDING), NULL); if (!r) { break; } xcb_rectangle_t *xrects = xcb_shape_get_rectangles_rectangles(r); int nrects = xcb_shape_get_rectangles_rectangles_length(r); rect_t *rects = from_x_rects(nrects, xrects); free(r); region_t br; pixman_region32_init_rects(&br, rects, nrects); free(rects); // 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, 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 // rectangle pixman_region32_intersect(&w->bounding_shape, &w->bounding_shape, &br); pixman_region32_fini(&br); break; } if (w->bounding_shaped && detect_rounded_corners) { w->rounded_corners = win_has_rounded_corners(w); } } /** * Retrieve frame extents from a window. */ void win_update_frame_extents(struct x_connection *c, struct atom *atoms, struct win *w, xcb_window_t client, double frame_opacity) { if (client == XCB_NONE) { w->frame_extents = (margin_t){0}; return; } 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.", prop.c32[i]); memset(extents, 0, sizeof(extents)); break; } extents[i] = (int)prop.c32[i]; } const bool changed = w->frame_extents.left != extents[0] || w->frame_extents.right != extents[1] || w->frame_extents.top != extents[2] || w->frame_extents.bottom != extents[3]; w->frame_extents.left = extents[0]; w->frame_extents.right = extents[1]; w->frame_extents.top = extents[2]; w->frame_extents.bottom = extents[3]; // If frame_opacity != 1, then frame of this window // is not included in reg_ignore of underneath windows if (frame_opacity == 1 && changed) { w->reg_ignore_valid = false; } } log_trace("(%#010x): %d, %d, %d, %d", win_id(w), w->frame_extents.left, w->frame_extents.right, w->frame_extents.top, w->frame_extents.bottom); free_winprop(&prop); } bool win_is_region_ignore_valid(session_t *ps, const struct win *w) { wm_stack_foreach(ps->wm, cursor) { auto i = wm_ref_deref(cursor); if (i == w) { break; } if (i != NULL && !i->reg_ignore_valid) { return false; } } return true; } /// Finish the destruction of a window (e.g. after fading has finished). /// Frees `w` void win_destroy_finish(session_t *ps, struct win *w) { log_debug("Trying to finish destroying (%#010x)", win_id(w)); unmap_win_finish(ps, w); // Unmapping might preserve the shadow image, so free it here win_release_shadow(ps->backend_data, w); win_release_mask(ps->backend_data, w); free_win_res(ps, 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 wm_stack_foreach(ps->wm, cursor) { auto w2 = wm_ref_deref(cursor); if (w2 != NULL && w == w2->prev_trans) { w2->prev_trans = NULL; } } wm_reap_zombie(w->tree_ref); free(w); } /// Start destroying a window. Windows cannot always be destroyed immediately /// because of fading and such. void win_destroy_start(session_t *ps, struct win *w) { BUG_ON(w == NULL); log_debug("Destroying %#010x (%s)", win_id(w), w->name); if (w->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?", win_id(w)); } // 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(w, 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(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { add_damage_from_win(ps, w); } // Clear some flags about stale window information. Because now // the window is destroyed, we can't update them anyway. win_clear_flags(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE | WIN_FLAGS_PROPERTY_STALE | WIN_FLAGS_FACTOR_CHANGED); // Update state flags of a managed window w->state = WSTATE_DESTROYED; w->opacity = 0.0f; w->a.map_state = XCB_MAP_STATE_UNMAPPED; w->in_openclose = true; } void unmap_win_start(struct win *w) { assert(w); assert(w->a._class != XCB_WINDOW_CLASS_INPUT_ONLY); log_debug("Unmapping %#010x (%s)", win_id(w), w->name); assert(w->state != WSTATE_DESTROYED); 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; } // Note we don't update focused window here. This will either be // triggered by subsequence Focus{In, Out} event, or by recheck_focus w->a.map_state = XCB_MAP_STATE_UNMAPPED; w->state = WSTATE_UNMAPPED; w->opacity = 0.0f; } struct win_script_context win_script_context_prepare(struct session *ps, struct win *w) { auto monitor_index = win_find_monitor(&ps->monitors, w); auto monitor = monitor_index >= 0 ? *pixman_region32_extents(&ps->monitors.regions[monitor_index]) : (pixman_box32_t){ .x1 = 0, .y1 = 0, .x2 = ps->root_width, .y2 = ps->root_height}; struct win_script_context ret = { .x = w->g.x, .y = w->g.y, .width = w->widthb, .height = w->heightb, .opacity = w->opacity, .x_before = w->previous.g.x, .y_before = w->previous.g.y, .width_before = w->previous.g.width + w->previous.g.border_width * 2, .height_before = w->previous.g.height + w->previous.g.border_width * 2, .opacity_before = w->previous.opacity, .monitor_x = monitor.x1, .monitor_y = monitor.y1, .monitor_width = monitor.x2 - monitor.x1, .monitor_height = monitor.y2 - monitor.y1, }; return ret; } double win_animatable_get(const struct win *w, enum win_script_output output) { if (w->running_animation_instance && w->running_animation.output_indices[output] >= 0) { return w->running_animation_instance ->memory[w->running_animation.output_indices[output]]; } switch (output) { case WIN_SCRIPT_BLUR_OPACITY: return w->state == WSTATE_MAPPED ? 1.0 : 0.0; case WIN_SCRIPT_OPACITY: case WIN_SCRIPT_SHADOW_OPACITY: return w->opacity; case WIN_SCRIPT_CROP_X: case WIN_SCRIPT_CROP_Y: case WIN_SCRIPT_OFFSET_X: case WIN_SCRIPT_OFFSET_Y: case WIN_SCRIPT_SHADOW_OFFSET_X: case WIN_SCRIPT_SHADOW_OFFSET_Y: return 0; case WIN_SCRIPT_SCALE_X: case WIN_SCRIPT_SCALE_Y: case WIN_SCRIPT_SHADOW_SCALE_X: case WIN_SCRIPT_SHADOW_SCALE_Y: return 1; case WIN_SCRIPT_CROP_WIDTH: case WIN_SCRIPT_CROP_HEIGHT: return INFINITY; case WIN_SCRIPT_SAVED_IMAGE_BLEND: return 0; default: unreachable(); } unreachable(); } #define WSTATE_PAIR(a, b) ((int)(a) * NUM_OF_WSTATES + (int)(b)) /// Advance the animation of a window. /// /// Returns true if animation was running before this function is called, and is no /// longer running now. Returns false if animation is still running, or if there was no /// animation running when this is called. static bool win_advance_animation(struct win *w, double delta_t, const struct win_script_context *win_ctx) { // No state changes, if there's a animation running, we just continue it. if (w->running_animation_instance == NULL) { return false; } log_verbose("Advance animation for %#010x (%s) %f seconds", win_id(w), w->name, delta_t); if (!script_instance_is_finished(w->running_animation_instance)) { auto elapsed_slot = script_elapsed_slot(w->running_animation_instance->script); w->running_animation_instance->memory[elapsed_slot] += delta_t; auto result = script_instance_evaluate(w->running_animation_instance, (void *)win_ctx); if (result != SCRIPT_EVAL_OK) { log_error("Failed to run animation script: %d", result); return true; } return false; } return true; } bool win_process_animation_and_state_change(struct session *ps, struct win *w, double delta_t) { // If the window hasn't ever been damaged yet, it won't be rendered in this frame. // Or if it doesn't have a image bound, it won't be rendered either. (This can // happen is a window is destroyed during a backend reset. Backend resets releases // all images, and if a window is freed during that, its image cannot be // reacquired.) // // If the window won't be rendered, and it is also unmapped/destroyed, it won't // receive damage events or reacquire an image. In this case we can skip its // animation. For mapped windows, we need to provisionally start animation, // because its first damage event might come a bit late. bool will_never_render = (!w->ever_damaged || w->win_image == NULL) && w->state != WSTATE_MAPPED; auto win_ctx = win_script_context_prepare(ps, w); bool geometry_changed = !win_geometry_eq(w->previous.g, w->g); auto old_state = w->previous.state; w->previous.state = w->state; w->previous.opacity = w->opacity; w->previous.g = w->g; if (!ps->redirected || will_never_render) { // This window won't be rendered, so we don't need to run the animations. bool state_changed = old_state != w->state || win_ctx.opacity_before != win_ctx.opacity || geometry_changed; return state_changed || (w->running_animation_instance != NULL); } // Try to determine the right animation trigger based on state changes. Note there // is some complications here. X automatically unmaps windows before destroying // them. So a "close" trigger will also be fired from a UNMAPPED -> DESTROYED // transition, besides the more obvious MAPPED -> DESTROYED transition. But this // also means, if the user didn't configure a animation for "hide", but did // for "close", there is a chance this animation won't be triggered, if there is a // gap between the UnmapNotify and DestroyNotify. There is no way on our end of // fixing this without using hacks. enum animation_trigger trigger = ANIMATION_TRIGGER_INVALID; // Animation trigger priority: // state > geometry > opacity if (old_state != w->state) { // Send D-Bus signal if (ps->o.dbus) { switch (w->state) { case WSTATE_UNMAPPED: cdbus_ev_win_unmapped(session_get_cdbus(ps), w); break; case WSTATE_MAPPED: cdbus_ev_win_mapped(session_get_cdbus(ps), w); break; case WSTATE_DESTROYED: cdbus_ev_win_destroyed(session_get_cdbus(ps), w); break; } } switch (WSTATE_PAIR(old_state, w->state)) { case WSTATE_PAIR(WSTATE_UNMAPPED, WSTATE_MAPPED): trigger = w->in_openclose ? ANIMATION_TRIGGER_OPEN : ANIMATION_TRIGGER_SHOW; break; case WSTATE_PAIR(WSTATE_UNMAPPED, WSTATE_DESTROYED): if ((!ps->o.no_fading_destroyed_argb || !win_has_alpha(w)) && w->running_animation_instance != NULL) { trigger = ANIMATION_TRIGGER_CLOSE; } break; case WSTATE_PAIR(WSTATE_MAPPED, WSTATE_DESTROYED): // TODO(yshui) we should deprecate "no-fading-destroyed-argb" and // ask user to write fading rules (after we have added such // rules). Ditto below. if (!ps->o.no_fading_destroyed_argb || !win_has_alpha(w)) { trigger = ANIMATION_TRIGGER_CLOSE; } break; case WSTATE_PAIR(WSTATE_MAPPED, WSTATE_UNMAPPED): trigger = ANIMATION_TRIGGER_HIDE; break; default: log_error("Impossible state transition from %d to %d", old_state, w->state); assert(false); return true; } } else if (geometry_changed) { assert(w->state == WSTATE_MAPPED); trigger = ANIMATION_TRIGGER_GEOMETRY; } else if (win_ctx.opacity_before != win_ctx.opacity) { assert(w->state == WSTATE_MAPPED); trigger = win_ctx.opacity > win_ctx.opacity_before ? ANIMATION_TRIGGER_INCREASE_OPACITY : ANIMATION_TRIGGER_DECREASE_OPACITY; } if (trigger == ANIMATION_TRIGGER_INVALID) { // No state changes, if there's a animation running, we just continue it. return win_advance_animation(w, delta_t, &win_ctx); } else if (w->running_animation_instance && (w->running_animation.suppressions & (1 << trigger)) != 0) { log_debug("Not starting animation %s for window %#010x (%s) because it " "is being suppressed.", animation_trigger_names[trigger], win_id(w), w->name); return win_advance_animation(w, delta_t, &win_ctx); } else if (w->animation_block[trigger] > 0) { log_debug("Not starting animation %s for window %#010x (%s) because it " "is blocked.", animation_trigger_names[trigger], win_id(w), w->name); return win_advance_animation(w, delta_t, &win_ctx); } auto wopts = win_options(w); if (wopts.animations[trigger].script == NULL) { return true; } if (wopts.animations[trigger].is_generated && !wopts.fade) { // Window's animation is fading (as signified by the fact that it's // generated), but the user has disabled fading for this window. return true; } log_debug("Starting animation %s for window %#010x (%s)", animation_trigger_names[trigger], win_id(w), w->name); if (win_check_flags_any(w, WIN_FLAGS_PIXMAP_STALE)) { // Grab the old pixmap, animations might need it if (w->saved_win_image) { win_release_saved_win_image(ps->backend_data, w); } if (ps->drivers & DRIVER_NVIDIA) { if (w->win_image != NULL) { w->saved_win_image = ps->backend_data->ops.new_image( ps->backend_data, BACKEND_IMAGE_FORMAT_PIXMAP, (ivec2){ .width = (int)win_ctx.width_before, .height = (int)win_ctx.height_before, }); region_t copy_region; pixman_region32_init_rect(©_region, 0, 0, (uint)win_ctx.width_before, (uint)win_ctx.height_before); ps->backend_data->ops.copy_area( ps->backend_data, (ivec2){}, w->saved_win_image, w->win_image, ©_region); pixman_region32_fini(©_region); } } else { w->saved_win_image = w->win_image; w->win_image = NULL; } w->saved_win_image_scale = (vec2){ .x = win_ctx.width / win_ctx.width_before, .y = win_ctx.height / win_ctx.height_before, }; } auto new_animation = script_instance_new(wopts.animations[trigger].script); if (w->running_animation_instance) { // Interrupt the old animation and start the new animation from where the // old has left off. Note we still need to advance the old animation for // the last interval. win_advance_animation(w, delta_t, &win_ctx); auto memory = w->running_animation_instance->memory; auto output_indices = w->running_animation.output_indices; if (output_indices[WIN_SCRIPT_SAVED_IMAGE_BLEND] >= 0) { memory[output_indices[WIN_SCRIPT_SAVED_IMAGE_BLEND]] = 1 - memory[output_indices[WIN_SCRIPT_SAVED_IMAGE_BLEND]]; } if (geometry_changed) { // If the window has moved, we need to adjust scripts // outputs so that the window will stay in the same position and // size after applying the animation. This way the window's size // and position won't change discontinuously. struct { int output; double delta; } adjustments[] = { {WIN_SCRIPT_OFFSET_X, win_ctx.x_before - win_ctx.x}, {WIN_SCRIPT_OFFSET_Y, win_ctx.y_before - win_ctx.y}, {WIN_SCRIPT_SHADOW_OFFSET_X, win_ctx.x_before - win_ctx.x}, {WIN_SCRIPT_SHADOW_OFFSET_Y, win_ctx.y_before - win_ctx.y}, }; for (size_t i = 0; i < ARR_SIZE(adjustments); i++) { if (output_indices[adjustments[i].output] >= 0) { memory[output_indices[adjustments[i].output]] += adjustments[i].delta; } } struct { int output; double factor; } factors[] = { {WIN_SCRIPT_SCALE_X, win_ctx.width_before / win_ctx.width}, {WIN_SCRIPT_SCALE_Y, win_ctx.height_before / win_ctx.height}, {WIN_SCRIPT_SHADOW_SCALE_X, win_ctx.width_before / win_ctx.width}, {WIN_SCRIPT_SHADOW_SCALE_Y, win_ctx.height_before / win_ctx.height}, }; for (size_t i = 0; i < ARR_SIZE(factors); i++) { if (output_indices[factors[i].output] >= 0) { memory[output_indices[factors[i].output]] *= factors[i].factor; } } } script_instance_resume_from(w->running_animation_instance, new_animation); free(w->running_animation_instance); } w->running_animation_instance = new_animation; w->running_animation = wopts.animations[trigger]; script_instance_evaluate(w->running_animation_instance, &win_ctx); return script_instance_is_finished(w->running_animation_instance); } #undef WSTATE_PAIR /// Find which monitor a window is on. int win_find_monitor(const struct x_monitors *monitors, const struct win *mw) { int ret = -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) { log_verbose("Window %#010x (%s), %dx%d+%dx%d, is entirely on the " "monitor %d (%dx%d+%dx%d)", win_id(mw), 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 i; } } log_verbose("Window %#010x (%s), %dx%d+%dx%d, is not entirely on any monitor", win_id(mw), mw->name, mw->g.x, mw->g.y, mw->widthb, mw->heightb); return ret; } bool win_set_pending_geometry(struct win *w, struct win_geometry g) { // We check against pending_g here, because there might have been multiple // configure notifies in this cycle, or the window could receive multiple updates // while it's unmapped. `pending_g` should be equal to `g` otherwise. bool position_changed = w->pending_g.x != g.x || w->pending_g.y != g.y; bool size_changed = w->pending_g.width != g.width || w->pending_g.height != g.height || w->pending_g.border_width != g.border_width; if (position_changed || size_changed) { // Queue pending updates win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); // At least one of the following if's is true if (position_changed) { log_trace("Window %#010x position changed, %dx%d -> %dx%d", win_id(w), w->g.x, w->g.y, g.x, g.y); w->pending_g.x = g.x; w->pending_g.y = g.y; win_set_flags(w, WIN_FLAGS_POSITION_STALE); } if (size_changed) { log_trace("Window %#010x size changed, %dx%d -> %dx%d", win_id(w), w->g.width, w->g.height, g.width, g.height); w->pending_g.width = g.width; w->pending_g.height = g.height; w->pending_g.border_width = g.border_width; win_set_flags(w, WIN_FLAGS_SIZE_STALE); } } return position_changed || size_changed; } struct win_get_geometry_request { struct x_async_request_base base; struct session *ps; xcb_window_t wid; }; static void win_handle_get_geometry_reply(struct x_connection * /*c*/, struct x_async_request_base *req_base, const xcb_raw_generic_event_t *reply_or_error) { auto req = (struct win_get_geometry_request *)req_base; auto wid = req->wid; auto ps = req->ps; free(req); if (reply_or_error == NULL) { // Shutting down return; } if (reply_or_error->response_type == 0) { log_debug("Failed to get geometry of window %#010x: %s", wid, x_strerror((xcb_generic_error_t *)reply_or_error)); return; } auto cursor = wm_find(ps->wm, wid); if (cursor == NULL) { // Rare, window is destroyed then its ID is reused if (wm_is_consistent(ps->wm)) { log_error("Successfully fetched geometry of a non-existent " "window %#010x", wid); assert(false); } return; } auto w = wm_ref_deref(cursor); if (w == NULL) { // Not yet managed. Rare, window is destroyed then its ID is reused return; } auto r = (const xcb_get_geometry_reply_t *)reply_or_error; ps->pending_updates |= win_set_pending_geometry(w, win_geometry_from_get_geometry(r)); } /// Start the mapping of a window. We cannot map immediately since we might need to fade /// the window in. void win_map_start(struct session *ps, struct win *w) { assert(w); // Don't care about window mapping if it's an InputOnly window // Also, try avoiding mapping a window twice if (w->a._class == XCB_WINDOW_CLASS_INPUT_ONLY) { return; } log_debug("Mapping (%#010x \"%s\"), old state %d", win_id(w), w->name, w->state); assert(w->state != WSTATE_DESTROYED); if (w->state == WSTATE_MAPPED) { log_error("Mapping an already mapped window"); assert(false); return; } // 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; // Update window mode here to check for ARGB windows w->mode = win_calc_mode(w); w->state = WSTATE_MAPPED; win_set_flags( w, WIN_FLAGS_PIXMAP_STALE | WIN_FLAGS_CLIENT_STALE | WIN_FLAGS_FACTOR_CHANGED); auto req = ccalloc(1, struct win_get_geometry_request); req->base = (struct x_async_request_base){ .callback = win_handle_get_geometry_reply, .sequence = xcb_get_geometry(ps->c.c, win_id(w)).sequence, }; req->wid = win_id(w); req->ps = ps; x_await_request(&ps->c, &req->base); } /// Set flags on a window. Some sanity checks are performed void win_set_flags(struct win *w, uint64_t flags) { log_verbose("Set flags %" PRIu64 " to window %#010x (%s)", flags, win_id(w), w->name); if (unlikely(w->state == WSTATE_DESTROYED)) { log_error("Flags set on a destroyed window %#010x (%s)", win_id(w), w->name); return; } w->flags |= flags; } /// Clear flags on a window. Some sanity checks are performed void win_clear_flags(struct win *w, uint64_t flags) { log_verbose("Clear flags %" PRIu64 " from window %#010x (%s)", flags, win_id(w), w->name); if (unlikely(w->state == WSTATE_DESTROYED)) { log_warn("Flags %" PRIu64 " cleared on a destroyed window %#010x (%s)", flags, win_id(w), w->name); return; } w->flags = w->flags & (~flags); } void win_set_properties_stale(struct win *w, const xcb_atom_t *props, int nprops) { 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 for (int i = 0; i < nprops; i++) { if (props[i] >= new_capacity * bits_per_element) { new_capacity = props[i] / bits_per_element + 1; } } // Reallocate if necessary if (new_capacity > w->stale_props_capacity) { w->stale_props = realloc(w->stale_props, new_capacity * sizeof(*w->stale_props)); // Clear the content of the newly allocated bytes memset(w->stale_props + w->stale_props_capacity, 0, (new_capacity - w->stale_props_capacity) * sizeof(*w->stale_props)); w->stale_props_capacity = new_capacity; } // Set the property bits for (int i = 0; i < nprops; i++) { w->stale_props[props[i] / bits_per_element] |= 1UL << (props[i] % bits_per_element); } win_set_flags(w, WIN_FLAGS_PROPERTY_STALE); } static void win_clear_all_properties_stale(struct win *w) { memset(w->stale_props, 0, w->stale_props_capacity * sizeof(*w->stale_props)); win_clear_flags(w, WIN_FLAGS_PROPERTY_STALE); } static bool win_fetch_and_unset_property_stale(struct win *w, xcb_atom_t prop) { auto const bits_per_element = sizeof(*w->stale_props) * 8; if (prop >= w->stale_props_capacity * bits_per_element) { return false; } 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; } bool win_check_flags_any(struct win *w, uint64_t flags) { return (w->flags & flags) != 0; } bool win_check_flags_all(struct win *w, uint64_t flags) { return (w->flags & flags) == flags; } /** * Check if a window is a fullscreen window. * * It's not using w->border_size for performance measures. */ void win_update_is_fullscreen(const session_t *ps, struct win *w) { if (!ps->o.no_ewmh_fullscreen && w->is_ewmh_fullscreen) { w->is_fullscreen = true; return; } 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); } /** * Check if a window has BYPASS_COMPOSITOR property set * * TODO(yshui) cache this property */ bool win_is_bypassing_compositor(const session_t *ps, const struct win *w) { bool ret = false; auto wid = win_client_id(w, /*fallback_to_self=*/true); auto prop = x_get_prop(&ps->c, wid, ps->atoms->a_NET_WM_BYPASS_COMPOSITOR, 1L, XCB_ATOM_CARDINAL, 32); if (prop.nitems && *prop.c32 == 1) { ret = true; } free_winprop(&prop); return ret; } picom-12.5/src/wm/win.h000066400000000000000000000625371471504570600147560ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2011-2013, Christopher Jeffrey // Copyright (c) 2013 Richard Grenville // Copyright (c) 2018 Yuxuan Shui #pragma once #include #include #include #include #include #include #include "c2.h" #include "compiler.h" #include "config.h" #include "defs.h" #include "region.h" #include "render.h" #include "transition/script.h" #include "utils/list.h" #include "utils/misc.h" #include "wm/wm.h" #include "x.h" #include "xcb/xproto.h" struct backend_base; typedef struct session session_t; typedef struct _glx_texture glx_texture_t; struct wm_cursor; #define wm_stack_foreach(wm, i) \ for (struct wm_ref * (i) = wm_ref_topmost_child(wm_root_ref(wm)); (i); \ (i) = wm_ref_below(i)) #define wm_stack_foreach_rev(wm, i) \ for (struct wm_ref * (i) = wm_ref_bottommost_child(wm_root_ref(wm)); (i); \ (i) = wm_ref_above(i)) #define wm_stack_foreach_safe(wm, i, next_i) \ for (struct wm_ref * (i) = wm_ref_topmost_child(wm_root_ref(wm)), \ *(next_i) = (i) != NULL ? wm_ref_below(i) : NULL; \ (i); (i) = (next_i), (next_i) = (i) != NULL ? wm_ref_below(i) : NULL) #ifdef CONFIG_OPENGL // FIXME this type should be in opengl.h // it is very unideal for it to be here typedef struct { /// Framebuffer used for blurring. GLuint fbo; /// Textures used for blurring. GLuint textures[2]; /// Width of the textures. int width; /// Height of the textures. 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; /// The actual window correspond to this stack entry. NULL if we didn't know about /// this window (e.g. an InputOnly window, or we haven't handled the window /// creation yet) struct win *win; /// The window id. Might not be unique in the stack, because there might be /// destroyed window still fading out in the stack. xcb_window_t id; }; /** * About coordinate systems * * In general, X is the horizontal axis, Y is the vertical axis. * X goes from left to right, Y goes downwards. * * Global: the origin is the top left corner of the Xorg screen. * Local: the origin is the top left corner of the window, border is * considered part of the window. */ struct win_geometry { int16_t x; int16_t y; uint16_t width; uint16_t height; uint16_t border_width; }; /// These are changes of window state that might trigger an animation. We separate them /// out and delay their application so determining which animation to run is easier. /// /// These values are only hold for an instant, and once the animation is started they are /// updated to reflect the latest state. struct win_state_change { winstate_t state; double opacity; struct win_geometry g; }; struct win { /// Reference back to the position of this window inside the window tree. struct wm_ref *tree_ref; /// backend data attached to this window. Only available when /// `state` is not UNMAPPED image_handle win_image; /// The old window image before the window image is refreshed. This is used for /// animation, and is only kept alive for the duration of the animation. image_handle saved_win_image; /// How much to scale the saved_win_image, so that it is the same size as the /// current window image. vec2 saved_win_image_scale; image_handle shadow_image; image_handle mask_image; // TODO(yshui) only used by legacy backends, remove. /// Pointer to the next higher window to paint. struct win *prev_trans; // TODO(yshui) rethink reg_ignore // Core members winstate_t state; /// Window attributes. xcb_get_window_attributes_reply_t a; /// The geometry of the window body, excluding the window border region. struct win_geometry g; /// Updated geometry received in events struct win_geometry pending_g; /// 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; // TODO(yshui) only used by legacy backends, remove. /// 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; /// Damage of the window. xcb_damage_damage_t damage; /// Paint info of the window. paint_t paint; /// bitmap for properties which needs to be updated uint64_t *stale_props; /// number of uint64_ts that has been allocated for stale_props size_t stale_props_capacity; /// Bounding shape of the window. In local coordinates. /// See above about coordinate systems. region_t bounding_shape; /// Window flags. Definitions above. uint64_t flags; /// The region of screen that will be obscured when windows above is painted, /// in global coordinates. /// We use this to reduce the pixels that needed to be paint when painting /// this window and anything underneath. Depends on window frame /// opacity state, window geometry, window mapped/unmapped state, /// window mode of the windows above. DOES NOT INCLUDE the body of THIS WINDOW. /// NULL means reg_ignore has not been calculated for this window. rc_region_t *reg_ignore; /// Whether the reg_ignore of all windows beneath this window are valid bool reg_ignore_valid; /// Cached width/height of the window including border. int widthb, heightb; /// Whether the window is bounding-shaped. bool bounding_shaped; /// Whether the window just have rounded corners. bool rounded_corners; /// Whether this window is to be painted. bool to_paint; /// Whether this window is in open/close state. bool in_openclose; // Client window related members /// A bitflag of window types. According to ICCCM, a window can have more than one /// type. uint32_t window_types; // Blacklist related members /// Name of the window. char *name; /// Window instance class of the window. char *class_instance; /// Window general class of the window. 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 active window. bool is_focused; /// Whether the window group this window belongs to is focused. bool is_group_focused; // Opacity-related members /// The final window opacity if no animation is running double opacity; /// true if window (or client window, for broken window managers /// 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; /// Last window opacity value set by the rules. double opacity_set; float border_col[4]; // Frame-opacity-related members /// Current window frame opacity. Affected by window opacity. double frame_opacity; /// Frame extents. Acquired from _NET_FRAME_EXTENTS. margin_t frame_extents; // Shadow-related members /// 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 command line argument. int shadow_dx; /// Y offset of shadow. Affected by command line argument. int shadow_dy; /// Width of shadow. Affected by window size and command line argument. int shadow_width; /// 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 long prop_shadow; struct c2_window_state c2_state; // Animation related #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; /// Per-window options coming from rules struct window_maybe_options options; /// Override of per-window options, used by dbus interface struct window_maybe_options options_override; /// Global per-window options default const struct window_options *options_default; /// Previous state of the window before state changed. This is used /// by `win_process_animation_and_state_change` to trigger appropriate /// animations. struct win_state_change previous; struct script_instance *running_animation_instance; struct win_script running_animation; /// Number of times each animation trigger is blocked unsigned int animation_block[ANIMATION_TRIGGER_COUNT]; }; struct win_script_context { double x, y, width, height; double x_before, y_before, width_before, height_before; double opacity_before, opacity; double monitor_x, monitor_y; double monitor_width, monitor_height; }; static_assert(SCRIPT_CTX_PLACEHOLDER_BASE > sizeof(struct win_script_context), "win_script_context too large"); static const struct script_context_info win_script_context_info[] = { {"window-x", offsetof(struct win_script_context, x)}, {"window-y", offsetof(struct win_script_context, y)}, {"window-width", offsetof(struct win_script_context, width)}, {"window-height", offsetof(struct win_script_context, height)}, {"window-x-before", offsetof(struct win_script_context, x_before)}, {"window-y-before", offsetof(struct win_script_context, y_before)}, {"window-width-before", offsetof(struct win_script_context, width_before)}, {"window-height-before", offsetof(struct win_script_context, height_before)}, {"window-raw-opacity-before", offsetof(struct win_script_context, opacity_before)}, {"window-raw-opacity", offsetof(struct win_script_context, opacity)}, {"window-monitor-x", offsetof(struct win_script_context, monitor_x)}, {"window-monitor-y", offsetof(struct win_script_context, monitor_y)}, {"window-monitor-width", offsetof(struct win_script_context, monitor_width)}, {"window-monitor-height", offsetof(struct win_script_context, monitor_height)}, {NULL, 0} // }; static const struct script_output_info win_script_outputs[] = { [WIN_SCRIPT_OFFSET_X] = {"offset-x"}, [WIN_SCRIPT_OFFSET_Y] = {"offset-y"}, [WIN_SCRIPT_SHADOW_OFFSET_X] = {"shadow-offset-x"}, [WIN_SCRIPT_SHADOW_OFFSET_Y] = {"shadow-offset-y"}, [WIN_SCRIPT_OPACITY] = {"opacity"}, [WIN_SCRIPT_BLUR_OPACITY] = {"blur-opacity"}, [WIN_SCRIPT_SHADOW_OPACITY] = {"shadow-opacity"}, [WIN_SCRIPT_SCALE_X] = {"scale-x"}, [WIN_SCRIPT_SCALE_Y] = {"scale-y"}, [WIN_SCRIPT_SHADOW_SCALE_X] = {"shadow-scale-x"}, [WIN_SCRIPT_SHADOW_SCALE_Y] = {"shadow-scale-y"}, [WIN_SCRIPT_CROP_X] = {"crop-x"}, [WIN_SCRIPT_CROP_Y] = {"crop-y"}, [WIN_SCRIPT_CROP_WIDTH] = {"crop-width"}, [WIN_SCRIPT_CROP_HEIGHT] = {"crop-height"}, [WIN_SCRIPT_SAVED_IMAGE_BLEND] = {"saved-image-blend"}, [NUM_OF_WIN_SCRIPT_OUTPUTS] = {NULL}, }; static const struct window_maybe_options WIN_MAYBE_OPTIONS_DEFAULT = { .blur_background = TRI_UNKNOWN, .clip_shadow_above = TRI_UNKNOWN, .shadow = TRI_UNKNOWN, .fade = TRI_UNKNOWN, .invert_color = TRI_UNKNOWN, .paint = TRI_UNKNOWN, .dim = NAN, .opacity = NAN, .shader = NULL, .corner_radius = -1, .unredir = WINDOW_UNREDIR_INVALID, }; static inline void win_script_fold(const struct win_script *upper, const struct win_script *lower, struct win_script *output) { for (size_t i = 0; i < ANIMATION_TRIGGER_COUNT; i++) { output[i] = upper[i].script ? upper[i] : lower[i]; } } /// Combine two window options. The `upper` value has higher priority, the `lower` value /// will only be used if the corresponding value in `upper` is not set (e.g. it is /// TRI_UNKNOWN for tristate values, NaN for opacity, -1 for corner_radius). static inline struct window_maybe_options __attribute__((always_inline)) win_maybe_options_fold(struct window_maybe_options upper, struct window_maybe_options lower) { struct window_maybe_options ret = { .unredir = upper.unredir == WINDOW_UNREDIR_INVALID ? lower.unredir : upper.unredir, .blur_background = tri_or(upper.blur_background, lower.blur_background), .clip_shadow_above = tri_or(upper.clip_shadow_above, lower.clip_shadow_above), .full_shadow = tri_or(upper.full_shadow, lower.full_shadow), .shadow = tri_or(upper.shadow, lower.shadow), .fade = tri_or(upper.fade, lower.fade), .invert_color = tri_or(upper.invert_color, lower.invert_color), .paint = tri_or(upper.paint, lower.paint), .transparent_clipping = tri_or(upper.transparent_clipping, lower.transparent_clipping), .opacity = !safe_isnan(upper.opacity) ? upper.opacity : lower.opacity, .dim = !safe_isnan(upper.dim) ? upper.dim : lower.dim, .shader = upper.shader ? upper.shader : lower.shader, .corner_radius = upper.corner_radius >= 0 ? upper.corner_radius : lower.corner_radius, }; win_script_fold(upper.animations, lower.animations, ret.animations); return ret; } /// Unwrap a `window_maybe_options` to a `window_options`, using the default value for /// values that are not set in the `window_maybe_options`. static inline struct window_options __attribute__((always_inline)) win_maybe_options_or(struct window_maybe_options maybe, struct window_options def) { assert(def.unredir != WINDOW_UNREDIR_INVALID); struct window_options ret = { .unredir = maybe.unredir == WINDOW_UNREDIR_INVALID ? def.unredir : maybe.unredir, .blur_background = tri_or_bool(maybe.blur_background, def.blur_background), .clip_shadow_above = tri_or_bool(maybe.clip_shadow_above, def.clip_shadow_above), .full_shadow = tri_or_bool(maybe.full_shadow, def.full_shadow), .shadow = tri_or_bool(maybe.shadow, def.shadow), .corner_radius = maybe.corner_radius >= 0 ? (unsigned int)maybe.corner_radius : def.corner_radius, .fade = tri_or_bool(maybe.fade, def.fade), .invert_color = tri_or_bool(maybe.invert_color, def.invert_color), .paint = tri_or_bool(maybe.paint, def.paint), .transparent_clipping = tri_or_bool(maybe.transparent_clipping, def.transparent_clipping), .opacity = !safe_isnan(maybe.opacity) ? maybe.opacity : def.opacity, .dim = !safe_isnan(maybe.dim) ? maybe.dim : def.dim, .shader = maybe.shader ? maybe.shader : def.shader, }; win_script_fold(maybe.animations, def.animations, ret.animations); return ret; } static inline struct window_options __attribute__((always_inline)) win_options(const struct win *w) { return win_maybe_options_or( win_maybe_options_fold(w->options_override, w->options), *w->options_default); } /// Check if win_geometry `a` and `b` have the same sizes and positions. Border width is /// not considered. static inline bool win_geometry_eq(struct win_geometry a, struct win_geometry b) { return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height; } /// Process pending updates/images flags on a window. Has to be called in X critical /// section. Returns true if the window had an animation running and it has just finished, /// or if the window's states just changed and there is no animation defined for this /// state change. bool win_process_animation_and_state_change(struct session *ps, struct win *w, double delta_t); double win_animatable_get(const struct win *w, enum win_script_output output); void win_process_primary_flags(session_t *ps, struct win *w); void win_process_secondary_flags(session_t *ps, struct win *w); void win_process_image_flags(session_t *ps, struct win *w); /// Start the unmap of a window. We cannot unmap immediately since we might need to fade /// the window out. void unmap_win_start(struct win *); void unmap_win_finish(session_t *ps, struct win *w); /// Start the destroying of a window. Windows cannot always be destroyed immediately /// because of fading and such. void win_destroy_start(session_t *ps, struct win *w); void win_map_start(struct session *ps, struct win *w); void win_release_saved_win_image(backend_t *base, 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 *backend, struct win *w); winmode_t attr_pure win_calc_mode_raw(const struct win *w); // TODO(yshui) `win_calc_mode` is only used by legacy backends winmode_t attr_pure win_calc_mode(const struct win *w); /** * Set real focused state of a window. */ void win_set_focused(session_t *ps, struct win *w); void win_on_factor_change(session_t *ps, struct win *w); void win_on_client_update(session_t *ps, struct win *w); int attr_pure win_find_monitor(const struct x_monitors *monitors, const struct win *mw); /// Recheck if a window is fullscreen void win_update_is_fullscreen(const session_t *ps, struct win *w); /** * Check if a window has BYPASS_COMPOSITOR property set */ bool win_is_bypassing_compositor(const session_t *ps, const struct win *w); /** * Get a rectangular region in global coordinates a window (and possibly * its shadow) occupies. * * Note w->shadow and shadow geometry must be correct before calling this * function. */ void win_extents(const struct win *w, region_t *res); region_t win_extents_by_val(const struct win *w); /** * Add a window to damaged area. * * @param ps current session * @param w struct _win element representing the window */ void add_damage_from_win(session_t *ps, const struct win *w); /** * Get a rectangular region a window occupies, excluding frame and shadow. * * Return region in global coordinates. */ void win_get_region_noframe_local(const struct win *w, region_t *); void win_get_region_noframe_local_without_corners(const struct win *w, region_t *); /// Get the region for the frame of the window void win_get_region_frame_local(const struct 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 win *w); /// Query the Xorg for information about window `win` /// `win` pointer might become invalid after this function returns struct win *win_maybe_allocate(session_t *ps, struct wm_ref *cursor, const xcb_get_window_attributes_reply_t *attrs); /** * Release a destroyed window that is no longer needed. */ void win_destroy_finish(session_t *ps, struct win *w); /// Return whether the group a window belongs to is really focused. /// /// @param leader leader window ID /// @return true if the window group is focused, false otherwise bool win_is_group_focused(session_t *ps, struct win *w); /// check if window has ARGB visual bool attr_pure win_has_alpha(const struct 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 win *w) { return wm_ref_client_of(w->tree_ref) == NULL && !w->a.override_redirect; } static inline xcb_window_t win_id(const struct win *w) { return wm_ref_win_id(w->tree_ref); } /// Returns the client window of a window. If a client window does not exist, returns the /// window itself when `fallback_to_self` is true, otherwise returns XCB_NONE. static inline xcb_window_t win_client_id(const struct win *w, bool fallback_to_self) { auto client_win = wm_ref_client_of(w->tree_ref); if (client_win == NULL) { return fallback_to_self ? win_id(w) : XCB_NONE; } return wm_ref_win_id(client_win); } /// 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 win *w); /// Whether a given window is mapped on the X server side bool win_is_mapped_in_x(const struct win *w); /// Set flags on a window. Some sanity checks are performed void win_set_flags(struct win *w, uint64_t flags); /// Clear flags on a window. Some sanity checks are performed void win_clear_flags(struct win *w, uint64_t flags); /// Returns true if any of the flags in `flags` is set bool win_check_flags_any(struct win *w, uint64_t flags); /// Returns true if all of the flags in `flags` are set bool win_check_flags_all(struct win *w, uint64_t flags); /// Mark properties as stale for a window void win_set_properties_stale(struct win *w, const xcb_atom_t *prop, int nprops); static inline struct win_geometry win_geometry_from_configure_notify(const xcb_configure_notify_event_t *ce) { return (struct win_geometry){ .x = ce->x, .y = ce->y, .width = ce->width, .height = ce->height, .border_width = ce->border_width, }; } static inline struct win_geometry win_geometry_from_get_geometry(const xcb_get_geometry_reply_t *g) { return (struct win_geometry){ .x = g->x, .y = g->y, .width = g->width, .height = g->height, .border_width = g->border_width, }; } /// Set the pending geometry of a window. And set appropriate flags when the geometry /// changes. /// Returns true if the geometry has changed, false otherwise. bool win_set_pending_geometry(struct win *w, struct win_geometry g); bool win_update_wintype(struct x_connection *c, struct atom *atoms, struct win *w); /** * Retrieve frame extents from a window. */ void win_update_frame_extents(struct x_connection *c, struct atom *atoms, struct 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 win *w); int win_update_role(struct x_connection *c, struct atom *atoms, struct win *w); int win_update_name(struct x_connection *c, struct atom *atoms, struct win *w); void win_on_win_size_change(struct win *w, int shadow_offset_x, int shadow_offset_y, int shadow_radius); void win_update_bounding_shape(struct x_connection *c, struct win *w, bool shape_exists, bool detect_rounded_corners); bool win_update_prop_fullscreen(struct x_connection *c, const struct atom *atoms, struct win *w); static inline attr_unused void win_set_property_stale(struct win *w, xcb_atom_t prop) { return win_set_properties_stale(w, (xcb_atom_t[]){prop}, 1); } /// Free all resources in a struct win void free_win_res(session_t *ps, struct win *w); /// 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 win *w, ivec2 origin, region_t *res) { static const int corner_index[][2] = { {0, 0}, {0, 1}, {1, 0}, {1, 1}, }; int corner_radius = (int)win_options(w).corner_radius; 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 - corner_radius), .y1 = origin.y + corner_index[i][1] * (w->heightb - corner_radius), }; rectangles[i].x2 = rectangles[i].x1 + corner_radius; rectangles[i].y2 = rectangles[i].y1 + corner_radius; } region_t corners; 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 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 win *w) { region_t ret; pixman_region32_init(&ret); pixman_region32_copy(&ret, &w->bounding_shape); pixman_region32_translate(&ret, w->g.x, w->g.y); return ret; } static inline region_t win_get_bounding_shape_global_without_corners_by_val(struct win *w) { region_t ret; pixman_region32_init(&ret); pixman_region32_copy(&ret, &w->bounding_shape); win_region_remove_corners_local(w, &ret); pixman_region32_translate(&ret, w->g.x, w->g.y); return ret; } /** * Calculate the extents of the frame of the given window based on EWMH * _NET_FRAME_EXTENTS and the X window border width. */ static inline margin_t attr_pure attr_unused win_calc_frame_extents(const struct win *w) { margin_t result = w->frame_extents; result.top = max2(result.top, w->g.border_width); result.left = max2(result.left, w->g.border_width); result.bottom = max2(result.bottom, w->g.border_width); result.right = max2(result.right, w->g.border_width); return result; } /** * Check whether a window has WM frames. */ static inline bool attr_pure attr_unused win_has_frame(const struct win *w) { return w->g.border_width || w->frame_extents.top || w->frame_extents.left || w->frame_extents.right || w->frame_extents.bottom; } static inline const char *attr_pure attr_unused win_wm_ref_name(const struct wm_ref *ref) { if (ref == NULL) { return NULL; } auto win = wm_ref_deref(ref); if (win == NULL) { return NULL; } return win->name; } picom-12.5/src/wm/wm.c000066400000000000000000000663471471504570600146020ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) Yuxuan Shui #include #include #include #include "common.h" #include "log.h" #include "utils/list.h" #include "x.h" #include "win.h" #include "wm.h" #include "wm_internal.h" struct wm_query_tree_request { struct x_async_request_base base; struct wm *wm; struct atom *atoms; xcb_window_t wid; }; struct wm_get_property_request { struct x_async_request_base base; struct wm *wm; xcb_window_t wid; }; struct wm { /// The toplevel currently being focused. Concretely this is the toplevel window /// that contains the window currently considered by the X server to be focused. struct wm_tree_node *focused_win; struct wm_tree tree; /// This is a virtual root for all "orphaned" windows. A window is orphaned /// if it is not reachable from the root node. This can only be non-empty if /// the tree is not consistent, i.e. there are pending async query tree requests. /// /// Note an orphaned window cannot be a toplevel. This is trivially true because /// a toplevel has the root window as its parent, and once the root window is /// created its list of children is always up to date. struct wm_tree_node orphan_root; /// Number of pending async imports. This tracks the async event mask setups and /// query tree queries. We also have async get property requests, but they are not /// tracked because they don't affect the tree structure. unsigned n_pending_imports; /// Whether cached window leaders should be recalculated. Following tree changes /// will trigger a leader refresh: /// - A toplevel is added. This is because a window leader might be set before /// we /// have imported the target window into our tree. /// - A toplevel is removed. /// - The leader property of any toplevel is changed. A window's leader changing /// affects /// all of its descendants in the leadership tree (this is different from the /// window tree). But keeping track of the leadership tree is too much work, /// so we just refresh all cached leaders. bool needs_leader_refresh; }; // TODO(yshui): this is a bit weird and I am not decided on it yet myself. Maybe we can // expose `wm_tree_node` directly. But maybe we want to bundle some additional data with // it. Anyway, this is probably easy to get rid of if we want to. /// A wrapper of `wm_tree_node`. This points to the `siblings` `struct list_node` in a /// `struct wm_tree_node`. struct wm_ref { struct list_node inner; }; static_assert(offsetof(struct wm_ref, inner) == 0, "wm_cursor should be usable as a " "wm_tree_node"); static_assert(alignof(struct wm_ref) == alignof(struct list_node), "wm_cursor should have the same alignment as wm_tree_node"); static inline const struct wm_tree_node *to_tree_node(const struct wm_ref *cursor) { return cursor != NULL ? list_entry(&cursor->inner, struct wm_tree_node, siblings) : NULL; } static inline struct wm_tree_node *to_tree_node_mut(struct wm_ref *cursor) { return cursor != NULL ? list_entry(&cursor->inner, struct wm_tree_node, siblings) : NULL; } xcb_window_t wm_ref_win_id(const struct wm_ref *cursor) { return to_tree_node(cursor)->id.x; } wm_treeid wm_ref_treeid(const struct wm_ref *cursor) { return to_tree_node(cursor)->id; } struct win *wm_ref_deref(const struct wm_ref *cursor) { auto node = to_tree_node(cursor); if (node->parent == NULL) { log_error("Trying to dereference a root node. Expect malfunction."); return NULL; } if (node->parent->parent != NULL) { // Don't return the client window if this is not a toplevel node. This // saves us from needing to clear `->win` when a window is reparented. return NULL; } return node->win; } void wm_ref_set(struct wm_ref *cursor, struct win *w) { to_tree_node_mut(cursor)->win = w; } void wm_ref_set_focused(struct wm *wm, struct wm_ref *cursor) { auto node = to_tree_node_mut(cursor); if (wm->focused_win == node) { return; } log_debug("Focused window changed from %#010x to %#010x", wm->focused_win ? wm->focused_win->id.x : 0, node ? node->id.x : 0); wm->focused_win = node; } void wm_ref_set_leader(struct wm *wm, struct wm_ref *cursor, xcb_window_t leader) { // This function only changes `leader`, not the cached `leader_final`. So this has // no impact on the returned node of `wm_focused_leader()` - it will be updated // along with all `leader_final`s in `wm_refresh_leaders`. struct wm_tree_node *node = wm_tree_find_toplevel_for(&wm->tree, to_tree_node_mut(cursor)); if (node->leader == leader) { return; } wm->needs_leader_refresh = true; node->leader = leader; } struct wm_ref *wm_focused_win(struct wm *wm) { return wm->focused_win ? (struct wm_ref *)&wm->focused_win->siblings : NULL; } const struct wm_ref *wm_focused_leader(struct wm *wm) { return wm->focused_win != NULL ? (struct wm_ref *)&wm->focused_win->leader_final->siblings : NULL; } const struct wm_ref *wm_ref_leader(const struct wm_ref *cursor) { auto node = to_tree_node(cursor); return (struct wm_ref *)&node->leader_final->siblings; } bool wm_ref_is_zombie(const struct wm_ref *cursor) { return to_tree_node(cursor)->is_zombie; } struct wm_ref *wm_ref_below(const struct wm_ref *cursor) { return &to_tree_node(cursor)->parent->children != cursor->inner.next ? (struct wm_ref *)cursor->inner.next : NULL; } struct wm_ref *wm_ref_above(const struct wm_ref *cursor) { return &to_tree_node(cursor)->parent->children != cursor->inner.prev ? (struct wm_ref *)cursor->inner.prev : NULL; } struct wm_ref *wm_root_ref(const struct wm *wm) { return (struct wm_ref *)&wm->tree.root->siblings; } struct wm_ref *wm_ref_topmost_child(const struct wm_ref *cursor) { auto node = to_tree_node(cursor); return !list_is_empty(&node->children) ? (struct wm_ref *)node->children.next : NULL; } struct wm_ref *wm_ref_bottommost_child(const struct wm_ref *cursor) { auto node = to_tree_node(cursor); return !list_is_empty(&node->children) ? (struct wm_ref *)node->children.prev : NULL; } struct wm_ref *wm_find(const struct wm *wm, xcb_window_t id) { auto node = wm_tree_find(&wm->tree, id); return node != NULL ? (struct wm_ref *)&node->siblings : NULL; } struct wm_ref *wm_find_by_client(const struct wm *wm, xcb_window_t client) { auto node = wm_tree_find(&wm->tree, client); if (node == NULL || node->parent == NULL) { // If `client` is the root window, we return NULL too, technically // the root window doesn't have a client window. return NULL; } auto toplevel = wm_tree_find_toplevel_for(&wm->tree, node); return toplevel != NULL ? (struct wm_ref *)&toplevel->siblings : NULL; } struct wm_ref *wm_ref_toplevel_of(const struct wm *wm, struct wm_ref *cursor) { auto toplevel = wm_tree_find_toplevel_for(&wm->tree, to_tree_node_mut(cursor)); return toplevel != NULL ? (struct wm_ref *)&toplevel->siblings : NULL; } struct wm_ref *wm_ref_client_of(struct wm_ref *cursor) { auto client = to_tree_node(cursor)->client_window; return client != NULL ? (struct wm_ref *)&client->siblings : NULL; } struct wm_ref *wm_stack_end(struct wm *wm) { return (struct wm_ref *)&wm->tree.root->children; } static struct wm_tree_node *wm_find_leader(struct wm *wm, struct wm_tree_node *node) { if (node->leader_final != NULL) { if (node->visited) { log_warn("Window %#010x is part of a cycle in the leadership " "tree", node->id.x); } return node->leader_final; } node->leader_final = node; if (node->leader != node->id.x) { auto leader_node = wm_tree_find(&wm->tree, node->leader); if (leader_node == NULL) { return node->leader_final; } leader_node = wm_tree_find_toplevel_for(&wm->tree, leader_node); if (leader_node == NULL) { log_debug("Cannot find toplevel for leader %#010x of window " "%#010x. tree consistency: %d", node->leader, node->id.x, wm_is_consistent(wm)); wm->needs_leader_refresh = true; return node->leader_final; } node->visited = true; node->leader_final = wm_find_leader(wm, leader_node); node->visited = false; } return node->leader_final; } void wm_refresh_leaders(struct wm *wm) { if (!wm->needs_leader_refresh) { return; } log_debug("Refreshing window leaders, tree consistency: %d", wm_is_consistent(wm)); wm->needs_leader_refresh = false; list_foreach(struct wm_tree_node, i, &wm->tree.root->children, siblings) { if (i->is_zombie) { // Don't change anything about a zombie window. continue; } i->leader_final = NULL; } list_foreach(struct wm_tree_node, i, &wm->tree.root->children, siblings) { if (i->is_zombie) { continue; } wm_find_leader(wm, i); BUG_ON_NULL(i->leader_final); log_verbose("Window %#010x has leader %#010x", i->id.x, i->leader_final->id.x); } if (wm->needs_leader_refresh) { log_debug("Leaders not fully resolved, will try again later."); } } /// Move window `w` so it's right above `below`, if `below` is XCB_NONE, `w` is moved /// to the bottom of the stack void wm_stack_move_to_above(struct wm *wm, struct wm_ref *cursor, xcb_window_t below) { BUG_ON_NULL(cursor); auto node = to_tree_node_mut(cursor); if (node->parent == &wm->orphan_root) { // If this window is orphaned, moving it around its siblings is // meaningless. Same below. log_debug("Ignoring restack request for orphaned window %#010x", node->id.x); return; } if (below == XCB_NONE) { // `below` being XCB_NONE means the window is put at the bottom. wm_tree_move_to_end(&wm->tree, node, /*to_bottom=*/true); return; } auto below_node = wm_tree_find(&wm->tree, below); if (below_node == NULL) { log_error("Trying to restack window %#010x, but its sibling window " "%#010x is not in our tree. Expect malfunction.", node->id.x, below); assert(false); return; } wm_tree_move_to_above(&wm->tree, node, below_node); } void wm_stack_move_to_end(struct wm *wm, struct wm_ref *cursor, bool to_bottom) { auto node = to_tree_node_mut(cursor); if (node->parent == &wm->orphan_root) { return; } wm_tree_move_to_end(&wm->tree, node, to_bottom); } struct wm *wm_new(void) { auto wm = ccalloc(1, struct wm); wm_tree_init(&wm->tree); list_init_head(&wm->orphan_root.children); wm->n_pending_imports = 0; return wm; } void wm_free(struct wm *wm) { // Free all `struct win`s associated with tree nodes, this leaves dangling // pointers, but we are freeing the tree nodes immediately after, so everything // is fine (TM). if (wm->tree.root != NULL) { wm_stack_foreach_safe(wm, i, next) { auto w = wm_ref_deref(i); auto tree_node = to_tree_node_mut(i); free(w); if (tree_node->is_zombie) { // This mainly happens on `session_destroy`, e.g. when // there's ongoing animations. log_debug("Leftover zombie node for window %#010x", tree_node->id.x); wm_tree_reap_zombie(tree_node); } } } wm_tree_clear(&wm->tree); assert(wm_is_consistent(wm)); assert(list_is_empty(&wm->orphan_root.children)); free(wm); } /// Move `from->win` to `to->win`, update `win->tree_ref`. static void wm_move_win(struct wm_tree_node *from, struct wm_tree_node *to) { if (from->win != NULL) { from->win->tree_ref = (struct wm_ref *)&to->siblings; } to->win = from->win; from->win = NULL; } static void wm_detach_inner(struct wm *wm, struct wm_tree_node *child) { auto zombie = wm_tree_detach(&wm->tree, child); assert(zombie != NULL || child->win == NULL); if (zombie != NULL) { wm_move_win(child, zombie); } } /// Disconnect `child` from `parent`. This is called when a window is removed from its /// parent's children list. This can be a result of the window being destroyed, or /// reparented to another window. This `child` is attached to the orphan root. void wm_disconnect(struct wm *wm, xcb_window_t child, xcb_window_t parent, xcb_window_t new_parent) { auto parent_node = wm_tree_find(&wm->tree, parent); auto new_parent_node = wm_tree_find(&wm->tree, new_parent); BUG_ON_NULL(parent_node); auto child_node = wm_tree_find(&wm->tree, child); if (child_node == NULL) { // X sends DestroyNotify from child first, then from parent. So if the // child was imported, we will get here. This is the normal case. // // This also happens if the child is created then immediately destroyed, // before we get the CreateNotify event for it. In // `wm_import_start_no_flush` we will ignore this window because setting // the event mask will fail. Then once we get the DestroyNotify for it // from the parent, we get here. log_debug("Child window %#010x of %#010x is not in our tree, ignoring", child, parent); return; } // If child_node->parent is not parent_node, then the parent must be in the // process of being queried. In that case the child_node must be orphaned. BUG_ON(child_node->parent != parent_node && (parent_node->tree_queried || child_node->parent != &wm->orphan_root)); wm_detach_inner(wm, child_node); if ((new_parent_node != NULL && new_parent_node->receiving_events) || child_node->receiving_events) { // We need to be sure we will be able to keep track of all orphaned // windows to know none of them will be destroyed without us knowing. If // we have already set up event mask for the new parent, we will be // getting a reparent event from the new parent, which means we will have // an eye on `child` at all times, so that's fine. If we have already set // up event mask on the child itself, that's even better. log_debug("Disconnecting window %#010x from window %#010x", child, parent); wm_tree_attach(&wm->tree, child_node, &wm->orphan_root); } else { // Otherwise, we might potentially lose track of this window, so we have // to destroy it. log_debug("Destroy window %#010x because we can't keep track of it", child); HASH_DEL(wm->tree.nodes, child_node); free(child_node); } } void wm_destroy(struct wm *wm, xcb_window_t wid) { struct wm_tree_node *node = wm_tree_find(&wm->tree, wid); BUG_ON_NULL(node); log_debug("Destroying window %#010x", wid); if (!list_is_empty(&node->children)) { log_error("Window %#010x is destroyed but it still has children. " "Orphaning them.", wid); list_foreach(struct wm_tree_node, i, &node->children, siblings) { log_error(" Child: %#010x", i->id.x); } list_splice(&node->children, &wm->orphan_root.children); } if (node == wm->focused_win) { wm->focused_win = NULL; } wm_detach_inner(wm, node); HASH_DEL(wm->tree.nodes, node); free(node); } void wm_reap_zombie(struct wm_ref *zombie) { wm_tree_reap_zombie(to_tree_node_mut(zombie)); } struct wm_wid_or_node { union { xcb_window_t wid; struct wm_tree_node *node; }; bool is_wid; }; /// Start the import process of `wid`. If `new` is not NULL, it means the window is /// reusing the same window ID as a previously destroyed window, and that destroyed window /// is in our orphan tree. In this case, we revive the orphaned window instead of creating /// a new one. static void wm_import_start_inner(struct wm *wm, struct x_connection *c, struct atom *atoms, xcb_window_t wid, struct wm_tree_node *parent); static void wm_reparent_inner(struct wm *wm, struct x_connection *c, struct atom *atoms, xcb_window_t wid, struct wm_tree_node *new_parent) { BUG_ON_NULL(new_parent); auto window = wm_tree_find(&wm->tree, wid); /// If a previously unseen window is reparented to a window that has been fully /// imported, we must treat it as a newly created window. Because it will not be /// included in a query tree reply, so we must initiate its import process /// explicitly. if (window == NULL) { if (new_parent->tree_queried) { // Contract: we just checked `wid` is not in the tree. wm_import_start_inner(wm, c, atoms, wid, new_parent); } return; } if (window->parent == new_parent) { // Reparent to the same parent moves the window to the top of the // stack BUG_ON(!new_parent->tree_queried); wm_tree_move_to_end(&wm->tree, window, false); return; } BUG_ON(window->parent != &wm->orphan_root); // Attaching `window` to `new_parent` will change the children list of // `new_parent`, if there is a pending query tree request for `new_parent`, doing // so will create an overlap. In other words, `window` will appear in the query // tree reply too. Generally speaking, we want to keep a node's children list // empty while there is a pending query tree request for it. (Imagine sending the // query tree "locks" the children list until the reply is processed). Same logic // applies to `wm_import_start`. // // Alternatively if the new parent isn't in our tree yet, we orphan the window // too. Or if we have an orphaned window indicating the new parent was reusing // a destroyed window's ID, then we know we will re-query the new parent later // when we encounter it in a query tree reply, so we orphan the window in this // case as well. if (!new_parent->tree_queried) { log_debug("Window %#010x is attached to window %#010x that is currently " "been queried, orphaning.", window->id.x, new_parent->id.x); return; } log_debug("Reparented window %#010x to window %#010x", window->id.x, new_parent->id.x); BUG_ON(wm_tree_detach(&wm->tree, window) != NULL); wm_tree_attach(&wm->tree, window, new_parent); } void wm_reparent(struct wm *wm, struct x_connection *c, struct atom *atoms, xcb_window_t wid, xcb_window_t parent) { return wm_reparent_inner(wm, c, atoms, wid, wm_tree_find(&wm->tree, parent)); } void wm_set_has_wm_state(struct wm *wm, struct wm_ref *cursor, bool has_wm_state) { wm_tree_set_wm_state(&wm->tree, to_tree_node_mut(cursor), has_wm_state); } static const xcb_event_mask_t WM_IMPORT_EV_MASK = XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE; static void wm_handle_query_tree_reply(struct x_connection *c, struct x_async_request_base *base, const xcb_raw_generic_event_t *reply_or_error) { auto req = (struct wm_query_tree_request *)base; auto atoms = req->atoms; auto wm = req->wm; auto node = wm_tree_find(&wm->tree, req->wid); free(req); wm->n_pending_imports--; if (reply_or_error == NULL) { // The program is quitting... If this happens when wm is in an // inconsistent state, there could be errors. // TODO(yshui): fix this return; } if (node == NULL) { // If the node was previously destroyed, then it means a newly created // window has reused its window ID. We should ignore the query tree reply, // because we haven't setup event mask for this window yet, we won't be // able to stay up-to-date with this window, and would have to re-query in // `wm_import_start_no_flush` anyway. And we don't have a node to attach // the children to anyway. if (reply_or_error->response_type != 0) { log_debug("Ignoring query tree reply for window not in our " "tree."); BUG_ON(wm_is_consistent(wm)); } return; } if (!node->receiving_events) { log_debug("Window ID %#010x is destroyed then reused, and hasn't had " "event mask set yet.", node->id.x); return; } if (reply_or_error->response_type == 0) { // This is an error, most likely the window is gone when we tried // to query it. // A window we are tracking died without us knowing, this should // be impossible. xcb_generic_error_t *err = (xcb_generic_error_t *)reply_or_error; log_error("Query tree request for window %#010x failed with error %s.", node == NULL ? 0 : node->id.x, x_strerror(err)); BUG_ON(false); } if (node->tree_queried) { log_debug("Window %#010x has already been queried.", node->id.x); return; } node->tree_queried = true; auto reply = (const xcb_query_tree_reply_t *)reply_or_error; log_debug("Finished querying tree for window %#010x", node->id.x); auto children = xcb_query_tree_children(reply); log_debug("Window %#010x has %d children", node->id.x, xcb_query_tree_children_length(reply)); for (int i = 0; i < xcb_query_tree_children_length(reply); i++) { auto child = children[i]; // wm_reparent handles both the case where child is new, and the case // where the child is an known orphan. wm_reparent_inner(wm, c, atoms, child, node); } } static void wm_handle_get_wm_state_reply(struct x_connection * /*c*/, struct x_async_request_base *base, const xcb_raw_generic_event_t *reply_or_error) { auto req = (struct wm_get_property_request *)base; if (reply_or_error == NULL) { free(req); return; } // We guarantee that if a query tree request is pending, its corresponding // window tree node won't be reaped. But we don't guarantee the same for // get property requests. So we need to search the node by window ID again. if (reply_or_error->response_type == 0) { // This is an error, most likely the window is gone when we tried // to query it. (Note the tree node might have been freed at this // point if the query tree request also failed earlier.) xcb_generic_error_t *err = (xcb_generic_error_t *)reply_or_error; log_debug("Get WM_STATE request for window %#010x failed with " "error %s", req->wid, x_strerror(err)); free(req); return; } auto node = wm_tree_find(&req->wm->tree, req->wid); if (node == NULL) { // An in-flight query tree request will keep node alive, but if the query // tree request is completed, this node will be allowed to be destroyed. // So if we received a DestroyNotify for `node` in between the reply to // query tree and the reply to get property, `node` will be NULL here. free(req); return; } auto reply = (const xcb_get_property_reply_t *)reply_or_error; wm_tree_set_wm_state(&req->wm->tree, node, reply->type != XCB_NONE); free(req); } struct wm_set_event_mask_request { struct x_async_request_base base; struct wm *wm; struct atom *atoms; xcb_window_t wid; }; static void wm_handle_set_event_mask_reply(struct x_connection *c, struct x_async_request_base *base, const xcb_raw_generic_event_t *reply_or_error) { auto req = (struct wm_set_event_mask_request *)base; auto wm = req->wm; auto atoms = req->atoms; auto wid = req->wid; free(req); if (reply_or_error == NULL) { goto end_import; } if (reply_or_error->response_type == 0) { log_debug("Failed to set event mask for window %#010x: %s, ignoring this " "window.", wid, x_strerror((const xcb_generic_error_t *)reply_or_error)); goto end_import; } auto node = wm_tree_find(&wm->tree, wid); if (node == NULL) { // The window initiated this request is gone, but a window with this wid // does exist - we just haven't gotten the event for its creation yet. We // create a placeholder for it. node = wm_tree_new_window(&wm->tree, wid); wm_tree_add_window(&wm->tree, node); wm_tree_attach(&wm->tree, node, &wm->orphan_root); } if (node->receiving_events) { // This means another set event mask request was already completed before // us. We don't need to do anything. log_debug("Event mask already set for window %#010x", wid); goto end_import; } log_debug("Event mask set for window %#010x, sending query tree.", wid); node->receiving_events = true; { auto req2 = ccalloc(1, struct wm_query_tree_request); req2->base.callback = wm_handle_query_tree_reply; req2->wid = node->id.x; req2->wm = wm; req2->atoms = atoms; x_async_query_tree(c, node->id.x, &req2->base); } // (It's OK to resend the get property request even if one is already in-flight, // unlike query tree.) { auto req2 = ccalloc(1, struct wm_get_property_request); req2->base.callback = wm_handle_get_wm_state_reply; req2->wm = wm; req2->wid = node->id.x; x_async_get_property(c, node->id.x, atoms->aWM_STATE, XCB_ATOM_ANY, 0, 2, &req2->base); } return; end_import: wm->n_pending_imports--; } /// Create a window for `wid`. Send query tree and get property requests, for this new /// window. Caller must guarantee `wid` isn't already in the tree. /// /// Note this function does not flush the X connection. static void wm_import_start_inner(struct wm *wm, struct x_connection *c, struct atom *atoms, xcb_window_t wid, struct wm_tree_node *parent) { // We need to create a tree node immediate before we even know if it still exists. // Because otherwise we have nothing to keep track of its stacking order. auto new = wm_tree_new_window(&wm->tree, wid); wm_tree_add_window(&wm->tree, new); wm_tree_attach(&wm->tree, new, parent); log_debug("Starting import process for window %#010x", new->id.x); auto req = ccalloc(1, struct wm_set_event_mask_request); req->base.callback = wm_handle_set_event_mask_reply; req->wid = wid; req->atoms = atoms; req->wm = wm; x_async_change_window_attributes( c, wid, XCB_CW_EVENT_MASK, (const uint32_t[]){WM_IMPORT_EV_MASK}, &req->base); wm->n_pending_imports++; } void wm_import_start(struct wm *wm, struct x_connection *c, struct atom *atoms, xcb_window_t wid, struct wm_ref *parent) { struct wm_tree_node *parent_node = parent != NULL ? to_tree_node_mut(parent) : NULL; if (parent_node != NULL && !parent_node->tree_queried) { // Parent node is currently being queried, we can't attach the new window // to it as that will change its children list. return; } // Contract: how do we know `wid` is not in the tree? First of all, if `wid` is in // the tree, it must be an orphan node, otherwise that would mean the same `wid` // is children of two different windows. Notice a node is added to the orphan root // only after we have confirmed its existence by setting up its event mask. Also // notice replies to event mask setup requests are processed in order. So if we // haven't received a CreateNotify before a window enters the orphan tree, we will // _never_ get a CreateNotify for it. // // We get to here by receiving a CreateNotify for `wid`, therefore `wid` cannot be // in the orphan tree. wm_import_start_inner(wm, c, atoms, wid, parent_node); } bool wm_is_consistent(const struct wm *wm) { return wm->n_pending_imports == 0 && list_is_empty(&wm->orphan_root.children); } bool wm_has_tree_changes(const struct wm *wm) { return !list_is_empty(&wm->tree.changes); } struct wm_change wm_dequeue_change(struct wm *wm) { auto tree_change = wm_tree_dequeue_change(&wm->tree); struct wm_change ret = { .type = tree_change.type, .toplevel = NULL, }; switch (tree_change.type) { case WM_TREE_CHANGE_CLIENT: ret.client.old = tree_change.client.old; ret.client.new_ = tree_change.client.new_; ret.toplevel = (struct wm_ref *)&tree_change.client.toplevel->siblings; break; case WM_TREE_CHANGE_TOPLEVEL_KILLED: ret.toplevel = (struct wm_ref *)&tree_change.killed->siblings; wm->needs_leader_refresh = true; break; case WM_TREE_CHANGE_TOPLEVEL_NEW: ret.toplevel = (struct wm_ref *)&tree_change.new_->siblings; wm->needs_leader_refresh = true; break; default: break; } return ret; } struct wm_ref *wm_new_mock_window(struct wm *wm, xcb_window_t wid) { auto node = wm_tree_new_window(&wm->tree, wid); return (struct wm_ref *)&node->siblings; } void wm_free_mock_window(struct wm * /*wm*/, struct wm_ref *cursor) { free(to_tree_node_mut(cursor)); } picom-12.5/src/wm/wm.h000066400000000000000000000250351471504570600145740ustar00rootroot00000000000000// 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 #include #include #include "compiler.h" struct wm; struct 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; }; enum wm_tree_change_type { /// The client window of a toplevel changed WM_TREE_CHANGE_CLIENT, /// A toplevel window is killed on the X server side /// A zombie will be left in its place. WM_TREE_CHANGE_TOPLEVEL_KILLED, /// A new toplevel window appeared WM_TREE_CHANGE_TOPLEVEL_NEW, // TODO(yshui): This is a stop-gap measure to make sure we invalidate `reg_ignore` // of windows. Once we get rid of `reg_ignore`, which is only used by the legacy // backends, this event should be removed. // // (`reg_ignore` is the cached cumulative opaque region of all windows above a // window in the stacking order. If it actually is performance critical, we // can probably cache it more cleanly in renderer layout.) /// The stacking order of toplevel windows changed. Note, toplevel gone/new /// changes also imply a restack. WM_TREE_CHANGE_TOPLEVEL_RESTACKED, /// Nothing changed WM_TREE_CHANGE_NONE, }; typedef struct wm_treeid { /// The generation of the window ID. This is used to detect if the window ID is /// reused. Inherited from the wm_tree at cr alignas(8) uint64_t gen; /// The X window ID. xcb_window_t x; /// Explicit padding char padding[4]; } wm_treeid; static const wm_treeid WM_TREEID_NONE = {.gen = 0, .x = XCB_NONE}; static_assert(sizeof(wm_treeid) == 16, "wm_treeid size is not 16 bytes"); static_assert(alignof(wm_treeid) == 8, "wm_treeid alignment is not 8 bytes"); struct wm_change { enum wm_tree_change_type type; /// The toplevel window this change is about. For /// `WM_TREE_CHANGE_TOPLEVEL_KILLED`, this is the zombie window left in place of /// the killed toplevel. For `WM_TREE_CHANGE_TOPLEVEL_RESTACKED`, this is NULL. struct wm_ref *toplevel; struct { wm_treeid old; wm_treeid new_; } client; }; /// Reference to a window in the `struct wm`. Most of wm operations operate on wm_refs. If /// the referenced window is managed, a `struct window` can be retrieved by /// `wm_ref_deref`. struct wm_ref; struct atom; static inline bool wm_treeid_eq(wm_treeid a, wm_treeid b) { return a.gen == b.gen && a.x == b.x; } /// Create a new window management object. /// Caller is expected to first call `wm_import_start` with the root window after /// creating the object. Otherwise many operations will fail or crash. struct wm *wm_new(void); void wm_free(struct wm *wm); /// The current focused toplevel, based on information from X server. struct wm_ref *wm_focused_win(struct wm *wm); /// The current cached focused leader. This is only update to date after a call to /// `wm_refresh_leaders`, and before any changes to the wm tree. Otherwise this could /// return a dangling pointer. const struct wm_ref *wm_focused_leader(struct wm *wm); // 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 have close animation. 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 wm_ref *attr_pure wm_find(const struct wm *wm, xcb_window_t id); // Find the WM frame of a client window. `id` is the client window id. struct wm_ref *attr_pure wm_find_by_client(const struct wm *wm, xcb_window_t client); /// Find the toplevel of a window by going up the window tree. struct wm_ref *attr_pure wm_ref_toplevel_of(const struct wm *wm, struct wm_ref *cursor); /// Return the client window of a window. Must be called with a cursor to a toplevel. /// Returns NULL if there is no client window. struct wm_ref *attr_pure wm_ref_client_of(struct wm_ref *cursor); /// Find the next window in the window stack. Returns NULL if `cursor` is the last window. struct wm_ref *attr_pure wm_ref_below(const struct wm_ref *cursor); struct wm_ref *attr_pure wm_ref_above(const struct wm_ref *cursor); struct wm_ref *attr_pure wm_root_ref(const struct wm *wm); struct wm_ref *attr_pure wm_ref_topmost_child(const struct wm_ref *cursor); struct wm_ref *attr_pure wm_ref_bottommost_child(const struct wm_ref *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_to_above(struct wm *wm, struct wm_ref *cursor, xcb_window_t below); /// Move window `w` to the top of the stack. void wm_stack_move_to_end(struct wm *wm, struct wm_ref *cursor, bool to_bottom); struct win *attr_pure wm_ref_deref(const struct wm_ref *cursor); xcb_window_t attr_pure wm_ref_win_id(const struct wm_ref *cursor); wm_treeid attr_pure wm_ref_treeid(const struct wm_ref *cursor); /// Assign a window to a cursor. The cursor must not already have a window assigned. void wm_ref_set(struct wm_ref *cursor, struct win *w); /// Mark `cursor` as the focused window. void wm_ref_set_focused(struct wm *wm, struct wm_ref *cursor); void wm_ref_set_leader(struct wm *wm, struct wm_ref *cursor, xcb_window_t leader); /// Get the current cached group leader of a window. This is only update to date after a /// call to `wm_refresh_leaders`. const struct wm_ref *wm_ref_leader(const struct wm_ref *cursor); bool attr_pure wm_ref_is_zombie(const struct wm_ref *cursor); /// Recalculate all cached leaders, and update `wm_focused_leader`. void wm_refresh_leaders(struct wm *wm); /// Destroy a window. Children of this window should already have been destroyed. This /// will cause a `WM_TREE_CHANGE_TOPLEVEL_KILLED` event to be generated, and a zombie /// window to be placed where the window was. void wm_destroy(struct wm *wm, xcb_window_t wid); /// Remove a zombie window from the window tree. void wm_reap_zombie(struct wm_ref *zombie); void wm_reparent(struct wm *wm, struct x_connection *c, struct atom *atoms, xcb_window_t wid, xcb_window_t parent); /// Disconnect `child` from its `parent`. If `new_parent_known` is true, the new parent /// is a fully imported window in our tree. Otherwise, the new parent is either unknown, /// or in the process of being imported. void wm_disconnect(struct wm *wm, xcb_window_t child, xcb_window_t parent, xcb_window_t new_parent); void wm_set_has_wm_state(struct wm *wm, struct wm_ref *cursor, bool has_wm_state); /// Start the import process for `wid`. /// /// This function sets up event masks for `wid`, and start an async query tree request on /// it. When the query tree request is completed, `wm_handle_query_tree_reply` will be /// called to actually insert the window into the window tree. /// `wm_handle_query_tree_reply` will also in turn start the import process for all /// children of `wid`. /// /// The reason for this two step process is because we want to catch all windows ever /// created on X server's side. It's not enough to just set up event masks and wait for /// events. Because at the point in time we set up the event mask, some child windows /// could have already been created. It would have been nice if there is a way to listen /// for events on the whole window tree, for all present and future windows. But alas, the /// X11 protocol is lacking in this area. /// /// The best thing we can do is set up the event mask to catch all future events, and then /// query the current state. But there are complications with this approach, too. Because /// there is no way to atomically do these two things in one go, things could happen /// between these two steps, for which we will receive events. Some of these are easy to /// deal with, e.g. if a window is created, we will get an event for that, and later we /// will see that window again in the query tree reply. These are easy to ignore. Some are /// more complex. Because there could be some child windows we are not aware of. We could /// get events for windows that we don't know about. We try our best to ignore those /// events. /// /// Another problem with this is, the usual way we send a request then process the reply /// is synchronous. i.e. with `xcb__reply(xcb_(...))`. As previously /// mentioned, we might receive events before the reply. And in this case we would process /// the reply _before_ any of those events! This might be benign, but it is difficult to /// audit all the possible cases and events to make sure this always work. One example is, /// imagine a window A is being imported, and another window B, which is already in the /// tree got reparented to A. We would think B appeared in two places if we processed /// query tree reply before the reparent event. For that, we introduce the concept of /// "async requests". Replies to these requests are received and processed like other X /// events. With that, we don't need to worry about the order of events and replies. /// /// (Now you have a glimpse of how much X11 sucks.) void wm_import_start(struct wm *wm, struct x_connection *c, struct atom *atoms, xcb_window_t wid, struct wm_ref *parent); /// Check if there are tree change events bool wm_has_tree_changes(const struct wm *wm); struct wm_change wm_dequeue_change(struct wm *wm); /// Whether the window tree should be in a consistent state. When the window tree is /// consistent, we should not be receiving X events that refer to windows that we don't /// know about. And we also should not have any orphaned windows in the tree. bool wm_is_consistent(const struct wm *wm); // Unit testing helpers struct wm_ref *wm_new_mock_window(struct wm *wm, xcb_window_t wid); void wm_free_mock_window(struct wm *wm, struct wm_ref *cursor); picom-12.5/src/wm/wm_internal.h000066400000000000000000000142111471504570600164620ustar00rootroot00000000000000#pragma once #include #include #include #include "utils/list.h" #include "wm.h" struct wm_tree { /// The generation of the wm tree. This number is incremented every time a new /// window is created. /// /// Because X server recycles window IDs, X ID alone is not enough to uniquely /// identify a window. This generation number is incremented every time a window /// is created, so even if a window ID is reused, its generation number is /// guaranteed to be different from before. Unless, of course, 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 gen; /// wm tree nodes indexed by their X window ID. struct wm_tree_node *nodes; struct wm_tree_node *root; struct list_node changes; struct list_node free_changes; }; struct wm_query_tree_request; struct wm_tree_node { UT_hash_handle hh; struct list_node siblings; struct list_node children; wm_treeid id; struct win *win; struct wm_tree_node *parent; /// The client window. Only a toplevel can have a client window. struct wm_tree_node *client_window; /// The leader of the window group. /// `leader` is the immediate leader of the window, while `leader_final` is the /// "final" leader, i.e. the last leader if you follow the leader chain. /// `leader` is a direct property coming from the X server, while `leader_final` /// is calculated. `leader_final` is calculated by `wm_refresh_leaders` if `struct /// wm::need_leader_refresh` is true. /// /// Note we cannot store pointer to tree node for `leader`. Because leader update /// and window destruction are not atomic, e.g. when a window is destroyed, some /// window's leader may still point to the destroyed window. This also means X /// leader is inherently racy w.r.t. window ID reuse. Leader tracking really is /// just best effort. struct wm_tree_node *leader_final; xcb_window_t leader; bool has_wm_state : 1; /// Whether this window exists only on our side. A zombie window is a toplevel /// that has been destroyed or reparented (i.e. no long a toplevel) on the X /// server side, but is kept on our side for things like animations. A zombie /// window cannot be found in the wm_tree hash table. bool is_zombie : 1; bool visited : 1; /// Whether we have set up event masks on this window. This means we can reliably /// detect if the window is destroyed. bool receiving_events : 1; /// If the initial query tree request has completed. This means the children list /// of this window is complete w.r.t. the event stream. bool tree_queried : 1; }; /// Describe a change of a toplevel's client window. /// A `XCB_NONE` in either `old_client` or `new_client` means a missing client window. /// i.e. if `old_client` is `XCB_NONE`, it means the toplevel window did not have a client /// window before the change, and if `new_client` is `XCB_NONE`, it means the toplevel /// window lost its client window after the change. struct wm_tree_change { wm_treeid toplevel; union { /// Information for `WM_TREE_CHANGE_CLIENT`. struct { struct wm_tree_node *toplevel; /// The old and new client windows. wm_treeid old, new_; } client; /// Information for `WM_TREE_CHANGE_TOPLEVEL_KILLED`. /// The zombie window left in place of the killed toplevel. struct wm_tree_node *killed; struct wm_tree_node *new_; }; enum wm_tree_change_type type; }; /// Free all tree nodes and changes, without generating any change events. Used when /// shutting down. void wm_tree_clear(struct wm_tree *tree); struct wm_tree_node *attr_pure wm_tree_find(const struct wm_tree *tree, xcb_window_t id); /// Find the toplevel that is an ancestor of `node` or `node` itself. Returns NULL if /// `node` is part of an orphaned subtree. struct wm_tree_node *attr_pure wm_tree_find_toplevel_for(const struct wm_tree *tree, struct wm_tree_node *node); struct wm_tree_node *attr_pure wm_tree_next(struct wm_tree_node *node, struct wm_tree_node *subroot); /// Create a new window node in the tree, with X window ID `id`, and parent `parent`. If /// `parent` is NULL, the new node will be the root window. Only one root window is /// permitted, and the root window cannot be destroyed once created, until /// `wm_tree_clear` is called. If `parent` is not NULL, the new node will be put at the /// top of the stacking order among its siblings. struct wm_tree_node *wm_tree_new_window(struct wm_tree *tree, xcb_window_t id); void wm_tree_add_window(struct wm_tree *tree, struct wm_tree_node *node); void wm_tree_destroy_window(struct wm_tree *tree, struct wm_tree_node *node); /// Detach the subtree rooted at `subroot` from `tree`. The subtree root is removed from /// its parent, and the disconnected tree nodes won't be able to be found via /// `wm_tree_find`. Relevant events will be generated. /// /// Returns the zombie tree node if one is created, or NULL. struct wm_tree_node *must_use wm_tree_detach(struct wm_tree *tree, struct wm_tree_node *subroot); /// Attach `node` to `parent`. `node` becomes the topmost child of `parent`. If `parent` /// is NULL, `node` becomes the root window. void wm_tree_attach(struct wm_tree *tree, struct wm_tree_node *child, struct wm_tree_node *parent); void wm_tree_move_to_above(struct wm_tree *tree, struct wm_tree_node *node, struct wm_tree_node *other); /// Move `node` to the top or the bottom of its parent's child window stack. void wm_tree_move_to_end(struct wm_tree *tree, struct wm_tree_node *node, bool to_bottom); struct wm_tree_change wm_tree_dequeue_change(struct wm_tree *tree); void wm_tree_reap_zombie(struct wm_tree_node *zombie); void wm_tree_set_wm_state(struct wm_tree *tree, struct wm_tree_node *node, bool has_wm_state); struct wm_tree_node *attr_pure wm_tree_find_client(struct wm_tree_node *subroot); static inline void wm_tree_init(struct wm_tree *tree) { tree->nodes = NULL; tree->gen = 1; list_init_head(&tree->changes); list_init_head(&tree->free_changes); } picom-12.5/src/x.c000066400000000000000000001116471471504570600137750ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "atom.h" #include "common.h" #include "compiler.h" #include "log.h" #include "region.h" #include "utils/kernel.h" #include "utils/misc.h" #include "x.h" static inline uint64_t x_widen_sequence(struct x_connection *c, uint32_t sequence) { if (sequence < c->last_sequence) { // The sequence number has wrapped around return (uint64_t)sequence + UINT32_MAX + 1; } return (uint64_t)sequence; } // === Error handling === /// Discard pending error handlers. /// /// We have received reply with sequence number `sequence`, which means all pending /// replies with sequence number strictly less than `sequence` will never be received. So /// discard them. static void x_discard_pending_errors(struct x_connection *c, uint64_t sequence) { list_foreach_safe(struct pending_x_error, i, &c->pending_x_errors, siblings) { if (x_widen_sequence(c, i->sequence) >= sequence) { break; } list_remove(&i->siblings); free(i); } } enum { XSyncBadCounter = 0, XSyncBadAlarm = 1, XSyncBadFence = 2, }; /// Convert a X11 error to string /// /// @return a pointer to a string. this pointer shouldn NOT be freed, same buffer is used /// for multiple calls to this function, static const char *x_error_code_to_string(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code) { session_t *const ps = ps_g; int o = 0; const char *name = "Unknown"; #define CASESTRRET(s) \ case s: name = #s; break #define CASESTRRET2(s) \ case XCB_##s: name = #s; break // TODO(yshui) separate error code out from session_t o = error_code - ps->xfixes_error; switch (o) { CASESTRRET2(XFIXES_BAD_REGION); } o = error_code - ps->damage_error; switch (o) { CASESTRRET2(DAMAGE_BAD_DAMAGE); } o = error_code - ps->render_error; switch (o) { CASESTRRET2(RENDER_PICT_FORMAT); CASESTRRET2(RENDER_PICTURE); CASESTRRET2(RENDER_PICT_OP); CASESTRRET2(RENDER_GLYPH_SET); CASESTRRET2(RENDER_GLYPH); } if (ps->glx_exists) { o = error_code - ps->glx_error; switch (o) { CASESTRRET2(GLX_BAD_CONTEXT); CASESTRRET2(GLX_BAD_CONTEXT_STATE); CASESTRRET2(GLX_BAD_DRAWABLE); CASESTRRET2(GLX_BAD_PIXMAP); CASESTRRET2(GLX_BAD_CONTEXT_TAG); CASESTRRET2(GLX_BAD_CURRENT_WINDOW); CASESTRRET2(GLX_BAD_RENDER_REQUEST); CASESTRRET2(GLX_BAD_LARGE_REQUEST); CASESTRRET2(GLX_UNSUPPORTED_PRIVATE_REQUEST); CASESTRRET2(GLX_BAD_FB_CONFIG); CASESTRRET2(GLX_BAD_PBUFFER); CASESTRRET2(GLX_BAD_CURRENT_DRAWABLE); CASESTRRET2(GLX_BAD_WINDOW); CASESTRRET2(GLX_GLX_BAD_PROFILE_ARB); } } if (ps->xsync_exists) { o = error_code - ps->xsync_error; switch (o) { CASESTRRET(XSyncBadCounter); CASESTRRET(XSyncBadAlarm); CASESTRRET(XSyncBadFence); } } switch (error_code) { CASESTRRET2(ACCESS); CASESTRRET2(ALLOC); CASESTRRET2(ATOM); CASESTRRET2(COLORMAP); CASESTRRET2(CURSOR); CASESTRRET2(DRAWABLE); CASESTRRET2(FONT); CASESTRRET2(G_CONTEXT); CASESTRRET2(ID_CHOICE); CASESTRRET2(IMPLEMENTATION); CASESTRRET2(LENGTH); CASESTRRET2(MATCH); CASESTRRET2(NAME); CASESTRRET2(PIXMAP); CASESTRRET2(REQUEST); CASESTRRET2(VALUE); CASESTRRET2(WINDOW); } #undef CASESTRRET #undef CASESTRRET2 thread_local static char buffer[256]; snprintf(buffer, sizeof(buffer), "X error %d %s request %d minor %d serial %lu", error_code, name, major, minor, serial); return buffer; } void x_print_error_impl(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code, const char *func) { if (unlikely(LOG_LEVEL_DEBUG >= log_get_level_tls())) { log_printf(tls_logger, LOG_LEVEL_DEBUG, func, "%s", x_error_code_to_string(serial, major, minor, error_code)); } } /// Handle X errors. /// /// This function logs X errors, or aborts the program based on severity of the error. static void x_handle_error(struct x_connection *c, xcb_generic_error_t *ev) { x_discard_pending_errors(c, ev->full_sequence); struct pending_x_error *first_error_action = NULL; if (!list_is_empty(&c->pending_x_errors)) { first_error_action = list_entry(c->pending_x_errors.next, struct pending_x_error, siblings); } if (first_error_action != NULL && first_error_action->sequence == ev->full_sequence) { if (first_error_action->action != PENDING_REPLY_ACTION_IGNORE) { log_error("X error for request in %s at %s:%d: %s", first_error_action->func, first_error_action->file, first_error_action->line, x_error_code_to_string(ev->full_sequence, ev->major_code, ev->minor_code, ev->error_code)); } else { log_debug("Expected X error for request in %s at %s:%d: %s", first_error_action->func, first_error_action->file, first_error_action->line, x_error_code_to_string(ev->full_sequence, ev->major_code, ev->minor_code, ev->error_code)); } switch (first_error_action->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; } log_warn("Stray X error: %s", x_error_code_to_string(ev->full_sequence, ev->major_code, ev->minor_code, ev->error_code)); } struct x_generic_async_request { struct x_async_request_base base; enum x_error_action error_action; const char *func; const char *file; int line; }; static void x_generic_async_callback(struct x_connection * /*c*/, struct x_async_request_base *req_base, const xcb_raw_generic_event_t *reply_or_error) { auto req = (struct x_generic_async_request *)req_base; auto error_action = req->error_action; auto func = req->func == NULL ? "(unknown)" : req->func; auto file = req->file == NULL ? "(unknown)" : req->file; auto line = req->line; free(req); if (reply_or_error == NULL || reply_or_error->response_type != 0) { return; } auto error = (xcb_generic_error_t *)reply_or_error; if (error_action != PENDING_REPLY_ACTION_IGNORE) { log_error("X error for request in %s at %s:%d: %s", func, file, line, x_error_code_to_string(error->full_sequence, error->major_code, error->minor_code, error->error_code)); } else { log_debug("Expected X error for request in %s at %s:%d: %s", func, file, line, x_error_code_to_string(error->full_sequence, error->major_code, error->minor_code, error->error_code)); } switch (error_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; } } /** * 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; } /// 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); list_init_head(&c->pending_x_errors); list_init_head(&c->pending_x_requests); c->previous_xerror_handler = XSetErrorHandler(xerror); c->screen = DefaultScreen(dpy); c->screen_info = xcb_aux_get_screen(c->c, c->screen); // Do a round trip to fetch the current sequence number auto cookie = xcb_get_input_focus(c->c); free(xcb_get_input_focus_reply(c->c, cookie, NULL)); c->last_sequence = cookie.sequence; } /** * Get a specific attribute of a window. * * Returns a blank structure if the returned type and format does not * match the requested type and format. * * @param ps current session * @param w window * @param atom atom of attribute to fetch * @param length length to read * @param rtype atom of the requested type * @param rformat requested format * @return a winprop_t structure containing the attribute * and number of items. A blank one on failure. */ 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->c, xcb_get_property(c->c, 0, w, atom, rtype, to_u32_checked(offset), to_u32_checked(length)), NULL); if (r && xcb_get_property_value_length(r) && (rtype == XCB_GET_PROPERTY_TYPE_ANY || r->type == rtype) && (!rformat || r->format == rformat) && (r->format == 8 || r->format == 16 || r->format == 32)) { auto len = xcb_get_property_value_length(r); return (winprop_t){ .ptr = xcb_get_property_value(r), .nitems = (ulong)(len / (r->format / 8)), .type = r->type, .format = r->format, .r = r, }; } free(r); return (winprop_t){ .ptr = NULL, .nitems = 0, .type = XCB_GET_PROPERTY_TYPE_ANY, .format = 0}; } /// Get the type, format and size in bytes of a window's specific attribute. 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->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); return (winprop_info_t){ .type = XCB_GET_PROPERTY_TYPE_ANY, .format = 0, .length = 0}; } winprop_info_t winprop_info = { .type = r->type, .format = r->format, .length = r->bytes_after}; free(r); return winprop_info; } /** * Get the value of a type-xcb_window_t property of a window. * * @return the value if successful, 0 otherwise */ xcb_window_t wid_get_prop_window(struct x_connection *c, xcb_window_t wid, xcb_atom_t aprop, bool *exists) { // Get the attribute xcb_window_t p = XCB_NONE; winprop_t prop = x_get_prop(c, wid, aprop, 1L, XCB_ATOM_WINDOW, 32); // Return it if (prop.nitems) { *exists = true; p = (xcb_window_t)*prop.p32; } else { *exists = false; } free_winprop(&prop); return p; } /** * Get the value of a text property of a window. */ bool wid_get_text_prop(struct x_connection *c, struct atom *atoms, xcb_window_t wid, xcb_atom_t prop, char ***pstrlst, int *pnstr) { xcb_generic_error_t *e = NULL; auto r = xcb_get_property_reply( c->c, xcb_get_property(c->c, 0, wid, prop, XCB_ATOM_ANY, 0, UINT_MAX), &e); if (!r) { log_debug_x_error(e, "Failed to get window property for %#010x", wid); free(e); return false; } if (r->type == XCB_ATOM_NONE) { free(r); return false; } if (!x_is_type_string(atoms, r->type)) { log_warn("Text property %d of window %#010x has unsupported type: %d", prop, wid, r->type); free(r); return false; } if (r->format != 8) { log_warn("Text property %d of window %#010x has unexpected format: %d", prop, wid, r->format); free(r); return false; } uint32_t length = to_u32_checked(xcb_get_property_value_length(r)); void *data = xcb_get_property_value(r); unsigned int nstr = 0; uint32_t current_offset = 0; while (current_offset < length) { current_offset += (uint32_t)strnlen(data + current_offset, length - current_offset) + 1; nstr += 1; } if (nstr == 0) { // The property is set to an empty string, in that case, we return one // string char **strlst = malloc(sizeof(char *)); strlst[0] = ""; *pnstr = 1; *pstrlst = strlst; free(r); return true; } // Allocate the pointers and the strings together void *buf = NULL; if (posix_memalign(&buf, alignof(char *), length + sizeof(char *) * nstr + 1) != 0) { abort(); } char *strlst = buf + sizeof(char *) * nstr; memcpy(strlst, xcb_get_property_value(r), length); strlst[length] = '\0'; // X strings aren't guaranteed to be null terminated char **ret = buf; current_offset = 0; nstr = 0; while (current_offset < length) { ret[nstr] = strlst + current_offset; current_offset += (uint32_t)strlen(strlst + current_offset) + 1; nstr += 1; } *pnstr = to_int_checked(nstr); *pstrlst = ret; free(r); return true; } 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(c, wid, atoms->a_NET_WM_WINDOW_OPACITY, 1L, XCB_ATOM_CARDINAL, 32); if (prop.nitems) { *out = *prop.c32; ret = true; } free_winprop(&prop); return ret; } // A cache of pict formats. We assume they don't change during the lifetime // of this program static thread_local xcb_render_query_pict_formats_reply_t *g_pictfmts = NULL; 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->c, xcb_render_query_pict_formats(c->c), &e); if (e || !g_pictfmts) { log_fatal("failed to get pict formats\n"); abort(); } } const xcb_render_pictforminfo_t * 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); for (xcb_render_pictforminfo_iterator_t i = xcb_render_query_pict_formats_formats_iterator(g_pictfmts); i.rem; xcb_render_pictforminfo_next(&i)) { if (i.data->id == pv->format) { return i.data; } } return NULL; } static xcb_visualid_t attr_pure x_get_visual_for_pictfmt(xcb_render_query_pict_formats_reply_t *r, xcb_render_pictformat_t fmt) { for (auto screen = xcb_render_query_pict_formats_screens_iterator(r); screen.rem; xcb_render_pictscreen_next(&screen)) { for (auto depth = xcb_render_pictscreen_depths_iterator(screen.data); depth.rem; xcb_render_pictdepth_next(&depth)) { for (auto pv = xcb_render_pictdepth_visuals_iterator(depth.data); pv.rem; xcb_render_pictvisual_next(&pv)) { if (pv.data->format == fmt) { return pv.data->visual; } } } } return XCB_NONE; } 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); 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(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); return pictfmt->id; } xcb_render_picture_t 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; if (attr) { xcb_render_create_picture_value_list_serialize(&buf, valuemask, attr); if (!buf) { log_error("failed to serialize picture attributes"); return XCB_NONE; } } xcb_render_picture_t tmp_picture = x_new_id(c); xcb_generic_error_t *e = 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(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->id, pixmap, valuemask, attr); } xcb_render_picture_t 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->id, pixmap, valuemask, attr); } xcb_render_picture_t 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, w, h, pictfmt->id, pictfmt->depth, valuemask, attr); } /** * Create an picture. */ xcb_render_picture_t 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) { xcb_pixmap_t tmp_pixmap = x_create_pixmap(c, depth, w, h); if (!tmp_pixmap) { return XCB_NONE; } xcb_render_picture_t picture = x_create_picture_with_pictfmt_and_pixmap( c, pictfmt, tmp_pixmap, valuemask, attr); x_set_error_action_abort(c, xcb_free_pixmap(c->c, tmp_pixmap)); return picture; } xcb_render_picture_t 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, w, h, pictfmt->id, pictfmt->depth, valuemask, attr); } 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->c, xcb_xfixes_fetch_region(c->c, r), &e); if (!xr) { log_error_x_error(e, "Failed to fetch rectangles"); return false; } int nrect = xcb_xfixes_fetch_region_rectangles_length(xr); auto b = ccalloc(nrect, pixman_box32_t); xcb_rectangle_t *xrect = xcb_xfixes_fetch_region_rectangles(xr); for (int i = 0; i < nrect; i++) { b[i] = (pixman_box32_t){.x1 = xrect[i].x, .y1 = xrect[i].y, .x2 = xrect[i].x + xrect[i].width, .y2 = xrect[i].y + xrect[i].height}; } bool ret = pixman_region32_init_rects(res, b, nrect); free(b); free(xr); return ret; } 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_async_change_window_attributes(struct x_connection *c, xcb_window_t wid, uint32_t mask, const uint32_t *values, struct x_async_request_base *req) { req->sequence = xcb_change_window_attributes(c->c, wid, mask, values).sequence; req->no_reply = true; x_await_request(c, req); } void x_async_query_tree(struct x_connection *c, xcb_window_t wid, struct x_async_request_base *req) { req->sequence = xcb_query_tree(c->c, wid).sequence; x_await_request(c, req); } void x_async_get_property(struct x_connection *c, xcb_window_t wid, xcb_atom_t atom, xcb_atom_t type, uint32_t long_offset, uint32_t long_length, struct x_async_request_base *req) { req->sequence = xcb_get_property(c->c, 0, wid, atom, type, long_offset, long_length).sequence; x_await_request(c, req); } void x_destroy_region(struct x_connection *c, xcb_xfixes_region_t r) { if (r != XCB_NONE) { x_set_error_action_debug_abort(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; const rect_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_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); } free(xrects); } 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->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); x_set_error_action_debug_abort(c, cookie); } /* * Convert a xcb_generic_error_t to a string that describes the error * * @return a pointer to a string. this pointer shouldn NOT be freed, same buffer is used * for multiple calls to this function, */ const char *x_strerror(const xcb_generic_error_t *e) { if (!e) { return "No error"; } return x_error_code_to_string(e->full_sequence, e->major_code, e->minor_code, e->error_code); } void x_flush(struct x_connection *c) { xcb_flush(c->c); } /** * Create a pixmap and check that creation succeeded. */ 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->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; } log_error_x_error(err, "Failed to create pixmap"); free(err); return XCB_NONE; } /// 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; 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); break; } free_winprop(&prop); } return pixmap; } bool x_is_root_back_pixmap_atom(struct atom *atoms, xcb_atom_t atom) { 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(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->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->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->c, xcb_sync_reset_fence_checked(c->c, f)); if (e) { log_error_x_error(e, "Failed to reset the fence"); goto err; } return true; err: free(e); return false; } 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); x_set_error_action_abort(c, cookie); } /** * Convert a struct conv to a X picture convolution filter, normalizing the kernel * in the process. Allow the caller to specify the element at the center of the kernel, * for compatibility with legacy code. * * @param[in] kernel the convolution kernel * @param[in] center the element to put at the center of the matrix * @param[inout] ret pointer to an array of `size`, if `size` is too small, more space * will be allocated, and `*ret` will be updated * @param[inout] size size of the array pointed to by `ret`, in number of elements * @return number of elements filled into `*ret` */ void x_create_convolution_kernel(const conv *kernel, double center, struct x_convolution_kernel **ret) { assert(ret); if (!*ret || (*ret)->capacity < kernel->w * kernel->h + 2) { free(*ret); *ret = cvalloc(sizeof(struct x_convolution_kernel) + (size_t)(kernel->w * kernel->h + 2) * sizeof(xcb_render_fixed_t)); (*ret)->capacity = kernel->w * kernel->h + 2; } (*ret)->size = kernel->w * kernel->h + 2; auto buf = (*ret)->kernel; buf[0] = DOUBLE_TO_XFIXED(kernel->w); buf[1] = DOUBLE_TO_XFIXED(kernel->h); double sum = center; bool has_neg = false; for (int i = 0; i < kernel->w * kernel->h; i++) { if (i == kernel->w * kernel->h / 2) { // Ignore center continue; } sum += kernel->data[i]; if (kernel->data[i] < 0 && !has_neg) { has_neg = true; log_warn("A X convolution kernel with negative values may not " "work properly."); } } // Note for floating points a / b != a * (1 / b), but this shouldn't have any real // impact on the result double factor = sum != 0 ? 1.0 / sum : 1; for (int i = 0; i < kernel->w * kernel->h; i++) { buf[i + 2] = DOUBLE_TO_XFIXED(kernel->data[i] * factor); } buf[kernel->h / 2 * kernel->w + kernel->w / 2 + 2] = DOUBLE_TO_XFIXED(center * factor); } /// 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(struct x_connection *c, xcb_visualid_t visual) { auto pictfmt = x_get_pictform_for_visual(c, visual); 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}; } if (pictfmt->type != XCB_RENDER_PICT_TYPE_DIRECT) { log_error("We cannot handle non-DirectColor visuals. Report an " "issue if you see this error message."); return (struct xvisual_info){-1, -1, -1, -1, -1, 0}; } int red_size = popcntul(pictfmt->direct.red_mask), blue_size = popcntul(pictfmt->direct.blue_mask), green_size = popcntul(pictfmt->direct.green_mask), alpha_size = popcntul(pictfmt->direct.alpha_mask); return (struct xvisual_info){ .red_size = red_size, .green_size = green_size, .blue_size = blue_size, .alpha_size = alpha_size, .visual_depth = depth, .visual = visual, }; } struct x_update_monitors_request { struct x_async_request_base base; struct x_monitors *monitors; }; static void x_handle_update_monitors_reply(struct x_connection * /*c*/, struct x_async_request_base *req_base, const xcb_raw_generic_event_t *reply_or_error) { auto m = ((struct x_update_monitors_request *)req_base)->monitors; free(req_base); if (reply_or_error == NULL) { // Shutting down return; } if (reply_or_error->response_type == 0) { log_warn("Failed to get monitor information using RandR: %s", x_strerror((xcb_generic_error_t *)reply_or_error)); return; } x_free_monitor_info(m); auto reply = (const xcb_randr_get_monitors_reply_t *)reply_or_error; m->count = xcb_randr_get_monitors_monitors_length(reply); m->regions = ccalloc(m->count, region_t); xcb_randr_monitor_info_iterator_t monitor_info_it = xcb_randr_get_monitors_monitors_iterator(reply); 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); } } void x_update_monitors_async(struct x_connection *c, struct x_monitors *m) { auto req = ccalloc(1, struct x_update_monitors_request); req->base.callback = x_handle_update_monitors_reply; req->base.sequence = xcb_randr_get_monitors(c->c, c->screen_info->root, 1).sequence; req->monitors = m; x_await_request(c, &req->base); } 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; } static inline xcb_raw_generic_event_t * x_ingest_event(struct x_connection *c, xcb_generic_event_t *event) { if (event != NULL) { assert(event->response_type != 1); uint64_t seq = x_widen_sequence(c, event->full_sequence); if (event->response_type != 0) { // For true events, we can discard pending errors with a lower or // equal sequence number. This is because only a reply or an error // increments the sequence number. seq += 1; } x_discard_pending_errors(c, seq); c->last_sequence = event->full_sequence; } return (xcb_raw_generic_event_t *)event; } static const xcb_raw_generic_event_t no_reply_success = {.response_type = 1}; /// Read the X connection once, and return the first event or unchecked error. If any /// replies to pending X requests are read, they will be processed and callbacks will be /// called. static xcb_raw_generic_event_t *x_poll_for_event_impl(struct x_connection *c, bool queued) { auto e = queued ? xcb_poll_for_queued_event(c->c) : xcb_poll_for_event(c->c); if (e == NULL) { return NULL; } auto seq = x_widen_sequence(c, e->full_sequence); list_foreach_safe(struct x_async_request_base, i, &c->pending_x_requests, siblings) { auto head_seq = x_widen_sequence(c, i->sequence); if (head_seq > seq) { break; } if (head_seq == seq && e->response_type == 0) { // Error replies are handled in `x_poll_for_event`. break; } auto reply_or_error = &no_reply_success; if (!i->no_reply) { // We have received something with sequence number `seq >= // head_seq`, so we are sure that a reply for `i` is available in // xcb's buffer, so we can safely call `xcb_poll_for_reply` // without reading from X. xcb_generic_error_t *err = NULL; auto has_reply = xcb_poll_for_reply( c->c, i->sequence, (void **)&reply_or_error, &err); BUG_ON(has_reply == 0); if (reply_or_error == NULL) { reply_or_error = (xcb_raw_generic_event_t *)err; } } c->latest_completed_request = i->sequence; list_remove(&i->siblings); i->callback(c, i, reply_or_error); if (reply_or_error != &no_reply_success) { free((void *)reply_or_error); } } return x_ingest_event(c, e); } bool x_prepare_for_sleep(struct x_connection *c) { if (!list_is_empty(&c->pending_x_requests)) { auto last = list_entry(c->pending_x_requests.prev, struct x_async_request_base, siblings); if (c->event_sync != last->sequence) { // Send an async request that is guaranteed to error, see comments // on `event_sync` for why. auto req = ccalloc(1, struct x_generic_async_request); req->func = __func__; req->file = __FILE__; req->line = __LINE__; req->error_action = PENDING_REPLY_ACTION_IGNORE; req->base.sequence = xcb_free_pixmap(c->c, XCB_NONE).sequence; req->base.callback = x_generic_async_callback; req->base.no_reply = true; c->event_sync = req->base.sequence; x_await_request(c, &req->base); log_trace("Sending event sync request to catch response to " "pending request, last sequence: %u, event sync: %u", last->sequence, c->event_sync); } } XFlush(c->dpy); xcb_flush(c->c); return true; } xcb_generic_event_t *x_poll_for_event(struct x_connection *c, bool queued) { xcb_raw_generic_event_t *ret = NULL; while (true) { ret = x_poll_for_event_impl(c, queued); if (ret == NULL || ret->response_type != 0) { break; } // We received an error, handle it and try again to see if there are real // events. struct x_async_request_base *head = NULL; xcb_generic_error_t *error = (xcb_generic_error_t *)ret; if (!list_is_empty(&c->pending_x_requests)) { head = list_entry(c->pending_x_requests.next, struct x_async_request_base, siblings); } if (head != NULL && error->full_sequence == head->sequence) { // This is an error response to the head of pending requests. c->latest_completed_request = head->sequence; list_remove(&head->siblings); head->callback(c, head, ret); } else { x_handle_error(c, error); } free(ret); } return (xcb_generic_event_t *)ret; } picom-12.5/src/x.h000066400000000000000000000461631471504570600140020ustar00rootroot00000000000000// SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2018 Yuxuan Shui #pragma once #include #include #include #include #include #include #include #include #include #include "atom.h" #include "compiler.h" #include "log.h" #include "region.h" #include "utils/kernel.h" #include "utils/list.h" typedef struct session session_t; struct atom; /// Structure representing Window property value. typedef struct winprop { union { void *ptr; int8_t *p8; int16_t *p16; int32_t *p32; uint32_t *c32; // 32bit cardinal xcb_atom_t *atom; }; unsigned long nitems; xcb_atom_t type; int format; xcb_get_property_reply_t *r; } winprop_t; typedef struct winprop_info { xcb_atom_t type; uint8_t format; uint32_t length; } winprop_info_t; enum x_error_action { PENDING_REPLY_ACTION_IGNORE, PENDING_REPLY_ACTION_ABORT, PENDING_REPLY_ACTION_DEBUG_ABORT, }; /// Represents a X request we sent that might error. struct pending_x_error { uint32_t sequence; enum x_error_action action; // Debug information, where in the code was this request sent. const char *func; const char *file; int line; struct list_node siblings; }; struct x_connection { // Public fields // These are part of the public ABI, changing these // requires bumping PICOM_API_MAJOR. /// XCB connection. xcb_connection_t *c; /// Display in use. Display *dpy; /// Default screen int screen; // Private fields /// The error handling list. struct list_node pending_x_errors; /// The list of pending async requests that we have /// yet to receive a reply for. struct list_node pending_x_requests; /// Previous handler of X errors XErrorHandler previous_xerror_handler; /// Information about the default screen xcb_screen_t *screen_info; /// The sequence number of the last message returned by /// `x_poll_for_message`. Used for sequence number overflow /// detection. uint32_t last_sequence; /// The sequence number of the last completed request. uint32_t latest_completed_request; /// The sequence number of the "event sync" request we sent. This is /// a request we sent that is guaranteed to error, so we can be sure /// `xcb_poll_for_event` will return something. This is akin to `xcb_aux_sync`, /// except that guarantees a reply, this one guarantees an error. /// /// # Why do we need this? /// /// To understand why we need this, first notice we need a way to fetch replies /// that are already in xcb's buffer, without reading from the X connection. /// Because otherwise we can't going into sleep while being confident that there /// is no buffered events we haven't handled. /// /// For events or unchecked errors (we will refer to both of them as events /// without distinction), this is possible with `xcb_poll_for_queued_event`, but /// for replies, there is no `xcb_poll_for_queued_reply` (ridiculous, if you /// ask me). Luckily, if there is a reply already in the buffer, /// `xcb_poll_for_reply` will return it without reading from X. And we can deduce /// whether a reply is already received from the sequence number of received /// events. The only problem, if no events are coming, we will be stuck /// indefinitely, so we have to make our own events. uint32_t event_sync; }; /// Monitor info struct x_monitors { int count; region_t *regions; }; #define XCB_AWAIT_VOID(func, c, ...) \ ({ \ bool __success = true; \ __auto_type __e = xcb_request_check(c, func##_checked(c, __VA_ARGS__)); \ if (__e) { \ x_print_error(__e->sequence, __e->major_code, __e->minor_code, \ __e->error_code); \ free(__e); \ __success = false; \ } \ __success; \ }) #define XCB_AWAIT(func, c, ...) \ ({ \ xcb_generic_error_t *__e = NULL; \ __auto_type __r = func##_reply(c, func(c, __VA_ARGS__), &__e); \ if (__e) { \ x_print_error(__e->sequence, __e->major_code, __e->minor_code, \ __e->error_code); \ free(__e); \ } \ __r; \ }) #define log_debug_x_error(e, fmt, ...) \ LOG(DEBUG, fmt " (%s)", ##__VA_ARGS__, x_strerror(e)) #define log_error_x_error(e, fmt, ...) \ LOG(ERROR, fmt " (%s)", ##__VA_ARGS__, x_strerror(e)) #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(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 " "an issue about this problem. The compositor will die."); abort(); } return ret; } /// Set error handler for a specific X request. /// /// @param c X connection /// @param sequence sequence number of the X request to set error handler for /// @param action action to take when error occurs static inline void x_set_error_action(struct x_connection *c, uint32_t sequence, enum x_error_action action, const char *func, const char *file, int line) { auto i = cmalloc(struct pending_x_error); i->sequence = sequence; i->action = action; i->func = func; i->file = file; i->line = line; list_insert_before(&c->pending_x_errors, &i->siblings); } /// Convenience wrapper for x_set_error_action with action `PENDING_REPLY_ACTION_IGNORE` #define x_set_error_action_ignore(c, cookie) \ x_set_error_action(c, (cookie).sequence, PENDING_REPLY_ACTION_IGNORE, __func__, \ __FILE__, __LINE__) /// Convenience wrapper for x_set_error_action with action `PENDING_REPLY_ACTION_ABORT` #define x_set_error_action_abort(c, cookie) \ x_set_error_action(c, (cookie).sequence, PENDING_REPLY_ACTION_ABORT, __func__, \ __FILE__, __LINE__) /// Convenience wrapper for x_set_error_action with action /// `PENDING_REPLY_ACTION_DEBUG_ABORT` #ifndef NDEBUG #define x_set_error_action_debug_abort(c, cookie) \ x_set_error_action(c, (cookie).sequence, PENDING_REPLY_ACTION_DEBUG_ABORT, \ __func__, __FILE__, __LINE__) #else #define x_set_error_action_debug_abort(c, cookie) \ ((void)(c)); \ ((void)(cookie)) #endif struct x_async_request_base { struct list_node siblings; /// The callback function to call when the reply is received. If `reply_or_error` /// is NULL, it means the X connection is closed while waiting for the reply. void (*callback)(struct x_connection *, struct x_async_request_base *, const xcb_raw_generic_event_t *reply_or_error); /// The sequence number of the X request. unsigned int sequence; /// This request doesn't expect a reply. If this is true, in the success case, /// `callback` will be called with a dummy reply whose `response_type` is 1. bool no_reply; }; static inline void attr_unused free_x_connection(struct x_connection *c) { list_foreach_safe(struct pending_x_error, i, &c->pending_x_errors, siblings) { list_remove(&i->siblings); free(i); } list_foreach_safe(struct x_async_request_base, i, &c->pending_x_requests, siblings) { list_remove(&i->siblings); i->callback(c, i, NULL); } 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); /** * Get a specific attribute of a window. * * Returns a blank structure if the returned type and format does not * match the requested type and format. * * @param ps current session * @param w window * @param atom atom of attribute to fetch * @param length length to read * @param rtype atom of the requested type * @param rformat requested format * @return a winprop_t structure containing the attribute * and number of items. A blank one on failure. */ 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(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(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(struct x_connection *c) { xcb_generic_event_t *e; while ((e = xcb_poll_for_event(c->c))) { free(e); } } /** * Get the value of a type-xcb_window_t property of a window. * * @return the value if successful, 0 otherwise */ xcb_window_t wid_get_prop_window(struct x_connection *c, xcb_window_t wid, xcb_atom_t aprop, bool *exists); bool wid_get_opacity_prop(struct x_connection *c, struct atom *atoms, xcb_window_t wid, opacity_t def, opacity_t *out); /** * Get the value of a text property of a window. * * @param[out] pstrlst Out parameter for an array of strings, caller needs to free this * array * @param[out] pnstr Number of strings in the array */ 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(struct x_connection *, xcb_visualid_t); xcb_render_picture_t 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); xcb_render_picture_t 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(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(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); /** * Create an picture. */ xcb_render_picture_t 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); xcb_render_picture_t 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(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_async_change_window_attributes(struct x_connection *c, xcb_window_t wid, uint32_t mask, const uint32_t *values, struct x_async_request_base *req); void x_async_query_tree(struct x_connection *c, xcb_window_t wid, struct x_async_request_base *req); void x_async_get_property(struct x_connection *c, xcb_window_t wid, xcb_atom_t atom, xcb_atom_t type, uint32_t long_offset, uint32_t long_length, struct x_async_request_base *req); /// Destroy a X region void x_destroy_region(struct x_connection *c, uint32_t region); 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_impl(unsigned long serial, uint8_t major, uint16_t minor, uint8_t error_code, const char *func); #define x_print_error(serial, major, minor, error_code) \ x_print_error_impl(serial, major, minor, error_code, __func__) /* * Convert a xcb_generic_error_t to a string that describes the error * * @return a pointer to a string. this pointer shouldn NOT be freed, same buffer is used * for multiple calls to this function, */ const char *x_strerror(const xcb_generic_error_t *e); void x_flush(struct x_connection *c); xcb_pixmap_t x_create_pixmap(struct x_connection *, uint8_t depth, int width, int height); /** * Free a winprop_t. * * @param pprop pointer to the winprop_t to free. */ static inline void free_winprop(winprop_t *pprop) { // Empty the whole structure to avoid possible issues 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(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(struct x_connection *, xcb_sync_fence_t); struct x_convolution_kernel { int size; int capacity; xcb_render_fixed_t kernel[]; }; /** * Convert a struct conv to a X picture convolution filter, normalizing the kernel * in the process. Allow the caller to specify the element at the center of the kernel, * for compatibility with legacy code. * * @param[in] kernel the convolution kernel * @param[in] center the element to put at the center of the matrix * @param[inout] ret pointer to an array of `size`, if `size` is too small, more space * will be allocated, and `*ret` will be updated. * @param[inout] size size of the array pointed to by `ret`. */ void attr_nonnull(1, 3) x_create_convolution_kernel(const conv *kernel, double center, struct x_convolution_kernel **ret); /// 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(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_depth(xcb_screen_t *screen, uint8_t depth); xcb_render_pictformat_t x_get_pictfmt_for_standard(struct x_connection *c, xcb_pict_standard_t std); /// Populates a `struct x_monitors` with the current monitor configuration asynchronously. void x_update_monitors_async(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); /// Register an X request as async request. Its reply will be processed as part of the /// event stream. i.e. the registered callback will only be called when all preceding /// events have been retrieved via `x_poll_for_event`. /// `req` store information about the request, including the callback. The callback is /// responsible for freeing `req`. static inline void x_await_request(struct x_connection *c, struct x_async_request_base *req) { list_insert_before(&c->pending_x_requests, &req->siblings); } /// Flush all X buffers to ensure we don't sleep with outgoing messages not sent. /// /// If there are requests pending replies, an event sync request will /// be sent if necessary. See comments on `event_sync` for more information. MUST be /// called before sleep to ensure we can handle replies/events in a timely manner. This /// function MIGHT read data from X into xcb buffer (because `xcb_flush` might read, /// ridiculous, I know), so `x_poll_for_event(queued = true)` MUST be called after this to /// drain the buffer. bool x_prepare_for_sleep(struct x_connection *c); /// Poll for the next X event. This is like `xcb_poll_for_event`, but also includes /// machinery for handling async replies. Calling `xcb_poll_for_event` directly will /// cause replies to async requests to be lost, so that should never be called. /// /// @param[out] queued if true, only return events that are already in the queue, don't /// attempt to read from the X connection. xcb_generic_event_t *x_poll_for_event(struct x_connection *c, bool queued); picom-12.5/src/xrescheck.c000066400000000000000000000035111471504570600154730ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2014 Richard Grenville #include "compiler.h" #include "log.h" #include "xrescheck.h" static xrc_xid_record_t *gs_xid_records = NULL; #define HASH_ADD_XID(head, xidfield, add) HASH_ADD(hh, head, xidfield, sizeof(xid), add) #define HASH_FIND_XID(head, findxid, out) HASH_FIND(hh, head, findxid, sizeof(xid), out) #define M_CPY_POS_DATA(prec) \ prec->file = file; \ prec->func = func; \ prec->line = line; /** * @brief Add a record of given XID to the allocation table. */ void xrc_add_xid_(XID xid, const char *type, M_POS_DATA_PARAMS) { auto prec = ccalloc(1, xrc_xid_record_t); prec->xid = xid; prec->type = type; M_CPY_POS_DATA(prec); HASH_ADD_XID(gs_xid_records, xid, prec); } /** * @brief Delete a record of given XID in the allocation table. */ void xrc_delete_xid_(XID xid, M_POS_DATA_PARAMS) { xrc_xid_record_t *prec = NULL; HASH_FIND_XID(gs_xid_records, &xid, prec); if (!prec) { log_error("XRC: %s:%d %s(): Can't find XID %#010lx we want to delete.", file, line, func, xid); return; } HASH_DEL(gs_xid_records, prec); free(prec); } /** * @brief Report about issues found in the XID allocation table. */ void xrc_report_xid(void) { for (xrc_xid_record_t *prec = gs_xid_records; prec; prec = prec->hh.next) log_trace("XRC: %s:%d %s(): %#010lx (%s) not freed.\n", prec->file, prec->line, prec->func, prec->xid, prec->type); } /** * @brief Clear the XID allocation table. */ void xrc_clear_xid(void) { xrc_xid_record_t *prec = NULL, *ptmp = NULL; HASH_ITER(hh, gs_xid_records, prec, ptmp) { HASH_DEL(gs_xid_records, prec); free(prec); } } picom-12.5/src/xrescheck.h000066400000000000000000000040101471504570600154730ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2014 Richard Grenville #pragma once #include "common.h" #include "uthash.h" typedef struct { XID xid; const char *type; const char *file; const char *func; int line; UT_hash_handle hh; } xrc_xid_record_t; #define M_POS_DATA_PARAMS const char *file, int line, const char *func #define M_POS_DATA_PASSTHROUGH file, line, func #define M_POS_DATA __FILE__, __LINE__, __func__ void xrc_add_xid_(XID xid, const char *type, M_POS_DATA_PARAMS); #define xrc_add_xid(xid, type) xrc_add_xid_(xid, type, M_POS_DATA) void xrc_delete_xid_(XID xid, M_POS_DATA_PARAMS); #define xrc_delete_xid(xid) xrc_delete_xid_(xid, M_POS_DATA) void xrc_report_xid(void); void xrc_clear_xid(void); // Pixmap static inline void xcb_create_pixmap_(xcb_connection_t *c, uint8_t depth, xcb_pixmap_t pixmap, xcb_drawable_t drawable, uint16_t width, uint16_t height, M_POS_DATA_PARAMS) { xcb_create_pixmap(c, depth, pixmap, drawable, width, height); xrc_add_xid_(pixmap, "Pixmap", M_POS_DATA_PASSTHROUGH); } #define xcb_create_pixmap(c, depth, pixmap, drawable, width, height) \ xcb_create_pixmap_(c, depth, pixmap, drawable, width, height, M_POS_DATA) static inline xcb_void_cookie_t xcb_composite_name_window_pixmap_(xcb_connection_t *c, xcb_window_t window, xcb_pixmap_t pixmap, M_POS_DATA_PARAMS) { xcb_void_cookie_t ret = xcb_composite_name_window_pixmap(c, window, pixmap); xrc_add_xid_(pixmap, "PixmapC", M_POS_DATA_PASSTHROUGH); return ret; } #define xcb_composite_name_window_pixmap(dpy, window, pixmap) \ xcb_composite_name_window_pixmap_(dpy, window, pixmap, M_POS_DATA) static inline void xcb_free_pixmap_(xcb_connection_t *c, xcb_pixmap_t pixmap, M_POS_DATA_PARAMS) { xcb_free_pixmap(c, pixmap); xrc_delete_xid_(pixmap, M_POS_DATA_PASSTHROUGH); } #define xcb_free_pixmap(c, pixmap) xcb_free_pixmap_(c, pixmap, M_POS_DATA); picom-12.5/subprojects/000077500000000000000000000000001471504570600151245ustar00rootroot00000000000000picom-12.5/subprojects/libconfig.wrap000066400000000000000000000001671471504570600177570ustar00rootroot00000000000000[wrap-git] url = https://github.com/hyperrealm/libconfig revision = f9404f60a435aa06321f4ccd8357364dcb216d46 depth = 1 picom-12.5/subprojects/test.h/000077500000000000000000000000001471504570600163315ustar00rootroot00000000000000picom-12.5/subprojects/test.h/meson.build000066400000000000000000000001461471504570600204740ustar00rootroot00000000000000project('test.h', 'c') test_h_dep = declare_dependency(include_directories: include_directories('.')) picom-12.5/subprojects/test.h/test.h000066400000000000000000000235711471504570600174710ustar00rootroot00000000000000// SPDX-License-Identifier: MIT #pragma once #ifdef UNIT_TEST #include #include #include #include #if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \ defined(__NetBSD__) || defined(__OpenBSD__) #define USE_SYSCTL_FOR_ARGS 1 // clang-format off #include #include // clang-format on #include // getpid #endif struct test_file_metadata; struct test_failure { bool present; const char *message; const char *file; int line; bool owned; }; struct test_case_metadata { void (*fn)(struct test_case_metadata *, struct test_file_metadata *); struct test_failure failure; const char *name; struct test_case_metadata *next; }; struct test_file_metadata { bool registered; const char *name; struct test_file_metadata *next; struct test_case_metadata *tests; }; struct test_file_metadata __attribute__((weak)) * test_file_head; #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, false); \ return; \ } \ } while (0) #define TEST_NOTEQUAL(a, b) \ do { \ if ((a) == (b)) { \ SET_FAILURE(#a " == " #b, false); \ return; \ } \ } while (0) #define TEST_TRUE(a) \ do { \ if (!(a)) { \ SET_FAILURE(#a " is not true", false); \ return; \ } \ } while (0) #define TEST_STREQUAL(a, b) \ do { \ if (strcmp(a, b) != 0) { \ 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) #define TEST_CASE(_name) \ static void __test_h_##_name(struct test_case_metadata *, \ struct test_file_metadata *); \ static struct test_file_metadata __test_h_file; \ static struct test_case_metadata __test_h_meta_##_name = { \ .name = #_name, \ .fn = __test_h_##_name, \ }; \ static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \ __test_h_meta_##_name.next = __test_h_file.tests; \ __test_h_file.tests = &__test_h_meta_##_name; \ if (!__test_h_file.registered) { \ __test_h_file.name = __FILE__; \ __test_h_file.next = test_file_head; \ test_file_head = &__test_h_file; \ __test_h_file.registered = true; \ } \ } \ static void __test_h_##_name( \ struct test_case_metadata *metadata __attribute__((unused)), \ struct test_file_metadata *file_metadata __attribute__((unused))) extern void __attribute__((weak)) (*test_h_unittest_setup)(void); /// Run defined tests, return true if all tests succeeds /// @param[out] tests_run if not NULL, set to whether tests were run static inline void __attribute__((constructor(102))) run_tests(void) { bool should_run = false; #ifdef USE_SYSCTL_FOR_ARGS int mib[] = { CTL_KERN, #if defined(__NetBSD__) || defined(__OpenBSD__) KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV, #else KERN_PROC, KERN_PROC_ARGS, getpid(), #endif }; char *arg = NULL; size_t arglen; sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &arglen, NULL, 0); arg = malloc(arglen); sysctl(mib, sizeof(mib) / sizeof(mib[0]), arg, &arglen, NULL, 0); #else FILE *cmdlinef = fopen("/proc/self/cmdline", "r"); char *arg = NULL; int arglen; fscanf(cmdlinef, "%ms%n", &arg, &arglen); fclose(cmdlinef); #endif for (char *pos = arg; pos < arg + arglen; pos += strlen(pos) + 1) { if (strcmp(pos, "--unittest") == 0) { should_run = true; break; } } free(arg); if (!should_run) { return; } if (&test_h_unittest_setup) { test_h_unittest_setup(); } struct test_file_metadata *i = test_file_head; int failed = 0, success = 0; while (i) { fprintf(stderr, "Running tests from %s:\n", i->name); struct test_case_metadata *j = i->tests; while (j) { fprintf(stderr, "\t%s ... ", j->name); j->failure.present = false; j->fn(j, i); 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"); success++; } j = j->next; } fprintf(stderr, "\n"); i = i->next; } int total = failed + success; fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total, failed, total); exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE); } #else #include #define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) #define TEST_EQUAL(a, b) (void)((a) == (b)) #define TEST_NOTEQUAL(a, b) (void)((a) != (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 picom-12.5/tests/000077500000000000000000000000001471504570600137235ustar00rootroot00000000000000picom-12.5/tests/configs/000077500000000000000000000000001471504570600153535ustar00rootroot00000000000000picom-12.5/tests/configs/clear_shadow_unredirected.conf000066400000000000000000000001241471504570600234070ustar00rootroot00000000000000shadow = true; shadow-exclude = [ "name = 'NoShadow'" ] unredir-if-possible = true; picom-12.5/tests/configs/empty.conf000066400000000000000000000000001471504570600173460ustar00rootroot00000000000000picom-12.5/tests/configs/issue239.conf000066400000000000000000000001571471504570600176130ustar00rootroot00000000000000fading = true; fade-in-step = 1; fade-out-step = 0.01; shadow = true; shadow-exclude = [ "name = 'NoShadow'" ] picom-12.5/tests/configs/issue239_2.conf000066400000000000000000000002131471504570600200250ustar00rootroot00000000000000fading = true; fade-in-step = 1; fade-out-step = 0.01; shadow = true; shadow-exclude = [ "name = 'NoShadow'" ] unredir-if-possible = true; picom-12.5/tests/configs/issue239_3.conf000066400000000000000000000000701471504570600200270ustar00rootroot00000000000000shadow = true; shadow-exclude = [ "name = 'NoShadow'" ] picom-12.5/tests/configs/issue239_universal.conf000066400000000000000000000002011471504570600216710ustar00rootroot00000000000000fading = true; fade-in-step = 1; fade-out-step = 0.01; shadow = true; rules = ({ match = "name = 'NoShadow'"; shadow = false; }) picom-12.5/tests/configs/issue314.conf000066400000000000000000000001721471504570600176020ustar00rootroot00000000000000fading = true fade-in-step = 0.01 fade-out-step = 0.01 inactive-opacity = 0 blur-background = true force-win-blend = true picom-12.5/tests/configs/issue357.conf000066400000000000000000000000671471504570600176140ustar00rootroot00000000000000fading = true; fade-in-step = 1; fade-out-step = 0.01; picom-12.5/tests/configs/issue394.conf000066400000000000000000000001061471504570600176070ustar00rootroot00000000000000fading = true; fade-in-step = 1; fade-out-step = 0.01; shadow = true; picom-12.5/tests/configs/issue465.conf000066400000000000000000000000701471504570600176060ustar00rootroot00000000000000shadow = true; shadow-exclude = [ "focused != 1" ]; picom-12.5/tests/configs/parsing_test.conf000066400000000000000000000340661471504570600207350ustar00rootroot00000000000000################################# # 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'" ] animations = ({ triggers = ["close", "hide"]; offset-y = { start = 0; duration = 0.2; end = "- window-height - window-y"; }; opacity = 1; }, { triggers = ["open"]; preset = "slide-in"; duration = 1; }, { triggers = ["open"]; preset = "slide-out"; duration = 1; }, { triggers = ["open"]; preset = "fly-in"; duration = 1; }, { triggers = ["open"]; preset = "fly-out"; duration = 1; }, { triggers = ["open"]; preset = "appear"; duration = 1; }, { triggers = ["open"]; preset = "disappear"; duration = 1; }); picom-12.5/tests/configs/pull1091.conf000066400000000000000000000000341471504570600175060ustar00rootroot00000000000000unredir-if-possible = true; picom-12.5/tests/configs/shader.frag000066400000000000000000000000301471504570600174530ustar00rootroot00000000000000vec4 window_shader() {} picom-12.5/tests/run_one_test.sh000077500000000000000000000010771471504570600167730ustar00rootroot00000000000000#!/usr/bin/env bash set -xe if [ -z $DISPLAY ]; then exec xvfb-run -s "+extension composite" -a $0 $1 $2 $3 fi echo "Running test $2" picom_exe=$1 config=$2 test_script=$3 function test_with_backend() { backend=$1 # TODO keep the log file, and parse it to see if test is successful ($picom_exe --dbus --backend $backend --log-level=debug --log-file=$PWD/log --config=$config) & main_pid=$! $test_script kill -INT $main_pid || true cat log rm log wait $main_pid } test_with_backend dummy test_with_backend xrender test_with_backend glx # test_with_backend egl picom-12.5/tests/run_tests.sh000077500000000000000000000024561471504570600163170ustar00rootroot00000000000000#!/bin/sh set -e exe=$(realpath $1) cd $(dirname $0) eval `dbus-launch --sh-syntax` ./run_one_test.sh $exe configs/empty.conf testcases/basic.py ./run_one_test.sh $exe configs/issue357.conf testcases/issue357.py ./run_one_test.sh $exe configs/issue239.conf testcases/issue239.py ./run_one_test.sh $exe configs/issue239_universal.conf testcases/issue239.py ./run_one_test.sh $exe configs/issue239_2.conf testcases/issue239_2.py ./run_one_test.sh $exe configs/issue239_3.conf testcases/issue239_3.py ./run_one_test.sh $exe configs/issue239_3.conf testcases/issue239_3_norefresh.py ./run_one_test.sh $exe configs/issue314.conf testcases/issue314.py ./run_one_test.sh $exe configs/issue314.conf testcases/issue314_2.py ./run_one_test.sh $exe configs/issue314.conf testcases/issue314_3.py ./run_one_test.sh $exe /dev/null testcases/issue299.py ./run_one_test.sh $exe configs/issue465.conf testcases/issue465.py ./run_one_test.sh $exe configs/clear_shadow_unredirected.conf testcases/clear_shadow_unredirected.py ./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 picom-12.5/tests/testcases/000077500000000000000000000000001471504570600157215ustar00rootroot00000000000000picom-12.5/tests/testcases/basic.py000077500000000000000000000007541471504570600173650ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth wid = conn.generate_id() conn.core.CreateWindowChecked(depth, wid, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() conn.core.MapWindowChecked(wid).check() conn.core.UnmapWindowChecked(wid).check() conn.core.DestroyWindowChecked(wid).check() picom-12.5/tests/testcases/clear_shadow_unredirected.py000077500000000000000000000026451471504570600234750ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth name = "_NET_WM_STATE" name_atom = conn.core.InternAtom(False, len(name), name).reply().atom atom = "ATOM" atom_atom = conn.core.InternAtom(False, len(atom), atom).reply().atom fs = "_NET_WM_STATE_FULLSCREEN" fs_atom = conn.core.InternAtom(False, len(fs), fs).reply().atom # making sure disabling shadow while screen is unredirected doesn't cause assertion failure 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() # Set Window name so it does get a shadow set_window_name(conn, wid, "YesShadow") # Map the window print("mapping") conn.core.MapWindowChecked(wid).check() time.sleep(0.5) # Set fullscreen property, causing screen to be unredirected conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, name_atom, atom_atom, 32, 1, [fs_atom]).check() time.sleep(0.5) # Set the Window name so it loses its shadow print("set new name") set_window_name(conn, wid, "NoShadow") # Unmap the window conn.core.UnmapWindowChecked(wid).check() time.sleep(0.5) # Destroy the window conn.core.DestroyWindowChecked(wid).check() picom-12.5/tests/testcases/common.py000066400000000000000000000072351471504570600175720ustar00rootroot00000000000000import xcffib.xproto as xproto import xcffib.randr as randr import xcffib import time import random import string def to_atom(conn, string): return conn.core.InternAtom(False, len(string), string).reply().atom def set_window_name(conn, wid, name): prop_name = to_atom(conn, "_NET_WM_NAME") str_type = to_atom(conn, "UTF8_STRING") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, prop_name, str_type, 8, len(name), name).check() prop_name = to_atom(conn, "WM_NAME") str_type = to_atom(conn, "STRING") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, prop_name, str_type, 8, len(name), name).check() def set_window_state(conn, wid, state): prop_name = to_atom(conn, "WM_STATE") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, prop_name, prop_name, 32, 2, [state, 0]).check() def set_window_class(conn, wid, name): if not isinstance(name, bytearray): name = name.encode() name = name+b"\0"+name+b"\0" prop_name = to_atom(conn, "WM_CLASS") str_type = to_atom(conn, "STRING") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, prop_name, str_type, 8, len(name), name).check() def set_window_size_async(conn, wid, width, height): value_mask = xproto.ConfigWindow.Width | xproto.ConfigWindow.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() root = setup.roots[0].root windows = conn.core.QueryTree(root).reply() ext = xproto.xprotoExtension(conn) for w in windows.children: name = ext.GetProperty(False, w, prop_name, xproto.GetPropertyType.Any, 0, (2 ** 32) - 1).reply() if name.value.buf() == b"picom": return w 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 = 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) reply = rr.CreateMode(root, mode_info, len(name), name).reply() mode = reply.mode reply = rr.GetScreenResourcesCurrent(root).reply() # our xvfb is setup to only have 1 output output = reply.outputs[0] rr.AddOutputModeChecked(output, mode).check() return reply, mode, output def trigger_root_configure(conn, reply, mode, output): rr = conn(randr.key) return rr.SetCrtcConfig(reply.crtcs[0], reply.timestamp, reply.config_timestamp, 0, 0, mode, randr.Rotation.Rotate_0, 1, [output]) def find_32bit_visual(conn): setup = conn.get_setup() render = conn(xcffib.render.key) r = render.QueryPictFormats().reply() pictfmt_ids = set() for pictform in r.formats: if (pictform.depth == 32 and pictform.type == xcffib.render.PictType.Direct and pictform.direct.alpha_mask != 0): pictfmt_ids.add(pictform.id) print(pictfmt_ids) for screen in r.screens: for depth in screen.depths: for pv in depth.visuals: if pv.format in pictfmt_ids: return pv.visual picom-12.5/tests/testcases/issue239.py000077500000000000000000000017141471504570600176670ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth # issue 239 is caused by a window gaining a shadow during its fade-out transition 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() # Set Window name so it doesn't get a shadow set_window_name(conn, wid, "NoShadow") # Map the window print("mapping") conn.core.MapWindowChecked(wid).check() time.sleep(0.5) # Set the Window name so it gets a shadow print("set new name") set_window_name(conn, wid, "YesShadow") # Unmap the window conn.core.UnmapWindowChecked(wid).check() time.sleep(0.5) # Destroy the window conn.core.DestroyWindowChecked(wid).check() picom-12.5/tests/testcases/issue239_2.py000077500000000000000000000031211471504570600201020ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth # issue 239 is caused by a window gaining a shadow during its fade-out transition wid = conn.generate_id() print("Window ids are ", hex(wid)) # Create a window mask = xproto.CW.BackPixel value = [ setup.roots[0].white_pixel ] conn.core.CreateWindowChecked(depth, wid, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, mask, value).check() name = "_NET_WM_STATE" name_atom = conn.core.InternAtom(False, len(name), name).reply().atom atom = "ATOM" atom_atom = conn.core.InternAtom(False, len(atom), atom).reply().atom fs = "_NET_WM_STATE_FULLSCREEN" fs_atom = conn.core.InternAtom(False, len(fs), fs).reply().atom # Map the window, causing screen to be redirected conn.core.MapWindowChecked(wid).check() time.sleep(0.5) # Set fullscreen property, causing screen to be unredirected conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, name_atom, atom_atom, 32, 1, [fs_atom]).check() time.sleep(0.5) # Clear fullscreen property, causing screen to be redirected conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid, name_atom, atom_atom, 32, 0, []).check() # Do a round trip to X server so the compositor has a chance to start the rerun of _draw_callback conn.core.GetInputFocus().reply() # Unmap the window, triggers the bug conn.core.UnmapWindowChecked(wid).check() time.sleep(0.5) # Destroy the window conn.core.DestroyWindowChecked(wid).check() picom-12.5/tests/testcases/issue239_3.py000077500000000000000000000020461471504570600201100ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth # issue 239 is caused by a window gaining a shadow during its fade-out transition 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() # Set Window name so it gets a shadow set_window_name(conn, wid, "YesShadow") # Map the window print("mapping") conn.core.MapWindowChecked(wid).check() time.sleep(0.5) print("set new name") set_window_name(conn, wid, "NoShadow") time.sleep(0.5) # Set the Window name so it gets a shadow print("set new name") set_window_name(conn, wid, "YesShadow") time.sleep(0.5) # Unmap the window conn.core.UnmapWindowChecked(wid).check() time.sleep(0.5) # Destroy the window conn.core.DestroyWindowChecked(wid).check() picom-12.5/tests/testcases/issue239_3_norefresh.py000077500000000000000000000020251471504570600221600ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth # issue 239 is caused by a window gaining a shadow during its fade-out transition 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() # Set Window name so it gets a shadow set_window_name(conn, wid, "YesShadow") # Map the window print("mapping") conn.core.MapWindowChecked(wid).check() time.sleep(0.5) print("set new name") set_window_name(conn, wid, "NoShadow") # Set the Window name so it gets a shadow print("set new name") set_window_name(conn, wid, "YesShadow") time.sleep(0.5) # Unmap the window conn.core.UnmapWindowChecked(wid).check() time.sleep(0.5) # Destroy the window conn.core.DestroyWindowChecked(wid).check() picom-12.5/tests/testcases/issue299.py000077500000000000000000000067751471504570600177110ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time import os import subprocess import asyncio from dbus_next.aio import MessageBus from dbus_next.message import Message, MessageType from common import * display = os.environ["DISPLAY"].replace(":", "_") conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth x = xproto.xprotoExtension(conn) visual32 = find_32bit_visual(conn) async def get_client_win_async(wid): message = await bus.call(Message(destination='com.github.chjj.compton.'+display, path='/com/github/chjj/compton', interface='com.github.chjj.compton', member='win_get', signature='us', body=[wid, 'client_win'])) return message.body[0] def get_client_win(wid): return loop.run_until_complete(get_client_win_async(wid)) def wait(): time.sleep(0.5) def create_client_window(name): client_win = conn.generate_id() print("Window : ", hex(client_win)) conn.core.CreateWindowChecked(depth, client_win, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() set_window_name(conn, client_win, "Test window "+name) set_window_class(conn, client_win, "Test windows") set_window_state(conn, client_win, 1) conn.core.MapWindowChecked(client_win).check() return client_win loop = asyncio.get_event_loop() bus = loop.run_until_complete(MessageBus().connect()) cmid = conn.generate_id() colormap = conn.core.CreateColormapChecked(xproto.ColormapAlloc._None, cmid, root, visual32).check() # Create window client_wins = [] for i in range(0,2): client_wins.append(create_client_window(str(i))) # Create frame window frame_win = conn.generate_id() print("Window : ", hex(frame_win)) conn.core.CreateWindowChecked(depth, frame_win, root, 0, 0, 200, 200, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() set_window_name(conn, frame_win, "Frame") conn.core.MapWindowChecked(frame_win).check() # Scenario 1.1 # 1. reparent placeholder to frame conn.core.ReparentWindowChecked(client_wins[0], frame_win, 0, 0).check() wait() # 2. reparent real client to frame conn.core.ReparentWindowChecked(client_wins[1], frame_win, 0, 0).check() wait() # 3. detach the placeholder conn.core.ReparentWindowChecked(client_wins[0], root, 0, 0).check() wait() assert get_client_win(frame_win) == client_wins[1] # Scenario 1.2 # 1. reparent placeholder to frame conn.core.ReparentWindowChecked(client_wins[0], frame_win, 0, 0).check() wait() # 2. reparent real client to frame conn.core.ReparentWindowChecked(client_wins[1], frame_win, 0, 0).check() wait() # 3. destroy the placeholder conn.core.DestroyWindowChecked(client_wins[0]).check() wait() assert get_client_win(frame_win) == client_wins[1] client_wins[0] = create_client_window("0") # Scenario 2 # 1. frame is unmapped conn.core.UnmapWindowChecked(frame_win).check() wait() # 2. reparent placeholder to frame conn.core.ReparentWindowChecked(client_wins[0], frame_win, 0, 0).check() wait() # 3. destroy placeholder, map frame and reparent real client to frame conn.core.DestroyWindowChecked(client_wins[0]).check() conn.core.MapWindowChecked(frame_win).check() conn.core.ReparentWindowChecked(client_wins[1], frame_win, 0, 0).check() wait() assert get_client_win(frame_win) == client_wins[1] client_wins[0] = create_client_window("0") # Destroy the windows for wid in client_wins: conn.core.DestroyWindowChecked(wid).check() conn.core.DestroyWindowChecked(frame_win).check() picom-12.5/tests/testcases/issue314.py000077500000000000000000000025521471504570600176620ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth x = xproto.xprotoExtension(conn) # issue 314 is caused by changing a windows target opacity during its fade-in/-out transition wid1 = conn.generate_id() print("Window 1: ", hex(wid1)) wid2 = conn.generate_id() print("Window 2: ", hex(wid2)) # Create windows conn.core.CreateWindowChecked(depth, wid1, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() conn.core.CreateWindowChecked(depth, wid2, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() # Set Window names set_window_name(conn, wid1, "Test window 1") set_window_name(conn, wid2, "Test window 2") # Check updating opacity while UNMAPPING/DESTROYING windows print("Mapping 1") conn.core.MapWindowChecked(wid1).check() print("Mapping 2") conn.core.MapWindowChecked(wid2).check() time.sleep(0.5) x.SetInputFocusChecked(0, wid1, xproto.Time.CurrentTime).check() time.sleep(0.5) # Destroy the windows print("Destroy 1 while fading out") conn.core.DestroyWindowChecked(wid1).check() x.SetInputFocusChecked(0, wid2, xproto.Time.CurrentTime).check() time.sleep(1) conn.core.DestroyWindowChecked(wid2).check() picom-12.5/tests/testcases/issue314_2.py000077500000000000000000000026461471504570600201070ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth x = xproto.xprotoExtension(conn) opacity_80 = [int(0xffffffff * 0.8), ] opacity_single = [int(0xffffffff * 0.002), ] # issue 314 is caused by changing a windows target opacity during its fade-in/-out transition wid1 = conn.generate_id() print("Window 1: ", hex(wid1)) atom = "_NET_WM_WINDOW_OPACITY" opacity_atom = conn.core.InternAtom(False, len(atom), atom).reply().atom # Create windows conn.core.CreateWindowChecked(depth, wid1, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() # Set Window names set_window_name(conn, wid1, "Test window 1") # Check updating opacity while MAPPING windows print("Mapping window") conn.core.MapWindowChecked(wid1).check() time.sleep(0.5) print("Update opacity while fading in") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_80).check() time.sleep(0.2) conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_single).check() time.sleep(1) conn.core.DeletePropertyChecked(wid1, opacity_atom).check() time.sleep(0.5) # Destroy the windows conn.core.DestroyWindowChecked(wid1).check() picom-12.5/tests/testcases/issue314_3.py000077500000000000000000000045311471504570600201030ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth x = xproto.xprotoExtension(conn) opacity_100 = [0xffffffff, ] opacity_80 = [int(0xffffffff * 0.8), ] opacity_single = [int(0xffffffff * 0.002), ] opacity_0 = [0, ] # issue 314 is caused by changing a windows target opacity during its fade-in/-out transition wid1 = conn.generate_id() print("Window 1: ", hex(wid1)) atom = "_NET_WM_WINDOW_OPACITY" opacity_atom = conn.core.InternAtom(False, len(atom), atom).reply().atom # Create windows conn.core.CreateWindowChecked(depth, wid1, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() # Set Window names set_window_name(conn, wid1, "Test window 1") # Check updating opacity while FADING windows print("Mapping window") conn.core.MapWindowChecked(wid1).check() time.sleep(1.2) print("Update opacity while fading out") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_single).check() time.sleep(0.2) conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_0).check() time.sleep(1) print("Change from fading in to fading out") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_80).check() time.sleep(0.5) conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_0).check() time.sleep(1) print("Update opacity while fading in") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_80).check() time.sleep(0.2) conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_100).check() time.sleep(1) print("Change from fading out to fading in") conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_0).check() time.sleep(0.5) conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid1, opacity_atom, xproto.Atom.CARDINAL, 32, 1, opacity_80).check() time.sleep(1) # Destroy the windows conn.core.DestroyWindowChecked(wid1).check() picom-12.5/tests/testcases/issue357.py000077500000000000000000000016261471504570600176720ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name, trigger_root_configure, prepare_root_configure conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth # issue 357 is triggered when a window is destroyed right after configure_root wid = conn.generate_id() print("Window 1: ", hex(wid)) # Create a window conn.core.CreateWindowChecked(depth, wid, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() # Set Window name set_window_name(conn, wid, "Test window 1") print("mapping 1") conn.core.MapWindowChecked(wid).check() time.sleep(0.5) reply, mode, output = prepare_root_configure(conn) trigger_root_configure(conn, reply, mode, output).reply() # Destroy the windows conn.core.DestroyWindowChecked(wid).check() time.sleep(1) picom-12.5/tests/testcases/issue394.py000077500000000000000000000016761471504570600177000ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name, set_window_size_async conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth # issue 394 is caused by a window getting a size update just before destroying leading to a shadow update on destroyed window. 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() # Set Window name so it doesn't get a shadow set_window_name(conn, wid, "Test Window") # Map the window print("mapping") conn.core.MapWindowChecked(wid).check() time.sleep(0.5) # Resize the window and destroy print("resize and destroy") set_window_size_async(conn, wid, 150, 150) conn.core.DestroyWindowChecked(wid).check() time.sleep(0.5) picom-12.5/tests/testcases/issue465.py000077500000000000000000000022441471504570600176670ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth x = xproto.xprotoExtension(conn) # issue 465 is triggered when focusing a new window with a shadow-exclude rule for unfocused windows. wid1 = conn.generate_id() print("Window 1: ", hex(wid1)) wid2 = conn.generate_id() print("Window 2: ", hex(wid2)) # Create a window conn.core.CreateWindowChecked(depth, wid1, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() conn.core.CreateWindowChecked(depth, wid2, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() # Set Window name set_window_name(conn, wid1, "Test window 1") set_window_name(conn, wid2, "Test window 2") print("mapping 1") conn.core.MapWindowChecked(wid1).check() print("mapping 2") conn.core.MapWindowChecked(wid2).check() time.sleep(0.5) x.SetInputFocusChecked(0, wid1, xproto.Time.CurrentTime).check() time.sleep(0.5) # Destroy the windows conn.core.DestroyWindowChecked(wid1).check() time.sleep(1) picom-12.5/tests/testcases/issue525.py000077500000000000000000000016421471504570600176650ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth # issue 525 happens when a window is unmapped with pixmap stale flag set 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) # change window size, invalidate the pixmap conn.core.ConfigureWindow(wid, xproto.ConfigWindow.X | xproto.ConfigWindow.Width, [100, 200]) # unmap the window immediately after conn.core.UnmapWindowChecked(wid).check() time.sleep(0.1) # Destroy the window conn.core.DestroyWindowChecked(wid).check() picom-12.5/tests/testcases/pull1091.py000077500000000000000000000021061471504570600175640ustar00rootroot00000000000000#!/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) picom-12.5/tests/testcases/redirect_when_unmapped_window_has_shadow.py000077500000000000000000000035161471504570600266050ustar00rootroot00000000000000#!/usr/bin/env python3 import xcffib.xproto as xproto import xcffib import time from common import set_window_name conn = xcffib.connect() setup = conn.get_setup() root = setup.roots[0].root visual = setup.roots[0].root_visual depth = setup.roots[0].root_depth name = "_NET_WM_STATE" name_atom = conn.core.InternAtom(False, len(name), name).reply().atom atom = "ATOM" atom_atom = conn.core.InternAtom(False, len(atom), atom).reply().atom fs = "_NET_WM_STATE_FULLSCREEN" fs_atom = conn.core.InternAtom(False, len(fs), fs).reply().atom wid1 = conn.generate_id() print("Window 1 id is ", hex(wid1)) # Create a window conn.core.CreateWindowChecked(depth, wid1, root, 0, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() # Map the window print("mapping 1") conn.core.MapWindowChecked(wid1).check() time.sleep(0.5) print("unmapping 1") # Unmap the window conn.core.UnmapWindowChecked(wid1).check() time.sleep(0.5) # create and map a second window wid2 = conn.generate_id() print("Window 2 id is ", hex(wid2)) conn.core.CreateWindowChecked(depth, wid2, root, 200, 0, 100, 100, 0, xproto.WindowClass.InputOutput, visual, 0, []).check() print("mapping 2") conn.core.MapWindowChecked(wid2).check() time.sleep(0.5) # Set fullscreen property on the second window, causing screen to be unredirected conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid2, name_atom, atom_atom, 32, 1, [fs_atom]).check() time.sleep(0.5) # Unset fullscreen property on the second window, causing screen to be redirected conn.core.ChangePropertyChecked(xproto.PropMode.Replace, wid2, name_atom, atom_atom, 32, 0, []).check() time.sleep(0.5) # map the first window again print("mapping 1") conn.core.MapWindowChecked(wid1).check() time.sleep(0.5) # Destroy the windows conn.core.DestroyWindowChecked(wid1).check() conn.core.DestroyWindowChecked(wid2).check() picom-12.5/tools/000077500000000000000000000000001471504570600137215ustar00rootroot00000000000000picom-12.5/tools/animgen.c000066400000000000000000000410761471504570600155130ustar00rootroot00000000000000#include #include #include "compiler.h" // IWYU pragma: keep #include "transition/script.h" #include "transition/script_internal.h" #include "utils/dynarr.h" #include "wm/win.h" enum knob_type { KNOB_NUMBER, KNOB_CHOICE, }; struct knob { UT_hash_handle hh; const char *name; enum knob_type type; union { struct { double default_value; } number; struct { char **choices; unsigned n_choices; unsigned default_choice; }; }; bool emitted; }; struct placeholder { struct knob *source; double *value_for_choices; }; bool config_extra_get_float(config_setting_t *setting, double *value) { if (config_setting_type(setting) != CONFIG_TYPE_FLOAT && config_setting_type(setting) != CONFIG_TYPE_INT && config_setting_type(setting) != CONFIG_TYPE_INT64) { return false; } *value = config_setting_get_float(setting); return true; } bool config_extra_get_int(config_setting_t *setting, int *value) { if (config_setting_type(setting) != CONFIG_TYPE_INT && config_setting_type(setting) != CONFIG_TYPE_INT64) { return false; } *value = config_setting_get_int(setting); return true; } char *sanitized_name(const char *name) { char *ret = strdup(name); for (char *p = ret; *p; p++) { if (*p == '-') { *p = '_'; } } return ret; } void free_charp(void *p) { free(*(char **)p); } #define scopedp(type) cleanup(free_##type##p) type * #define MAX_PLACEHOLDERS 10 void codegen(const char *name, const char *body, const struct placeholder *placeholders) { auto ident = sanitized_name(name); printf("static struct script *script_template__%s(int *output_slots)\n%s\n", ident, body); printf("static bool win_script_preset__%s(struct win_script *output, " "config_setting_t *setting) {\n", ident); printf(" output->script = script_template__%s(output->output_indices);\n", ident); for (size_t i = 0; i < MAX_PLACEHOLDERS; i++) { if (placeholders[i].source == NULL || placeholders[i].source->emitted) { continue; } auto knob = placeholders[i].source; scopedp(char) knob_ident = sanitized_name(knob->name); knob->emitted = true; if (knob->type == KNOB_NUMBER) { printf(" double knob_%s = %a;\n", knob_ident, knob->number.default_value); printf(" config_setting_lookup_float(setting, \"%s\", " "&knob_%s);\n", knob->name, knob_ident); continue; } printf(" const char *knob_%s = \"%s\";\n", knob_ident, knob->choices[knob->default_choice]); printf(" config_setting_lookup_string(setting, \"%s\", &knob_%s);\n", knob->name, knob_ident); for (unsigned j = 0; j < MAX_PLACEHOLDERS; j++) { if (placeholders[j].source != knob) { continue; } printf(" double placeholder%u_%s;\n", j, knob_ident); } for (unsigned j = 0; j < knob->n_choices; j++) { printf(" if (strcmp(knob_%s, \"%s\") == 0) {\n", knob_ident, knob->choices[j]); for (unsigned k = 0; k < MAX_PLACEHOLDERS; k++) { if (placeholders[k].source != knob) { continue; } printf(" placeholder%u_%s = %a;\n", k, knob_ident, placeholders[k].value_for_choices[j]); } printf(" } else "); } printf("{\n"); printf(" log_error(\"Invalid choice \\\"%%s\\\" for " "option \\\"%s\\\". Line %%d.\", knob_%s, " "config_setting_source_line(config_setting_get_member(setting, " "\"%s\")));\n", knob->name, knob_ident, knob->name); printf(" log_error(\" Valid ones are: "); for (unsigned j = 0; j < knob->n_choices; j++) { printf("%s\\\"%s\\\"", j ? ", " : "", knob->choices[j]); } printf("\");\n"); printf(" script_free(output->script);\n"); printf(" output->script = NULL;\n"); printf(" return false;\n"); printf(" }\n"); } printf(" struct script_specialization_context spec[] = {\n"); for (size_t i = 0; i < 10; i++) { if (placeholders[i].source == NULL) { continue; } auto knob = placeholders[i].source; auto knob_ident = sanitized_name(knob->name); if (knob->type == KNOB_NUMBER) { printf(" {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + %zu, " ".value = knob_%s},\n", i * 4, knob_ident); } else { printf(" {.offset = SCRIPT_CTX_PLACEHOLDER_BASE + %zu, " ".value = placeholder%zu_%s},\n", i * 4, i, knob_ident); } free(knob_ident); } printf(" };\n"); printf(" script_specialize(output->script, spec, ARR_SIZE(spec));\n"); printf(" return true;\n"); printf("}\n"); free(ident); } /// Syntax for defining knobs and placeholders: /// /// { /// # other settings... /// # .... /// /// __knobs = { /// knob1 = 0.5; # knob1 is a number, default value 0.5 /// /// # knob2 is a choice, default choice is "default_choice" (index 2) /// # ┌----- index of the default choice /// # v /// knob2 = (2, ["choice1", "choice2", "default_choice"]); /// }; /// __placeholders = ( /// #┌----- index of the placeholder /// #v /// (1, "knob1"), # placeholder1 takes value from knob1 /// /// # placeholder2 takes value from knob2. Because knob2 is a choice, /// # we need to provide a mapping from choice to value. /// (2, "knob2", [1, 2, 0]); /// ); /// } static bool parse_knobs(const char *preset_name, config_setting_t *knob_settings, config_setting_t *placeholder_settings, struct knob *knobs, struct placeholder *placeholders) { struct knob *knobs_by_name = NULL; if (config_setting_length(knob_settings) > MAX_PLACEHOLDERS) { fprintf(stderr, "Too many knobs in %s, max %d allowed\n", preset_name, MAX_PLACEHOLDERS); return false; } if (config_setting_length(placeholder_settings) > MAX_PLACEHOLDERS) { fprintf(stderr, "Too many placeholders in %s, max %d allowed\n", preset_name, MAX_PLACEHOLDERS); return false; } unsigned n_knobs = 0; for (unsigned i = 0; i < (unsigned)config_setting_length(knob_settings); i++) { auto config = config_setting_get_elem(knob_settings, i); const char *name = config_setting_name(config); double default_value; auto knob = &knobs[n_knobs++]; knob->name = strdup(name); if (config_extra_get_float(config, &default_value)) { knob->type = KNOB_NUMBER; knob->number.default_value = default_value; HASH_ADD_STR(knobs_by_name, name, knob); n_knobs++; continue; } if (!config_setting_is_list(config) || config_setting_length(config) != 2) { fprintf(stderr, "Invalid placeholder %s in %s, line %d. It must be a " "list of length 2.\n", name, preset_name, config_setting_source_line(config)); continue; } int default_choice; config_setting_t *choices = config_setting_get_elem(config, 1); if (!config_extra_get_int(config_setting_get_elem(config, 0), &default_choice) || choices == NULL || !config_setting_is_array(choices)) { fprintf(stderr, "Invalid placeholder %s in %s, line %d. Failed to get " "elements.\n", name, preset_name, config_setting_source_line(config)); continue; } auto n_choices = (unsigned)config_setting_length(choices); if (default_choice < 0 || (unsigned)default_choice >= n_choices) { fprintf(stderr, "Invalid knob choice in %s, knob %s line %d. Default " "choice out of range.\n", preset_name, name, config_setting_source_line(config)); continue; } knob->type = KNOB_CHOICE; knob->n_choices = 0; knob->choices = malloc(n_choices * sizeof(char *)); knob->default_choice = (unsigned)default_choice; bool has_error = false; for (unsigned j = 0; j < n_choices; j++) { auto choice = config_setting_get_string(config_setting_get_elem(choices, j)); if (choice == NULL) { fprintf(stderr, "Invalid knob choice in %s, knob %s line %d. " "Failed to get choice.\n", preset_name, name, config_setting_source_line(config)); has_error = true; break; } for (unsigned k = 0; k < j; k++) { if (strcmp(knob->choices[k], choice) == 0) { fprintf(stderr, "Invalid knob choice in %s, knob %s line " "%d. Duplicate choice %s.\n", preset_name, name, config_setting_source_line(config), choice); has_error = true; break; } } if (has_error) { break; } knob->choices[knob->n_choices++] = strdup(choice); } if (has_error) { for (unsigned j = 0; j < knob->n_choices; j++) { free(knob->choices[j]); } free(knob->choices); free((void *)knob->name); knob->choices = NULL; knob->name = NULL; continue; } HASH_ADD_STR(knobs_by_name, name, knob); n_knobs++; } for (unsigned i = 0; i < (unsigned)config_setting_length(placeholder_settings); i++) { auto config = config_setting_get_elem(placeholder_settings, i); if (!config_setting_is_list(config) || config_setting_length(config) < 2) { fprintf(stderr, "Invalid placeholder in preset %s, line %d. Must be a " "non-empty list.\n", preset_name, config_setting_source_line(config)); continue; } int index; if (!config_extra_get_int(config_setting_get_elem(config, 0), &index)) { fprintf(stderr, "Invalid placeholder in preset %s, line %d. Index must " "be an integer.\n", preset_name, config_setting_source_line(config)); continue; } auto placeholder = &placeholders[index]; if (placeholder->source) { fprintf(stderr, "Invalid placeholder in preset %s, line %d. Placeholder " "with index %d already defined.\n", preset_name, config_setting_source_line(config), index); continue; } BUG_ON(placeholder->value_for_choices != NULL); const char *source = config_setting_get_string(config_setting_get_elem(config, 1)); struct knob *knob; HASH_FIND_STR(knobs_by_name, source, knob); if (!knob) { fprintf(stderr, "Invalid placeholder%d definition in %s, line " "%d. Source knob %s not found.\n", index, preset_name, config_setting_source_line(config), source); continue; } if (config_setting_length(config) == 2) { if (knob->type != KNOB_NUMBER) { fprintf(stderr, "Invalid placeholder%d definition in %s, line " "%d. Source knob %s is not a number.\n", index, preset_name, config_setting_source_line(config), source); continue; } placeholder->source = knob; } else if (config_setting_length(config) == 3) { config_setting_t *value_for_choices = config_setting_get_elem(config, 2); if (value_for_choices == NULL || !config_setting_is_array(value_for_choices)) { fprintf(stderr, "Invalid placeholder%d definition in %s, line " "%d. Failed to get elements.\n", index, preset_name, config_setting_source_line(config)); continue; } if (knob->type != KNOB_CHOICE) { fprintf(stderr, "Invalid placeholder%d definition in %s, line " "%d. Source knob %s is not a choice.\n", index, preset_name, config_setting_source_line(config), source); continue; } if (knob->n_choices != (unsigned)config_setting_length(value_for_choices)) { fprintf(stderr, "Invalid placeholder%d definition in %s, line " "%d. Number of choices doesn't match.\n", index, preset_name, config_setting_source_line(config)); continue; } placeholder->value_for_choices = malloc(sizeof(double) * knob->n_choices); for (unsigned j = 0; j < knob->n_choices; j++) { double value; if (!config_extra_get_float( config_setting_get_elem(value_for_choices, j), &value)) { fprintf(stderr, "Invalid placeholder%d definition in %s, " "line %d. Failed to get value.\n", index, preset_name, config_setting_source_line(config)); free(placeholder->value_for_choices); placeholder->value_for_choices = NULL; break; } placeholder->value_for_choices[j] = value; } if (placeholder->value_for_choices == NULL) { continue; } placeholder->source = knob; } else { fprintf(stderr, "Invalid placeholder%d definition in %s, line %d. " "Excessive elements.\n", index, preset_name, config_setting_source_line(config)); continue; } } struct knob *k, *nk; HASH_ITER(hh, knobs_by_name, k, nk) { HASH_DEL(knobs_by_name, k); } return true; } int main(int argc, const char **argv) { if (argc != 2) { return 1; } log_init_tls(); char **presets = dynarr_new(char *, 10); config_t cfg; config_init(&cfg); config_set_auto_convert(&cfg, 1); if (!config_read_file(&cfg, argv[1])) { fprintf(stderr, "Failed to read config file %s: %s\n", argv[1], config_error_text(&cfg)); config_destroy(&cfg); return 1; } auto settings = config_root_setting(&cfg); // win_script_context_info and 10 extra placeholder contexts, for // script_specialize() static const ptrdiff_t base = SCRIPT_CTX_PLACEHOLDER_BASE; struct script_context_info context_info[ARR_SIZE(win_script_context_info) + MAX_PLACEHOLDERS] = { {"placeholder0", base}, {"placeholder1", base + 4}, {"placeholder2", base + 8}, {"placeholder3", base + 12}, {"placeholder4", base + 16}, {"placeholder5", base + 20}, {"placeholder6", base + 24}, {"placeholder7", base + 28}, {"placeholder8", base + 32}, {"placeholder9", base + 36}, }; memcpy(context_info + 10, win_script_context_info, sizeof(win_script_context_info)); struct script_output_info outputs[ARR_SIZE(win_script_outputs)]; memcpy(outputs, win_script_outputs, sizeof(win_script_outputs)); struct script_parse_config parse_config = { .context_info = context_info, .output_info = NULL, }; printf("// This file is generated by tools/animgen.c from %s\n", argv[1]); printf("// This file is included in git repository for " "convenience only.\n"); printf("// DO NOT EDIT THIS FILE!\n\n"); printf("#include \n"); printf("#include \"../script.h\"\n"); printf("#include \"../curve.h\"\n"); printf("#include \"../script_internal.h\"\n"); printf("#include \"utils/misc.h\"\n"); printf("#include \"config.h\"\n"); for (unsigned i = 0; i < (unsigned)config_setting_length(settings); i++) { auto sub = config_setting_get_elem(settings, i); auto name = config_setting_name(sub); struct knob knobs[MAX_PLACEHOLDERS] = {}; struct placeholder placeholders[MAX_PLACEHOLDERS] = {}; auto knob_settings = config_setting_get_member(sub, "*knobs"); if (knob_settings) { auto placeholder_settings = config_setting_get_member(sub, "*placeholders"); BUG_ON(!placeholder_settings); parse_knobs(name, knob_settings, placeholder_settings, knobs, placeholders); config_setting_remove(sub, "*knobs"); config_setting_remove(sub, "*placeholders"); knob_settings = NULL; } char *err = NULL; auto script = script_compile(sub, parse_config, &err); if (!script) { fprintf(stderr, "Failed to compile script %s: %s\n", name, err); free(err); continue; } bool has_err = false; for (size_t j = 0; j < script->len; j++) { if (script->instrs[j].type != INST_LOAD_CTX) { continue; } if (script->instrs[j].ctx < base) { continue; } size_t index = (size_t)(script->instrs[j].ctx - base) / 4; BUG_ON(index >= ARR_SIZE(knobs)); if (placeholders[index].source == NULL) { fprintf(stderr, "Placeholder %zu used, but not defined\n", index); has_err = true; break; } } if (!has_err) { char *code = script_to_c(script, outputs); codegen(name, code, placeholders); free(code); dynarr_push(presets, strdup(name)); } for (size_t j = 0; j < MAX_PLACEHOLDERS; j++) { if (placeholders[j].value_for_choices) { free(placeholders[j].value_for_choices); } } for (size_t j = 0; j < MAX_PLACEHOLDERS; j++) { if (knobs[j].type == KNOB_CHOICE) { for (unsigned k = 0; k < knobs[j].n_choices; k++) { free(knobs[j].choices[k]); } free(knobs[j].choices); } free((void *)knobs[j].name); } script_free(script); } config_destroy(&cfg); printf("struct {\n" " const char *name;\n" " bool (*func)(struct win_script *output, " "config_setting_t *setting);\n" "} win_script_presets[] = {\n"); dynarr_foreach(presets, p) { auto ident = sanitized_name(*p); printf(" {\"%s\", win_script_preset__%s},\n", *p, ident); free(*p); free(ident); } printf(" {NULL, NULL},\n};\n"); dynarr_free_pod(presets); return 0; } picom-12.5/tools/meson.build000066400000000000000000000003111471504570600160560ustar00rootroot00000000000000executable( 'animgen', 'animgen.c', dependencies: [ base_deps, libconfig_dep, test_h_dep, cc.find_library('m')], link_with: [libtools], build_by_default: false, include_directories: picom_inc, )