فهرست منبع

fixed few bugs on wallpapers, linted and added release process

UwU 1 روز پیش
والد
کامیت
8ce258408f
98فایلهای تغییر یافته به همراه1808 افزوده شده و 1279 حذف شده
  1. 59 12
      .github/workflows/cmake.yml
  2. 12 5
      CMakeLists.txt
  3. 10 7
      README.md
  4. 1 1
      packaging/archlinux/PKGBUILD
  5. 53 36
      src/WallpaperEngine/Application/ApplicationContext.cpp
  6. 4 2
      src/WallpaperEngine/Application/ApplicationContext.h
  7. 22 22
      src/WallpaperEngine/Application/WallpaperApplication.cpp
  8. 4 2
      src/WallpaperEngine/Application/WallpaperApplication.h
  9. 3 1
      src/WallpaperEngine/Audio/AudioContext.cpp
  10. 2 1
      src/WallpaperEngine/Audio/AudioStream.h
  11. 21 22
      src/WallpaperEngine/Audio/Drivers/Recorders/PulseAudioPlaybackRecorder.cpp
  12. 1 1
      src/WallpaperEngine/Audio/Drivers/Recorders/PulseAudioPlaybackRecorder.h
  13. 3 4
      src/WallpaperEngine/Audio/SpectrumAnalyzer.cpp
  14. 2 1
      src/WallpaperEngine/Data/Model/Object.h
  15. 2 1
      src/WallpaperEngine/Data/Model/Property.h
  16. 20 16
      src/WallpaperEngine/Data/Parsers/DynamicValueParser.cpp
  17. 2 3
      src/WallpaperEngine/Data/Parsers/Localization.cpp
  18. 31 23
      src/WallpaperEngine/Data/Parsers/ObjectParser.cpp
  19. 4 3
      src/WallpaperEngine/Data/Parsers/PropertyParser.cpp
  20. 10 9
      src/WallpaperEngine/Data/Parsers/WallpaperParser.cpp
  21. 11 5
      src/WallpaperEngine/Desktop/IconTheme.cpp
  22. 21 0
      src/WallpaperEngine/Desktop/KDESession.cpp
  23. 6 0
      src/WallpaperEngine/Desktop/KDESession.h
  24. 19 9
      src/WallpaperEngine/Desktop/UserShortcut.cpp
  25. 7 4
      src/WallpaperEngine/FileSystem/Adapters/Package.cpp
  26. 4 3
      src/WallpaperEngine/FileSystem/Container.h
  27. 12 8
      src/WallpaperEngine/Input/Drivers/KDECursorInput.cpp
  28. 2 2
      src/WallpaperEngine/Input/Drivers/KDECursorInput.h
  29. 17 7
      src/WallpaperEngine/Input/Drivers/WaylandMouseInput.cpp
  30. 6 5
      src/WallpaperEngine/Input/Drivers/WaylandMouseInput.h
  31. 2 2
      src/WallpaperEngine/Media/ThumbnailPalette.cpp
  32. 1 1
      src/WallpaperEngine/Media/ThumbnailPalette.h
  33. 1 3
      src/WallpaperEngine/Render/AlbumTexture.cpp
  34. 19 10
      src/WallpaperEngine/Render/CFBO.cpp
  35. 5 5
      src/WallpaperEngine/Render/CTexture.cpp
  36. 84 86
      src/WallpaperEngine/Render/CWallpaper.cpp
  37. 6 5
      src/WallpaperEngine/Render/Camera.cpp
  38. 2 2
      src/WallpaperEngine/Render/Drivers/Detectors/KDEWaylandFullScreenDetector.h
  39. 4 27
      src/WallpaperEngine/Render/Drivers/Detectors/WaylandFullScreenDetector.cpp
  40. 2 0
      src/WallpaperEngine/Render/Drivers/GLFWOpenGLDriver.cpp
  41. 36 15
      src/WallpaperEngine/Render/Drivers/HeadlessOpenGLDriver.cpp
  42. 5 5
      src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.cpp
  43. 2 2
      src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.h
  44. 23 11
      src/WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.cpp
  45. 4 3
      src/WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.h
  46. 3 4
      src/WallpaperEngine/Render/FBOProvider.cpp
  47. 146 111
      src/WallpaperEngine/Render/Objects/CImage.cpp
  48. 4 3
      src/WallpaperEngine/Render/Objects/CImage.h
  49. 36 16
      src/WallpaperEngine/Render/Objects/CMesh.cpp
  50. 482 346
      src/WallpaperEngine/Render/Objects/CParticle.cpp
  51. 2 3
      src/WallpaperEngine/Render/Objects/CParticle.h
  52. 4 4
      src/WallpaperEngine/Render/Objects/CSound.cpp
  53. 38 22
      src/WallpaperEngine/Render/Objects/CText.cpp
  54. 43 42
      src/WallpaperEngine/Render/Objects/Effects/CPass.cpp
  55. 2 1
      src/WallpaperEngine/Render/Objects/Effects/CPass.h
  56. 32 28
      src/WallpaperEngine/Render/Objects/TextLayout.cpp
  57. 5 4
      src/WallpaperEngine/Render/RenderContext.cpp
  58. 2 3
      src/WallpaperEngine/Render/RenderContext.h
  59. 6 8
      src/WallpaperEngine/Render/Shaders/GLSLContext.cpp
  60. 2 3
      src/WallpaperEngine/Render/Shaders/GLSLContext.h
  61. 19 12
      src/WallpaperEngine/Render/Shaders/ShaderUnit.cpp
  62. 5 4
      src/WallpaperEngine/Render/TextureCache.h
  63. 2 2
      src/WallpaperEngine/Render/Utils/NoiseUtils.h
  64. 27 12
      src/WallpaperEngine/Render/Volumetrics.cpp
  65. 2 1
      src/WallpaperEngine/Render/WallpaperState.cpp
  66. 2 1
      src/WallpaperEngine/Render/WallpaperState.h
  67. 59 37
      src/WallpaperEngine/Render/Wallpapers/CScene.cpp
  68. 2 1
      src/WallpaperEngine/Render/Wallpapers/CScene.h
  69. 30 19
      src/WallpaperEngine/Render/Wallpapers/CSplat.cpp
  70. 8 7
      src/WallpaperEngine/Render/Wallpapers/CSplat.h
  71. 20 17
      src/WallpaperEngine/Render/Wallpapers/CWeb.cpp
  72. 34 25
      src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.cpp
  73. 7 7
      src/WallpaperEngine/Scripting/AnimationSystem.cpp
  74. 11 14
      src/WallpaperEngine/Scripting/EngineObject.cpp
  75. 40 20
      src/WallpaperEngine/Scripting/SceneObject.cpp
  76. 9 8
      src/WallpaperEngine/Scripting/ScriptEngine.cpp
  77. 12 6
      src/WallpaperEngine/Scripting/ScriptEngine.h
  78. 3 4
      src/WallpaperEngine/Scripting/ScriptableObject.cpp
  79. 4 2
      src/WallpaperEngine/Scripting/ScriptableObject.h
  80. 30 5
      src/WallpaperEngine/Splat/SogLoader.cpp
  81. 1 1
      src/WallpaperEngine/Testing/Cases/Localization.cpp
  82. 18 15
      src/WallpaperEngine/Testing/Cases/ModuleNamespaceTiming.cpp
  83. 4 2
      src/WallpaperEngine/Testing/Cases/PropertyParser.cpp
  84. 2 1
      src/WallpaperEngine/Testing/Cases/SpectrumPipeline.cpp
  85. 0 1
      src/WallpaperEngine/Testing/Cases/TextInputProperty.cpp
  86. 2 1
      src/WallpaperEngine/Testing/Cases/TextLayoutRules.cpp
  87. 3 1
      src/WallpaperEngine/Testing/Cases/ThumbnailPalette.cpp
  88. 2 3
      src/WallpaperEngine/Testing/Cases/UserShortcut.cpp
  89. 4 3
      src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.cpp
  90. 2 1
      src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.h
  91. 33 26
      src/WallpaperEngine/WebBrowser/CEF/BrowserClient.cpp
  92. 14 12
      src/WallpaperEngine/WebBrowser/CEF/PageBridge.cpp
  93. 5 3
      src/WallpaperEngine/WebBrowser/CEF/SharedMemoryRenderHandler.cpp
  94. 2 2
      src/WallpaperEngine/WebBrowser/CEF/SharedMemoryRenderHandler.h
  95. 9 5
      src/WallpaperEngine/WebBrowser/CEF/WPSchemeHandler.cpp
  96. 1 1
      src/WallpaperEngine/WebBrowser/CEF/WPSchemeHandlerFactory.cpp
  97. 3 4
      src/WallpaperEngine/WebBrowser/IPC/WebHostSharedMemory.cpp
  98. 3 3
      src/WallpaperEngine/WebBrowser/WebBrowserContext.cpp

+ 59 - 12
.github/workflows/cmake.yml

@@ -9,6 +9,7 @@ on:
       - CMakeLists.txt
       - 'protocols/**'
     branches: [ main ]
+    tags: [ 'v*' ]
   pull_request:
     paths:
       - '.github/workflows/cmake.yml'
@@ -57,7 +58,7 @@ jobs:
           key: ${{ github.job }}-${{ matrix.os }}
 
       - name: Install dependencies
-        run: sudo apt-get update && sudo apt-get -y install libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libfftw3-dev libfreetype-dev
+        run: sudo apt-get update && sudo apt-get -y install libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libfftw3-dev libfreetype-dev libdbus-1-dev
 
       - name: Configure CMake
         # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
@@ -81,10 +82,16 @@ jobs:
 
     strategy:
       matrix:
-        build_type: [Release, Debug]
-    # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac.
-    # You can convert this to a matrix build if you need cross-platform coverage.
-    # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
+        include:
+          - build_type: Release
+            variant: kde
+            disable_kde: 'OFF'
+          - build_type: Release
+            variant: generic
+            disable_kde: 'ON'
+          - build_type: Debug
+            variant: kde
+            disable_kde: 'OFF'
     runs-on: ubuntu-24.04
 
     steps:
@@ -95,7 +102,7 @@ jobs:
       - name: ccache
         uses: hendrikmuhs/ccache-action@v1.2
         with:
-          key: ${{ github.job }}-ubuntu-24.04-${{ matrix.build_type }}
+          key: ${{ github.job }}-ubuntu-24.04-${{ matrix.build_type }}-${{ matrix.variant }}
 
       - name: Initialize CodeQL
         # Initializes the CodeQL tools for scanning.
@@ -105,21 +112,61 @@ jobs:
           languages: 'cpp'
 
       - name: Install dependencies
-        run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev wayland-scanner++ wayland-protocols libwayland-dev
+        run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev libdbus-1-dev wayland-scanner++ wayland-protocols libwayland-dev
 
       - name: Configure CMake
-        # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
-        # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
-        run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache
+        run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DDISABLE_KDE_FEATURES=${{matrix.disable_kde}} -D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache
 
       - name: Build
-        # Build your program with the given configuration
         run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} --verbose
 
       - name: Perform CodeQL Analysis
         if: ${{ matrix.build_type == 'Debug' }}
         uses: github/codeql-action/analyze@v3
 
+      - name: Package
+        if: ${{ matrix.build_type == 'Release' }}
+        run: |
+          name=linux-wallpaperengine-${{ matrix.variant }}-ubuntu-24.04-x86_64
+          cmake --install build --prefix "$RUNNER_TEMP/install"
+          # the install also carries the dependencies' own tools, headers and static libs, keep only what runs
+          pkg="$RUNNER_TEMP/$name"
+          mkdir -p "$pkg/lib"
+          cd "$RUNNER_TEMP/install"
+          cp -a linux-wallpaperengine liblinux-wallpaperengine-lib.so libcef.so libEGL.so libGLESv2.so \
+            libvk_swiftshader.so vk_swiftshader_icd.json chrome-sandbox icudtl.dat v8_context_snapshot.bin \
+            *.pak locales "$pkg/"
+          cp -a lib/libkissfft-float.so* "$pkg/lib/"
+          # CEF ships libcef.so with debug symbols, 1.4 GB instead of about 220 MB
+          strip --strip-unneeded "$pkg/linux-wallpaperengine" "$pkg/liblinux-wallpaperengine-lib.so" "$pkg/libcef.so"
+          tar -czf "$RUNNER_TEMP/$name.tar.gz" -C "$RUNNER_TEMP" "$name"
+
+      - uses: actions/upload-artifact@v4
+        if: ${{ matrix.build_type == 'Release' }}
+        with:
+          name: linux-wallpaperengine-${{ matrix.variant }}
+          path: ${{ runner.temp }}/linux-wallpaperengine-${{ matrix.variant }}-ubuntu-24.04-x86_64.tar.gz
+          if-no-files-found: error
+
+  release:
+    needs: build
+    if: startsWith(github.ref, 'refs/tags/v')
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/download-artifact@v4
+        with:
+          path: release
+          merge-multiple: true
+
+      - name: Create release
+        env:
+          GH_TOKEN: ${{ github.token }}
+        run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes release/*.tar.gz
+
   clang-build:
     needs: tests
     permissions:
@@ -146,7 +193,7 @@ jobs:
           key: ${{ github.job }}-ubuntu-24.04-${{ matrix.build_type }}
 
       - name: Install dependencies
-        run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev wayland-scanner++ wayland-protocols libwayland-dev
+        run: sudo apt-get update && sudo apt-get -y install libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev libdbus-1-dev wayland-scanner++ wayland-protocols libwayland-dev
 
       - name: Configure CMake
         # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.

+ 12 - 5
CMakeLists.txt

@@ -160,7 +160,7 @@ if(BUILD_TESTING)
 endif()
 # try to enable wayland builds when possible
 pkg_check_modules(WAYLAND_SUPPORT wayland-cursor wayland-protocols egl wayland-egl)
-option(ENABLE_KDE_EXPERIMENTAL_FEATURES "Enable KDE-specific Wayland fullscreen detection" OFF)
+option(DISABLE_KDE_FEATURES "Build without the KDE Plasma integration (plasma-shell surfaces, KWin fullscreen detection and cursor tracking)" OFF)
 
 if(WAYLAND_SUPPORT_FOUND)
     find_program(WaylandScanner NAMES wayland-scanner)
@@ -229,8 +229,13 @@ if(WAYLAND_SUPPORT_FOUND)
         ${WAYLAND_OUTPUT_DIR}/color-management-v1-protocol.c
         ${WAYLAND_OUTPUT_DIR}/color-management-v1-protocol.h)
 
-    if(ENABLE_KDE_EXPERIMENTAL_FEATURES)
-        pkg_check_modules(DBUS REQUIRED dbus-1)
+    if(NOT DISABLE_KDE_FEATURES)
+        pkg_check_modules(DBUS dbus-1)
+        if(NOT DBUS_FOUND)
+            message(FATAL_ERROR "The KDE integration needs the dbus-1 development files (libdbus-1-dev, dbus-devel or dbus depending on the distro). Install them or configure with -DDISABLE_KDE_FEATURES=ON")
+        endif()
+        set(KDE_FEATURES_ENABLED TRUE)
+        message(STATUS "KDE integration enabled")
         include_directories(${DBUS_INCLUDE_DIRS})
         list(APPEND WAYLAND_LIBRARIES ${DBUS_LIBRARIES})
         add_custom_command(OUTPUT ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.h
@@ -242,6 +247,8 @@ if(WAYLAND_SUPPORT_FOUND)
             src/WallpaperEngine/Render/Drivers/Detectors/KDEWaylandFullScreenDetector.h
             src/WallpaperEngine/Input/Drivers/KDECursorInput.cpp
             src/WallpaperEngine/Input/Drivers/KDECursorInput.h
+            src/WallpaperEngine/Desktop/KDESession.cpp
+            src/WallpaperEngine/Desktop/KDESession.h
             ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.c
             ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.h)
     endif()
@@ -733,8 +740,8 @@ if(WAYLAND_SUPPORT_FOUND)
     target_compile_definitions(linux-wallpaperengine-lib PUBLIC ENABLE_WAYLAND)
 endif()
 
-if(WAYLAND_SUPPORT_FOUND AND ENABLE_KDE_EXPERIMENTAL_FEATURES)
-    target_compile_definitions(linux-wallpaperengine-lib PUBLIC ENABLE_KDE_EXPERIMENTAL_FEATURES)
+if(KDE_FEATURES_ENABLED)
+    target_compile_definitions(linux-wallpaperengine-lib PUBLIC ENABLE_KDE_FEATURES)
 endif()
 
 if(WAYLAND_SUPPORT_FOUND AND NOT X11_SUPPORT_FOUND)

+ 10 - 7
README.md

@@ -26,29 +26,30 @@ No GUI. This is a command-line tool driven entirely by flags - see Usage below.
 - MPV
 - PulseAudio
 - FFTW3
+- D-Bus (for the KDE integration, see Build)
 
 ### Ubuntu 22.04
 ```bash
 sudo apt-get update
-sudo apt-get install build-essential cmake libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv1 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev
+sudo apt-get install build-essential cmake libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv1 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev libdbus-1-dev
 ```
 
 ### Ubuntu 24.04
 ```bash
 sudo apt-get update
-sudo apt-get install build-essential cmake libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev
+sudo apt-get install build-essential cmake libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libgl-dev libglew-dev freeglut3-dev libsdl2-dev liblz4-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libxxf86vm-dev libglm-dev libglfw3-dev libmpv-dev mpv libmpv2 libpulse-dev libpulse0 libfftw3-dev libfreetype-dev libdbus-1-dev
 ```
 
 ### Fedora 42
 ```bash
 sudo dnf update
-sudo dnf install gcc g++ cmake libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel mesa-libGL-devel glew-devel freeglut-devel SDL2-devel lz4-devel ffmpeg ffmpeg-free-devel libXxf86vm-devel glm-devel glfw-devel mpv mpv-devel pulseaudio-libs-devel fftw-devel gmp-devel
+sudo dnf install gcc g++ cmake libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel mesa-libGL-devel glew-devel freeglut-devel SDL2-devel lz4-devel ffmpeg ffmpeg-free-devel libXxf86vm-devel glm-devel glfw-devel mpv mpv-devel pulseaudio-libs-devel fftw-devel gmp-devel dbus-devel
 ```
 
 ### ALT Linux
 ```bash
 sudo epm update
-sudo epm install gcc-c++ make cmake libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel libGL-devel libGLEW-devel freeglut-devel libSDL2-devel liblz4-devel libavcodec-devel libavformat-devel libavutil-devel libswscale-devel libXxf86vm-devel libglm-devel libglfw3-devel libmpv-devel mpv libpulseaudio-devel libpulseaudio libfftw3-devel libpng-devel libffi-devel libswresample-devel libgmpxx-devel
+sudo epm install gcc-c++ make cmake libXrandr-devel libXinerama-devel libXcursor-devel libXi-devel libGL-devel libGLEW-devel freeglut-devel libSDL2-devel liblz4-devel libavcodec-devel libavformat-devel libavutil-devel libswscale-devel libXxf86vm-devel libglm-devel libglfw3-devel libmpv-devel mpv libpulseaudio-devel libpulseaudio libfftw3-devel libpng-devel libffi-devel libswresample-devel libgmpxx-devel libdbus-devel
 ```
 
 ## Build
@@ -63,12 +64,14 @@ make
 
 The binary and support files end up in `build/output`.
 
-If you want the "pause on fullscreen" feature on KDE Plasma, install the [KWin Maximize Detector](https://github.com/LS-FCEFyN/Maximize-Detector) script separately, and build with:
+The KDE Plasma integration (plasma-shell desktop surfaces, cursor tracking through KWin, fullscreen detection) is built in by default and only kicks in inside a Plasma session, other desktops use the generic Wayland/X11 paths. It needs the D-Bus development files. To build without it:
 
 ```bash
-cmake -DCMAKE_BUILD_TYPE='Release' -DENABLE_KDE_EXPERIMENTAL_FEATURES=ON ..
+cmake -DCMAKE_BUILD_TYPE='Release' -DDISABLE_KDE_FEATURES=ON ..
 ```
 
+The "pause on fullscreen" feature on KDE Plasma also needs the [KWin Maximize Detector](https://github.com/LS-FCEFyN/Maximize-Detector) script installed separately.
+
 ## Assets
 
 You need Wallpaper Engine installed through Steam - this is where the backgrounds and shared assets come from. The binary looks for it automatically under:
@@ -218,7 +221,7 @@ linux-wallpaperengine --disable-object Clock --disable-object 3 2370927443
 - Light and VolumeLight scene objects are parsed but not rendered - wallpapers that depend on them for lighting will look different from the Windows original.
 - Passthrough image effects aren't implemented.
 - Text objects can't sample the background behind them (no copybackground-style effects on `Text`).
-- KDE fullscreen-pause detection is experimental and requires the separate KWin Maximize Detector script plus `-DENABLE_KDE_EXPERIMENTAL_FEATURES=ON`.
+- KDE fullscreen-pause detection is experimental and requires the separate KWin Maximize Detector script.
 - On X11, a compositor or DE drawing its own background will block the wallpaper. Disabling the compositor is currently the only fix.
 - Some NVIDIA setups hit GLFW/OpenGL init failures; try `__GL_THREADED_OPTIMIZATIONS=0 linux-wallpaperengine` if you run into this.
 

+ 1 - 1
packaging/archlinux/PKGBUILD

@@ -7,7 +7,7 @@ pkgdesc="use steam's wallpaperengine on linux"
 arch=('x86_64')
 url="https://github.com/Almamu/linux-wallpaperengine"
 license=('GPL-3.0-only')
-depends=('lz4' 'ffmpeg' 'mpv' 'glfw' 'glew' 'freeglut' 'libpulse' 'libcups' 'at-spi2-core' 'nss' 'libxcomposite' 'libxdamage' 'nspr')
+depends=('lz4' 'ffmpeg' 'mpv' 'glfw' 'glew' 'freeglut' 'libpulse' 'libcups' 'at-spi2-core' 'nss' 'libxcomposite' 'libxdamage' 'nspr' 'dbus')
 makedepends=('git' 'cmake' 'sdl2' 'glm' 'wayland-protocols' 'xorg-xrandr')
 provides=("linux-wallpaperengine")
 source=("${pkgname}::git+https://github.com/Almamu/linux-wallpaperengine.git#branch=main")

+ 53 - 36
src/WallpaperEngine/Application/ApplicationContext.cpp

@@ -331,7 +331,8 @@ std::optional<float> ApplicationContext::resolveAudioSensitivity (int id, const
 }
 
 std::optional<float> ApplicationContext::resolveSoundVolume (int id, const std::string& name) const {
-    // "*" is a wildcard default, checked last so a specific id/name match always wins (same rule as resolveAudioSensitivity).
+    // "*" is a wildcard default, checked last so a specific id/name match always wins (same rule as
+    // resolveAudioSensitivity).
     std::optional<float> wildcard;
 
     for (const auto& [token, volume] : this->settings.general.soundVolume) {
@@ -378,10 +379,7 @@ void ApplicationContext::loadSettingsFromArgv () {
 	});
 
     // Internal, hidden: marks a self-re-exec as a disposable CEF host for one Web wallpaper.
-    backgroundGroup.add_argument ("--web-host")
-	.flag ()
-	.hidden ()
-	.store_into (this->settings.general.webHost);
+    backgroundGroup.add_argument ("--web-host").flag ().hidden ().store_into (this->settings.general.webHost);
 
     backgroundGroup.add_argument ("--web-host-shm")
 	.default_value ("")
@@ -839,8 +837,10 @@ void ApplicationContext::loadSettingsFromArgv () {
 	.store_into (this->settings.render.maximumFPS);
 
     performanceGroup.add_argument ("--speed")
-	.help ("Global playback speed multiplier for animations, particles and effects (e.g. 0.5 for half speed, "
-	       "2.0 for double speed). Useful to calm down backgrounds with overly fast particle effects")
+	.help (
+	    "Global playback speed multiplier for animations, particles and effects (e.g. 0.5 for half speed, "
+	    "2.0 for double speed). Useful to calm down backgrounds with overly fast particle effects"
+	)
 	.action ([this] (const std::string& value) -> void {
 	    float speed;
 
@@ -1031,9 +1031,7 @@ void ApplicationContext::loadSettingsFromArgv () {
 	    "whose texture tiles/repeats are always exempt from the clamp, since going past their edges is seamless"
 	)
 	.flag ()
-	.action ([this] (const std::string& value) -> void {
-	    this->settings.mouse.clampParallaxToImageSize = false;
-	});
+	.action ([this] (const std::string& value) -> void { this->settings.mouse.clampParallaxToImageSize = false; });
 
     configurationGroup.add_argument ("-l", "--list-properties")
 	.help ("List all the available properties and their configuration")
@@ -1061,48 +1059,60 @@ void ApplicationContext::loadSettingsFromArgv () {
 
     configurationGroup.add_argument ("--disable-object")
 	.help ("Hides an object/layer (parallax layer, clock, particles, etc), matched by id or name. Can be repeated")
-	.action ([this] (const std::string& value) -> void { this->settings.general.disabledObjects.push_back (value); }
-	)
+	.action ([this] (const std::string& value) -> void {
+	    this->settings.general.disabledObjects.push_back (value);
+	})
 	.append ();
 
     configurationGroup.add_argument ("--enable-object")
-	.help ("Forces an object/layer to show even if the background hides it by default, matched by id or name. "
-	       "Can be repeated")
-	.action ([this] (const std::string& value) -> void { this->settings.general.enabledObjects.push_back (value); }
+	.help (
+	    "Forces an object/layer to show even if the background hides it by default, matched by id or name. "
+	    "Can be repeated"
 	)
+	.action ([this] (const std::string& value) -> void { this->settings.general.enabledObjects.push_back (value); })
 	.append ();
 
     configurationGroup.add_argument ("--list-effects")
-	.help ("List all effects (bloom, blur, glow, etc) attached to a background's objects, with their id, editor "
-	       "name and category")
+	.help (
+	    "List all effects (bloom, blur, glow, etc) attached to a background's objects, with their id, editor "
+	    "name and category"
+	)
 	.flag ()
 	.store_into (this->settings.general.onlyListEffects);
 
     configurationGroup.add_argument ("--disable-effect")
-	.help ("Disables an object's visual effect (bloom, blur, glow, etc), matched by effect id or editor name. "
-	       "Can be repeated")
-	.action ([this] (const std::string& value) -> void { this->settings.general.disabledEffects.push_back (value); }
+	.help (
+	    "Disables an object's visual effect (bloom, blur, glow, etc), matched by effect id or editor name. "
+	    "Can be repeated"
 	)
+	.action ([this] (const std::string& value) -> void {
+	    this->settings.general.disabledEffects.push_back (value);
+	})
 	.append ();
 
     configurationGroup.add_argument ("--enable-effect")
-	.help ("Forces an effect to show even if the scene hides it by default, matched by effect id or editor name. "
-	       "Can be repeated")
-	.action ([this] (const std::string& value) -> void { this->settings.general.enabledEffects.push_back (value); }
+	.help (
+	    "Forces an effect to show even if the scene hides it by default, matched by effect id or editor name. "
+	    "Can be repeated"
 	)
+	.action ([this] (const std::string& value) -> void { this->settings.general.enabledEffects.push_back (value); })
 	.append ();
 
     configurationGroup.add_argument ("--list-audio-objects")
-	.help ("List objects/properties whose script reacts to music (via engine.registerAudioBuffers), with their "
-	       "current minvalue/maxvalue/frequency/smoothing")
+	.help (
+	    "List objects/properties whose script reacts to music (via engine.registerAudioBuffers), with their "
+	    "current minvalue/maxvalue/frequency/smoothing"
+	)
 	.flag ()
 	.store_into (this->settings.general.onlyListAudioObjects);
 
     configurationGroup.add_argument ("--audio-sensitivity")
-	.help ("Scales how much an audio-reactive object's music-driven properties swing around their authored "
-	       "midpoint, matched by id or name: 0 locks it (no pulse), 1 is the wallpaper's original behavior, "
-	       ">1 exaggerates it. Use \"*\" as the id to set a default for every audio-reactive object with no "
-	       "more specific match. Format: <id-or-name-or-*>=<multiplier>. Can be repeated")
+	.help (
+	    "Scales how much an audio-reactive object's music-driven properties swing around their authored "
+	    "midpoint, matched by id or name: 0 locks it (no pulse), 1 is the wallpaper's original behavior, "
+	    ">1 exaggerates it. Use \"*\" as the id to set a default for every audio-reactive object with no "
+	    "more specific match. Format: <id-or-name-or-*>=<multiplier>. Can be repeated"
+	)
 	.action ([this] (const std::string& value) -> void {
 	    const std::string::size_type equals = value.find ('=');
 
@@ -1122,10 +1132,12 @@ void ApplicationContext::loadSettingsFromArgv () {
 	.append ();
 
     configurationGroup.add_argument ("--sound-volume")
-	.help ("Sets a Sound object's own volume (0-1), independent of the global volume - lets a wallpaper with "
-	       "several alternate music tracks play only one, matched by id or name. Use \"*\" as the id to set a "
-	       "default for every Sound object with no more specific match. Format: <id-or-name-or-*>=<volume>. Can "
-	       "be repeated")
+	.help (
+	    "Sets a Sound object's own volume (0-1), independent of the global volume - lets a wallpaper with "
+	    "several alternate music tracks play only one, matched by id or name. Use \"*\" as the id to set a "
+	    "default for every Sound object with no more specific match. Format: <id-or-name-or-*>=<volume>. Can "
+	    "be repeated"
+	)
 	.action ([this] (const std::string& value) -> void {
 	    const std::string::size_type equals = value.find ('=');
 
@@ -1153,7 +1165,8 @@ void ApplicationContext::loadSettingsFromArgv () {
 
     debuggingGroup.add_argument ("--render-debug")
 	.help (
-	    "Scene render debug mode: base-only, no-solid-final, pass-log, brightness-log, no-puppet-animation, object=<id>, "
+	    "Scene render debug mode: base-only, no-solid-final, pass-log, brightness-log, no-puppet-animation, "
+	    "object=<id>, "
 	    "skip-object=<id>, or skip-effect=<id>. Can be repeated."
 	)
 	.action ([this] (const std::string& value) -> void {
@@ -1232,8 +1245,12 @@ void ApplicationContext::loadSettingsFromArgv () {
 	    if (onKDE) {
 		this->settings.render.wayland.layer = WAYLAND_LAYER_BACKGROUND;
 
-		if (!isHelperProcess (this->m_argc, this->m_argv, this->settings.general.webHost))
-		    sLog.out ("KDE detected: using --layer background to prevent 'show desktop' from hiding the wallpaper (use --layer bottom to override)");
+		if (!isHelperProcess (this->m_argc, this->m_argv, this->settings.general.webHost)) {
+		    sLog.out (
+			"KDE detected: using --layer background to prevent 'show desktop' from hiding the wallpaper "
+			"(use --layer bottom to override)"
+		    );
+		}
 	    }
 	}
 

+ 4 - 2
src/WallpaperEngine/Application/ApplicationContext.h

@@ -34,7 +34,8 @@ public:
      */
     [[nodiscard]] std::optional<bool> resolveObjectVisibility (int id, const std::string& name) const;
 
-    /** true to force visible, false to force hidden, nullopt to leave the scene's own value, from --disable-effect/--enable-effect */
+    /** true to force visible, false to force hidden, nullopt to leave the scene's own value, from
+     * --disable-effect/--enable-effect */
     [[nodiscard]] std::optional<bool> resolveEffectVisibility (int id, const std::string& name) const;
 
     /**
@@ -169,7 +170,8 @@ public:
 	    int maximumFPS;
 	    /** Global playback speed multiplier for animations, particles and effects, see --speed */
 	    float playbackSpeed;
-	    /** Freezes scene time entirely (scripts, particles, effects and puppet meshes all stop advancing), see --disable-animations */
+	    /** Freezes scene time entirely (scripts, particles, effects and puppet meshes all stop advancing), see
+	     * --disable-animations */
 	    bool freezeAnimations;
 	    bool pauseOnFullscreen;
 	    /**

+ 22 - 22
src/WallpaperEngine/Application/WallpaperApplication.cpp

@@ -325,8 +325,7 @@ std::optional<float> presetNumber (const WallpaperEngine::Data::JSON::JSON& pres
     if (it->is_string ()) {
 	try {
 	    return std::stof (it->get<std::string> ());
-	} catch (const std::exception&) {
-	}
+	} catch (const std::exception&) { }
     }
 
     return std::nullopt;
@@ -863,8 +862,10 @@ HotswapRequest parseHotswapRequest (std::istream& file) {
 		    = enable;
 		request.imageAdjustmentsProvided = true;
 	    }
-	} else if (key == "image-filter-strength" || key == "brightness" || key == "contrast" || key == "saturation"
-		   || key == "hue") {
+	} else if (
+	    key == "image-filter-strength" || key == "brightness" || key == "contrast" || key == "saturation"
+	    || key == "hue"
+	) {
 	    float amount;
 
 	    try {
@@ -953,9 +954,9 @@ void WallpaperApplication::checkHotswapRequest () {
     if (!request.path.has_value () && !request.layersProvided && !request.volume.has_value ()
 	&& !request.xray.has_value () && !request.scaling.has_value () && !request.zoom.has_value ()
 	&& !request.offset.has_value () && !request.disableParallax.has_value () && !request.expandCanvas.has_value ()
-	&& !request.cornerColor.has_value () && !request.imageAdjustmentsProvided
-	&& !request.speed.has_value () && !request.audioScreen.has_value () && !request.ambientVolume.has_value ()
-	&& !request.propertiesProvided && !request.audioSensitivityProvided && !request.soundVolumeProvided) {
+	&& !request.cornerColor.has_value () && !request.imageAdjustmentsProvided && !request.speed.has_value ()
+	&& !request.audioScreen.has_value () && !request.ambientVolume.has_value () && !request.propertiesProvided
+	&& !request.audioSensitivityProvided && !request.soundVolumeProvided) {
 	sLog.error ("Hotswap requested but control file was empty");
 	return;
     }
@@ -1121,8 +1122,8 @@ void WallpaperApplication::checkHotswapRequest () {
 
 		if (this->m_renderContext) {
 		    auto wallpaper = WallpaperEngine::Render::CWallpaper::fromWallpaper (
-			*background->wallpaper, *this->m_renderContext, *this->m_audioContext, targetPath, scaling, clamp,
-			this->resolveScreenRenderSize (screen)
+			*background->wallpaper, *this->m_renderContext, *this->m_audioContext, targetPath, scaling,
+			clamp, this->resolveScreenRenderSize (screen)
 		    );
 		    wallpaper->setZoom (this->resolveScreenZoom (screen));
 		    const auto offset = this->resolveScreenOffset (screen);
@@ -1170,7 +1171,7 @@ std::string WallpaperApplication::resolveScreenBackgroundPath (const std::string
 	for (const auto& spanGroup : this->m_context.settings.general.spanGroups) {
 	    if (!spanGroup.screens.empty () && "span:" + spanGroup.screens.front () == screen) {
 		return spanGroup.background.empty () ? this->m_context.settings.general.defaultBackground.string ()
-						      : spanGroup.background.string ();
+						     : spanGroup.background.string ();
 	    }
 	}
 
@@ -1190,15 +1191,14 @@ float WallpaperApplication::resolveScreenZoom (const std::string& screen) const
     const auto it = this->m_context.settings.general.screenZooms.find (screen);
 
     return it != this->m_context.settings.general.screenZooms.end () ? it->second
-								      : this->m_context.settings.render.window.zoom;
+								     : this->m_context.settings.render.window.zoom;
 }
 
 glm::vec2 WallpaperApplication::resolveScreenOffset (const std::string& screen) const {
     const auto it = this->m_context.settings.general.screenOffsets.find (screen);
 
-    return it != this->m_context.settings.general.screenOffsets.end ()
-	? it->second
-	: this->m_context.settings.render.window.offset;
+    return it != this->m_context.settings.general.screenOffsets.end () ? it->second
+								       : this->m_context.settings.render.window.offset;
 }
 
 glm::vec4 WallpaperApplication::resolveScreenCornerColor (const std::string& screen) const {
@@ -1227,8 +1227,7 @@ WallpaperApplication::resolveScreenImageAdjustments (const std::string& screen,
 
     const auto it = this->m_context.settings.general.screenImageAdjustments.find (screen);
 
-    return it != this->m_context.settings.general.screenImageAdjustments.end () ? it->second.over (fallback)
-										: fallback;
+    return it != this->m_context.settings.general.screenImageAdjustments.end () ? it->second.over (fallback) : fallback;
 }
 
 glm::ivec2 WallpaperApplication::resolveScreenRenderSize (const std::string& screen) const {
@@ -1239,7 +1238,8 @@ glm::ivec2 WallpaperApplication::resolveScreenRenderSize (const std::string& scr
 	return { it->second->viewport.z, it->second->viewport.w };
     }
 
-    return { this->m_renderContext->getOutput ().getFullWidth (), this->m_renderContext->getOutput ().getFullHeight () };
+    return { this->m_renderContext->getOutput ().getFullWidth (),
+	     this->m_renderContext->getOutput ().getFullHeight () };
 }
 
 void WallpaperApplication::applyVolumeHotswap (int volume) {
@@ -1707,11 +1707,11 @@ void WallpaperApplication::listAudioObjectsForProject (const std::string& backgr
     for (const auto& object : scene->objects) {
 	for (const auto& reactive : collectAudioReactiveProperties (*object)) {
 	    sLog.out (
-		"  ", object->id, " - ", object->name, " (", reactive.name, "): minvalue=",
-		readScriptPropertyFloat (*reactive.value, "minvalue"), " maxvalue=",
-		readScriptPropertyFloat (*reactive.value, "maxvalue"), " frequency=",
-		readScriptPropertyFloat (*reactive.value, "frequency"), " smoothing=",
-		readScriptPropertyFloat (*reactive.value, "smoothing")
+		"  ", object->id, " - ", object->name, " (", reactive.name,
+		"): minvalue=", readScriptPropertyFloat (*reactive.value, "minvalue"),
+		" maxvalue=", readScriptPropertyFloat (*reactive.value, "maxvalue"),
+		" frequency=", readScriptPropertyFloat (*reactive.value, "frequency"),
+		" smoothing=", readScriptPropertyFloat (*reactive.value, "smoothing")
 	    );
 	}
     }

+ 4 - 2
src/WallpaperEngine/Application/WallpaperApplication.h

@@ -183,7 +183,8 @@ private:
      */
     void applyAudioScreenHotswap (const std::string& value);
 
-    /** Pushes a new --ambient-volume (0-128) live, then re-applies audio policy to every currently rendered wallpaper */
+    /** Pushes a new --ambient-volume (0-128) live, then re-applies audio policy to every currently rendered wallpaper
+     */
     void applyAmbientVolumeHotswap (const std::string& value);
 
     /**
@@ -211,7 +212,8 @@ private:
     [[nodiscard]] glm::vec2 resolveScreenOffset (const std::string& screen) const;
     [[nodiscard]] glm::vec4 resolveScreenCornerColor (const std::string& screen) const;
     /** Command line and control file settings for this screen (or span group) over what the preset carries */
-    [[nodiscard]] ImageAdjustments resolveScreenImageAdjustments (const std::string& screen, const Project& project) const;
+    [[nodiscard]] ImageAdjustments
+    resolveScreenImageAdjustments (const std::string& screen, const Project& project) const;
     // The resolution a Web wallpaper on this screen will actually be rendered at - falls back to
     // the combined bounding box of every active screen if this one isn't registered yet.
     [[nodiscard]] glm::ivec2 resolveScreenRenderSize (const std::string& screen) const;

+ 3 - 1
src/WallpaperEngine/Audio/AudioContext.cpp

@@ -7,7 +7,9 @@ AudioContext::AudioContext (Drivers::AudioDriver& driver) : m_driver (driver) {
 int AudioContext::addStream (AudioStream* stream) const { return this->m_driver.addStream (stream); }
 void AudioContext::removeStream (int streamId) const { this->m_driver.removeStream (streamId); }
 
-void AudioContext::setStreamVolume (int streamId, int volume) const { this->m_driver.setStreamVolume (streamId, volume); }
+void AudioContext::setStreamVolume (int streamId, int volume) const {
+    this->m_driver.setStreamVolume (streamId, volume);
+}
 
 AVSampleFormat AudioContext::getFormat () const { return this->m_driver.getFormat (); }
 

+ 2 - 1
src/WallpaperEngine/Audio/AudioStream.h

@@ -198,7 +198,8 @@ private:
     AVFrame* m_decodeFrame = nullptr;
     /** Bytes left to decode from m_decodePacket, carried between decodeFrame() calls */
     int m_audioPacketSize = 0;
-    /** Guards m_context: avcodec_flush_buffers() on the reader thread races send/receive on the decode thread otherwise */
+    /** Guards m_context: avcodec_flush_buffers() on the reader thread races send/receive on the decode thread otherwise
+     */
     SDL_mutex* m_codecMutex = nullptr;
 
     /**

+ 21 - 22
src/WallpaperEngine/Audio/Drivers/Recorders/PulseAudioPlaybackRecorder.cpp

@@ -1,33 +1,33 @@
 #include "PulseAudioPlaybackRecorder.h"
 #include "WallpaperEngine/Logging/Log.h"
-#include <pulse/rtclock.h>
 #include <algorithm>
 #include <chrono>
 #include <cmath>
 #include <cstring>
 #include <ctime>
 #include <iomanip>
+#include <pulse/rtclock.h>
 #include <sstream>
 
 namespace WallpaperEngine::Audio::Drivers::Recorders {
 namespace {
-constexpr int CAPTURE_RATE = 44100;
-constexpr int CAPTURE_CHANNELS = 2;
-constexpr auto CAPTURE_TIMEOUT = std::chrono::milliseconds (1000);
-
-// Timestamp helper backing the debug-only capture markers below - useful for tracking down
-// audio-to-visual delay regressions in the future.
-std::string wallClockTimestamp () {
-    const auto now = std::chrono::system_clock::now ();
-    const auto ms = std::chrono::duration_cast<std::chrono::milliseconds> (now.time_since_epoch ()) % 1000;
-    const std::time_t t = std::chrono::system_clock::to_time_t (now);
-    std::tm tmBuf {};
-    localtime_r (&t, &tmBuf);
-
-    std::ostringstream oss;
-    oss << std::put_time (&tmBuf, "%H:%M:%S") << '.' << std::setfill ('0') << std::setw (3) << ms.count ();
-    return oss.str ();
-}
+    constexpr int CAPTURE_RATE = 44100;
+    constexpr int CAPTURE_CHANNELS = 2;
+    constexpr auto CAPTURE_TIMEOUT = std::chrono::milliseconds (1000);
+
+    // Timestamp helper backing the debug-only capture markers below - useful for tracking down
+    // audio-to-visual delay regressions in the future.
+    std::string wallClockTimestamp () {
+	const auto now = std::chrono::system_clock::now ();
+	const auto ms = std::chrono::duration_cast<std::chrono::milliseconds> (now.time_since_epoch ()) % 1000;
+	const std::time_t t = std::chrono::system_clock::to_time_t (now);
+	std::tm tmBuf {};
+	localtime_r (&t, &tmBuf);
+
+	std::ostringstream oss;
+	oss << std::put_time (&tmBuf, "%H:%M:%S") << '.' << std::setfill ('0') << std::setw (3) << ms.count ();
+	return oss.str ();
+    }
 } // namespace
 
 void pa_server_info_cb (pa_context* ctx, const pa_server_info* info, void* userdata);
@@ -222,8 +222,7 @@ void pa_context_notify_cb (pa_context* ctx, void* userdata) {
 }
 
 PulseAudioPlaybackRecorder::PulseAudioPlaybackRecorder () :
-    m_captureData ({ .owner = this, .captureStream = nullptr, .captureLost = false }),
-    m_analyzer (CAPTURE_RATE),
+    m_captureData ({ .owner = this, .captureStream = nullptr, .captureLost = false }), m_analyzer (CAPTURE_RATE),
     m_webFFT (kiss_fftr_alloc (WAVE_BUFFER_SIZE, 0, nullptr, nullptr)) {
     this->m_dataMutex = SDL_CreateMutex ();
     this->m_mainloop = pa_mainloop_new ();
@@ -349,8 +348,8 @@ void PulseAudioPlaybackRecorder::processWebFrame () {
 
     for (int band = 0; band < 64; band++) {
 	const int index = band * 2;
-	const float power = this->m_FFTinfo[index].r * this->m_FFTinfo[index].r
-	    + this->m_FFTinfo[index].i * this->m_FFTinfo[index].i;
+	const float power
+	    = this->m_FFTinfo[index].r * this->m_FFTinfo[index].r + this->m_FFTinfo[index].i * this->m_FFTinfo[index].i;
 	float level = 0.0f;
 
 	if (power > 0.0f) {

+ 1 - 1
src/WallpaperEngine/Audio/Drivers/Recorders/PulseAudioPlaybackRecorder.h

@@ -7,8 +7,8 @@
 #include <SDL.h>
 #include <atomic>
 #include <chrono>
-#include <string>
 #include <pulse/pulseaudio.h>
+#include <string>
 
 #define WAVE_BUFFER_SIZE 1024
 

+ 3 - 4
src/WallpaperEngine/Audio/SpectrumAnalyzer.cpp

@@ -22,8 +22,7 @@ SpectrumAnalyzer::SpectrumAnalyzer (int sampleRate) {
     this->m_size = static_cast<int> (rateScale * 64.0f * SIZE_FACTOR);
     this->m_binCount = static_cast<int> (BIN_FACTOR * 64.0f);
     this->m_blockSize = static_cast<int> (
-	static_cast<float> (this->m_size)
-	- (BIN_FACTOR / SIZE_FACTOR) * static_cast<float> (this->m_size)
+	static_cast<float> (this->m_size) - (BIN_FACTOR / SIZE_FACTOR) * static_cast<float> (this->m_size)
     );
     this->m_config = kiss_fft_alloc (this->m_size, 0, nullptr, nullptr);
 
@@ -101,8 +100,8 @@ void SpectrumAnalyzer::analyze (int channels, float* bands) {
 	std::copy_n (bands, BANDS, bands + BANDS);
     }
 
-    const float scale = GAIN * 0.001f
-	* (static_cast<float> (this->m_binCount) / (static_cast<float> (this->m_size) * 0.5f));
+    const float scale
+	= GAIN * 0.001f * (static_cast<float> (this->m_binCount) / (static_cast<float> (this->m_size) * 0.5f));
 
     for (int i = 0; i < BANDS * 2; i++) {
 	bands[i] *= scale;

+ 2 - 1
src/WallpaperEngine/Data/Model/Object.h

@@ -42,7 +42,8 @@ struct ObjectData {
     UserSettingUniquePtr solid;
     /** A visible object hit by the cursor keeps the objects below it from getting cursor events */
     UserSettingUniquePtr disablePropagation;
-    /** Drawn with a perspective camera in 2D scenes (general.perspectiveoverridefov), only the object's own flag counts */
+    /** Drawn with a perspective camera in 2D scenes (general.perspectiveoverridefov), only the object's own flag counts
+     */
     UserSettingUniquePtr perspective;
 };
 

+ 2 - 1
src/WallpaperEngine/Data/Model/Property.h

@@ -197,7 +197,8 @@ private:
 
 class PropertyFile final : public Property {
 public:
-    /** fileType is what the project restricts the property to ("video", or empty for images), only relevant for directories */
+    /** fileType is what the project restricts the property to ("video", or empty for images), only relevant for
+     * directories */
     explicit PropertyFile (
 	PropertyData data, const std::string& value, bool directory = false, std::string fileType = ""
     ) : Property (std::move (data)), m_directory (directory), m_fileType (std::move (fileType)) {

+ 20 - 16
src/WallpaperEngine/Data/Parsers/DynamicValueParser.cpp

@@ -61,9 +61,9 @@ std::shared_ptr<const PropertyAnimation> parseAnimation (const json& data) {
     }
     if (const auto it = options->find ("mode"); it != options->end () && it->is_string ()) {
 	const auto mode = it->get<std::string> ();
-	animation->mode = mode == "loop"     ? PropertyAnimation::Mode::Loop
-			  : mode == "mirror" ? PropertyAnimation::Mode::Mirror
-					     : PropertyAnimation::Mode::Single;
+	animation->mode = mode == "loop" ? PropertyAnimation::Mode::Loop
+	    : mode == "mirror"           ? PropertyAnimation::Mode::Mirror
+					 : PropertyAnimation::Mode::Single;
     }
     if (const auto it = options->find ("startpaused"); it != options->end () && it->is_boolean ()) {
 	animation->startPaused = it->get<bool> ();
@@ -74,10 +74,12 @@ std::shared_ptr<const PropertyAnimation> parseAnimation (const json& data) {
     if (const auto it = options->find ("events"); it != options->end () && it->is_array ()) {
 	for (const auto& event : *it) {
 	    if (event.is_object () && event.contains ("frame") && event.contains ("name")) {
-		animation->events.push_back (AnimationEvent {
-		    .frame = event["frame"].get<float> (),
-		    .name = event["name"].get<std::string> (),
-		});
+		animation->events.push_back (
+		    AnimationEvent {
+			.frame = event["frame"].get<float> (),
+			.name = event["name"].get<std::string> (),
+		    }
+		);
 	    }
 	}
     }
@@ -163,13 +165,15 @@ std::vector<AnimationKeyframe> DynamicValueParser::parseKeyframes (const json& c
 	const auto step = key.find ("step");
 	const bool stepped = step != key.end () && step->is_boolean () && step->get<bool> ();
 
-	keys.push_back (AnimationKeyframe {
-	    .frame = static_cast<float> (whole),
-	    .value = value->get<float> (),
-	    .back = stepped ? AnimationKeyframe::Handle {} : readHandle (key, "back"),
-	    .front = stepped ? AnimationKeyframe::Handle {} : readHandle (key, "front"),
-	    .step = stepped,
-	});
+	keys.push_back (
+	    AnimationKeyframe {
+		.frame = static_cast<float> (whole),
+		.value = value->get<float> (),
+		.back = stepped ? AnimationKeyframe::Handle {} : readHandle (key, "back"),
+		.front = stepped ? AnimationKeyframe::Handle {} : readHandle (key, "front"),
+		.step = stepped,
+	    }
+	);
     }
 
     return keys;
@@ -258,8 +262,8 @@ DynamicValueUniquePtr DynamicValueParser::parse (const json& data, const Propert
     }
 
     if (data.is_object ()) {
-	if (const auto animation = data.find ("animation"); animation != data.end () && animation->is_object ()
-	    && animation->contains ("c0")) {
+	if (const auto animation = data.find ("animation");
+	    animation != data.end () && animation->is_object () && animation->contains ("c0")) {
 	    value->setAnimation (parseAnimation (*animation));
 	}
     }

+ 2 - 3
src/WallpaperEngine/Data/Parsers/Localization.cpp

@@ -55,9 +55,8 @@ Localization::Localization (const std::optional<JSON>& general, std::string loca
 	return entry.first.starts_with (language + "-") || entry.first == language;
     });
     const auto english = this->m_tables.find ("en-us");
-    const auto anyEnglish = std::ranges::find_if (this->m_tables, [] (const auto& entry) {
-	return entry.first.starts_with ("en");
-    });
+    const auto anyEnglish
+	= std::ranges::find_if (this->m_tables, [] (const auto& entry) { return entry.first.starts_with ("en"); });
 
     if (english != this->m_tables.end ()) {
 	this->m_english = &english->second;

+ 31 - 23
src/WallpaperEngine/Data/Parsers/ObjectParser.cpp

@@ -110,8 +110,8 @@ ParticleRemap parseRemap (const JSON& it, const char* defaultInput) {
     };
     static constexpr const char* operations[] = { "remap", "multiply", "add", "subtract" };
     static constexpr const char* components[] = { "all", "x", "y", "z", "sum", "average", "max", "min" };
-    static constexpr const char* transforms[] = { "none",	  "sine",	  "square",	 "saw",
-						  "triangle", "simplexnoise", "fbmnoise" };
+    static constexpr const char* transforms[]
+	= { "none", "sine", "square", "saw", "triangle", "simplexnoise", "fbmnoise" };
 
     return ParticleRemap {
 	.operation = parseName (it, "operation", "multiply", operations, ParticleRemapOperation::Unknown),
@@ -186,9 +186,8 @@ std::pair<glm::vec3, bool> deriveColorReference (const JSON& particle) {
     glm::vec3 reference (1.0f);
     bool hasColor = false;
 
-    const auto nameOf = [] (const JSON& entry) {
-	return entry.is_object () ? stringOr (entry, "name", "") : std::string ();
-    };
+    const auto nameOf
+	= [] (const JSON& entry) { return entry.is_object () ? stringOr (entry, "name", "") : std::string (); };
 
     if (const auto initializers = particle.find ("initializer");
 	initializers != particle.end () && initializers->is_array ()) {
@@ -202,12 +201,16 @@ std::pair<glm::vec3, bool> deriveColorReference (const JSON& particle) {
 		hasColor = true;
 	    } else if (name == "hsvcolorrandom") {
 		const auto middle = [&entry] (const char* low, float lowDefault, const char* high, float highDefault) {
-		    return std::clamp ((numberOr (entry, high, highDefault) + numberOr (entry, low, lowDefault)) * 0.5f, 0.0f, 1.0f);
+		    return std::clamp (
+			(numberOr (entry, high, highDefault) + numberOr (entry, low, lowDefault)) * 0.5f, 0.0f, 1.0f
+		    );
 		};
-		reference = WallpaperEngine::Maths::hsvToRgb (glm::vec3 (
-		    middle ("huemin", 0.0f, "huemax", 1.0f), middle ("saturationmin", 0.5f, "saturationmax", 1.0f),
-		    middle ("valuemin", 0.5f, "valuemax", 1.0f)
-		));
+		reference = WallpaperEngine::Maths::hsvToRgb (
+		    glm::vec3 (
+			middle ("huemin", 0.0f, "huemax", 1.0f), middle ("saturationmin", 0.5f, "saturationmax", 1.0f),
+			middle ("valuemin", 0.5f, "valuemax", 1.0f)
+		    )
+		);
 		hasColor = true;
 	    } else if (name == "colorlist") {
 		const auto colors = entry.find ("colors");
@@ -441,7 +444,7 @@ SceneCameraUniquePtr ObjectParser::parseCamera (const JSON& it, const Project& p
 	.zoom = it.user ("zoom", project.properties, 1.0f),
 	.timelines = {},
 	.queueMode = it.optional ("queuemode", std::string ("random")) == "sequential" ? CameraQueueMode::Sequential
-										: CameraQueueMode::Random,
+										       : CameraQueueMode::Random,
     };
 
     if (const auto path = it.optional<std::string> ("path"); path.has_value ()) {
@@ -454,8 +457,10 @@ SceneCameraUniquePtr ObjectParser::parseCamera (const JSON& it, const Project& p
 LightUniquePtr ObjectParser::parseLight (const JSON& it, const Project& project, ObjectData base) {
     const auto name = it.require<std::string> ("light", "Light must have a type");
     const std::map<std::string, LightType> types = {
-	{ "point", LightType::Legacy },	  { "lpoint", LightType::Point },
-	{ "lspot", LightType::Spot },	  { "ltube", LightType::Tube },
+	{ "point", LightType::Legacy },
+	{ "lpoint", LightType::Point },
+	{ "lspot", LightType::Spot },
+	{ "ltube", LightType::Tube },
 	{ "ldirectional", LightType::Directional },
     };
     const auto type = types.find (name);
@@ -569,7 +574,8 @@ TextUniquePtr ObjectParser::parseText (const JSON& it, const Project& project, O
 	    .alpha = it.user ("alpha", project.properties, 1.0f),
 	    .visible = it.user ("visible", project.properties, true),
 	    .parallaxDepth = it.user ("parallaxDepth", project.properties, glm::vec2 (1.0f)),
-	    .horizontalAlign = it.user ("horizontalalign", properties, it.optional ("alignment", std::string ("center"))),
+	    .horizontalAlign
+	    = it.user ("horizontalalign", properties, it.optional ("alignment", std::string ("center"))),
 	    .verticalAlign = it.user ("verticalalign", properties, std::string ("center")),
 	    .anchor = it.user ("anchor", properties, std::string ("none")),
 	    .colorBlendMode = it.user ("colorBlendMode", properties, 0),
@@ -615,9 +621,8 @@ ObjectParser::parseImage (const JSON& it, const Project& project, ObjectData bas
 	    .visible = it.user ("visible", properties, true),
 	    .alpha = it.user ("alpha", properties, 1.0f),
 	    .color = it.color ("color", properties, Builders::ColorBuilder::White),
-	    .alignment = parseAlignment (
-		it.optional ("horizontalalign", it.optional ("alignment", std::string ("center")))
-	    ),
+	    .alignment
+	    = parseAlignment (it.optional ("horizontalalign", it.optional ("alignment", std::string ("center")))),
 	    .size = it.user ("size", properties, glm::vec2 (0.0f))->value->getVec2 (),
 	    .parallaxDepth = it.user ("parallaxDepth", properties, glm::vec2 (1.0f)),
 	    .colorBlendMode = it.user ("colorBlendMode", properties, 0),
@@ -1270,8 +1275,8 @@ ParticleOperatorUniquePtr ObjectParser::parseParticleOperator (const JSON& it, c
 	return std::make_unique<InheritValueFromEventOperator> (
 	    parseParticleEventInput (it, ParticleEventInput::SetColorOpacity),
 	    glm::vec4 (
-		it.optional ("blendinstart", 0.0f), it.optional ("blendinend", 0.0f), it.optional ("blendoutstart", 1.0f),
-		it.optional ("blendoutend", 1.0f)
+		it.optional ("blendinstart", 0.0f), it.optional ("blendinend", 0.0f),
+		it.optional ("blendoutstart", 1.0f), it.optional ("blendoutend", 1.0f)
 	    )
 	);
     } else if (name == "controlpointattract") {
@@ -1396,9 +1401,11 @@ ParticleRenderer ObjectParser::parseParticleRenderer (const JSON& it) {
 ParticleEventInput ObjectParser::parseParticleEventInput (const JSON& it, ParticleEventInput fallback) {
     // wallpaper64.exe off_140484D90
     static constexpr const char* names[] = {
-	"setcolor", "multiplycolor", "setopacity", "multiplyopacity", "setcoloropacity", "multiplycoloropacity",
-	"setvelocity", "addvelocity", "setsize", "multiplysize", "setrotation", "addrotation", "setangularvelocity",
-	"addangularvelocity",
+	"setcolor",           "multiplycolor",      "setopacity",
+	"multiplyopacity",    "setcoloropacity",    "multiplycoloropacity",
+	"setvelocity",        "addvelocity",        "setsize",
+	"multiplysize",       "setrotation",        "addrotation",
+	"setangularvelocity", "addangularvelocity",
     };
 
     const auto inputIt = it.find ("input");
@@ -1514,7 +1521,8 @@ ParticleChild ObjectParser::parseParticleChild (
     }
 
     // a stable id of its own keeps its random seed (LWE_FIXED_TIMESTEP) apart from the owner's
-    const uint32_t hashed = (static_cast<uint32_t> (ownerBase.id) * 31u + static_cast<uint32_t> (index) + 1u) * 2654435761u;
+    const uint32_t hashed
+	= (static_cast<uint32_t> (ownerBase.id) * 31u + static_cast<uint32_t> (index) + 1u) * 2654435761u;
 
     ParticleUniquePtr particle = nullptr;
     if (!name.empty ()) {

+ 4 - 3
src/WallpaperEngine/Data/Parsers/PropertyParser.cpp

@@ -137,8 +137,7 @@ PropertySharedPtr PropertyParser::parseFile (const JSON& it, const std::string&
 	    .name = name,
 	    .text = it.optional<std::string> ("text", ""),
 	},
-	it.optional<std::string> ("value", ""),
-	it.optional<std::string> ("type", "") == "directory",
+	it.optional<std::string> ("value", ""), it.optional<std::string> ("type", "") == "directory",
 	it.optional<std::string> ("fileType", "")
     );
 }
@@ -158,6 +157,8 @@ PropertySharedPtr PropertyParser::parseTextInput (const JSON& it, const std::str
 	    .name = name,
 	    .text = it.optional<std::string> ("text", ""),
 	},
-	!value.has_value () ? "" : value->is_string () ? value->get<std::string> () : value->dump ()
+	!value.has_value ()       ? ""
+	    : value->is_string () ? value->get<std::string> ()
+				  : value->dump ()
     );
 }

+ 10 - 9
src/WallpaperEngine/Data/Parsers/WallpaperParser.cpp

@@ -22,8 +22,7 @@ glm::vec3 parsePathVector (const JSON& transform, const char* key) {
 
     std::istringstream stream (it->get<std::string> ());
 
-    for (int component = 0; component < 3 && stream >> result[component]; component++) {
-    }
+    for (int component = 0; component < 3 && stream >> result[component]; component++) { }
 
     return result;
 }
@@ -92,13 +91,15 @@ void parseCameraPathFile (const std::string& filename, const Project& project, s
 		time = static_cast<float> (index) / static_cast<float> (count - 1) * result.duration;
 	    }
 
-	    result.keys.push_back (CameraPathKey {
-		.time = time,
-		.eye = parsePathVector (transform, "eye"),
-		.center = parsePathVector (transform, "center"),
-		.up = parsePathVector (transform, "up"),
-		.zoom = numberOr (transform, "zoom", 1.0f),
-	    });
+	    result.keys.push_back (
+		CameraPathKey {
+		    .time = time,
+		    .eye = parsePathVector (transform, "eye"),
+		    .center = parsePathVector (transform, "center"),
+		    .up = parsePathVector (transform, "up"),
+		    .zoom = numberOr (transform, "zoom", 1.0f),
+		}
+	    );
 	}
 
 	paths.push_back (std::move (result));

+ 11 - 5
src/WallpaperEngine/Desktop/IconTheme.cpp

@@ -20,7 +20,9 @@ std::vector<std::filesystem::path> dataDirs () {
     const char* dataHome = std::getenv ("XDG_DATA_HOME");
     const char* dirs = std::getenv ("XDG_DATA_DIRS");
 
-    result.emplace_back (dataHome != nullptr && *dataHome ? std::filesystem::path (dataHome) : homeDir () / ".local/share");
+    result.emplace_back (
+	dataHome != nullptr && *dataHome ? std::filesystem::path (dataHome) : homeDir () / ".local/share"
+    );
 
     std::stringstream stream (dirs != nullptr && *dirs ? dirs : "/usr/local/share:/usr/share");
     std::string dir;
@@ -34,7 +36,8 @@ std::vector<std::filesystem::path> dataDirs () {
     return result;
 }
 
-std::optional<std::string> iniValue (const std::filesystem::path& file, const std::string& section, const std::string& key) {
+std::optional<std::string>
+iniValue (const std::filesystem::path& file, const std::string& section, const std::string& key) {
     std::ifstream stream (file);
     std::string line;
     bool inSection = false;
@@ -55,7 +58,8 @@ std::optional<std::string> iniValue (const std::filesystem::path& file, const st
 
 std::string userTheme () {
     const char* configHome = std::getenv ("XDG_CONFIG_HOME");
-    const auto config = configHome != nullptr && *configHome ? std::filesystem::path (configHome) : homeDir () / ".config";
+    const auto config
+	= configHome != nullptr && *configHome ? std::filesystem::path (configHome) : homeDir () / ".config";
 
     if (const auto theme = iniValue (config / "kdeglobals", "Icons", "Theme"); theme.has_value () && !theme->empty ()) {
 	return *theme;
@@ -180,7 +184,8 @@ const std::map<std::string, IconTheme::Candidate>& IconTheme::index (const std::
 	    }
 
 	    const int candidateScore = score (path);
-	    const auto [entry, inserted] = result.try_emplace (path.stem ().string (), Candidate { path, candidateScore });
+	    const auto [entry, inserted]
+		= result.try_emplace (path.stem ().string (), Candidate { path, candidateScore });
 
 	    if (!inserted && candidateScore > entry->second.score) {
 		entry->second = Candidate { path, candidateScore };
@@ -199,7 +204,8 @@ std::optional<std::filesystem::path> IconTheme::find (const std::string& icon) {
     std::error_code error;
 
     if (icon.starts_with ('/')) {
-	return std::filesystem::is_regular_file (icon, error) ? std::optional<std::filesystem::path> (icon) : std::nullopt;
+	return std::filesystem::is_regular_file (icon, error) ? std::optional<std::filesystem::path> (icon)
+							      : std::nullopt;
     }
 
     for (const auto& theme : this->m_themes) {

+ 21 - 0
src/WallpaperEngine/Desktop/KDESession.cpp

@@ -0,0 +1,21 @@
+#include "KDESession.h"
+
+#include <cstdlib>
+#include <string_view>
+
+bool WallpaperEngine::Desktop::isKDESession () {
+    if (const char* desktopSession = std::getenv ("DESKTOP_SESSION")) {
+	const std::string_view session (desktopSession);
+	if (session.find ("kde") != std::string_view::npos || session.find ("plasma") != std::string_view::npos) {
+	    return true;
+	}
+    }
+
+    if (const char* xdgDesktop = std::getenv ("XDG_SESSION_DESKTOP");
+	xdgDesktop && std::string_view (xdgDesktop) == "KDE") {
+	return true;
+    }
+
+    const char* kdeSession = std::getenv ("KDE_FULL_SESSION");
+    return kdeSession && std::string_view (kdeSession) == "true";
+}

+ 6 - 0
src/WallpaperEngine/Desktop/KDESession.h

@@ -0,0 +1,6 @@
+#pragma once
+
+namespace WallpaperEngine::Desktop {
+/** True inside a KDE Plasma session, going by the session environment variables */
+bool isKDESession ();
+} // namespace WallpaperEngine::Desktop

+ 19 - 9
src/WallpaperEngine/Desktop/UserShortcut.cpp

@@ -70,7 +70,8 @@ std::optional<std::filesystem::path> findDesktopFile (const std::string& value)
     std::error_code error;
 
     if (value.starts_with ('/')) {
-	return std::filesystem::is_regular_file (value, error) ? std::optional<std::filesystem::path> (value) : std::nullopt;
+	return std::filesystem::is_regular_file (value, error) ? std::optional<std::filesystem::path> (value)
+							       : std::nullopt;
     }
 
     const std::string id = value.ends_with (".desktop") ? value : value + ".desktop";
@@ -178,10 +179,18 @@ std::vector<std::string> expandExec (
 	    }
 
 	    switch (argument[++i]) {
-		case '%': expanded += '%'; break;
-		case 'c': expanded += value ("Name"); break;
-		case 'k': expanded += file.string (); break;
-		default: dropped = argument.size () == 2; break;
+		case '%':
+		    expanded += '%';
+		    break;
+		case 'c':
+		    expanded += value ("Name");
+		    break;
+		case 'k':
+		    expanded += file.string ();
+		    break;
+		default:
+		    dropped = argument.size () == 2;
+		    break;
 	    }
 	}
 
@@ -365,9 +374,9 @@ std::optional<UserShortcut> UserShortcut::parse (const std::string& raw) {
 	}
 
 	const std::string type = json.value ("commandtype", "");
-	UserShortcut result {
-	    .type = Type::File, .target = trim (json.value ("file", "")), .arguments = json.value ("arguments", "")
-	};
+	UserShortcut result { .type = Type::File,
+			      .target = trim (json.value ("file", "")),
+			      .arguments = json.value ("arguments", "") };
 
 	if (type == "application") {
 	    result.type = Type::Application;
@@ -481,7 +490,8 @@ bool UserShortcut::launch () const {
 	    }
 	case Type::Web:
 	    return spawnDetached (
-		{ "xdg-open", this->target.find ("://") == std::string::npos ? "https://" + this->target : this->target }
+		{ "xdg-open",
+		  this->target.find ("://") == std::string::npos ? "https://" + this->target : this->target }
 	    );
 	case Type::Directory:
 	    return spawnDetached ({ "xdg-open", this->target });

+ 7 - 4
src/WallpaperEngine/FileSystem/Adapters/Package.cpp

@@ -16,11 +16,12 @@ using namespace WallpaperEngine::FileSystem::Adapters;
 
 // scenes are authored on Windows, so fall back to a case-insensitive lookup when the exact path is missing
 static bool equalsIgnoreCase (const std::string& a, const std::string& b) {
-    return std::ranges::equal (a, b, [] (unsigned char x, unsigned char y) { return std::tolower (x) == std::tolower (y); });
+    return std::ranges::equal (a, b, [] (unsigned char x, unsigned char y) {
+	return std::tolower (x) == std::tolower (y);
+    });
 }
 
-template <typename Files>
-static auto findFile (const Files& files, const std::filesystem::path& path) {
+template <typename Files> static auto findFile (const Files& files, const std::filesystem::path& path) {
     const auto wanted = path.string ();
     const auto exact = std::ranges::find_if (files, [&wanted] (const auto& file) { return file->filename == wanted; });
 
@@ -28,7 +29,9 @@ static auto findFile (const Files& files, const std::filesystem::path& path) {
 	return exact;
     }
 
-    return std::ranges::find_if (files, [&wanted] (const auto& file) { return equalsIgnoreCase (file->filename, wanted); });
+    return std::ranges::find_if (files, [&wanted] (const auto& file) {
+	return equalsIgnoreCase (file->filename, wanted);
+    });
 }
 
 ReadStreamSharedPtr PackageAdapter::open (const std::filesystem::path& path) const {

+ 4 - 3
src/WallpaperEngine/FileSystem/Container.h

@@ -57,9 +57,10 @@ public:
      */
     void registerAdapterFactory (FactoryUniquePtr factory);
 
-    /** Looks for a workshop-dependency-prefixed variant of path across every mounted adapter, only as a fallback when the literal path fails */
-    [[nodiscard]] std::optional<std::filesystem::path> resolveWorkshopDependencyAlias (const std::filesystem::path& path
-    ) const;
+    /** Looks for a workshop-dependency-prefixed variant of path across every mounted adapter, only as a fallback when
+     * the literal path fails */
+    [[nodiscard]] std::optional<std::filesystem::path>
+    resolveWorkshopDependencyAlias (const std::filesystem::path& path) const;
 
 private:
     /**

+ 12 - 8
src/WallpaperEngine/Input/Drivers/KDECursorInput.cpp

@@ -17,7 +17,9 @@ KDECursorInput::KDECursorInput () {
     }
 
     if (!loadKWinScript ()) {
-	sLog.out ("KDE cursor input could not load its KWin script, mouse-reactive effects may not work on this session");
+	sLog.out (
+	    "KDE cursor input could not load its KWin script, mouse-reactive effects may not work on this session"
+	);
 	stopDBus ();
     }
 }
@@ -47,7 +49,8 @@ bool KDECursorInput::initializeDBus () {
 
     dbus_error_init (&error);
     const std::string serviceName = std::string (kServiceName) + "_" + std::to_string (getpid ());
-    const auto requestResult = dbus_bus_request_name (m_connection, serviceName.c_str (), DBUS_NAME_FLAG_DO_NOT_QUEUE, &error);
+    const auto requestResult
+	= dbus_bus_request_name (m_connection, serviceName.c_str (), DBUS_NAME_FLAG_DO_NOT_QUEUE, &error);
 
     if (dbus_error_is_set (&error)) {
 	sLog.error ("Failed to request DBus service ", serviceName, ": ", error.message);
@@ -57,7 +60,8 @@ bool KDECursorInput::initializeDBus () {
 	return false;
     }
 
-    if (requestResult != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER && requestResult != DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) {
+    if (requestResult != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
+	&& requestResult != DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) {
 	sLog.error ("DBus service ", serviceName, " is already owned by another process");
 	dbus_connection_unref (m_connection);
 	m_connection = nullptr;
@@ -117,16 +121,15 @@ bool KDECursorInput::loadKWinScript () {
 
     m_pluginName = "lwe-cursor-input-" + std::to_string (getpid ());
 
-    DBusMessage* loadCall = dbus_message_new_method_call ("org.kde.KWin", "/Scripting", "org.kde.kwin.Scripting", "loadScript");
+    DBusMessage* loadCall
+	= dbus_message_new_method_call ("org.kde.KWin", "/Scripting", "org.kde.kwin.Scripting", "loadScript");
     if (loadCall == nullptr) {
 	return false;
     }
 
     const char* pathArg = m_scriptPath.c_str ();
     const char* nameArg = m_pluginName.c_str ();
-    dbus_message_append_args (
-	loadCall, DBUS_TYPE_STRING, &pathArg, DBUS_TYPE_STRING, &nameArg, DBUS_TYPE_INVALID
-    );
+    dbus_message_append_args (loadCall, DBUS_TYPE_STRING, &pathArg, DBUS_TYPE_STRING, &nameArg, DBUS_TYPE_INVALID);
 
     DBusError error;
     dbus_error_init (&error);
@@ -153,7 +156,8 @@ bool KDECursorInput::loadKWinScript () {
     }
 
     const std::string scriptObjectPath = "/Scripting/Script" + std::to_string (scriptId);
-    DBusMessage* runCall = dbus_message_new_method_call ("org.kde.KWin", scriptObjectPath.c_str (), "org.kde.kwin.Script", "run");
+    DBusMessage* runCall
+	= dbus_message_new_method_call ("org.kde.KWin", scriptObjectPath.c_str (), "org.kde.kwin.Script", "run");
     if (runCall != nullptr) {
 	dbus_error_init (&error);
 	DBusMessage* runReply = dbus_connection_send_with_reply_and_block (m_connection, runCall, 2000, &error);

+ 2 - 2
src/WallpaperEngine/Input/Drivers/KDECursorInput.h

@@ -1,7 +1,7 @@
 #pragma once
 
 #ifdef ENABLE_WAYLAND
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 
 #include <dbus/dbus.h>
 #include <glm/vec2.hpp>
@@ -52,5 +52,5 @@ private:
 
 } // namespace WallpaperEngine::Input::Drivers
 
-#endif /* ENABLE_KDE_EXPERIMENTAL_FEATURES */
+#endif /* ENABLE_KDE_FEATURES */
 #endif /* ENABLE_WAYLAND */

+ 17 - 7
src/WallpaperEngine/Input/Drivers/WaylandMouseInput.cpp

@@ -15,10 +15,21 @@
 #include <X11/Xlib.h>
 #endif /* ENABLE_X11 */
 
+#ifdef ENABLE_KDE_FEATURES
+#include "WallpaperEngine/Desktop/KDESession.h"
+#endif /* ENABLE_KDE_FEATURES */
+
 using namespace WallpaperEngine::Input::Drivers;
 
 WaylandMouseInput::WaylandMouseInput (const WallpaperEngine::Render::Drivers::WaylandOpenGLDriver& driver) :
-    m_waylandDriver (driver) { }
+    m_waylandDriver (driver) {
+#ifdef ENABLE_KDE_FEATURES
+    // outside Plasma there is no KWin to load the cursor script into
+    if (WallpaperEngine::Desktop::isKDESession ()) {
+	this->m_kdeCursor = std::make_unique<KDECursorInput> ();
+    }
+#endif /* ENABLE_KDE_FEATURES */
+}
 
 void WaylandMouseInput::update () {
     if (!this->m_waylandDriver.getApp ().getContext ().settings.mouse.enabled) {
@@ -31,12 +42,12 @@ void WaylandMouseInput::update () {
 	return;
     }
 
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
-    if (const auto kdeCursor = this->m_kdeCursor.position ();
+#ifdef ENABLE_KDE_FEATURES
+    if (const auto kdeCursor = this->m_kdeCursor ? this->m_kdeCursor->position () : std::nullopt;
 	kdeCursor.has_value () && this->matchViewport (*kdeCursor)) {
 	return;
     }
-#endif /* ENABLE_KDE_EXPERIMENTAL_FEATURES */
+#endif /* ENABLE_KDE_FEATURES */
 
     const auto now = std::chrono::steady_clock::now ();
     if (now - this->m_lastGlobalCursorQuery < std::chrono::milliseconds (16)) {
@@ -187,9 +198,8 @@ std::optional<glm::dvec2> WaylandMouseInput::queryX11CursorPosition () const {
     int childY = 0;
     unsigned int mask = 0;
 
-    const Bool ok = XQueryPointer (
-	display, root, &returnedRoot, &returnedChild, &rootX, &rootY, &childX, &childY, &mask
-    );
+    const Bool ok
+	= XQueryPointer (display, root, &returnedRoot, &returnedChild, &rootX, &rootY, &childX, &childY, &mask);
 
     XCloseDisplay (display);
 

+ 6 - 5
src/WallpaperEngine/Input/Drivers/WaylandMouseInput.h

@@ -6,11 +6,12 @@
 
 #include <chrono>
 #include <glm/vec2.hpp>
+#include <memory>
 #include <optional>
 
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 #include "KDECursorInput.h"
-#endif /* ENABLE_KDE_EXPERIMENTAL_FEATURES */
+#endif /* ENABLE_KDE_FEATURES */
 
 namespace WallpaperEngine::Render::Drivers {
 namespace Output {
@@ -45,9 +46,9 @@ private:
     glm::dvec2 m_pos = {};
     std::chrono::steady_clock::time_point m_lastGlobalCursorQuery = {};
 
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
-    KDECursorInput m_kdeCursor;
-#endif /* ENABLE_KDE_EXPERIMENTAL_FEATURES */
+#ifdef ENABLE_KDE_FEATURES
+    std::unique_ptr<KDECursorInput> m_kdeCursor;
+#endif /* ENABLE_KDE_FEATURES */
 };
 } // namespace WallpaperEngine::Input::Drivers
 

+ 2 - 2
src/WallpaperEngine/Media/ThumbnailPalette.cpp

@@ -112,7 +112,8 @@ ThumbnailPalette WallpaperEngine::Media::computeThumbnailPalette (const uint8_t*
     const glm::vec3 white (1.0f);
     const glm::vec3 black (0.0f);
 
-    palette.highContrast = contrastRatio (palette.primary, white) >= contrastRatio (palette.primary, black) ? white : black;
+    palette.highContrast
+	= contrastRatio (palette.primary, white) >= contrastRatio (palette.primary, black) ? white : black;
     palette.text = palette.highContrast;
 
     // a color from the cover itself is nicer than plain black/white as long as it is readable
@@ -130,7 +131,6 @@ ThumbnailPalette WallpaperEngine::Media::computeThumbnailPalette (const uint8_t*
     return palette;
 }
 
-
 ThumbnailPalette WallpaperEngine::Media::loadThumbnailPalette (const std::string& path) {
     int width = 0;
     int height = 0;

+ 1 - 1
src/WallpaperEngine/Media/ThumbnailPalette.h

@@ -4,8 +4,8 @@
 #include <cstdint>
 #include <string>
 
-#include <glm/vec3.hpp>
 #include <glm/geometric.hpp>
+#include <glm/vec3.hpp>
 
 namespace WallpaperEngine::Media {
 /**

+ 1 - 3
src/WallpaperEngine/Render/AlbumTexture.cpp

@@ -123,6 +123,4 @@ void AlbumTexture::load () const {
     }
 }
 
-bool AlbumTexture::isReady () const {
-    return this->m_width > 0 && this->m_height > 0;
-}
+bool AlbumTexture::isReady () const { return this->m_width > 0 && this->m_height > 0; }

+ 19 - 10
src/WallpaperEngine/Render/CFBO.cpp

@@ -10,14 +10,21 @@ namespace {
 // 3-channel formats get an alpha channel, RGB targets aren't guaranteed to be renderable
 GLint internalFormat (const TextureFormat format) {
     switch (format) {
-	case TextureFormat_RG88: return GL_RG8;
-	case TextureFormat_R8: return GL_R8;
-	case TextureFormat_RG1616f: return GL_RG16F;
-	case TextureFormat_R16f: return GL_R16F;
+	case TextureFormat_RG88:
+	    return GL_RG8;
+	case TextureFormat_R8:
+	    return GL_R8;
+	case TextureFormat_RG1616f:
+	    return GL_RG16F;
+	case TextureFormat_R16f:
+	    return GL_R16F;
 	case TextureFormat_RGBA16161616f:
-	case TextureFormat_RGB161616f: return GL_RGBA16F;
-	case TextureFormat_RGBa1010102: return GL_RGB10_A2;
-	default: return GL_RGBA8;
+	case TextureFormat_RGB161616f:
+	    return GL_RGBA16F;
+	case TextureFormat_RGBa1010102:
+	    return GL_RGB10_A2;
+	default:
+	    return GL_RGBA8;
     }
 }
 } // namespace
@@ -26,7 +33,9 @@ CFBO::CFBO (
     std::string name, const TextureFormat format, const uint32_t flags, const float scale, uint32_t realWidth,
     uint32_t realHeight, uint32_t textureWidth, uint32_t textureHeight, const glm::vec4& borderColor,
     const uint32_t mipLevels
-) : m_scale (scale), m_name (std::move (name)), m_format (format), m_flags (flags), m_mipLevels (std::max (mipLevels, 1u)) {
+) :
+    m_scale (scale), m_name (std::move (name)), m_format (format), m_flags (flags),
+    m_mipLevels (std::max (mipLevels, 1u)) {
     constexpr GLenum drawBuffers[1] = { GL_COLOR_ATTACHMENT0 };
     glGenFramebuffers (1, &this->m_framebuffer);
     glBindFramebuffer (GL_FRAMEBUFFER, this->m_framebuffer);
@@ -79,8 +88,8 @@ CFBO::CFBO (
 	sLog.error ("FBO ", this->m_name, " can't render to format ", format, ", falling back to RGBA8");
 	for (uint32_t level = 0; level < this->m_mipLevels; level++) {
 	    glTexImage2D (
-		GL_TEXTURE_2D, level, GL_RGBA8, std::max (textureWidth >> level, 1u), std::max (textureHeight >> level, 1u),
-		0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr
+		GL_TEXTURE_2D, level, GL_RGBA8, std::max (textureWidth >> level, 1u),
+		std::max (textureHeight >> level, 1u), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr
 	    );
 	}
 	this->m_format = TextureFormat_ARGB8888;

+ 5 - 5
src/WallpaperEngine/Render/CTexture.cpp

@@ -18,11 +18,11 @@ CTexture::CTexture (RenderContext& context, TextureUniquePtr header) :
     GLint maxTextureSize = 0;
     glGetIntegerv (GL_MAX_TEXTURE_SIZE, &maxTextureSize);
     if (this->m_header->textureWidth > static_cast<uint32_t> (maxTextureSize)
-        || this->m_header->textureHeight > static_cast<uint32_t> (maxTextureSize)) {
-        sLog.error (
-            "Texture ", this->m_header->textureWidth, "x", this->m_header->textureHeight,
-            " exceeds this GL context's GL_MAX_TEXTURE_SIZE (", maxTextureSize, "); it will fail to upload"
-        );
+	|| this->m_header->textureHeight > static_cast<uint32_t> (maxTextureSize)) {
+	sLog.error (
+	    "Texture ", this->m_header->textureWidth, "x", this->m_header->textureHeight,
+	    " exceeds this GL context's GL_MAX_TEXTURE_SIZE (", maxTextureSize, "); it will fail to upload"
+	);
     }
 
     // videos only get one framebuffer and one mipmap

+ 84 - 86
src/WallpaperEngine/Render/CWallpaper.cpp

@@ -118,83 +118,84 @@ void CWallpaper::setupShaders () {
     const GLuint fragmentShaderID = glCreateShader (GL_FRAGMENT_SHADER);
 
     // image filter and color options are assets/shaders/ccsimple.frag
-    sourcePointer = "#version 330\n"
-		    "precision highp float;\n"
-		    "uniform sampler2D g_Texture0;\n"
-		    "uniform sampler3D g_Texture1;\n"
-		    "uniform vec4 g_Params;\n"
-		    "uniform float g_LutParams;\n"
-		    "uniform bool u_ColorEnabled;\n"
-		    "uniform bool u_LutEnabled;\n"
-		    "uniform bool u_InputLinear;\n"
-		    "uniform bool u_OutputPQ;\n"
-		    "in vec2 v_TexCoord;\n"
-		    "out vec4 out_FragColor;\n"
-		    // BT.2408 reference white, what the PQ image description of HDR surfaces anchors SDR content to
-		    "const float referenceWhite = 203.0;\n"
-		    "const mat3 bt709to2020 = mat3 (0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956);\n"
-		    "const mat3 bt2020to709 = mat3 (1.6605, -0.1246, -0.0182, -0.5876, 1.1329, -0.1006, -0.0728, -0.0083, "
-		    "1.1187);\n"
-		    "vec3 srgbToLinear (vec3 c) {\n"
-		    "return mix (c / 12.92, pow ((c + 0.055) / 1.055, vec3 (2.4)), step (0.04045, c));\n"
-		    "}\n"
-		    "vec3 linearToSrgb (vec3 c) {\n"
-		    "return mix (c * 12.92, 1.055 * pow (c, vec3 (1.0 / 2.4)) - 0.055, step (0.0031308, c));\n"
-		    "}\n"
-		    // mpv decodes SDR video with BT.1886, a pure 2.4 power, going back the same way keeps SDR videos
-		    // exactly as they look without --hdr
-		    "vec3 videoToLinear (vec3 c) { return pow (c, vec3 (2.4)); }\n"
-		    "vec3 linearToVideo (vec3 c) { return pow (c, vec3 (1.0 / 2.4)); }\n"
-		    "vec3 pqEncode (vec3 nits) {\n"
-		    "vec3 y = pow (clamp (nits / 10000.0, 0.0, 1.0), vec3 (0.1593017578125));\n"
-		    "return pow ((0.8359375 + 18.8515625 * y) / (1.0 + 18.6875 * y), vec3 (78.84375));\n"
-		    "}\n"
-		    // highlights of HDR video on an SDR output, a soft knee instead of a hard clip
-		    "vec3 softClip (vec3 x) {\n"
-		    "return mix (x, 0.8 + 0.2 * (1.0 - exp (-(x - 0.8) / 0.2)), step (0.8, x));\n"
-		    "}\n"
-		    "vec3 hsv2rgb (vec3 c) {\n"
-		    "vec4 K = vec4 (1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n"
-		    "vec3 p = abs (fract (c.xxx + K.xyz) * 6.0 - K.www);\n"
-		    "return c.z * mix (K.xxx, clamp (p - K.xxx, 0.0, 1.0), c.y);\n"
-		    "}\n"
-		    "vec3 rgb2hsv (vec3 RGB) {\n"
-		    "vec4 P = (RGB.g < RGB.b) ? vec4 (RGB.bg, -1.0, 2.0 / 3.0) : vec4 (RGB.gb, 0.0, -1.0 / 3.0);\n"
-		    "vec4 Q = (RGB.r < P.x) ? vec4 (P.xyw, RGB.r) : vec4 (RGB.r, P.yzx);\n"
-		    "float C = Q.x - min (Q.w, Q.y);\n"
-		    "float H = abs ((Q.w - Q.y) / (6.0 * C + 1e-10) + Q.z);\n"
-		    "return vec3 (H, C / (Q.x + 1e-10), Q.x);\n"
-		    "}\n"
-		    "void main () {\n"
-		    "vec4 albedo = texture (g_Texture0, v_TexCoord);\n"
-		    "bool filtered = u_ColorEnabled || u_LutEnabled;\n"
-		    "vec3 overbright = vec3 (0.0);\n"
-		    // the filters work on SDR-encoded values like in Wallpaper Engine, what's past reference white is kept aside
-		    "if (u_InputLinear && filtered) {\n"
-		    "vec3 light = max (bt2020to709 * albedo.rgb, 0.0);\n"
-		    "overbright = max (light - 1.0, 0.0);\n"
-		    "albedo.rgb = linearToVideo (min (light, 1.0));\n"
-		    "}\n"
-		    "if (u_ColorEnabled) {\n"
-		    "albedo.rgb = mix (vec3 (0.5), albedo.rgb, g_Params.y);\n"
-		    "vec3 hsv = rgb2hsv (albedo.rgb);\n"
-		    "hsv.z *= g_Params.x;\n"
-		    "hsv.y *= g_Params.z;\n"
-		    "hsv.x += g_Params.w;\n"
-		    "albedo.rgb = hsv2rgb (hsv);\n"
-		    "}\n"
-		    "if (u_LutEnabled) {\n"
-		    "albedo.rgb = mix (albedo.rgb, texture (g_Texture1, albedo.rgb).rgb, g_LutParams);\n"
-		    "}\n"
-		    "if (u_InputLinear || u_OutputPQ) {\n"
-		    "vec3 encoded = clamp (albedo.rgb, 0.0, 1.0);\n"
-		    "vec3 linear2020 = !u_InputLinear ? bt709to2020 * srgbToLinear (encoded)\n"
-		    "    : filtered ? bt709to2020 * (videoToLinear (encoded) + overbright) : albedo.rgb;\n"
-		    "albedo.rgb = u_OutputPQ ? pqEncode (max (linear2020, 0.0) * referenceWhite)\n"
-		    "    : linearToVideo (min (softClip (max (bt2020to709 * linear2020, 0.0)), 1.0));\n"
-		    "}\n"
-		    "out_FragColor = albedo;\n"
-		    "}";
+    sourcePointer
+	= "#version 330\n"
+	  "precision highp float;\n"
+	  "uniform sampler2D g_Texture0;\n"
+	  "uniform sampler3D g_Texture1;\n"
+	  "uniform vec4 g_Params;\n"
+	  "uniform float g_LutParams;\n"
+	  "uniform bool u_ColorEnabled;\n"
+	  "uniform bool u_LutEnabled;\n"
+	  "uniform bool u_InputLinear;\n"
+	  "uniform bool u_OutputPQ;\n"
+	  "in vec2 v_TexCoord;\n"
+	  "out vec4 out_FragColor;\n"
+	  // BT.2408 reference white, what the PQ image description of HDR surfaces anchors SDR content to
+	  "const float referenceWhite = 203.0;\n"
+	  "const mat3 bt709to2020 = mat3 (0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956);\n"
+	  "const mat3 bt2020to709 = mat3 (1.6605, -0.1246, -0.0182, -0.5876, 1.1329, -0.1006, -0.0728, -0.0083, "
+	  "1.1187);\n"
+	  "vec3 srgbToLinear (vec3 c) {\n"
+	  "return mix (c / 12.92, pow ((c + 0.055) / 1.055, vec3 (2.4)), step (0.04045, c));\n"
+	  "}\n"
+	  "vec3 linearToSrgb (vec3 c) {\n"
+	  "return mix (c * 12.92, 1.055 * pow (c, vec3 (1.0 / 2.4)) - 0.055, step (0.0031308, c));\n"
+	  "}\n"
+	  // mpv decodes SDR video with BT.1886, a pure 2.4 power, going back the same way keeps SDR videos
+	  // exactly as they look without --hdr
+	  "vec3 videoToLinear (vec3 c) { return pow (c, vec3 (2.4)); }\n"
+	  "vec3 linearToVideo (vec3 c) { return pow (c, vec3 (1.0 / 2.4)); }\n"
+	  "vec3 pqEncode (vec3 nits) {\n"
+	  "vec3 y = pow (clamp (nits / 10000.0, 0.0, 1.0), vec3 (0.1593017578125));\n"
+	  "return pow ((0.8359375 + 18.8515625 * y) / (1.0 + 18.6875 * y), vec3 (78.84375));\n"
+	  "}\n"
+	  // highlights of HDR video on an SDR output, a soft knee instead of a hard clip
+	  "vec3 softClip (vec3 x) {\n"
+	  "return mix (x, 0.8 + 0.2 * (1.0 - exp (-(x - 0.8) / 0.2)), step (0.8, x));\n"
+	  "}\n"
+	  "vec3 hsv2rgb (vec3 c) {\n"
+	  "vec4 K = vec4 (1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n"
+	  "vec3 p = abs (fract (c.xxx + K.xyz) * 6.0 - K.www);\n"
+	  "return c.z * mix (K.xxx, clamp (p - K.xxx, 0.0, 1.0), c.y);\n"
+	  "}\n"
+	  "vec3 rgb2hsv (vec3 RGB) {\n"
+	  "vec4 P = (RGB.g < RGB.b) ? vec4 (RGB.bg, -1.0, 2.0 / 3.0) : vec4 (RGB.gb, 0.0, -1.0 / 3.0);\n"
+	  "vec4 Q = (RGB.r < P.x) ? vec4 (P.xyw, RGB.r) : vec4 (RGB.r, P.yzx);\n"
+	  "float C = Q.x - min (Q.w, Q.y);\n"
+	  "float H = abs ((Q.w - Q.y) / (6.0 * C + 1e-10) + Q.z);\n"
+	  "return vec3 (H, C / (Q.x + 1e-10), Q.x);\n"
+	  "}\n"
+	  "void main () {\n"
+	  "vec4 albedo = texture (g_Texture0, v_TexCoord);\n"
+	  "bool filtered = u_ColorEnabled || u_LutEnabled;\n"
+	  "vec3 overbright = vec3 (0.0);\n"
+	  // the filters work on SDR-encoded values like in Wallpaper Engine, what's past reference white is kept aside
+	  "if (u_InputLinear && filtered) {\n"
+	  "vec3 light = max (bt2020to709 * albedo.rgb, 0.0);\n"
+	  "overbright = max (light - 1.0, 0.0);\n"
+	  "albedo.rgb = linearToVideo (min (light, 1.0));\n"
+	  "}\n"
+	  "if (u_ColorEnabled) {\n"
+	  "albedo.rgb = mix (vec3 (0.5), albedo.rgb, g_Params.y);\n"
+	  "vec3 hsv = rgb2hsv (albedo.rgb);\n"
+	  "hsv.z *= g_Params.x;\n"
+	  "hsv.y *= g_Params.z;\n"
+	  "hsv.x += g_Params.w;\n"
+	  "albedo.rgb = hsv2rgb (hsv);\n"
+	  "}\n"
+	  "if (u_LutEnabled) {\n"
+	  "albedo.rgb = mix (albedo.rgb, texture (g_Texture1, albedo.rgb).rgb, g_LutParams);\n"
+	  "}\n"
+	  "if (u_InputLinear || u_OutputPQ) {\n"
+	  "vec3 encoded = clamp (albedo.rgb, 0.0, 1.0);\n"
+	  "vec3 linear2020 = !u_InputLinear ? bt709to2020 * srgbToLinear (encoded)\n"
+	  "    : filtered ? bt709to2020 * (videoToLinear (encoded) + overbright) : albedo.rgb;\n"
+	  "albedo.rgb = u_OutputPQ ? pqEncode (max (linear2020, 0.0) * referenceWhite)\n"
+	  "    : linearToVideo (min (softClip (max (bt2020to709 * linear2020, 0.0)), 1.0));\n"
+	  "}\n"
+	  "out_FragColor = albedo;\n"
+	  "}";
 
     glShaderSource (fragmentShaderID, 1, &sourcePointer, nullptr);
     glCompileShader (fragmentShaderID);
@@ -372,9 +373,7 @@ void CWallpaper::render (
 	static uint32_t brightnessFrameCounter = 0;
 	if ((brightnessFrameCounter++ % 30) == 0) {
 	    std::vector<unsigned char> px (viewport.z * viewport.w * 4);
-	    glReadPixels (
-		viewport.x, viewport.y, viewport.z, viewport.w, GL_RGBA, GL_UNSIGNED_BYTE, px.data ()
-	    );
+	    glReadPixels (viewport.x, viewport.y, viewport.z, viewport.w, GL_RGBA, GL_UNSIGNED_BYTE, px.data ());
 	    uint64_t sum = 0;
 	    for (size_t i = 0; i < px.size (); i += 4) {
 		sum += px[i] + px[i + 1] + px[i + 2];
@@ -428,8 +427,7 @@ void CWallpaper::setupFramebuffers (const bool depth, const TextureFormat format
     const uint32_t clamp = this->m_state.getClampingMode ();
 
     const auto sceneFBO = this->create (
-	"_rt_FullFrameBuffer", format, clamp, 1.0, { width, height }, { width, height },
-	this->m_cornerColor
+	"_rt_FullFrameBuffer", format, clamp, 1.0, { width, height }, { width, height }, this->m_cornerColor
     );
 
     if (depth) {
@@ -467,9 +465,7 @@ void CWallpaper::setImageAdjustments (const ImageAdjustments& adjustments) {
 
     this->m_colorEnabled = adjustments.colorEnabled.value_or (false)
 	&& (contrast != 1.0f || brightness != 1.0f || saturation != 1.0f || hue != 0.0f);
-    this->m_colorParams = {
-	std::pow (brightness, 2.0f), std::pow (contrast, 0.5f), std::pow (saturation, 0.5f), hue
-    };
+    this->m_colorParams = { std::pow (brightness, 2.0f), std::pow (contrast, 0.5f), std::pow (saturation, 0.5f), hue };
     this->m_lutStrength = adjustments.filterStrength.value_or (100.0f) / 100.0f;
     this->m_flipHorizontal = adjustments.flipHorizontal.value_or (false);
 
@@ -578,7 +574,9 @@ void CWallpaper::loadLut (const std::string& name) {
 	    }
 
 	    pixels = decoded;
-	} else if (texture->format != TextureFormat_ARGB8888 || static_cast<size_t> (mipmap->uncompressedSize) < expected) {
+	} else if (
+	    texture->format != TextureFormat_ARGB8888 || static_cast<size_t> (mipmap->uncompressedSize) < expected
+	) {
 	    sLog.error ("Image filter ", name, " has an unsupported format");
 	    return;
 	}

+ 6 - 5
src/WallpaperEngine/Render/Camera.cpp

@@ -151,7 +151,9 @@ void Camera::updatePerspectiveLayers (const glm::vec4& uvs, const float viewport
     const float centerY = height / 2.0f - ((uvs.z + uvs.w) / 2.0f - 0.5f) * canvasHeight;
 
     // sub_1401E5B60: the camera backs off until the visible height fills the fov, near 5, far max (15000, d + 1000)
-    const float fov = glm::radians (glm::clamp (this->m_camera.projection.perspectiveOverrideFov->value->getFloat (), 0.1f, 179.9f));
+    const float fov = glm::radians (
+	glm::clamp (this->m_camera.projection.perspectiveOverrideFov->value->getFloat (), 0.1f, 179.9f)
+    );
     // the zoom is part of the ortho projection WE derives the distance from
     const float distance = visibleHeight / (2.0f * this->m_zoom * std::tan (fov / 2.0f));
     const float nearZ = 5.0f;
@@ -169,11 +171,10 @@ void Camera::updatePerspectiveLayers (const glm::vec4& uvs, const float viewport
     const glm::mat4 projection = glm::frustum (
 	(width / 2.0f - canvasWidth / 2.0f - centerX) * aspectScale * scale,
 	(width / 2.0f + canvasWidth / 2.0f - centerX) * aspectScale * scale,
-	(height / 2.0f - canvasHeight / 2.0f - centerY) * scale, (height / 2.0f + canvasHeight / 2.0f - centerY) * scale,
-	nearZ, farZ
+	(height / 2.0f - canvasHeight / 2.0f - centerY) * scale,
+	(height / 2.0f + canvasHeight / 2.0f - centerY) * scale, nearZ, farZ
     );
 
     const glm::vec3 center (width / 2.0f, height / 2.0f, 0.0f);
-    this->m_perspectiveLayer
-	= kFlipY * projection * view * glm::translate (glm::mat4 (1.0f), center) * kFlipY;
+    this->m_perspectiveLayer = kFlipY * projection * view * glm::translate (glm::mat4 (1.0f), center) * kFlipY;
 }

+ 2 - 2
src/WallpaperEngine/Render/Drivers/Detectors/KDEWaylandFullScreenDetector.h

@@ -1,7 +1,7 @@
 #pragma once
 
 #ifdef ENABLE_WAYLAND
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 
 #include <dbus/dbus.h>
 #include <string>
@@ -75,5 +75,5 @@ private:
 };
 
 } // namespace WallpaperEngine::Render::Drivers::Detectors
-#endif /* ENABLE_KDE_EXPERIMENTAL_FEATURES */
+#endif /* ENABLE_KDE_FEATURES */
 #endif /* ENABLE_WAYLAND */

+ 4 - 27
src/WallpaperEngine/Render/Drivers/Detectors/WaylandFullScreenDetector.cpp

@@ -1,7 +1,8 @@
 #include "WaylandFullScreenDetector.h"
 
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 #include "KDEWaylandFullScreenDetector.h"
+#include "WallpaperEngine/Desktop/KDESession.h"
 #endif
 
 #include "WallpaperEngine/Logging/Log.h"
@@ -90,30 +91,6 @@ namespace {
 	}
     }
 
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
-    bool isRunningUnderKDE () {
-	const char* desktopSession = std::getenv ("DESKTOP_SESSION");
-	if (desktopSession != nullptr) {
-	    std::string_view session (desktopSession);
-	    if (session.find ("kde") != std::string_view::npos || session.find ("plasma") != std::string_view::npos) {
-		return true;
-	    }
-	}
-
-	const char* xdgDesktop = std::getenv ("XDG_SESSION_DESKTOP");
-	if (xdgDesktop != nullptr && std::string_view (xdgDesktop) == "KDE") {
-	    return true;
-	}
-
-	const char* kdeSession = std::getenv ("KDE_FULL_SESSION");
-	if (kdeSession != nullptr && std::string (kdeSession) == "true") {
-	    return true;
-	}
-
-	return false;
-    }
-#endif
-
     bool isRelevant (
 	const WallpaperEngine::Application::ApplicationContext& ctx, const bool fullscreen, const bool activated,
 	const std::string& appId
@@ -287,8 +264,8 @@ void WaylandFullScreenDetector::reset () { }
 __attribute__ ((constructor)) void registerWaylandFullscreenDetector () {
     sVideoFactories.registerFullscreenDetector (
 	"wayland", [] (ApplicationContext& context, VideoDriver& driver) -> std::unique_ptr<FullScreenDetector> {
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
-	    if (isRunningUnderKDE ()) {
+#ifdef ENABLE_KDE_FEATURES
+	    if (WallpaperEngine::Desktop::isKDESession ()) {
 		auto detector = std::make_unique<KDEWaylandFullScreenDetector> (context);
 		if (!detector->isInitialized ()) {
 		    return std::make_unique<WaylandFullScreenDetector> (context);

+ 2 - 0
src/WallpaperEngine/Render/Drivers/GLFWOpenGLDriver.cpp

@@ -144,9 +144,11 @@ void* GLFWOpenGLDriver::getProcAddress (const char* name) const {
 }
 
 void* GLFWOpenGLDriver::getX11Display () const {
+#if GLFW_VERSION_MAJOR > 3 || (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR >= 4)
     if (glfwGetPlatform () != GLFW_PLATFORM_X11) {
 	return nullptr;
     }
+#endif
 
     return glfwGetX11Display ();
 }

+ 36 - 15
src/WallpaperEngine/Render/Drivers/HeadlessOpenGLDriver.cpp

@@ -4,8 +4,8 @@
 #include "WallpaperEngine/Render/Drivers/Output/GLFWWindowOutput.h"
 
 #include <EGL/eglext.h>
-#include <cstdlib>
 #include <algorithm>
+#include <cstdlib>
 #include <cstring>
 #include <filesystem>
 #include <fstream>
@@ -62,8 +62,10 @@ EGLDisplay openDisplay () {
 	}
     }
 
-    sLog.exception ("No usable GPU found for headless rendering", wanted ? " (LWE_HEADLESS_DEVICE=" : "",
-		    wanted ? wanted : "", wanted ? ")" : "");
+    sLog.exception (
+	"No usable GPU found for headless rendering", wanted ? " (LWE_HEADLESS_DEVICE=" : "", wanted ? wanted : "",
+	wanted ? ")" : ""
+    );
     return EGL_NO_DISPLAY;
 }
 } // namespace
@@ -72,11 +74,23 @@ HeadlessOpenGLDriver::HeadlessOpenGLDriver (ApplicationContext& context, Wallpap
     VideoDriver (app, m_mouseInput), m_context (context), m_mouseInput (*this) {
     this->m_display = openDisplay ();
 
-    const EGLint configAttributes[] = {
-	EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
-	EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
-	EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, EGL_NONE
-    };
+    const EGLint configAttributes[] = { EGL_SURFACE_TYPE,
+					EGL_PBUFFER_BIT,
+					EGL_RENDERABLE_TYPE,
+					EGL_OPENGL_BIT,
+					EGL_RED_SIZE,
+					8,
+					EGL_GREEN_SIZE,
+					8,
+					EGL_BLUE_SIZE,
+					8,
+					EGL_ALPHA_SIZE,
+					8,
+					EGL_DEPTH_SIZE,
+					24,
+					EGL_STENCIL_SIZE,
+					8,
+					EGL_NONE };
     EGLint configs = 0;
 
     if (!eglChooseConfig (this->m_display, configAttributes, &this->m_config, 1, &configs) || configs == 0) {
@@ -85,11 +99,15 @@ HeadlessOpenGLDriver::HeadlessOpenGLDriver (ApplicationContext& context, Wallpap
 
     eglBindAPI (EGL_OPENGL_API);
 
-    const EGLint contextAttributes[] = {
-	EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3,
-	EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
-	EGL_CONTEXT_OPENGL_DEBUG, EGL_TRUE, EGL_NONE
-    };
+    const EGLint contextAttributes[] = { EGL_CONTEXT_MAJOR_VERSION,
+					 3,
+					 EGL_CONTEXT_MINOR_VERSION,
+					 3,
+					 EGL_CONTEXT_OPENGL_PROFILE_MASK,
+					 EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
+					 EGL_CONTEXT_OPENGL_DEBUG,
+					 EGL_TRUE,
+					 EGL_NONE };
     this->m_eglContext = eglCreateContext (this->m_display, this->m_config, EGL_NO_CONTEXT, contextAttributes);
 
     if (this->m_eglContext == EGL_NO_CONTEXT) {
@@ -153,7 +171,9 @@ void HeadlessOpenGLDriver::resizeWindow (glm::ivec2 size) {
     this->createSurface ();
 }
 
-void HeadlessOpenGLDriver::resizeWindow (glm::ivec4 sizeandpos) { this->resizeWindow (glm::ivec2 (sizeandpos.z, sizeandpos.w)); }
+void HeadlessOpenGLDriver::resizeWindow (glm::ivec4 sizeandpos) {
+    this->resizeWindow (glm::ivec2 (sizeandpos.z, sizeandpos.w));
+}
 
 void HeadlessOpenGLDriver::showWindow () { }
 
@@ -189,7 +209,8 @@ void* HeadlessOpenGLDriver::getProcAddress (const char* name) const {
 }
 
 __attribute__ ((constructor)) void registerHeadlessOpenGLDriver () {
-    const auto create = [] (ApplicationContext& context, WallpaperApplication& application) -> std::unique_ptr<VideoDriver> {
+    const auto create
+	= [] (ApplicationContext& context, WallpaperApplication& application) -> std::unique_ptr<VideoDriver> {
 	return std::make_unique<HeadlessOpenGLDriver> (context, application);
     };
 

+ 5 - 5
src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.cpp

@@ -11,7 +11,7 @@ extern "C" {
 #include "wlr-layer-shell-unstable-v1-protocol.h"
 #include "xdg-output-unstable-v1-protocol.h"
 #include "xdg-shell-protocol.h"
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 #include "plasma-shell-protocol.h"
 #endif
 }
@@ -195,7 +195,8 @@ static void descriptionInfoTransferNamed (void* data, wp_image_description_info_
     static_cast<WaylandOutputViewport*> (data)->pendingDescription.tf = tf;
 }
 
-static void descriptionInfoLuminances (void* data, wp_image_description_info_v1*, uint32_t, uint32_t max, uint32_t reference) {
+static void
+descriptionInfoLuminances (void* data, wp_image_description_info_v1*, uint32_t, uint32_t max, uint32_t reference) {
     auto& description = static_cast<WaylandOutputViewport*> (data)->pendingDescription;
 
     description.maxLuminance = max;
@@ -306,8 +307,7 @@ void WaylandOutputViewport::setupLS () {
     }
 
     layerSurface = zwlr_layer_shell_v1_get_layer_surface (
-	m_driver->getWaylandContext ()->layerShell, surface, output, wlrLayer,
-	"desktop"
+	m_driver->getWaylandContext ()->layerShell, surface, output, wlrLayer, "desktop"
     );
 
     if (!layerSurface) {
@@ -338,7 +338,7 @@ void WaylandOutputViewport::setupLS () {
     wl_surface_set_input_region (surface, region);
     wl_region_destroy (region);
 
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
     if (m_driver->getWaylandContext ()->plasmaShell) {
 	plasmaSurface = org_kde_plasma_shell_get_surface (m_driver->getWaylandContext ()->plasmaShell, surface);
 	if (plasmaSurface) {

+ 2 - 2
src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.h

@@ -20,7 +20,7 @@ struct zxdg_output_v1;
 struct zxdg_output_manager_v1;
 struct wp_color_management_output_v1;
 struct wp_color_management_surface_v1;
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 struct org_kde_plasma_surface;
 #endif
 
@@ -49,7 +49,7 @@ namespace Output {
 	wl_surface* surface = nullptr;
 	zwlr_layer_surface_v1* layerSurface = nullptr;
 	wl_callback* frameCallback = nullptr;
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 	org_kde_plasma_surface* plasmaSurface = nullptr;
 #endif
 	glm::dvec2 mousePos = { 0, 0 };

+ 23 - 11
src/WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.cpp

@@ -11,7 +11,7 @@ extern "C" {
 #include "wlr-layer-shell-unstable-v1-protocol.h"
 #include "xdg-output-unstable-v1-protocol.h"
 #include "xdg-shell-protocol.h"
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 #include "plasma-shell-protocol.h"
 #endif
 #include <linux/input-event-codes.h>
@@ -176,11 +176,14 @@ handleGlobal (void* data, struct wl_registry* registry, uint32_t name, const cha
 	driver->getWaylandContext ()->xdgOutputManager = static_cast<zxdg_output_manager_v1*> (
 	    wl_registry_bind (registry, name, &zxdg_output_manager_v1_interface, std::min (version, 3u))
 	);
-    } else if (strcmp (interface, wp_color_manager_v1_interface.name) == 0 && driver->getApp ().getContext ().settings.render.hdr) {
+    } else if (
+	strcmp (interface, wp_color_manager_v1_interface.name) == 0
+	&& driver->getApp ().getContext ().settings.render.hdr
+    ) {
 	driver->getWaylandContext ()->colorManager
 	    = static_cast<wp_color_manager_v1*> (wl_registry_bind (registry, name, &wp_color_manager_v1_interface, 1));
 	wp_color_manager_v1_add_listener (driver->getWaylandContext ()->colorManager, &colorManagerListener, driver);
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
     } else if (strcmp (interface, org_kde_plasma_shell_interface.name) == 0) {
 	driver->getWaylandContext ()->plasmaShell = static_cast<org_kde_plasma_shell*> (
 	    wl_registry_bind (registry, name, &org_kde_plasma_shell_interface, std::min (version, 8u))
@@ -194,9 +197,8 @@ static void handleGlobalRemoved (void* data, struct wl_registry* registry, uint3
 
     // a monitor being unplugged/disabled removes its global; leaving the viewport around leaks its
     // layer-shell/EGL surfaces for the rest of this client's connection since nothing else disconnects it
-    const auto it = std::ranges::find_if (
-	driver->m_screens, [id] (const auto* viewport) { return viewport->waylandName == id; }
-    );
+    const auto it
+	= std::ranges::find_if (driver->m_screens, [id] (const auto* viewport) { return viewport->waylandName == id; });
 
     if (it != driver->m_screens.end ()) {
 	driver->onLayerClose (*it);
@@ -262,8 +264,19 @@ void WaylandOpenGLDriver::initEGL () {
     // PQ in 8 bits bands visibly, so HDR outputs want a 10 bit buffer (fine for the SDR ones too)
     if (this->isHDRAvailable ()) {
 	const EGLint HDR_CONFIG_ATTRIBUTES[] = {
-	    EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RED_SIZE,	    10, EGL_GREEN_SIZE, 10, EGL_BLUE_SIZE, 10,
-	    EGL_SAMPLES,      4,	      EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE,
+	    EGL_SURFACE_TYPE,
+	    EGL_WINDOW_BIT,
+	    EGL_RED_SIZE,
+	    10,
+	    EGL_GREEN_SIZE,
+	    10,
+	    EGL_BLUE_SIZE,
+	    10,
+	    EGL_SAMPLES,
+	    4,
+	    EGL_RENDERABLE_TYPE,
+	    EGL_OPENGL_BIT,
+	    EGL_NONE,
 	};
 	EGLConfig configs[64];
 	EGLint count = 0;
@@ -359,7 +372,7 @@ void WaylandOpenGLDriver::onLayerClose (Output::WaylandOutputViewport* viewport)
 	zwlr_layer_surface_v1_destroy (viewport->layerSurface);
     }
 
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
     if (viewport->plasmaSurface) {
 	org_kde_plasma_surface_destroy (viewport->plasmaSurface);
 	viewport->plasmaSurface = nullptr;
@@ -536,8 +549,7 @@ void WaylandOpenGLDriver::initGLEW () {
 	// on Wayland+EGL, GLEW may report GLEW_ERROR_NO_GLX_DISPLAY or a null string when the build
 	// also includes X11 but no GLX display is present; non-fatal, the EGL context is already current
 	if (result == GLEW_ERROR_NO_GLX_DISPLAY || error == nullptr) {
-	    sLog.out ("Failed to initialize GLEW, but continuing with EGL context: ",
-		      error ? error : "No GLX display");
+	    sLog.out ("Failed to initialize GLEW, but continuing with EGL context: ", error ? error : "No GLX display");
 	} else {
 	    sLog.error ("Failed to initialize GLEW: ", error);
 	    sLog.exception ("Cannot continue...");

+ 4 - 3
src/WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.h

@@ -27,7 +27,7 @@ struct zwlr_layer_surface_v1;
 struct zxdg_output_manager_v1;
 struct wp_color_manager_v1;
 struct wp_image_description_v1;
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 struct org_kde_plasma_shell;
 #endif
 
@@ -63,7 +63,7 @@ public:
 	zxdg_output_manager_v1* xdgOutputManager = nullptr;
 	/** Only bound with --hdr */
 	wp_color_manager_v1* colorManager = nullptr;
-#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#ifdef ENABLE_KDE_FEATURES
 	org_kde_plasma_shell* plasmaShell = nullptr;
 #endif
     };
@@ -91,7 +91,8 @@ public:
     [[nodiscard]] void* getProcAddress (const char* name) const override;
     [[nodiscard]] void* getWaylandDisplay () const override;
     [[nodiscard]] bool isHDRAvailable () const override;
-    /** PQ with BT.2020 primaries (and the PQ default luminances, 203 nits reference white), shared by every HDR surface */
+    /** PQ with BT.2020 primaries (and the PQ default luminances, 203 nits reference white), shared by every HDR surface
+     */
     [[nodiscard]] wp_image_description_v1* getHDRDescription () const;
 
     void onLayerClose (Output::WaylandOutputViewport*);

+ 3 - 4
src/WallpaperEngine/Render/FBOProvider.cpp

@@ -36,8 +36,8 @@ FBOProvider::FBOProvider (const FBOProvider* parent) : m_parent (parent) { }
 
 std::shared_ptr<CFBO> FBOProvider::create (const FBO& base, uint32_t flags, const glm::vec2 size) {
     return this->m_fbos[base.name] = std::make_shared<CFBO> (
-	       base.name, parseFormat (base.format), flags, base.scale, size.x / base.scale, size.y / base.scale, size.x / base.scale,
-	       size.y / base.scale
+	       base.name, parseFormat (base.format), flags, base.scale, size.x / base.scale, size.y / base.scale,
+	       size.x / base.scale, size.y / base.scale
 	   );
 }
 
@@ -46,8 +46,7 @@ std::shared_ptr<CFBO> FBOProvider::create (
     glm::vec2 textureSize, const glm::vec4& borderColor, const uint32_t mipLevels
 ) {
     return this->m_fbos[name] = std::make_shared<CFBO> (
-	       name, format, flags, scale, realSize.x, realSize.y, textureSize.x, textureSize.y,
-	       borderColor, mipLevels
+	       name, format, flags, scale, realSize.x, realSize.y, textureSize.x, textureSize.y, borderColor, mipLevels
 	   );
 }
 

+ 146 - 111
src/WallpaperEngine/Render/Objects/CImage.cpp

@@ -89,8 +89,9 @@ struct PuppetMeshBlock {
 // care about the per-vertex stride - that's resolved afterwards against whatever candidates come back,
 // since the stride isn't reliably predictable from the MDLV header version alone (see
 // resolvePuppetVertexLayout).
-std::vector<PuppetMeshBlock>
-findPuppetMeshBlockCandidates (const BinaryReader& reader, size_t markerSize, size_t mdlsOffset, size_t meshHeaderSize) {
+std::vector<PuppetMeshBlock> findPuppetMeshBlockCandidates (
+    const BinaryReader& reader, size_t markerSize, size_t mdlsOffset, size_t meshHeaderSize
+) {
     std::vector<PuppetMeshBlock> candidates;
 
     for (size_t offset = markerSize; offset + meshHeaderSize + sizeof (uint32_t) < mdlsOffset; offset++) {
@@ -112,7 +113,8 @@ findPuppetMeshBlockCandidates (const BinaryReader& reader, size_t markerSize, si
 	}
 
 	candidates.push_back (
-	    PuppetMeshBlock { .headerOffset = offset, .vertexBytes = candidateVertexBytes, .indexBytes = candidateIndexBytes }
+	    PuppetMeshBlock {
+		.headerOffset = offset, .vertexBytes = candidateVertexBytes, .indexBytes = candidateIndexBytes }
 	);
     }
 
@@ -134,8 +136,9 @@ struct PuppetMeshData {
     std::vector<GLushort> indices;
 };
 
-std::optional<PuppetMeshData>
-readPuppetMeshData (const BinaryReader& reader, const PuppetMeshBlock& block, size_t meshHeaderSize, size_t vertexStride) {
+std::optional<PuppetMeshData> readPuppetMeshData (
+    const BinaryReader& reader, const PuppetMeshBlock& block, size_t meshHeaderSize, size_t vertexStride
+) {
     if (block.vertexBytes % vertexStride != 0) {
 	return std::nullopt;
     }
@@ -282,9 +285,8 @@ std::optional<double> scorePuppetMeshCoherence (const PuppetMeshData& data) {
 // bone influences a given puppet part carries. So instead of a fixed version->stride table, every
 // plausible stride is tried against every candidate mesh header found in the file, and whichever
 // combination produces the most coherent triangulated mesh wins.
-std::optional<PuppetVertexLayout> resolvePuppetVertexLayout (
-    const BinaryReader& reader, size_t markerSize, size_t mdlsOffset, size_t meshHeaderSize
-) {
+std::optional<PuppetVertexLayout>
+resolvePuppetVertexLayout (const BinaryReader& reader, size_t markerSize, size_t mdlsOffset, size_t meshHeaderSize) {
     constexpr size_t minVertexStride = 20; // position (12 bytes) + uv (8 bytes), no bone data at all
     constexpr size_t maxVertexStride = 256; // generous upper bound, comfortably covers multi-bone rigs
     constexpr size_t strideStep = 4; // every field observed so far is a 4-byte float/uint
@@ -311,7 +313,9 @@ std::optional<PuppetVertexLayout> resolvePuppetVertexLayout (
 	    }
 
 	    bestScore = *score;
-	    best = PuppetVertexLayout { .block = block, .vertexStride = stride, .uvOffset = stride - sizeof (GLfloat) * 2 };
+	    best = PuppetVertexLayout { .block = block,
+					.vertexStride = stride,
+					.uvOffset = stride - sizeof (GLfloat) * 2 };
 	}
     }
 
@@ -353,8 +357,8 @@ PuppetBoneSet parsePuppetBones (const BinaryReader& reader, size_t mdlsOffset) {
 
     for (uint32_t i = 0; i < boneCount; i++) {
 	// records start with a null-terminated name, empty for most rigs
-	(void) reader.nextNullTerminatedString ();
-	(void) reader.nextUInt32 (); // type, unused
+	(void)reader.nextNullTerminatedString ();
+	(void)reader.nextUInt32 (); // type, unused
 	const int parent = reader.nextInt ();
 	const uint32_t matrixBytes = reader.nextUInt32 ();
 
@@ -376,8 +380,8 @@ PuppetBoneSet parsePuppetBones (const BinaryReader& reader, size_t mdlsOffset) {
 	    constexpr uint32_t maxPlausibleMatrixBytes = 4096;
 	    if (matrixBytes > maxPlausibleMatrixBytes) {
 		sLog.error (
-		    "Puppet bone ", i, " has an implausible matrix byte count (", matrixBytes,
-		    "), stopping here (", result.bones.size (), " bone(s) kept)"
+		    "Puppet bone ", i, " has an implausible matrix byte count (", matrixBytes, "), stopping here (",
+		    result.bones.size (), " bone(s) kept)"
 		);
 		break;
 	    }
@@ -385,7 +389,7 @@ PuppetBoneSet parsePuppetBones (const BinaryReader& reader, size_t mdlsOffset) {
 	}
 
 	// trailing per-bone string, jiggle/physics JSON for some rigs
-	(void) reader.nextNullTerminatedString ();
+	(void)reader.nextNullTerminatedString ();
 
 	result.bones.push_back (PuppetBone { .parent = parent, .bindLocal = bindLocal });
     }
@@ -423,7 +427,8 @@ void resolveBoneWorldTransform (
     resolved[index] = true;
 }
 
-std::vector<glm::mat4> composeBoneWorldTransforms (const std::vector<int>& parents, const std::vector<glm::mat4>& locals) {
+std::vector<glm::mat4>
+composeBoneWorldTransforms (const std::vector<int>& parents, const std::vector<glm::mat4>& locals) {
     std::vector<glm::mat4> world (locals.size ());
     std::vector<bool> resolved (locals.size (), false);
     std::vector<bool> visiting (locals.size (), false);
@@ -444,7 +449,8 @@ struct PuppetAttachmentPointSet {
 // scene.json's "attachment": "orb" - see docs/rendering/MDL_FILES.md). Stops - keeping whatever
 // points parsed cleanly so far - the moment an entry looks implausible, since only two real point
 // names have been confirmed against real data and the tail of this section isn't fully understood.
-PuppetAttachmentPointSet parsePuppetAttachmentPoints (const BinaryReader& reader, size_t mdatOffset, uint32_t boneCount) {
+PuppetAttachmentPointSet
+parsePuppetAttachmentPoints (const BinaryReader& reader, size_t mdatOffset, uint32_t boneCount) {
     reader.base ().seekg (static_cast<std::streamoff> (mdatOffset), std::ios::beg);
 
     char header[9];
@@ -564,10 +570,10 @@ std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
     char header[9];
     reader.next (header, sizeof (header));
 
-    (void) reader.nextUInt32 (); // total content size, unused
+    (void)reader.nextUInt32 (); // total content size, unused
     const uint32_t clipCount = reader.nextUInt32 ();
-    (void) reader.nextUInt32 (); // ambiguous animation id when there's more than one clip; matched by name instead
-    (void) reader.nextUInt32 (); // unused, always 0 in every sample seen
+    (void)reader.nextUInt32 (); // ambiguous animation id when there's more than one clip; matched by name instead
+    (void)reader.nextUInt32 (); // unused, always 0 in every sample seen
 
     // this whole section is only trustworthy insofar as the MDLS "mdlaOffset" field that got us here
     // actually landed on a real MDLA layout for this MDLV sub-format - it's only been confirmed against
@@ -575,7 +581,8 @@ std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
     constexpr uint32_t maxPlausibleClipCount = 64;
     if (clipCount > maxPlausibleClipCount) {
 	sLog.error (
-	    "Puppet animation clip count (", clipCount, ") looks implausible, assuming this puppet's MDLA layout wasn't "
+	    "Puppet animation clip count (", clipCount,
+	    ") looks implausible, assuming this puppet's MDLA layout wasn't "
 	    "recognized and skipping animation entirely"
 	);
 	return {};
@@ -590,7 +597,7 @@ std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
 	clip.mode = reader.nextNullTerminatedString ();
 	clip.fps = reader.nextFloat ();
 	clip.frameCount = reader.nextUInt32 ();
-	(void) reader.nextUInt32 (); // unused, always 0 in every sample seen
+	(void)reader.nextUInt32 (); // unused, always 0 in every sample seen
 	const uint32_t boneCount = reader.nextUInt32 ();
 
 	constexpr uint32_t maxPlausibleFrameCount = 100000;
@@ -606,7 +613,7 @@ std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
 	clip.boneTracks.resize (boneCount);
 
 	for (uint32_t boneIndex = 0; boneIndex < boneCount; boneIndex++) {
-	    (void) reader.nextUInt32 (); // separator, always 0 in every sample seen
+	    (void)reader.nextUInt32 (); // separator, always 0 in every sample seen
 	    const uint32_t trackBytes = reader.nextUInt32 ();
 	    const uint32_t sampleCount = clip.frameCount + 1;
 	    const uint32_t expectedBytes = sampleCount * 9 * sizeof (float);
@@ -636,7 +643,8 @@ std::vector<PuppetAnimationClip> parsePuppetAnimationClips (
 
 	if (clipIndex + 1 < clipCount) {
 	    const auto pos = static_cast<size_t> (reader.base ().tellg ());
-	    const auto next = findNextPuppetClipHeader (data, pos, std::min (pos + 16384, data.size ()), expectedBoneCount);
+	    const auto next
+		= findNextPuppetClipHeader (data, pos, std::min (pos + 16384, data.size ()), expectedBoneCount);
 	    if (!next.has_value ()) {
 		sLog.error ("Could not resynchronize puppet animation data after clip ", clips.back ().name);
 		break;
@@ -776,13 +784,14 @@ CImage::ResolvedTransform CImage::resolveTransform (const Object& object) const
 		    if (!this->m_attachmentDiagnosticLogged.contains (chain[i]->id)) {
 			this->m_attachmentDiagnosticLogged.insert (chain[i]->id);
 			sLog.out (
-			    "Attachment resolve for ", chain[i]->name, " (", chain[i]->id, "): point=",
-			    *chain[i]->attachment, " meshPosition=(", meshTransform->position.x, ",",
+			    "Attachment resolve for ", chain[i]->name, " (", chain[i]->id,
+			    "): point=", *chain[i]->attachment, " meshPosition=(", meshTransform->position.x, ",",
 			    meshTransform->position.y, ") boneAngleDeg=", glm::degrees (meshTransform->angle),
 			    " boneScale=(", meshTransform->scale.x, ",", meshTransform->scale.y, ") parentOrigin=(",
 			    resolved.origin.x, ",", resolved.origin.y, ") parentScale=", resolved.scale.x,
-			    " anchorOrigin=(", anchorOrigin.x, ",", anchorOrigin.y, ") anchorAngleDeg=",
-			    glm::degrees (anchorAngle), " restAngleDeg=", glm::degrees (meshTransform->restAngle)
+			    " anchorOrigin=(", anchorOrigin.x, ",", anchorOrigin.y,
+			    ") anchorAngleDeg=", glm::degrees (anchorAngle),
+			    " restAngleDeg=", glm::degrees (meshTransform->restAngle)
 			);
 		    }
 		}
@@ -864,8 +873,10 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
     // declared one, or the scene's for project layers. fullscreen still wins over it
     if (this->getImage ().model->autosize && this->getImage ().model->projectlayer) {
 	size = { scene_width, scene_height };
-    } else if (this->getImage ().model->autosize && !placeholderTexture
-	&& std::dynamic_pointer_cast<const CFBO> (this->m_texture) == nullptr) {
+    } else if (
+	this->getImage ().model->autosize && !placeholderTexture
+	&& std::dynamic_pointer_cast<const CFBO> (this->m_texture) == nullptr
+    ) {
 	size.x = static_cast<float> (this->m_texture->getRealWidth ());
 	size.y = static_cast<float> (this->m_texture->getRealHeight ());
     }
@@ -924,8 +935,7 @@ CImage::CImage (Wallpapers::CScene& scene, const Image& image) :
 	    / static_cast<float> (this->getTexture ()->getTextureWidth (0));
 	height = static_cast<float> (this->getTexture ()->getRealHeight ())
 	    / static_cast<float> (this->getTexture ()->getTextureHeight (0));
-    }
-    else if (
+    } else if (
 	this->getTexture () != nullptr
 	&& (this->getTexture ()->getTextureWidth (0) != this->getTexture ()->getRealWidth ()
 	    || this->getTexture ()->getTextureHeight (0) != this->getTexture ()->getRealHeight ())
@@ -1130,7 +1140,9 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 
 	glGenBuffers (1, &this->m_puppetTexCoord);
 	glBindBuffer (GL_ARRAY_BUFFER, this->m_puppetTexCoord);
-	glBufferData (GL_ARRAY_BUFFER, mesh->texcoords.size () * sizeof (GLfloat), mesh->texcoords.data (), GL_STATIC_DRAW);
+	glBufferData (
+	    GL_ARRAY_BUFFER, mesh->texcoords.size () * sizeof (GLfloat), mesh->texcoords.data (), GL_STATIC_DRAW
+	);
 
 	glGenBuffers (1, &this->m_puppetIndices);
 	glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, this->m_puppetIndices);
@@ -1141,8 +1153,9 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 	this->m_puppetIndexCount = static_cast<GLsizei> (mesh->indices.size ());
 
 	sLog.out (
-	    "Loaded puppet mesh ", *this->getImage ().model->puppet, " version=", puppetVersion, " stride=",
-	    layout->vertexStride, " vertices=", this->m_puppetRawPositions.size () / 3, " indices=", this->m_puppetIndexCount
+	    "Loaded puppet mesh ", *this->getImage ().model->puppet, " version=", puppetVersion,
+	    " stride=", layout->vertexStride, " vertices=", this->m_puppetRawPositions.size () / 3,
+	    " indices=", this->m_puppetIndexCount
 	);
 
 	this->m_puppetBones.clear ();
@@ -1193,8 +1206,9 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 		bool mdlaOffsetLooksValid = magicAt (mdlaOffset, mdlaMagic);
 
 		if (!mdlaOffsetLooksValid && magicAt (mdlaOffset, mdatMagic)) {
-		    auto attachmentSet
-			= parsePuppetAttachmentPoints (reader, mdlaOffset, static_cast<uint32_t> (this->m_puppetBones.size ()));
+		    auto attachmentSet = parsePuppetAttachmentPoints (
+			reader, mdlaOffset, static_cast<uint32_t> (this->m_puppetBones.size ())
+		    );
 		    this->m_puppetAttachmentPoints = std::move (attachmentSet.points);
 		    mdlaOffset = attachmentSet.mdlaOffset;
 		    mdlaOffsetLooksValid = magicAt (mdlaOffset, mdlaMagic);
@@ -1216,9 +1230,10 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 		// movement, ...) - collect every matching one here; updatePuppetSkinning blend-weights
 		// them together per bone using each layer's own "blend" setting.
 		for (const auto& layer : this->getImage ().animationLayers) {
-		    auto match = std::find_if (clips.begin (), clips.end (), [&layer] (const PuppetAnimationClip& clip) {
-			return clip.name == layer->name;
-		    });
+		    auto match
+			= std::find_if (clips.begin (), clips.end (), [&layer] (const PuppetAnimationClip& clip) {
+			      return clip.name == layer->name;
+			  });
 		    if (match == clips.end ()) {
 			continue;
 		    }
@@ -1228,14 +1243,15 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 		    );
 		}
 
-		if (this->m_puppetActiveAnimations.empty () && !clips.empty () && !this->getImage ().animationLayers.empty ()) {
+		if (this->m_puppetActiveAnimations.empty () && !clips.empty ()
+		    && !this->getImage ().animationLayers.empty ()) {
 		    sLog.out (
-			"No puppet animation clip name matched an animation layer for ", *this->getImage ().model->puppet,
-			", defaulting to the first clip (", clips.front ().name, ")"
+			"No puppet animation clip name matched an animation layer for ",
+			*this->getImage ().model->puppet, ", defaulting to the first clip (", clips.front ().name, ")"
 		    );
 		    this->m_puppetActiveAnimations.push_back (
-			PuppetActiveAnimation {
-			    .clip = std::move (clips.front ()), .layer = this->getImage ().animationLayers.front ().get () }
+			PuppetActiveAnimation { .clip = std::move (clips.front ()),
+						.layer = this->getImage ().animationLayers.front ().get () }
 		    );
 		}
 
@@ -1258,8 +1274,8 @@ bool CImage::loadPuppetMesh (const glm::vec2& size) {
 		}
 	    } catch (const std::exception& ex) {
 		sLog.error (
-		    "Could not load puppet skeleton/animation from ", *this->getImage ().model->puppet, ": ", ex.what (),
-		    " (falling back to the static bind pose)"
+		    "Could not load puppet skeleton/animation from ", *this->getImage ().model->puppet, ": ",
+		    ex.what (), " (falling back to the static bind pose)"
 		);
 		this->m_puppetBones.clear ();
 		this->m_puppetActiveAnimations.clear ();
@@ -1279,8 +1295,9 @@ void CImage::updatePuppetPositionBuffer (const glm::vec2& size) {
     // once an animation clip is driving the mesh, its skinned output replaces the static bind pose
     // as the source of truth - the bind pose (m_puppetRawPositions) is kept around unchanged, since
     // skinning is recomputed from it fresh every frame, not accumulated from the previous frame
-    const auto& source
-	= !this->m_puppetActiveAnimations.empty () && !this->m_puppetSkinnedPositions.empty () ? this->m_puppetSkinnedPositions : this->m_puppetRawPositions;
+    const auto& source = !this->m_puppetActiveAnimations.empty () && !this->m_puppetSkinnedPositions.empty ()
+	? this->m_puppetSkinnedPositions
+	: this->m_puppetRawPositions;
 
     if (source.empty ()) {
 	return;
@@ -1331,10 +1348,10 @@ void CImage::updatePuppetPositionBuffer (const glm::vec2& size) {
 	    boundsMax = glm::max (boundsMax, p);
 	}
 	sLog.out (
-	    "Puppet position bake for ", this->getImage ().name, " (", this->getId (), "): bakeScenePosition=",
-	    bakeScenePosition, " passes=", this->m_passes.size (), " vertexCount=", positions.size () / 3,
-	    " boundsMin=(", boundsMin.x, ",", boundsMin.y, ",", boundsMin.z, ") boundsMax=(", boundsMax.x, ",",
-	    boundsMax.y, ",", boundsMax.z, ")"
+	    "Puppet position bake for ", this->getImage ().name, " (", this->getId (),
+	    "): bakeScenePosition=", bakeScenePosition, " passes=", this->m_passes.size (),
+	    " vertexCount=", positions.size () / 3, " boundsMin=(", boundsMin.x, ",", boundsMin.y, ",", boundsMin.z,
+	    ") boundsMax=(", boundsMax.x, ",", boundsMax.y, ",", boundsMax.z, ")"
 	);
     }
 
@@ -1399,9 +1416,13 @@ void CImage::updatePuppetSkinning () {
 	}
 
 	const auto frame0 = std::min (static_cast<uint32_t> (frameFloat), clip.frameCount);
-	samples.push_back (ActiveLayerSample {
-	    .clip = &clip, .frame0 = frame0, .frame1 = std::min (frame0 + 1, clip.frameCount),
-	    .alpha = frameFloat - static_cast<float> (frame0), .blend = candidate.layer->blend->value->getFloat () });
+	samples.push_back (
+	    ActiveLayerSample { .clip = &clip,
+				.frame0 = frame0,
+				.frame1 = std::min (frame0 + 1, clip.frameCount),
+				.alpha = frameFloat - static_cast<float> (frame0),
+				.blend = candidate.layer->blend->value->getFloat () }
+	);
     }
 
     if (samples.empty ()) {
@@ -1429,11 +1450,14 @@ void CImage::updatePuppetSkinning () {
 	    }
 
 	    const auto& track = sample.clip->boneTracks[i];
-	    const glm::vec3 trackPosition = lerp (track[sample.frame0].position, track[sample.frame1].position, sample.alpha);
-	    const glm::vec3 trackRotation = lerp (track[sample.frame0].rotation, track[sample.frame1].rotation, sample.alpha);
+	    const glm::vec3 trackPosition
+		= lerp (track[sample.frame0].position, track[sample.frame1].position, sample.alpha);
+	    const glm::vec3 trackRotation
+		= lerp (track[sample.frame0].rotation, track[sample.frame1].rotation, sample.alpha);
 	    const glm::vec3 trackScale = lerp (track[sample.frame0].scale, track[sample.frame1].scale, sample.alpha);
 
-	    // deltas are measured from the clip's own first frame, some rigs carry a static track pose far from bindLocal
+	    // deltas are measured from the clip's own first frame, some rigs carry a static track pose far from
+	    // bindLocal
 	    const glm::vec3 restPosition = track[0].position;
 	    if (!positionBased) {
 		position = restPosition;
@@ -1469,13 +1493,15 @@ void CImage::updatePuppetSkinning () {
 
     for (size_t v = 0; v < vertexCount; v++) {
 	const glm::vec4 bindPos (
-	    this->m_puppetRawPositions[v * 3], this->m_puppetRawPositions[v * 3 + 1], this->m_puppetRawPositions[v * 3 + 2],
-	    1.0f
+	    this->m_puppetRawPositions[v * 3], this->m_puppetRawPositions[v * 3 + 1],
+	    this->m_puppetRawPositions[v * 3 + 2], 1.0f
 	);
 
 	glm::vec3 skinned (0.0f);
-	const glm::uvec4& indices = v < this->m_puppetBlendIndices.size () ? this->m_puppetBlendIndices[v] : glm::uvec4 (0);
-	const glm::vec4& weights = v < this->m_puppetBlendWeights.size () ? this->m_puppetBlendWeights[v] : glm::vec4 (0.0f);
+	const glm::uvec4& indices
+	    = v < this->m_puppetBlendIndices.size () ? this->m_puppetBlendIndices[v] : glm::uvec4 (0);
+	const glm::vec4& weights
+	    = v < this->m_puppetBlendWeights.size () ? this->m_puppetBlendWeights[v] : glm::vec4 (0.0f);
 
 	for (int influence = 0; influence < 4; influence++) {
 	    const float weight = weights[influence];
@@ -1499,7 +1525,8 @@ void CImage::updatePuppetSkinning () {
     this->updatePuppetPositionBuffer (this->m_size);
 }
 
-std::optional<CImage::AttachmentPointTransform> CImage::getAttachmentPointMeshTransform (const std::string& name) const {
+std::optional<CImage::AttachmentPointTransform>
+CImage::getAttachmentPointMeshTransform (const std::string& name) const {
     if (this->m_puppetBoneWorldAnimated.empty ()) {
 	return std::nullopt;
     }
@@ -1509,7 +1536,8 @@ std::optional<CImage::AttachmentPointTransform> CImage::getAttachmentPointMeshTr
 	[&name] (const PuppetAttachmentPoint& point) { return point.name == name; }
     );
 
-    if (it == this->m_puppetAttachmentPoints.end () || static_cast<size_t> (it->boneIndex) >= this->m_puppetBoneWorldAnimated.size ()) {
+    if (it == this->m_puppetAttachmentPoints.end ()
+	|| static_cast<size_t> (it->boneIndex) >= this->m_puppetBoneWorldAnimated.size ()) {
 	return std::nullopt;
     }
 
@@ -1521,11 +1549,11 @@ std::optional<CImage::AttachmentPointTransform> CImage::getAttachmentPointMeshTr
     // rotation by `angle` would have produced - comes out negative if the bone's matrix includes a
     // reflection (mirrored bone), instead of folding that into a bogus rotation angle
     const float scaleX = glm::length (glm::vec2 (animatedWorld[0]));
-    const glm::vec2 scale
-	= scaleX > 1e-6f ? glm::vec2 (
+    const glm::vec2 scale = scaleX > 1e-6f
+	? glm::vec2 (
 	      scaleX, (animatedWorld[0][0] * animatedWorld[1][1] - animatedWorld[0][1] * animatedWorld[1][0]) / scaleX
 	  )
-			 : glm::vec2 (scaleX, glm::length (glm::vec2 (animatedWorld[1])));
+	: glm::vec2 (scaleX, glm::length (glm::vec2 (animatedWorld[1])));
 
     const glm::mat4 bindWorld = glm::inverse (this->m_puppetBones[it->boneIndex].inverseBindWorld) * it->localTransform;
     const float restAngle = std::atan2 (bindWorld[0][1], bindWorld[0][0]);
@@ -1544,9 +1572,9 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
 	    if (!this->m_puppetDrawDiagnosticLogged) {
 		this->m_puppetDrawDiagnosticLogged = true;
 		sLog.out (
-		    "Puppet draw setup for ", this->getImage ().name, " (", this->getId (), "): programID=",
-		    pass->getProgramID (), " a_Position=", position, " a_TexCoord=", texCoord, " indexCount=",
-		    this->m_puppetIndexCount, " size=", this->m_size.x, "x", this->m_size.y
+		    "Puppet draw setup for ", this->getImage ().name, " (", this->getId (),
+		    "): programID=", pass->getProgramID (), " a_Position=", position, " a_TexCoord=", texCoord,
+		    " indexCount=", this->m_puppetIndexCount, " size=", this->m_size.x, "x", this->m_size.y
 		);
 	    }
 
@@ -1602,7 +1630,10 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
 			    fwrite (&h, sizeof (int), 1, f);
 			    fwrite (pixels.data (), 1, pixels.size (), f);
 			    fclose (f);
-			    sLog.out ("TEMP-DIAG dumped FBO contents for mikasa eye bake pass: ", w, "x", h, " to /tmp/mikasa_eye_bakepass_dump.raw");
+			    sLog.out (
+				"TEMP-DIAG dumped FBO contents for mikasa eye bake pass: ", w, "x", h,
+				" to /tmp/mikasa_eye_bakepass_dump.raw"
+			    );
 			}
 		    }
 		}
@@ -1631,14 +1662,14 @@ void CImage::setupPuppetGeometryCallback (Effects::CPass* pass) const {
 		    "Puppet draw result for ", this->getImage ().name, " (", this->getId (), "): glError=", err,
 		    " boundFBO=", boundFBO, " sceneFBO=", this->getScene ().getFBO ()->getFramebuffer (), " viewport=(",
 		    viewport[0], ",", viewport[1], ",", viewport[2], ",", viewport[3], ") boundTexture=", boundTexture,
-		    " ownTextureReady=", (this->getTexture () != nullptr && this->getTexture ()->isReady ()), " ownTextureID=",
-		    (this->getTexture () != nullptr ? this->getTexture ()->getTextureID (0) : 0), " color4=(",
-		    this->getColor4 ().r, ",", this->getColor4 ().g, ",", this->getColor4 ().b, ",", this->getColor4 ().a,
-		    ") alpha=", this->getUserAlpha (), " brightness=", this->getBrightness (), " cullEnabled=",
-		    (int) cullEnabled, " cullFaceMode=", cullFaceMode, " frontFace=", frontFace, " depthEnabled=",
-		    (int) depthEnabled, " scissorEnabled=", (int) scissorEnabled, " blendEnabled=", (int) blendEnabled,
-		    " colorMask=(", (int) colorMask[0], ",", (int) colorMask[1], ",", (int) colorMask[2], ",",
-		    (int) colorMask[3], ")"
+		    " ownTextureReady=", (this->getTexture () != nullptr && this->getTexture ()->isReady ()),
+		    " ownTextureID=", (this->getTexture () != nullptr ? this->getTexture ()->getTextureID (0) : 0),
+		    " color4=(", this->getColor4 ().r, ",", this->getColor4 ().g, ",", this->getColor4 ().b, ",",
+		    this->getColor4 ().a, ") alpha=", this->getUserAlpha (), " brightness=", this->getBrightness (),
+		    " cullEnabled=", (int)cullEnabled, " cullFaceMode=", cullFaceMode, " frontFace=", frontFace,
+		    " depthEnabled=", (int)depthEnabled, " scissorEnabled=", (int)scissorEnabled,
+		    " blendEnabled=", (int)blendEnabled, " colorMask=(", (int)colorMask[0], ",", (int)colorMask[1], ",",
+		    (int)colorMask[2], ",", (int)colorMask[3], ")"
 		);
 	    }
 	},
@@ -1664,7 +1695,7 @@ void CImage::addEffectPasses (const ImageEffect& effect) {
 	fboProvider->create (
 	    *fbo,
 	    this->m_image.model->passthrough ? (this->m_texture->getFlags () | TextureFlags_ClampUVs)
-					      : this->m_texture->getFlags (),
+					     : this->m_texture->getFlags (),
 	    this->getSize ()
 	);
     }
@@ -1708,9 +1739,9 @@ void CImage::addEffectPasses (const ImageEffect& effect) {
 
 	    const auto& config = *this->m_virtualPassess.emplace_back (std::move (virtualPass));
 
-	    this->m_passes.push_back (new CPass (
-		*this, fboProvider, config, std::nullopt, std::nullopt, (*curEffect)->target.value ()
-	    ));
+	    this->m_passes.push_back (
+		new CPass (*this, fboProvider, config, std::nullopt, std::nullopt, (*curEffect)->target.value ())
+	    );
 	} else {
 	    for (auto& pass : (*curEffect)->material.value ()->passes) {
 		const auto override = curOverride != endOverride
@@ -1720,9 +1751,7 @@ void CImage::addEffectPasses (const ImageEffect& effect) {
 		    ? *(*curEffect)->target
 		    : std::optional<std::reference_wrapper<std::string>> (std::nullopt);
 
-		this->m_passes.push_back (
-		    new CPass (*this, fboProvider, *pass, override, (*curEffect)->binds, target)
-		);
+		this->m_passes.push_back (new CPass (*this, fboProvider, *pass, override, (*curEffect)->binds, target));
 	    }
 
 	    if (curOverride != endOverride) {
@@ -1765,9 +1794,10 @@ void CImage::setup () {
 		continue;
 	    }
 
-	    const auto effectVisibility = this->getScene ().getContext ().getApp ().getContext ().resolveEffectVisibility (
-		static_cast<int> (cur->id), cur->name
-	    );
+	    const auto effectVisibility
+		= this->getScene ().getContext ().getApp ().getContext ().resolveEffectVisibility (
+		    static_cast<int> (cur->id), cur->name
+		);
 
 	    // an explicit --disable-effect/--enable-effect override wins over the scene's own visibility
 	    if (effectVisibility.has_value () && !*effectVisibility) {
@@ -1852,7 +1882,8 @@ void CImage::setup () {
 	const auto& project = this->getScene ().getScene ().project;
 	this->m_materials.colorBlending.material = MaterialParser::load (
 	    project,
-	    project.sceneVersion >= 3 ? "materials/util/effectpassthrough_4.json" : "materials/util/effectpassthrough.json"
+	    project.sceneVersion >= 3 ? "materials/util/effectpassthrough_4.json"
+				      : "materials/util/effectpassthrough.json"
 	);
 	ComboMap combos;
 
@@ -1895,20 +1926,23 @@ void CImage::setup () {
 	if (this->m_passes.size () > 1 && !hasTrailingPasses && materialPasses.size () == 1
 	    && materialPasses.front ()->constants.empty ()) {
 	    const auto& base = *materialPasses.front ();
-	    const auto& config = *this->m_virtualPassess.emplace_back (std::make_unique<MaterialPass> (MaterialPass {
-		.blending = base.blending,
-		.cullmode = base.cullmode,
-		.depthtest = base.depthtest,
-		.depthwrite = base.depthwrite,
-		.shader = base.shader,
-		.textures = {},
-		.usertextures = {},
-		.combos = base.combos,
-		.constants = {},
-	    }));
+	    const auto& config = *this->m_virtualPassess.emplace_back (
+		std::make_unique<MaterialPass> (MaterialPass {
+		    .blending = base.blending,
+		    .cullmode = base.cullmode,
+		    .depthtest = base.depthtest,
+		    .depthwrite = base.depthwrite,
+		    .shader = base.shader,
+		    .textures = {},
+		    .usertextures = {},
+		    .combos = base.combos,
+		    .constants = {},
+		})
+	    );
 
-	    this->m_puppetMeshPass
-		= new CPass (*this, std::make_shared<FBOProvider> (this), config, std::nullopt, std::nullopt, std::nullopt);
+	    this->m_puppetMeshPass = new CPass (
+		*this, std::make_shared<FBOProvider> (this), config, std::nullopt, std::nullopt, std::nullopt
+	    );
 	    this->m_passes.push_back (this->m_puppetMeshPass);
 	    this->m_puppetMeshLast = true;
 	}
@@ -2024,7 +2058,9 @@ void CImage::setupPasses () {
 
 	pass->setModelMatrix (&this->m_modelMatrix);
 	pass->setViewProjectionMatrix (&this->m_viewProjectionMatrix);
-	pass->setEffectTextureProjectionMatrix (&this->m_effectTextureProjection, &this->m_effectTextureProjectionInverse);
+	pass->setEffectTextureProjectionMatrix (
+	    &this->m_effectTextureProjection, &this->m_effectTextureProjectionInverse
+	);
 
 	writesToTarget = this->configurePassTarget (pass, drawTo, asInput, effectInput, inTargetEffectSequence);
 	// TODO: PROPERLY CHECK IF THIS IS ALL THAT'S NEEDED
@@ -2203,7 +2239,6 @@ void CImage::render () {
 	}
 
 	(*cur)->render ();
-
     }
 
     // restore alpha writes - CParticle::render() never resets glColorMask, so leaving this
@@ -2397,8 +2432,9 @@ float clampParallaxAxis (float offset, float edgeA, float edgeB, float sceneExte
     const float maxOffset = -half - low;
     const float minOffset = half - high;
 
-    if (minOffset > maxOffset)
+    if (minOffset > maxOffset) {
 	return offset;
+    }
 
     return std::clamp (offset, minOffset, maxOffset);
 }
@@ -2422,7 +2458,8 @@ void CImage::updateScreenSpacePosition () {
     }
 
     if (transform.meshPivotAngle != 0.0f && this->m_hasPuppetMesh) {
-	const auto& source = !this->m_puppetSkinnedPositions.empty () ? this->m_puppetSkinnedPositions : this->m_puppetRawPositions;
+	const auto& source
+	    = !this->m_puppetSkinnedPositions.empty () ? this->m_puppetSkinnedPositions : this->m_puppetRawPositions;
 	glm::vec2 boundsMin (std::numeric_limits<float>::max ());
 	glm::vec2 boundsMax (std::numeric_limits<float>::lowest ());
 	for (size_t i = 0; i + 2 < source.size (); i += 3) {
@@ -2547,8 +2584,7 @@ void CImage::updateEffectTextureProjection () {
     }
 
     const glm::vec2 size = this->getSize ();
-    this->m_objectSpaceProjectionInverse
-	= glm::scale (glm::mat4 (1.0f), glm::vec3 (size.x / 2.0f, size.y / 2.0f, 1.0f))
+    this->m_objectSpaceProjectionInverse = glm::scale (glm::mat4 (1.0f), glm::vec3 (size.x / 2.0f, size.y / 2.0f, 1.0f))
 	* this->m_effectTextureProjectionInverse;
 }
 
@@ -2562,8 +2598,7 @@ glm::vec2 CImage::getSize () const {
     }
 
     // compose layers sample the whole scene, but effect masks map over the layer's own size
-    if (this->getImage ().model->passthrough && this->getImage ().size.x > 0.0f
-	&& this->getImage ().size.y > 0.0f) {
+    if (this->getImage ().model->passthrough && this->getImage ().size.x > 0.0f && this->getImage ().size.y > 0.0f) {
 	return this->getImage ().size;
     }
 

+ 4 - 3
src/WallpaperEngine/Render/Objects/CImage.h

@@ -79,7 +79,8 @@ public:
 
     /** Refreshes the image's own texture plus any video a pass pulled in from a user texture slot */
     void updateTextures () const;
-    /** Whether a point in scene coordinates (origin bottom-left, y up) lies inside the layer's on-screen box, rotation ignored */
+    /** Whether a point in scene coordinates (origin bottom-left, y up) lies inside the layer's on-screen box, rotation
+     * ignored */
     [[nodiscard]] bool containsScenePoint (const glm::vec2& point) const;
     /** Center of the layer's on-screen box in scene coordinates (origin bottom-left, y up) */
     [[nodiscard]] glm::vec2 getSceneCenter () const;
@@ -120,7 +121,8 @@ public:
      * @param name A named attachment point on this puppet's rig (see PuppetAttachmentPoint)
      * @return The point's current animated transform, or nullopt if there's no such point (or no puppet mesh)
      */
-    [[nodiscard]] std::optional<AttachmentPointTransform> getAttachmentPointMeshTransform (const std::string& name) const;
+    [[nodiscard]] std::optional<AttachmentPointTransform>
+    getAttachmentPointMeshTransform (const std::string& name) const;
 
 protected:
     void setupPasses ();
@@ -149,7 +151,6 @@ private:
     Effects::CPass* m_fogPass = nullptr;
 
 public:
-
     /**
      * Computes the object's own transform (origin/scale/angle) without walking the
      * parent chain. Used as the per-node step of resolveTransform.

+ 36 - 16
src/WallpaperEngine/Render/Objects/CMesh.cpp

@@ -20,10 +20,11 @@ struct VertexComponent {
 };
 
 constexpr VertexComponent VERTEX_COMPONENTS[] = {
-    { 0x1, 12 },	 { 0x10000, 16 }, { 0x2, 12 },	  { 0x4, 16 },	  { 0x800000, 16 }, { 0x1000000, 16 }, { 0x8, 8 },
-    { 0x10, 12 },	 { 0x20, 16 },	  { 0x40, 8 },	  { 0x80, 12 },	  { 0x100, 16 },    { 0x200, 8 },      { 0x400, 12 },
-    { 0x800, 16 },	 { 0x1000, 8 },	  { 0x2000, 12 }, { 0x4000, 16 }, { 0x20000, 8 },   { 0x40000, 12 },   { 0x80000, 16 },
-    { 0x100000, 8 }, { 0x200000, 12 }, { 0x400000, 16 }, { 0x8000, 16 },
+    { 0x1, 12 },       { 0x10000, 16 }, { 0x2, 12 },      { 0x4, 16 },      { 0x800000, 16 },
+    { 0x1000000, 16 }, { 0x8, 8 },      { 0x10, 12 },     { 0x20, 16 },     { 0x40, 8 },
+    { 0x80, 12 },      { 0x100, 16 },   { 0x200, 8 },     { 0x400, 12 },    { 0x800, 16 },
+    { 0x1000, 8 },     { 0x2000, 12 },  { 0x4000, 16 },   { 0x20000, 8 },   { 0x40000, 12 },
+    { 0x80000, 16 },   { 0x100000, 8 }, { 0x200000, 12 }, { 0x400000, 16 }, { 0x8000, 16 },
 };
 
 constexpr uint32_t FORMAT_POSITION = 0x1;
@@ -86,7 +87,9 @@ public:
 	    throw std::runtime_error ("truncated model");
 	}
 
-	std::vector<char> value (this->m_data.begin () + this->m_offset, this->m_data.begin () + this->m_offset + count);
+	std::vector<char> value (
+	    this->m_data.begin () + this->m_offset, this->m_data.begin () + this->m_offset + count
+	);
 	this->m_offset += count;
 	return value;
     }
@@ -154,7 +157,9 @@ std::vector<MdlMesh> readMdlMeshes (const std::vector<char>& data) {
 	const bool usable = mesh.stride != 0 && mesh.vertices.size () % mesh.stride == 0;
 
 	if (!usable) {
-	    sLog.error ("Skipping mesh ", index, " with vertex format ", mesh.format, ", its stride doesn't match its data");
+	    sLog.error (
+		"Skipping mesh ", index, " with vertex format ", mesh.format, ", its stride doesn't match its data"
+	    );
 	}
 
 	// version 23 files (the 2.4 engine only reads up to 19) close every mesh with a small block: two flag bytes, an
@@ -194,8 +199,8 @@ std::vector<MdlMesh> readMdlMeshes (const std::vector<char>& data) {
 class CMesh::Part final : public CRenderable {
 public:
     Part (CMesh& owner, const Material& material, MdlMesh mesh) :
-	CObject (owner.getScene (), owner.getObject ()),
-	CRenderable (owner.getScene (), owner.getObject (), material), m_owner (owner), m_mesh (std::move (mesh)) { }
+	CObject (owner.getScene (), owner.getObject ()), CRenderable (owner.getScene (), owner.getObject (), material),
+	m_owner (owner), m_mesh (std::move (mesh)) { }
 
     ~Part () override {
 	for (const auto* pass : this->m_passes) {
@@ -240,7 +245,9 @@ public:
 	this->m_fboProvider = std::make_shared<FBOProvider> (this);
 
 	for (const auto& materialPass : this->m_material.passes) {
-	    auto* pass = new Effects::CPass (*this, this->m_fboProvider, *materialPass, std::nullopt, std::nullopt, std::nullopt);
+	    auto* pass = new Effects::CPass (
+		*this, this->m_fboProvider, *materialPass, std::nullopt, std::nullopt, std::nullopt
+	    );
 
 	    pass->setDestination (this->getScene ().getFBO ());
 	    pass->setInput (this->getTexture ());
@@ -322,15 +329,28 @@ private:
 
 	    switch (component.bit) {
 		case FORMAT_POSITION:
-		case FORMAT_POSITION4: name = "a_Position"; break;
-		case FORMAT_NORMAL: name = "a_Normal"; break;
-		case FORMAT_TANGENT: name = "a_Tangent4"; break;
-		case FORMAT_BLENDINDICES: name = "a_BlendIndices"; break;
-		case FORMAT_BLENDWEIGHTS: name = "a_BlendWeights"; break;
+		case FORMAT_POSITION4:
+		    name = "a_Position";
+		    break;
+		case FORMAT_NORMAL:
+		    name = "a_Normal";
+		    break;
+		case FORMAT_TANGENT:
+		    name = "a_Tangent4";
+		    break;
+		case FORMAT_BLENDINDICES:
+		    name = "a_BlendIndices";
+		    break;
+		case FORMAT_BLENDWEIGHTS:
+		    name = "a_BlendWeights";
+		    break;
 		case FORMAT_TEXCOORD2:
 		case FORMAT_TEXCOORD3:
-		case FORMAT_TEXCOORD4: name = "a_TexCoord"; break;
-		default: break;
+		case FORMAT_TEXCOORD4:
+		    name = "a_TexCoord";
+		    break;
+		default:
+		    break;
 	    }
 
 	    const GLint location = name != nullptr ? glGetAttribLocation (program, name) : -1;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 482 - 346
src/WallpaperEngine/Render/Objects/CParticle.cpp


+ 2 - 3
src/WallpaperEngine/Render/Objects/CParticle.h

@@ -146,9 +146,8 @@ protected:
 	glm::vec3& offset
     );
 
-    [[nodiscard]] float sampleAudio (
-	int mode, const glm::vec2& bounds, float exponent, int frequencyStart, int frequencyEnd
-    ) const;
+    [[nodiscard]] float
+    sampleAudio (int mode, const glm::vec2& bounds, float exponent, int frequencyStart, int frequencyEnd) const;
 
     InitializerFunc createColorRandomInitializer (const ColorRandomInitializer& init);
     InitializerFunc createSizeRandomInitializer (const SizeRandomInitializer& init);

+ 4 - 4
src/WallpaperEngine/Render/Objects/CSound.cpp

@@ -9,7 +9,8 @@
 using namespace WallpaperEngine::Render::Objects;
 
 CSound::CSound (Wallpapers::CScene& scene, const Sound& sound) :
-    CObject (scene, sound), m_sound (sound), m_playing (scene.getSoundPlayRequest (sound.id).value_or (!sound.startsilent.value_or (false))) {
+    CObject (scene, sound), m_sound (sound),
+    m_playing (scene.getSoundPlayRequest (sound.id).value_or (!sound.startsilent.value_or (false))) {
     if (this->getContext ().getApp ().getContext ().settings.audio.enabled) {
 	this->load ();
     }
@@ -58,9 +59,8 @@ void CSound::applyEffectiveVolume () {
     // picking which of several alternate music tracks plays) are independent inputs that have to
     // combine, not overwrite each other - otherwise picking a track would undo screen muting, or
     // muting a screen would make track selection pointless.
-    const int base = this->m_screenVolumeOverride.value_or (
-	this->getContext ().getApp ().getContext ().state.audio.volume
-    );
+    const int base
+	= this->m_screenVolumeOverride.value_or (this->getContext ().getApp ().getContext ().state.audio.volume);
     const float fraction = this->m_sound.volume && this->m_sound.volume->value
 	? std::clamp (this->m_sound.volume->value->getFloat (), 0.0f, 1.0f)
 	: 1.0f;

+ 38 - 22
src/WallpaperEngine/Render/Objects/CText.cpp

@@ -170,8 +170,9 @@ float clampParallaxAxis (float offset, float edgeA, float edgeB, float sceneExte
     const float maxOffset = -half - low;
     const float minOffset = half - high;
 
-    if (minOffset > maxOffset)
+    if (minOffset > maxOffset) {
 	return offset;
+    }
 
     return std::clamp (offset, minOffset, maxOffset);
 }
@@ -283,8 +284,8 @@ void CText::setup () {
 	glGenBuffers (1, buffer);
     }
 
-    const GLfloat passSpacePosition[]
-	= { -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f };
+    const GLfloat passSpacePosition[] = { -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, 1.0f,  0.0f,
+					  1.0f,  1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f };
     const GLfloat quadTexcoords[] = { 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f };
 
     glBindBuffer (GL_ARRAY_BUFFER, m_passSpacePosition);
@@ -394,9 +395,9 @@ glm::vec2 CText::currentPadding () const {
 // when it letterboxes), so a text anchored to an edge or corner keeps its distance to what is actually visible there
 glm::vec2 CText::screenAnchorOffset () const {
     static const std::vector<std::pair<std::string, glm::ivec2>> anchors = {
-	{ "center", { 0, 0 } },	      { "top", { 0, 1 } },	      { "topright", { 1, 1 } },
-	{ "right", { 1, 0 } },	      { "bottomright", { 1, -1 } }, { "bottom", { 0, -1 } },
-	{ "bottomleft", { -1, -1 } }, { "left", { -1, 0 } },	      { "topleft", { -1, 1 } },
+	{ "center", { 0, 0 } },       { "top", { 0, 1 } },          { "topright", { 1, 1 } },
+	{ "right", { 1, 0 } },        { "bottomright", { 1, -1 } }, { "bottom", { 0, -1 } },
+	{ "bottomleft", { -1, -1 } }, { "left", { -1, 0 } },        { "topleft", { -1, 1 } },
     };
 
     const std::string& name = m_text.anchor->value->getString ();
@@ -483,17 +484,24 @@ void CText::uploadGeometry () {
 	const auto [x0, y0, x1, y1] = std::array { quad.rect.x, quad.rect.y, quad.rect.z, quad.rect.w };
 	const auto [u0, v0, u1, v1] = std::array { quad.uv.x, quad.uv.y, quad.uv.z, quad.uv.w };
 
-	positions.insert (positions.end (), { x0, y1, 0.0f, x0, y0, 0.0f, x1, y1, 0.0f,
-					      x1, y1, 0.0f, x0, y0, 0.0f, x1, y0, 0.0f });
+	positions.insert (
+	    positions.end (), { x0, y1, 0.0f, x0, y0, 0.0f, x1, y1, 0.0f, x1, y1, 0.0f, x0, y0, 0.0f, x1, y0, 0.0f }
+	);
 	texcoords.insert (texcoords.end (), { u0, v0, u0, v1, u1, v0, u1, v0, u0, v1, u1, v1 });
     }
 
     m_glyphVertexCount = static_cast<GLsizei> (m_result.quads.size () * 6);
 
     glBindBuffer (GL_ARRAY_BUFFER, m_glyphPositions);
-    glBufferData (GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (positions.size () * sizeof (GLfloat)), positions.data (), GL_DYNAMIC_DRAW);
+    glBufferData (
+	GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (positions.size () * sizeof (GLfloat)), positions.data (),
+	GL_DYNAMIC_DRAW
+    );
     glBindBuffer (GL_ARRAY_BUFFER, m_glyphTexcoords);
-    glBufferData (GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (texcoords.size () * sizeof (GLfloat)), texcoords.data (), GL_DYNAMIC_DRAW);
+    glBufferData (
+	GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (texcoords.size () * sizeof (GLfloat)), texcoords.data (),
+	GL_DYNAMIC_DRAW
+    );
 
     // the opaque background covers the text box plus padding (sub_140258050)
     const glm::vec2 padding = this->currentPadding ();
@@ -501,7 +509,8 @@ void CText::uploadGeometry () {
     const float by0 = m_result.bottom - padding.y;
     const float bx1 = m_result.maxX + padding.x;
     const float by1 = m_result.top + padding.y;
-    const GLfloat background[] = { bx0, by1, 0.0f, bx0, by0, 0.0f, bx1, by1, 0.0f, bx1, by1, 0.0f, bx0, by0, 0.0f, bx1, by0, 0.0f };
+    const GLfloat background[]
+	= { bx0, by1, 0.0f, bx0, by0, 0.0f, bx1, by1, 0.0f, bx1, by1, 0.0f, bx0, by0, 0.0f, bx1, by0, 0.0f };
 
     glBindBuffer (GL_ARRAY_BUFFER, m_backgroundPositions);
     glBufferData (GL_ARRAY_BUFFER, sizeof (background), background, GL_DYNAMIC_DRAW);
@@ -509,7 +518,8 @@ void CText::uploadGeometry () {
     // the effect buffer's composite quad, centered like the text box it holds
     const float hx = std::max (1.0f, static_cast<float> (m_passLayout.bufferSize.x)) * 0.5f;
     const float hy = std::max (1.0f, static_cast<float> (m_passLayout.bufferSize.y)) * 0.5f;
-    const GLfloat composite[] = { -hx, -hy, 0.0f, -hx, hy, 0.0f, hx, -hy, 0.0f, hx, -hy, 0.0f, -hx, hy, 0.0f, hx, hy, 0.0f };
+    const GLfloat composite[]
+	= { -hx, -hy, 0.0f, -hx, hy, 0.0f, hx, -hy, 0.0f, hx, -hy, 0.0f, -hx, hy, 0.0f, hx, hy, 0.0f };
 
     glBindBuffer (GL_ARRAY_BUFFER, m_compositePosition);
     glBufferData (GL_ARRAY_BUFFER, sizeof (composite), composite, GL_DYNAMIC_DRAW);
@@ -576,8 +586,8 @@ void CText::buildPasses () {
 	m_fontMaterial = MaterialParser::load (
 	    project, layout.msdf ? "materials/fonts/basefont_msdf.json" : "materials/fonts/basefont.json"
 	);
-	m_backgroundMaterial = layout.background ? MaterialParser::load (project, "materials/fonts/fontbackground.json")
-						 : nullptr;
+	m_backgroundMaterial
+	    = layout.background ? MaterialParser::load (project, "materials/fonts/fontbackground.json") : nullptr;
 	m_clearAlphaMaterial = layout.buffered && !layout.background
 	    ? MaterialParser::load (project, "materials/util/composelayer_clearalpha.json")
 	    : nullptr;
@@ -587,7 +597,8 @@ void CText::buildPasses () {
 		    || this->getScene ().hasHeightFog ())
 	    ? MaterialParser::load (
 		  project,
-		  project.sceneVersion >= 3 ? "materials/util/effectpassthrough_4.json" : "materials/util/effectpassthrough.json"
+		  project.sceneVersion >= 3 ? "materials/util/effectpassthrough_4.json"
+					    : "materials/util/effectpassthrough.json"
 	      )
 	    : nullptr;
     } catch (const std::exception& e) {
@@ -738,7 +749,9 @@ void CText::buildPasses () {
 			drawTo = resolved;
 			writesToTarget = true;
 		    } else {
-			sLog.error ("Text pass target FBO '", target->get (), "' could not be resolved for ", m_text.name);
+			sLog.error (
+			    "Text pass target FBO '", target->get (), "' could not be resolved for ", m_text.name
+			);
 		    }
 		}
 
@@ -782,7 +795,9 @@ void CText::buildPasses () {
 
     for (const auto& pass : finalMaterial.passes) {
 	auto* cpass = m_passthroughMaterial != nullptr
-	    ? new CPass (*this, std::make_shared<FBOProvider> (this), *pass, m_passthroughOverride, std::nullopt, std::nullopt)
+	    ? new CPass (
+		  *this, std::make_shared<FBOProvider> (this), *pass, m_passthroughOverride, std::nullopt, std::nullopt
+	      )
 	    : new CPass (*this, std::make_shared<FBOProvider> (this), *pass, std::nullopt, std::nullopt, std::nullopt);
 
 	if (m_passthroughMaterial != nullptr) {
@@ -874,7 +889,6 @@ void CText::render () {
     glm::vec3 scale = m_text.scale->value->getVec3 ();
     glm::vec3 origin = m_text.origin->value->getVec3 ();
 
-
     // texts sit under group/locator objects, same as CImage::resolveTransform
     if (m_text.parent.has_value ()) {
 	std::vector<const Object*> ancestors;
@@ -910,7 +924,8 @@ void CText::render () {
 		nodeAngle = node.as<Image> ()->angles->value->getVec3 ().z;
 	    }
 
-	    const glm::vec2 offset = rotate ({ nodeOrigin.x * parentScale.x, nodeOrigin.y * parentScale.y }, parentAngle);
+	    const glm::vec2 offset
+		= rotate ({ nodeOrigin.x * parentScale.x, nodeOrigin.y * parentScale.y }, parentAngle);
 	    parentOrigin = { parentOrigin.x + offset.x, parentOrigin.y + offset.y,
 			     parentOrigin.z + nodeOrigin.z * parentScale.z };
 	    parentScale *= nodeScale;
@@ -999,7 +1014,8 @@ void CText::render () {
     const glm::mat4 model = glm::scale (glm::translate (glm::mat4 (1.0f), gl_origin), scale);
 
     // layout space is y up, first baseline at 0; sub_140258050 centers the box: x - w/2 - min(minX, 0), y + h/2 - top
-    const glm::vec3 center = { -boxWidth * 0.5f - std::min (m_result.minX, 0.0f), boxHeight * 0.5f - m_result.top, 0.0f };
+    const glm::vec3 center
+	= { -boxWidth * 0.5f - std::min (m_result.minX, 0.0f), boxHeight * 0.5f - m_result.top, 0.0f };
 
     m_glyphSceneMatrix = viewProjection * glm::translate (glm::scale (model, glm::vec3 (1.0f, -1.0f, 1.0f)), center);
     m_glyphSceneMatrixInverse = glm::inverse (m_glyphSceneMatrix);
@@ -1047,8 +1063,8 @@ const float& CText::getAlpha () const { return m_text.alpha->value->getFloat ();
 
 const glm::vec3& CText::getColor () const {
     // brightness only scales the color in HDR scene rendering (renderer flag 0x2000)
-    m_colorCache = m_text.color->value->getVec3 ()
-	* (this->getScene ().isHDR () ? m_text.brightness->value->getFloat () : 1.0f);
+    m_colorCache
+	= m_text.color->value->getVec3 () * (this->getScene ().isHDR () ? m_text.brightness->value->getFloat () : 1.0f);
     return m_colorCache;
 }
 

+ 43 - 42
src/WallpaperEngine/Render/Objects/Effects/CPass.cpp

@@ -288,7 +288,8 @@ void CPass::setupRenderFramebuffer () const {
 
     glViewport (0, 0, this->m_drawTo->getRealWidth (), this->m_drawTo->getRealHeight ());
 
-    // the alpha source factor must be GL_ONE, GL_SRC_ALPHA squares every blended pass's alpha and compounds through chained effects
+    // the alpha source factor must be GL_ONE, GL_SRC_ALPHA squares every blended pass's alpha and compounds through
+    // chained effects
     switch (this->getBlendingMode ()) {
 	case BlendingMode_Translucent:
 	    glEnable (GL_BLEND);
@@ -303,7 +304,8 @@ void CPass::setupRenderFramebuffer () const {
 	    // the destination outright regardless of source alpha, which broke passes whose source
 	    // texture is partially transparent (e.g. unconfigured/placeholder effect textures).
 	    // Passes that always output alpha=1 render identically either way.
-	    // except into an intermediate target: blending there premultiplies RGB and darkens soft alpha edges in the final pass
+	    // except into an intermediate target: blending there premultiplies RGB and darkens soft alpha edges in the
+	    // final pass
 	    if (this->m_drawTo == this->m_renderable.getScene ().getFBO ()) {
 		glEnable (GL_BLEND);
 		glBlendFuncSeparate (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
@@ -785,7 +787,8 @@ void CPass::setGeometryCallback (
 GLuint CPass::compileShader (const char* shader, GLuint type) {
     const GLuint shaderID = glCreateShader (type);
 
-    // Mesa mis-reads a vec3 uniform followed by a float used as vec4(vec3, float), use the g_Color4 the engine already exposes
+    // Mesa mis-reads a vec3 uniform followed by a float used as vec4(vec3, float), use the g_Color4 the engine already
+    // exposes
     std::string patched;
 
     if (type == GL_FRAGMENT_SHADER) {
@@ -807,9 +810,11 @@ GLuint CPass::compileShader (const char* shader, GLuint type) {
 		size_t pos = 0;
 
 		while ((pos = patched.find (name, pos)) != std::string::npos && pos < codeEnd) {
-		    const bool wordStart = pos == 0 || !(std::isalnum (static_cast<unsigned char> (patched[pos - 1])) || patched[pos - 1] == '_');
+		    const bool wordStart = pos == 0
+			|| !(std::isalnum (static_cast<unsigned char> (patched[pos - 1])) || patched[pos - 1] == '_');
 		    const size_t end = pos + name.size ();
-		    const bool wordEnd = end >= patched.size () || !(std::isalnum (static_cast<unsigned char> (patched[end])) || patched[end] == '_');
+		    const bool wordEnd = end >= patched.size ()
+			|| !(std::isalnum (static_cast<unsigned char> (patched[end])) || patched[end] == '_');
 
 		    if (wordStart && wordEnd) {
 			return true;
@@ -927,13 +932,15 @@ void CPass::setupShaders () {
 
     if (scene.hasDistanceFog () || scene.hasHeightFog ()) {
 	const auto fogCombo = [this] () {
-	    for (const ComboMap* combos : std::initializer_list<const ComboMap*> { &this->m_override.combos, &this->m_combos }) {
+	    for (const ComboMap* combos :
+		 std::initializer_list<const ComboMap*> { &this->m_override.combos, &this->m_combos }) {
 		if (const auto it = combos->find ("FOG"); it != combos->end ()) {
 		    return it->second;
 		}
 	    }
 	    for (const auto* unit : { &this->m_shader->getFragment (), &this->m_shader->getVertex () }) {
-		if (const auto it = unit->getDiscoveredCombos ().find ("FOG"); it != unit->getDiscoveredCombos ().end ()) {
+		if (const auto it = unit->getDiscoveredCombos ().find ("FOG");
+		    it != unit->getDiscoveredCombos ().end ()) {
 		    return it->second;
 		}
 	    }
@@ -960,8 +967,10 @@ void CPass::setupShaders () {
 	this->m_xrayFullRevealPatched = patchXrayFullRevealBypass (fragment);
 
 	if (!this->m_xrayFullRevealPatched) {
-	    sLog.error ("Full xray toggle unavailable: couldn't find the expected reveal blend line in the "
-			"compiled effects/xray shader (spirv-cross output format may have changed)");
+	    sLog.error (
+		"Full xray toggle unavailable: couldn't find the expected reveal blend line in the "
+		"compiled effects/xray shader (spirv-cross output format may have changed)"
+	    );
 	}
     }
 
@@ -1144,7 +1153,8 @@ bool CPass::applyFormatCombos (const TextureMap& passTextures, const TextureMap&
 	    }
 
 	    try {
-		texture = this->getContext ().resolveTexture (*name, this->m_renderable.getScene ().getScene ().project);
+		texture
+		    = this->getContext ().resolveTexture (*name, this->m_renderable.getScene ().getScene ().project);
 	    } catch (const std::exception&) {
 		continue;
 	    }
@@ -1178,9 +1188,7 @@ void CPass::setupTextureUniforms () {
 	try {
 	    auto texture = textureName.find ("_rt_") == 0 || textureName.find ("_alias_") == 0
 		? this->resolveFBO (textureName)
-		: this->getContext ().resolveTexture (
-		    textureName, this->m_renderable.getScene ().getScene ().project
-		);
+		: this->getContext ().resolveTexture (textureName, this->m_renderable.getScene ().getScene ().project);
 
 	    // create chain entry
 	    this->m_textures[index] = std::make_shared<TextureChainEntry> (TextureChainEntry {
@@ -1189,8 +1197,8 @@ void CPass::setupTextureUniforms () {
 	    });
 	} catch (std::runtime_error& ex) {
 	    sLog.error (
-		"Cannot resolve texture '", textureName, "' (index=", index, ", object id=",
-		this->m_renderable.getId (), ") for fragment shader ", ex.what ()
+		"Cannot resolve texture '", textureName, "' (index=", index,
+		", object id=", this->m_renderable.getId (), ") for fragment shader ", ex.what ()
 	    );
 	}
     }
@@ -1199,9 +1207,7 @@ void CPass::setupTextureUniforms () {
 	try {
 	    auto texture = textureName.find ("_rt_") == 0 || textureName.find ("_alias_") == 0
 		? this->resolveFBO (textureName)
-		: this->getContext ().resolveTexture (
-		    textureName, this->m_renderable.getScene ().getScene ().project
-		);
+		: this->getContext ().resolveTexture (textureName, this->m_renderable.getScene ().getScene ().project);
 
 	    const auto it = this->m_textures.find (index);
 	    const auto chain = std::make_shared<TextureChainEntry> (TextureChainEntry {
@@ -1213,8 +1219,8 @@ void CPass::setupTextureUniforms () {
 
 	} catch (std::runtime_error& ex) {
 	    sLog.error (
-		"Cannot resolve texture '", textureName, "' (index=", index, ", object id=",
-		this->m_renderable.getId (), ") for fragment shader ", ex.what ()
+		"Cannot resolve texture '", textureName, "' (index=", index,
+		", object id=", this->m_renderable.getId (), ") for fragment shader ", ex.what ()
 	    );
 	}
     }
@@ -1223,9 +1229,7 @@ void CPass::setupTextureUniforms () {
 	try {
 	    auto texture = textureName.find ("_rt_") == 0 || textureName.find ("_alias_") == 0
 		? this->resolveFBO (textureName)
-		: this->getContext ().resolveTexture (
-		    textureName, this->m_renderable.getScene ().getScene ().project
-		);
+		: this->getContext ().resolveTexture (textureName, this->m_renderable.getScene ().getScene ().project);
 
 	    const auto it = this->m_textures.find (index);
 	    const auto chain = std::make_shared<TextureChainEntry> (TextureChainEntry {
@@ -1240,8 +1244,8 @@ void CPass::setupTextureUniforms () {
 	    }
 	} catch (std::runtime_error& ex) {
 	    sLog.error (
-		"Cannot resolve texture '", textureName, "' (index=", index, ", object id=",
-		this->m_renderable.getId (), ") for pass ", ex.what ()
+		"Cannot resolve texture '", textureName, "' (index=", index,
+		", object id=", this->m_renderable.getId (), ") for pass ", ex.what ()
 	    );
 	}
     }
@@ -1259,9 +1263,7 @@ void CPass::setupTextureUniforms () {
 	try {
 	    auto texture = textureName.find ("_rt_") == 0 || textureName.find ("_alias_") == 0
 		? this->resolveFBO (textureName)
-		: this->getContext ().resolveTexture (
-		    textureName, this->m_renderable.getScene ().getScene ().project
-		);
+		: this->getContext ().resolveTexture (textureName, this->m_renderable.getScene ().getScene ().project);
 
 	    const auto it = this->m_textures.find (index);
 	    const auto chain = std::make_shared<TextureChainEntry> (TextureChainEntry {
@@ -1275,8 +1277,8 @@ void CPass::setupTextureUniforms () {
 	    }
 	} catch (std::runtime_error& ex) {
 	    sLog.error (
-		"Cannot resolve user texture '", textureName, "' (index=", index, ", object id=",
-		this->m_renderable.getId (), ") for pass ", ex.what ()
+		"Cannot resolve user texture '", textureName, "' (index=", index,
+		", object id=", this->m_renderable.getId (), ") for pass ", ex.what ()
 	    );
 	}
     }
@@ -1286,9 +1288,7 @@ void CPass::setupTextureUniforms () {
 	try {
 	    auto texture = textureName.find ("_rt_") == 0 || textureName.find ("_alias_") == 0
 		? this->resolveFBO (textureName)
-		: this->getContext ().resolveTexture (
-		    textureName, this->m_renderable.getScene ().getScene ().project
-		);
+		: this->getContext ().resolveTexture (textureName, this->m_renderable.getScene ().getScene ().project);
 
 	    const auto it = this->m_textures.find (index);
 	    const auto chain = std::make_shared<TextureChainEntry> (TextureChainEntry {
@@ -1303,8 +1303,8 @@ void CPass::setupTextureUniforms () {
 	    }
 	} catch (std::runtime_error& ex) {
 	    sLog.error (
-		"Cannot resolve texture '", textureName, "' (index=", index, ", object id=",
-		this->m_renderable.getId (), ") for override ", ex.what ()
+		"Cannot resolve texture '", textureName, "' (index=", index,
+		", object id=", this->m_renderable.getId (), ") for override ", ex.what ()
 	    );
 	}
     }
@@ -1320,9 +1320,7 @@ void CPass::setupTextureUniforms () {
 	try {
 	    auto texture = textureName.find ("_rt_") == 0 || textureName.find ("_alias_") == 0
 		? this->resolveFBO (textureName)
-		: this->getContext ().resolveTexture (
-		    textureName, this->m_renderable.getScene ().getScene ().project
-		);
+		: this->getContext ().resolveTexture (textureName, this->m_renderable.getScene ().getScene ().project);
 
 	    const auto it = this->m_textures.find (index);
 	    const auto chain = std::make_shared<TextureChainEntry> (TextureChainEntry {
@@ -1337,8 +1335,8 @@ void CPass::setupTextureUniforms () {
 	    }
 	} catch (std::runtime_error& ex) {
 	    sLog.error (
-		"Cannot resolve user texture '", textureName, "' (index=", index, ", object id=",
-		this->m_renderable.getId (), ") for override ", ex.what ()
+		"Cannot resolve user texture '", textureName, "' (index=", index,
+		", object id=", this->m_renderable.getId (), ") for override ", ex.what ()
 	    );
 	}
     }
@@ -1379,7 +1377,8 @@ void CPass::setupTextureUniforms () {
 	this->addUniform (namestream.str (), res);
 
 	// the mip count of a mipmapped frame buffer, REFLECTION scales its roughness LOD by it
-	if (const auto fbo = std::dynamic_pointer_cast<const CFBO> (texture); fbo != nullptr && fbo->getMipLevels () > 1) {
+	if (const auto fbo = std::dynamic_pointer_cast<const CFBO> (texture);
+	    fbo != nullptr && fbo->getMipLevels () > 1) {
 	    this->addUniform (
 		"g_Texture" + std::to_string (textureIndex) + "MipMapInfo", static_cast<float> (fbo->getMipLevels ())
 	    );
@@ -1406,7 +1405,9 @@ void CPass::setupUniforms () {
     this->addUniform ("g_FogDistanceColor", &scene.getFog ().distanceColor);
     this->addUniform ("g_FogDistanceParams", &scene.getFog ().distanceParams);
     this->addUniform ("g_FogHeightColor", &scene.getFog ().heightColor);
-    this->addUniform ("g_FogHeightParams", this->m_fogWorld ? &scene.getFog ().heightParamsWorld : &scene.getFog ().heightParamsLocal);
+    this->addUniform (
+	"g_FogHeightParams", this->m_fogWorld ? &scene.getFog ().heightParamsWorld : &scene.getFog ().heightParamsLocal
+    );
     this->addUniform ("g_AltModelMatrix", &this->m_lightingModelMatrix);
     this->addUniform ("g_AltNormalModelMatrix", &this->m_lightingNormalMatrix);
     this->addUniform ("g_AltViewProjectionMatrix", &this->m_lightingViewProjectionMatrix);

+ 2 - 1
src/WallpaperEngine/Render/Objects/Effects/CPass.h

@@ -37,7 +37,8 @@ public:
     void render ();
     /** Binds the destination and clears it to transparent black without drawing anything */
     void clearDestination () const;
-    /** Advances video textures this pass pulls in from user/material slots, the image's own texture is updated by the scene */
+    /** Advances video textures this pass pulls in from user/material slots, the image's own texture is updated by the
+     * scene */
     void updatePlaybackTextures () const;
 
     void setDestination (std::shared_ptr<const CFBO> drawTo);

+ 32 - 28
src/WallpaperEngine/Render/Objects/TextLayout.cpp

@@ -240,9 +240,7 @@ bool TextLayout::addFace (std::vector<uint8_t> data, const std::string& path) {
 
     const FT_Error error = face->data.empty ()
 	? FT_New_Face (m_library, path.c_str (), 0, &face->face)
-	: FT_New_Memory_Face (
-	      m_library, face->data.data (), static_cast<FT_Long> (face->data.size ()), 0, &face->face
-	  );
+	: FT_New_Memory_Face (m_library, face->data.data (), static_cast<FT_Long> (face->data.size ()), 0, &face->face);
 
     if (error != 0) {
 	sLog.error ("TextLayout: cannot load font '", path, "' (FreeType error ", error, ")");
@@ -358,7 +356,9 @@ std::vector<TextLayout::ShapedGlyph> TextLayout::shape (const std::u32string& li
 	unsigned int count = 0;
 	const hb_glyph_info_t* infos = hb_buffer_get_glyph_infos (buffer, &count);
 	const hb_glyph_position_t* positions = hb_buffer_get_glyph_positions (buffer, nullptr);
-	const float pad = params.msdf ? static_cast<float> (std::max<int> (face.face->size->metrics.y_ppem, 1)) * 12.0f * 0.03125f : 0.0f;
+	const float pad = params.msdf
+	    ? static_cast<float> (std::max<int> (face.face->size->metrics.y_ppem, 1)) * 12.0f * 0.03125f
+	    : 0.0f;
 
 	for (unsigned int i = 0; i < count; i++) {
 	    const uint32_t glyph = infos[i].codepoint;
@@ -374,18 +374,20 @@ std::vector<TextLayout::ShapedGlyph> TextLayout::shape (const std::u32string& li
 	    const int yOffset = positions[i].y_offset >> 6;
 	    const uint32_t cluster = infos[i].cluster + static_cast<uint32_t> (run.start);
 
-	    glyphs.push_back ({
-		.key = glyphKey (run.face, glyph),
-		.cluster = cluster,
-		.codepoint = cluster < line.size () ? line[cluster] : 0,
-		.advance = static_cast<float> (positions[i].x_advance >> 6) + params.spacing.x,
-		.yAdvance = static_cast<float> (positions[i].y_advance >> 6),
-		.x0 = static_cast<float> (xOffset + bounds.x0),
-		.y0 = static_cast<float> (yOffset + bounds.y0),
-		.x1 = static_cast<float> (xOffset + bounds.x1),
-		.y1 = static_cast<float> (yOffset + bounds.y1),
-		.pad = pad,
-	    });
+	    glyphs.push_back (
+		{
+		    .key = glyphKey (run.face, glyph),
+		    .cluster = cluster,
+		    .codepoint = cluster < line.size () ? line[cluster] : 0,
+		    .advance = static_cast<float> (positions[i].x_advance >> 6) + params.spacing.x,
+		    .yAdvance = static_cast<float> (positions[i].y_advance >> 6),
+		    .x0 = static_cast<float> (xOffset + bounds.x0),
+		    .y0 = static_cast<float> (yOffset + bounds.y0),
+		    .x1 = static_cast<float> (xOffset + bounds.x1),
+		    .y1 = static_cast<float> (yOffset + bounds.y1),
+		    .pad = pad,
+		}
+	    );
 	}
 
 	hb_buffer_destroy (buffer);
@@ -445,10 +447,8 @@ const TextLayout::GlyphBox* TextLayout::glyphBox (size_t face, uint32_t glyph) {
     return &m_boxes
 		.emplace (
 		    key,
-		    GlyphBox {
-			static_cast<int> (bbox.xMin), static_cast<int> (bbox.yMin), static_cast<int> (bbox.xMax),
-			static_cast<int> (bbox.yMax)
-		    }
+		    GlyphBox { static_cast<int> (bbox.xMin), static_cast<int> (bbox.yMin), static_cast<int> (bbox.xMax),
+			       static_cast<int> (bbox.yMax) }
 		)
 		.first->second;
 }
@@ -560,7 +560,9 @@ bool TextLayout::renderMsdfGlyph (FT_Face face, uint32_t glyph, AtlasGlyph& out)
     const float range = 24.0f / scale;
     const msdfgen::Projection projection (
 	msdfgen::Vector2 (scale),
-	msdfgen::Vector2 (12.0f / scale - static_cast<float> (bbox.xMin), 12.0f / scale - static_cast<float> (bbox.yMin))
+	msdfgen::Vector2 (
+	    12.0f / scale - static_cast<float> (bbox.xMin), 12.0f / scale - static_cast<float> (bbox.yMin)
+	)
     );
     const msdfgen::SDFTransformation transformation (projection, msdfgen::DistanceMapping (msdfgen::Range (range)));
 
@@ -907,13 +909,15 @@ TextLayoutResult TextLayout::layout (const std::string& utf8, const TextLayoutPa
 		if (it != m_glyphs.end () && it->second.x >= 0) {
 		    const AtlasGlyph& entry = it->second;
 
-		    result.quads.push_back ({
-			.rect = { pen + glyph.x0 - glyph.pad + shift, lineY + glyph.y0 - glyph.pad,
-				  pen + glyph.x1 + glyph.pad + shift, lineY + glyph.y1 + glyph.pad },
-			.uv = { static_cast<float> (entry.x) / atlasSize, static_cast<float> (entry.y) / atlasSize,
-				static_cast<float> (entry.x + entry.width) / atlasSize,
-				static_cast<float> (entry.y + entry.height) / atlasSize },
-		    });
+		    result.quads.push_back (
+			{
+			    .rect = { pen + glyph.x0 - glyph.pad + shift, lineY + glyph.y0 - glyph.pad,
+				      pen + glyph.x1 + glyph.pad + shift, lineY + glyph.y1 + glyph.pad },
+			    .uv = { static_cast<float> (entry.x) / atlasSize, static_cast<float> (entry.y) / atlasSize,
+				    static_cast<float> (entry.x + entry.width) / atlasSize,
+				    static_cast<float> (entry.y + entry.height) / atlasSize },
+			}
+		    );
 		}
 
 		pen += glyph.advance;

+ 5 - 4
src/WallpaperEngine/Render/RenderContext.cpp

@@ -80,7 +80,9 @@ void RenderContext::renderWithStats (Drivers::Output::OutputViewport* viewport)
     const auto frameStart = Clock::now ();
 
     if (stats.lastFrame != Clock::time_point {}) {
-	stats.maxInterval = std::max (stats.maxInterval, std::chrono::duration<double, std::milli> (frameStart - stats.lastFrame).count ());
+	stats.maxInterval = std::max (
+	    stats.maxInterval, std::chrono::duration<double, std::milli> (frameStart - stats.lastFrame).count ()
+	);
     }
 
     stats.lastFrame = frameStart;
@@ -155,9 +157,8 @@ const Drivers::VideoDriver& RenderContext::getDriver () const { return this->m_d
 
 const Drivers::Output::Output& RenderContext::getOutput () const { return this->m_driver.getOutput (); }
 
-std::shared_ptr<const TextureProvider> RenderContext::resolveTexture (
-    const std::string& name, const Data::Model::Project& project
-) const {
+std::shared_ptr<const TextureProvider>
+RenderContext::resolveTexture (const std::string& name, const Data::Model::Project& project) const {
     return this->m_textureCache->resolve (name, project);
 }
 

+ 2 - 3
src/WallpaperEngine/Render/RenderContext.h

@@ -45,9 +45,8 @@ namespace Render {
 	[[nodiscard]] const WallpaperApplication& getApp () const;
 	[[nodiscard]] const Drivers::VideoDriver& getDriver () const;
 	[[nodiscard]] const Drivers::Output::Output& getOutput () const;
-	[[nodiscard]] std::shared_ptr<const TextureProvider> resolveTexture (
-	    const std::string& name, const Data::Model::Project& project
-	) const;
+	[[nodiscard]] std::shared_ptr<const TextureProvider>
+	resolveTexture (const std::string& name, const Data::Model::Project& project) const;
 	[[nodiscard]] const std::map<std::string, std::shared_ptr<CWallpaper>>& getWallpapers () const;
 	[[nodiscard]] Media::MediaSource& getMediaSource () const;
 

+ 6 - 8
src/WallpaperEngine/Render/Shaders/GLSLContext.cpp

@@ -143,9 +143,8 @@ GLSLContext& GLSLContext::get () {
 
 namespace {
 // glslang's log says "ERROR: 0:367: ..." against the exact string it was given, show those lines with some context
-std::string describeFailure (
-    const std::string& name, const std::string& stage, const std::string& source, const std::string& log
-) {
+std::string
+describeFailure (const std::string& name, const std::string& stage, const std::string& source, const std::string& log) {
     static const std::regex errorLine (R"((?:ERROR|WARNING): \d+:(\d+):)");
     constexpr int context = 4;
 
@@ -197,9 +196,8 @@ std::string describeFailure (
 }
 } // namespace
 
-std::pair<std::string, std::string> GLSLContext::toGlsl (
-    const std::string& vertex, const std::string& fragment, const std::string& name
-) {
+std::pair<std::string, std::string>
+GLSLContext::toGlsl (const std::string& vertex, const std::string& fragment, const std::string& name) {
     // pure function of the two sources, and passes get rebuilt often
     static std::mutex cacheMutex;
     static std::unordered_map<std::string, std::pair<std::string, std::string>> cache;
@@ -250,8 +248,8 @@ std::pair<std::string, std::string> GLSLContext::toGlsl (
 
     if (!program.link (EShMsgDefault)) {
 	throw std::runtime_error (
-	    "GLSL program " + (name.empty () ? std::string ("<unnamed shader>") : name) + " failed to link: "
-	    + program.getInfoLog ()
+	    "GLSL program " + (name.empty () ? std::string ("<unnamed shader>") : name)
+	    + " failed to link: " + program.getInfoLog ()
 	);
     }
 

+ 2 - 3
src/WallpaperEngine/Render/Shaders/GLSLContext.h

@@ -16,9 +16,8 @@ public:
 
     /** Throws with the error and the offending source lines when glslang rejects either unit, `name` is only
      *  used for that report. */
-    [[nodiscard]] std::pair<std::string, std::string> toGlsl (
-	const std::string& vertex, const std::string& fragment, const std::string& name = ""
-    );
+    [[nodiscard]] std::pair<std::string, std::string>
+    toGlsl (const std::string& vertex, const std::string& fragment, const std::string& name = "");
 
     [[nodiscard]] static GLSLContext& get ();
 

+ 19 - 12
src/WallpaperEngine/Render/Shaders/ShaderUnit.cpp

@@ -440,7 +440,9 @@ void ShaderUnit::preprocessScalarSwizzles () {
     // genericropeparticle's non geometry shader TRAILSCROLLALPHA + TRAILFADESIZE branch writes sizeStart.w on a
     // float, WE never compiles that branch (it draws ropes with a geometry shader) and GLSL rejects it. A single
     // component swizzle of a variable that is only ever declared as float is the variable itself
-    static const std::regex declaration (R"(\b(float|int|bool|u?int|[biu]?vec[234]|mat[234](?:x[234])?)\s+([A-Za-z_][A-Za-z0-9_]*)\b)");
+    static const std::regex declaration (
+	R"(\b(float|int|bool|u?int|[biu]?vec[234]|mat[234](?:x[234])?)\s+([A-Za-z_][A-Za-z0-9_]*)\b)"
+    );
 
     std::set<std::string> scalars;
     std::set<std::string> others;
@@ -489,8 +491,8 @@ std::string ShaderUnit::applyVectorTruncationCompatibility (std::string source)
     size_t last = 0;
     bool changed = false;
 
-    for (auto it = std::sregex_iterator (source.cbegin (), source.cend (), narrowAssign);
-	 it != std::sregex_iterator (); ++it) {
+    for (auto it = std::sregex_iterator (source.cbegin (), source.cend (), narrowAssign); it != std::sregex_iterator ();
+	 ++it) {
 	const int targetWidth = (*it)[1].str ().back () - '0';
 	const size_t exprStart = it->position (2);
 	const std::string expr = (*it)[2].str ();
@@ -513,7 +515,8 @@ std::string ShaderUnit::applyVectorTruncationCompatibility (std::string source)
 		    while (prev > 0 && std::isspace (static_cast<unsigned char> (expr[prev - 1]))) {
 			prev--;
 		    }
-		    if (prev > 0 && (std::isalnum (static_cast<unsigned char> (expr[prev - 1])) || expr[prev - 1] == '_')) {
+		    if (prev > 0
+			&& (std::isalnum (static_cast<unsigned char> (expr[prev - 1])) || expr[prev - 1] == '_')) {
 			grouping = false;
 		    }
 		}
@@ -568,7 +571,8 @@ std::string ShaderUnit::applyVectorTruncationCompatibility (std::string source)
 	    }
 
 	    const bool member = before > 0 && expr[before - 1] == '.';
-	    const bool accessed = after < expr.size () && (expr[after] == '.' || expr[after] == '(' || expr[after] == '[');
+	    const bool accessed
+		= after < expr.size () && (expr[after] == '.' || expr[after] == '(' || expr[after] == '[');
 	    const auto found = widths.find (ident);
 
 	    if (blocked == 0 && !member && !accessed && found != widths.end () && found->second > targetWidth) {
@@ -607,7 +611,7 @@ std::string ShaderUnit::applyFloatConditionCompatibility (std::string source) co
 	const std::string name = (*it)[2].str ();
 	const auto found = onlyFloat.find (name);
 
-	if (found == onlyFloat.end()) {
+	if (found == onlyFloat.end ()) {
 	    onlyFloat.emplace (name, isFloat);
 	} else if (!isFloat) {
 	    found->second = false;
@@ -727,8 +731,8 @@ std::string ShaderUnit::applyNarrowFragmentVaryingCompatibility (std::string sou
     std::string copyCode;
     std::string names;
 
-    for (auto it = std::sregex_iterator (linked.cbegin (), linked.cend (), vertexVarying); it != std::sregex_iterator ();
-	 ++it) {
+    for (auto it = std::sregex_iterator (linked.cbegin (), linked.cend (), vertexVarying);
+	 it != std::sregex_iterator (); ++it) {
 	const int vertexWidth = (*it)[1].str ()[0] - '0';
 	const std::string name = (*it)[2].str ();
 	const std::regex anyDecl ("\\bvarying\\s+\\w+\\s+" + name + "\\s*;");
@@ -738,11 +742,13 @@ std::string ShaderUnit::applyNarrowFragmentVaryingCompatibility (std::string sou
 	// conditionals are still in the source here, a varying declared once per #if branch
 	// (the stock generic shaders) can't be told apart from a real mismatch, leave those alone
 	const auto declarations = [&anyDecl] (const std::string& text) {
-	    return std::distance (std::sregex_iterator (text.cbegin (), text.cend (), anyDecl), std::sregex_iterator ());
+	    return std::distance (
+		std::sregex_iterator (text.cbegin (), text.cend (), anyDecl), std::sregex_iterator ()
+	    );
 	};
 
-	if (declarations (linked) != 1 || declarations (source) != 1 || !std::regex_search (source, declMatch, narrowDecl)
-	    || declMatch[1].str ()[0] - '0' >= vertexWidth) {
+	if (declarations (linked) != 1 || declarations (source) != 1
+	    || !std::regex_search (source, declMatch, narrowDecl) || declMatch[1].str ()[0] - '0' >= vertexWidth) {
 	    continue;
 	}
 
@@ -983,7 +989,8 @@ std::string ShaderUnit::applyNonConstantGlobalConstCompatibility (std::string so
 
     result.append (last, source.cend ());
 
-    const auto mains = std::distance (std::sregex_iterator (result.cbegin (), result.cend (), mainOpen), std::sregex_iterator ());
+    const auto mains
+	= std::distance (std::sregex_iterator (result.cbegin (), result.cend (), mainOpen), std::sregex_iterator ());
     if (mains != 1) {
 	return source;
     }

+ 5 - 4
src/WallpaperEngine/Render/TextureCache.h

@@ -7,8 +7,8 @@
 #include <utility>
 
 #include "TextureProvider.h"
-#include "WallpaperEngine/Render/Helpers/ContextAware.h"
 #include "WallpaperEngine/Data/Model/Types.h"
+#include "WallpaperEngine/Render/Helpers/ContextAware.h"
 #include "WallpaperEngine/Render/RenderContext.h"
 
 using namespace WallpaperEngine::Render;
@@ -26,7 +26,8 @@ public:
     explicit TextureCache (RenderContext& context);
     ~TextureCache () override;
 
-    /** Returns the texture for filename as seen by the given project, cached per project since wallpapers reuse file names */
+    /** Returns the texture for filename as seen by the given project, cached per project since wallpapers reuse file
+     * names */
     std::shared_ptr<const TextureProvider> resolve (const std::string& filename, const Data::Model::Project& project);
 
     /** Registers a texture that is shared by every project (media thumbnails and such) */
@@ -39,8 +40,8 @@ private:
     std::shared_ptr<const AlbumTexture> m_previousThumbnail = nullptr;
     std::shared_ptr<const AlbumTexture> m_currentThumbnail = nullptr;
     std::map<std::string, std::shared_ptr<const TextureProvider>> m_sharedTextures = {};
-    std::map<std::pair<const Data::Model::Project*, std::string>, std::shared_ptr<const TextureProvider>>
-	m_textureCache = {};
+    std::map<std::pair<const Data::Model::Project*, std::string>, std::shared_ptr<const TextureProvider>> m_textureCache
+	= {};
     std::function<void ()> m_mediaCallback;
 };
 } // namespace WallpaperEngine::Render

+ 2 - 2
src/WallpaperEngine/Render/Utils/NoiseUtils.h

@@ -175,8 +175,8 @@ inline float simplexNoise1D (float x) {
  */
 inline float simplexNoise3D (float x, float y, float z) {
     static constexpr float gradients[12][3] = {
-	{ 1, 1, 0 }, { -1, 1, 0 }, { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 }, { -1, 0, 1 },
-	{ 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 }, { 0, -1, 1 }, { 0, 1, -1 }, { 0, -1, -1 },
+	{ 1, 1, 0 },  { -1, 1, 0 },  { 1, -1, 0 }, { -1, -1, 0 }, { 1, 0, 1 },  { -1, 0, 1 },
+	{ 1, 0, -1 }, { -1, 0, -1 }, { 0, 1, 1 },  { 0, -1, 1 },  { 0, 1, -1 }, { 0, -1, -1 },
     };
     constexpr float F3 = 1.0f / 3.0f;
     constexpr float G3 = 1.0f / 6.0f;

+ 27 - 12
src/WallpaperEngine/Render/Volumetrics.cpp

@@ -240,7 +240,8 @@ void Volumetrics::setup () {
 	for (int slice = 0; slice <= 24; slice++) {
 	    const float longitude = static_cast<float> (slice) * 0.2617994f;
 	    vertices.emplace_back (
-		std::cos (longitude) * std::sin (latitude), std::cos (latitude), std::sin (longitude) * std::sin (latitude)
+		std::cos (longitude) * std::sin (latitude), std::cos (latitude),
+		std::sin (longitude) * std::sin (latitude)
 	    );
 	}
     }
@@ -251,14 +252,18 @@ void Volumetrics::setup () {
 	for (int slice = 0; slice < 24; slice++) {
 	    const auto a = static_cast<GLushort> (2 + 25 * ring + slice);
 	    const auto c = static_cast<GLushort> (a + 25);
-	    indices.insert (indices.end (), { a, static_cast<GLushort> (a + 1), c, static_cast<GLushort> (a + 1),
-					      static_cast<GLushort> (c + 1), c });
+	    indices.insert (
+		indices.end (),
+		{ a, static_cast<GLushort> (a + 1), c, static_cast<GLushort> (a + 1), static_cast<GLushort> (c + 1), c }
+	    );
 	}
     }
 
     for (int slice = 0; slice < 24; slice++) {
 	indices.insert (indices.end (), { 0, static_cast<GLushort> (3 + slice), static_cast<GLushort> (2 + slice) });
-	indices.insert (indices.end (), { 1, static_cast<GLushort> (552 + slice), static_cast<GLushort> (553 + slice) });
+	indices.insert (
+	    indices.end (), { 1, static_cast<GLushort> (552 + slice), static_cast<GLushort> (553 + slice) }
+	);
     }
 
     glGenVertexArrays (1, &this->m_vao);
@@ -266,7 +271,8 @@ void Volumetrics::setup () {
     glGenBuffers (1, &this->m_sphereVertices);
     glBindBuffer (GL_ARRAY_BUFFER, this->m_sphereVertices);
     glBufferData (
-	GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertices.size () * sizeof (glm::vec3)), vertices.data (), GL_STATIC_DRAW
+	GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (vertices.size () * sizeof (glm::vec3)), vertices.data (),
+	GL_STATIC_DRAW
     );
     glGenBuffers (1, &this->m_sphereIndices);
     glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, this->m_sphereIndices);
@@ -349,7 +355,8 @@ void Volumetrics::renderLight (const Data::Model::Light& light, const glm::mat4&
     }
 
     // light buffer at an eighth of the output resolution, a quarter from high quality up
-    const glm::ivec2 size = glm::max (this->m_scene.getOutputResolution () / (this->m_quality >= 3 ? 4 : 8), glm::ivec2 (1));
+    const glm::ivec2 size
+	= glm::max (this->m_scene.getOutputResolution () / (this->m_quality >= 3 ? 4 : 8), glm::ivec2 (1));
 
     if (size != this->m_size) {
 	this->allocate (size);
@@ -373,8 +380,8 @@ void Volumetrics::renderLight (const Data::Model::Light& light, const glm::mat4&
 	: -glm::vec3 (camera.getView ()[0][2], camera.getView ()[1][2], camera.getView ()[2][2]);
     const glm::vec3 probe = fog.eyeWorld + forward * 0.2f - origin;
     const bool inside = radius * radius > glm::dot (probe, probe);
-    const bool shadow = light.castShadow
-	&& this->m_scene.getContext ().getApp ().getContext ().settings.general.shadowQuality > 0;
+    const bool shadow
+	= light.castShadow && this->m_scene.getContext ().getApp ().getContext ().settings.general.shadowQuality > 0;
 
     glBindVertexArray (this->m_vao);
     glViewport (0, 0, size.x, size.y);
@@ -391,7 +398,9 @@ void Volumetrics::renderLight (const Data::Model::Light& light, const glm::mat4&
     glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
     glDepthFunc (GL_GREATER);
     glUseProgram (this->m_backProgram);
-    glUniformMatrix4fv (glGetUniformLocation (this->m_backProgram, "u_ViewProjection"), 1, GL_FALSE, &viewProjection[0][0]);
+    glUniformMatrix4fv (
+	glGetUniformLocation (this->m_backProgram, "u_ViewProjection"), 1, GL_FALSE, &viewProjection[0][0]
+    );
     glUniformMatrix4fv (glGetUniformLocation (this->m_backProgram, "u_Volume"), 1, GL_FALSE, &volume[0][0]);
     glDrawElements (GL_TRIANGLES, this->m_sphereIndexCount, GL_UNSIGNED_SHORT, nullptr);
 
@@ -424,7 +433,9 @@ void Volumetrics::renderLight (const Data::Model::Light& light, const glm::mat4&
     glUniformMatrix4fv (glGetUniformLocation (program, "u_ViewProjection"), 1, GL_FALSE, &viewProjection[0][0]);
     glUniformMatrix4fv (glGetUniformLocation (program, "u_Volume"), 1, GL_FALSE, &volume[0][0]);
     glUniformMatrix4fv (glGetUniformLocation (program, "u_InverseViewProjection"), 1, GL_FALSE, &inverse[0][0]);
-    glUniform2f (glGetUniformLocation (program, "u_BufferSize"), static_cast<float> (size.x), static_cast<float> (size.y));
+    glUniform2f (
+	glGetUniformLocation (program, "u_BufferSize"), static_cast<float> (size.x), static_cast<float> (size.y)
+    );
     glUniform3fv (glGetUniformLocation (program, "u_LightOrigin"), 1, &origin[0]);
     // g_RenderVar1.x is 99% of the radius, the falloff ends just inside the mesh
     glUniform1f (glGetUniformLocation (program, "u_Radius"), radius * 0.99f);
@@ -475,12 +486,16 @@ void Volumetrics::composite () {
 
 	glBindFramebuffer (GL_FRAMEBUFFER, this->m_lightB.framebuffer);
 	glBindTexture (GL_TEXTURE_2D, this->m_light.texture);
-	glUniform2f (glGetUniformLocation (this->m_blurProgram, "u_Direction"), 1.0f / static_cast<float> (this->m_size.x), 0.0f);
+	glUniform2f (
+	    glGetUniformLocation (this->m_blurProgram, "u_Direction"), 1.0f / static_cast<float> (this->m_size.x), 0.0f
+	);
 	glDrawArrays (GL_TRIANGLES, 0, 3);
 
 	glBindFramebuffer (GL_FRAMEBUFFER, this->m_light.framebuffer);
 	glBindTexture (GL_TEXTURE_2D, this->m_lightB.texture);
-	glUniform2f (glGetUniformLocation (this->m_blurProgram, "u_Direction"), 0.0f, 1.0f / static_cast<float> (this->m_size.y));
+	glUniform2f (
+	    glGetUniformLocation (this->m_blurProgram, "u_Direction"), 0.0f, 1.0f / static_cast<float> (this->m_size.y)
+	);
 	glDrawArrays (GL_TRIANGLES, 0, 3);
     }
 

+ 2 - 1
src/WallpaperEngine/Render/WallpaperState.cpp

@@ -217,7 +217,8 @@ void applyZoomToAxis (float& start, float& end, float zoom) {
     end = center + half / zoom;
 }
 
-// Slides the UV window toward one edge without changing its size, offset in [-1, 1]; no-op when nothing is cropped on the axis
+// Slides the UV window toward one edge without changing its size, offset in [-1, 1]; no-op when nothing is cropped on
+// the axis
 void applyOffsetToAxis (float& start, float& end, float offset) {
     if (offset == 0.0f) {
 	return;

+ 2 - 1
src/WallpaperEngine/Render/WallpaperState.h

@@ -69,7 +69,8 @@ public:
     [[nodiscard]] float getOffsetX () const;
     [[nodiscard]] float getOffsetY () const;
 
-    /** Re-centers the crop window, each axis in [-1, 1] (0 = centered), takes effect on the next frame like setZoom() */
+    /** Re-centers the crop window, each axis in [-1, 1] (0 = centered), takes effect on the next frame like setZoom()
+     */
     void setOffset (float offsetX, float offsetY);
 
     [[nodiscard]] int getViewportWidth () const;

+ 59 - 37
src/WallpaperEngine/Render/Wallpapers/CScene.cpp

@@ -1,6 +1,6 @@
-#include "WallpaperEngine/Render/Objects/CImage.h"
 #include "WallpaperEngine/Data/Model/Property.h"
 #include "WallpaperEngine/Render/Objects/CCamera.h"
+#include "WallpaperEngine/Render/Objects/CImage.h"
 #include "WallpaperEngine/Render/Objects/CLight.h"
 #include "WallpaperEngine/Render/Objects/CMesh.h"
 #include "WallpaperEngine/Render/Objects/CParticle.h"
@@ -85,7 +85,8 @@ CScene::CScene (
     this->m_volumetrics = std::make_unique<Volumetrics> (*this);
 
     // models depth test against each other, 2D scenes can hold some too
-    const bool hasModels = std::ranges::any_of (scene->objects, [] (const auto& object) { return object->template is<Mesh> (); });
+    const bool hasModels
+	= std::ranges::any_of (scene->objects, [] (const auto& object) { return object->template is<Mesh> (); });
 
     // needed before scene setup below, which creates FBOs
     this->setupFramebuffers (
@@ -405,7 +406,8 @@ void CScene::renderFrame (const glm::ivec4& viewport) {
 void CScene::renderFrameSteps (const glm::ivec4& viewport) {
     timeStep ("updateMouse", [&] { this->updateMouse (viewport); });
 
-    // after the tick, so a layer a script moves this frame (e.g. onto input.cursorWorldPosition) is hit tested where it is now
+    // after the tick, so a layer a script moves this frame (e.g. onto input.cursorWorldPosition) is hit tested where it
+    // is now
     timeStep ("script tick", [&] { this->getScriptEngine ().tick (); });
     // WE runs the object updates first, then the camera, then the parallax camera
     this->updateCamera ();
@@ -451,8 +453,9 @@ void CScene::renderFrameSteps (const glm::ivec4& viewport) {
 	    continue;
 	}
 
-	const auto visibility
-	    = this->getContext ().getApp ().getContext ().resolveObjectVisibility (cur->getId (), cur->getObject ().name);
+	const auto visibility = this->getContext ().getApp ().getContext ().resolveObjectVisibility (
+	    cur->getId (), cur->getObject ().name
+	);
 	if (visibility.has_value () && !visibility.value ()) {
 	    continue;
 	}
@@ -651,9 +654,8 @@ std::shared_ptr<const CFBO> CScene::requireMipMappedFrameBuffer () {
 
     // sub_1400D2C60 with flag 0x10: max (1, min (log2 (np2 (w) / 2), log2 (np2 (h) / 2)) - 2) levels,
     // np2 = the next power of two, 8 for a 1920x1058 buffer
-    const auto halfPowerOfTwo = [] (const uint32_t value) {
-	return std::bit_width (std::bit_ceil (std::max (value, 2u)) >> 1) - 1;
-    };
+    const auto halfPowerOfTwo
+	= [] (const uint32_t value) { return std::bit_width (std::bit_ceil (std::max (value, 2u)) >> 1) - 1; };
     const uint32_t width = this->m_sceneFBO->getRealWidth ();
     const uint32_t height = this->m_sceneFBO->getRealHeight ();
     const int levels = std::max (1, static_cast<int> (std::min (halfPowerOfTwo (width), halfPowerOfTwo (height))) - 2);
@@ -700,10 +702,8 @@ glm::ivec2 CScene::getOutputResolution () const {
     const float coverU = std::abs (uend - ustart) > 0.0f ? std::abs (uend - ustart) : 1.0f;
     const float coverV = std::abs (vend - vstart) > 0.0f ? std::abs (vend - vstart) : 1.0f;
 
-    return {
-	std::max (1, static_cast<int> (static_cast<float> (viewportWidth) / coverU)),
-	std::max (1, static_cast<int> (static_cast<float> (viewportHeight) / coverV))
-    };
+    return { std::max (1, static_cast<int> (static_cast<float> (viewportWidth) / coverU)),
+	     std::max (1, static_cast<int> (static_cast<float> (viewportHeight) / coverV)) };
 }
 
 glm::mat4 CScene::getWorldViewProjection () const {
@@ -796,16 +796,20 @@ void CScene::renderHDRBloom () {
     draw (this->m_bloomDownsampleThreshold, this->m_bloomLevels[0], this->m_sceneFBO->getTextureID (0), 1.0f);
 
     for (int level = 1; level < levels; level++) {
-	draw (this->m_bloomDownsample, this->m_bloomLevels[level], this->m_bloomLevels[level - 1].texture,
-	      static_cast<float> (1 << level));
+	draw (
+	    this->m_bloomDownsample, this->m_bloomLevels[level], this->m_bloomLevels[level - 1].texture,
+	    static_cast<float> (1 << level)
+	);
     }
 
     glEnable (GL_BLEND);
     glBlendFunc (GL_ONE, GL_ONE);
 
     for (int level = levels - 1; level > 0; level--) {
-	draw (level < levels - 2 ? this->m_bloomUpsample : this->m_bloomUpsampleCubic, this->m_bloomLevels[level - 1],
-	      this->m_bloomLevels[level].texture, static_cast<float> (2 << (level - 1)));
+	draw (
+	    level < levels - 2 ? this->m_bloomUpsample : this->m_bloomUpsampleCubic, this->m_bloomLevels[level - 1],
+	    this->m_bloomLevels[level].texture, static_cast<float> (2 << (level - 1))
+	);
     }
 
     glDisable (GL_BLEND);
@@ -880,8 +884,9 @@ void CScene::renderCameraFade () {
 
 void CScene::updateParallax () {
     if (this->getScene ().camera.parallax.enabled->value->getBool ()) {
-	// the wallpaper's own position rides through the same per-layer depth/clamp mechanism as mouse parallax, halved to match its range
-	// X keeps WallpaperState's sign flip, Y does not because this displacement is applied after the Y-up conversion
+	// the wallpaper's own position rides through the same per-layer depth/clamp mechanism as mouse parallax, halved
+	// to match its range X keeps WallpaperState's sign flip, Y does not because this displacement is applied after
+	// the Y-up conversion
 	const glm::vec2 positionBias
 	    = { -this->getState ().getOffsetX () * 0.5f, this->getState ().getOffsetY () * 0.5f };
 
@@ -1039,7 +1044,8 @@ void CScene::updateFog (const glm::vec3& eye) {
     this->m_fog.distanceParams
 	= params (fog.distanceStart, fog.distanceEnd, fog.distanceStartDensity, fog.distanceEndDensity);
     this->m_fog.heightColor = fog.heightColor->value->getVec3 ();
-    this->m_fog.heightParamsWorld = params (fog.heightStart, fog.heightEnd, fog.heightStartDensity, fog.heightEndDensity);
+    this->m_fog.heightParamsWorld
+	= params (fog.heightStart, fog.heightEnd, fog.heightStartDensity, fog.heightEndDensity);
     this->m_fog.heightParamsLocal = this->m_fog.heightParamsWorld;
     this->m_fog.eyeWorld = eye;
     this->m_fog.eyeLocal = eye;
@@ -1063,7 +1069,8 @@ void CScene::updateCamera () {
     for (CObject* object : this->m_sceneCameras) {
 	const auto override = appContext.resolveObjectVisibility (object->getId (), object->getObject ().name);
 
-	if (override.value_or (object->getObject ().groupVisible->value->getBool ()) && !this->isHiddenByAncestor (*object)) {
+	if (override.value_or (object->getObject ().groupVisible->value->getBool ())
+	    && !this->isHiddenByAncestor (*object)) {
 	    active = object->as<Objects::CCamera> ();
 	}
     }
@@ -1087,7 +1094,8 @@ void CScene::updateCamera () {
 	// a camera path file drives the camera's transform, zoom and fov (sub_1401F2AD0)
 	const auto& data = active->getObject ();
 	const CObject* parent = data.parent.has_value () ? this->getObject (data.parent.value ()) : nullptr;
-	const glm::mat4 parentWorld = parent != nullptr ? this->objectWorldMatrix (parent->getObject ()) : glm::mat4 (1.0f);
+	const glm::mat4 parentWorld
+	    = parent != nullptr ? this->objectWorldMatrix (parent->getObject ()) : glm::mat4 (1.0f);
 
 	if (const auto pose = active->updateTimeline (dt, world, parentWorld); pose.has_value ()) {
 	    world = pose->world;
@@ -1234,7 +1242,8 @@ void CScene::dispatchCursorEvents () {
 	    continue;
 	}
 
-	const auto visibility = this->getContext ().getApp ().getContext ().resolveObjectVisibility (id, image->getImage ().name);
+	const auto visibility
+	    = this->getContext ().getApp ().getContext ().resolveObjectVisibility (id, image->getImage ().name);
 
 	if (visibility.value_or (image->getImage ().visible->value->getBool ()) && !this->isHiddenByAncestor (*cur)) {
 	    break;
@@ -1298,12 +1307,13 @@ void CScene::expandCanvasToContent (
 
 	// children are positioned relative to their parent, and a layer with no declared size takes its
 	// texture's, which isn't known this early
-	if (image->parent.has_value () || image->size.x <= 0.0f || image->size.y <= 0.0f
-	    || image->origin == nullptr || image->scale == nullptr) {
+	if (image->parent.has_value () || image->size.x <= 0.0f || image->size.y <= 0.0f || image->origin == nullptr
+	    || image->scale == nullptr) {
 	    continue;
 	}
 
-	const auto visibility = this->getContext ().getApp ().getContext ().resolveObjectVisibility (image->id, image->name);
+	const auto visibility
+	    = this->getContext ().getApp ().getContext ().resolveObjectVisibility (image->id, image->name);
 
 	if (visibility.has_value () && !visibility.value ()) {
 	    continue;
@@ -1451,8 +1461,7 @@ void CScene::updateLights () {
 	    continue;
 	}
 
-	const auto override
-	    = this->getContext ().getApp ().getContext ().resolveObjectVisibility (data.id, data.name);
+	const auto override = this->getContext ().getApp ().getContext ().resolveObjectVisibility (data.id, data.name);
 	const bool visible = override.has_value () ? override.value () : data.visible->value->getBool ();
 
 	if (!visible || this->isHiddenByAncestor (*light)) {
@@ -1462,7 +1471,8 @@ void CScene::updateLights () {
 	const int slot = light->getSlot ();
 	const float intensity = data.intensity->value->getFloat ();
 
-	colors[slot] = glm::vec4 (glm::vec3 (data.color->value->getVec3 ()) * intensity, data.radius->value->getFloat ());
+	colors[slot]
+	    = glm::vec4 (glm::vec3 (data.color->value->getVec3 ()) * intensity, data.radius->value->getFloat ());
 	positions[slot] = data.origin->value->getVec3 ();
     }
 
@@ -1603,11 +1613,13 @@ Render::CObject* CScene::buildLayer (JSON layerJson) {
     return renderObject;
 }
 
-// scripts write into createLayer()'s result without a null check, so failures get a scriptable, non-rendering placeholder
+// scripts write into createLayer()'s result without a null check, so failures get a scriptable, non-rendering
+// placeholder
 Render::CObject* CScene::createPlaceholderLayer () {
     try {
 	const int id = this->m_nextDynamicLayerId++;
-	const JSON placeholderJson = { { "id", id }, { "name", "scriptlayer_" + std::to_string (id) }, { "visible", true } };
+	const JSON placeholderJson
+	    = { { "id", id }, { "name", "scriptlayer_" + std::to_string (id) }, { "visible", true } };
 	auto objectData = ObjectParser::parse (placeholderJson, this->getScene ().project);
 	auto* renderObject = new Scripting::ScriptableObject (*this, *objectData);
 
@@ -1623,15 +1635,18 @@ Render::CObject* CScene::createPlaceholderLayer () {
 }
 
 Render::CObject* CScene::createLayer (const std::string& imagePath) {
-    // createObject() throws on a missing asset, and unwinding a C++ exception through the QuickJS callback is undefined behavior
+    // createObject() throws on a missing asset, and unwinding a C++ exception through the QuickJS callback is undefined
+    // behavior
     try {
-	if (const auto cached = this->m_createLayerAliases.find (imagePath); cached != this->m_createLayerAliases.end ()) {
+	if (const auto cached = this->m_createLayerAliases.find (imagePath);
+	    cached != this->m_createLayerAliases.end ()) {
 	    return this->buildLayer ({ { "image", cached->second } });
 	}
 
 	return this->buildLayer ({ { "image", imagePath } });
     } catch (const std::exception& e) {
-	// scripts written against a workshop dependency's original layout use the un-prefixed name, try the prefixed copy first
+	// scripts written against a workshop dependency's original layout use the un-prefixed name, try the prefixed
+	// copy first
 	if (const auto alias = this->getScene ().project.assetLocator->resolveWorkshopDependencyAlias (imagePath);
 	    alias.has_value () && !this->m_createLayerAliases.contains (imagePath)) {
 	    try {
@@ -1643,10 +1658,14 @@ Render::CObject* CScene::createLayer (const std::string& imagePath) {
 		this->m_createLayerAliases.emplace (imagePath, alias->string ());
 		return layer;
 	    } catch (const std::exception& aliasError) {
-		sLog.error ("createLayer: workshop-dependency copy '", alias->string (), "' also failed: ", aliasError.what ());
+		sLog.error (
+		    "createLayer: workshop-dependency copy '", alias->string (), "' also failed: ", aliasError.what ()
+		);
 	    }
 	} else {
-	    sLog.error ("createLayer failed for '", imagePath, "', falling back to an invisible placeholder: ", e.what ());
+	    sLog.error (
+		"createLayer failed for '", imagePath, "', falling back to an invisible placeholder: ", e.what ()
+	    );
 	}
 
 	return this->createPlaceholderLayer ();
@@ -1655,7 +1674,8 @@ Render::CObject* CScene::createLayer (const std::string& imagePath) {
 
 Render::CObject* CScene::createLayerFromConfig (JSON config) {
     // WE (scenescript64 sub_180011340): a config with "color" and no layer type key becomes a solid layer
-    static constexpr std::array layerTypes = { "model", "particle", "sprite", "image", "text", "light", "sound", "camera" };
+    static constexpr std::array layerTypes
+	= { "model", "particle", "sprite", "image", "text", "light", "sound", "camera" };
 
     if (config.contains ("color")
 	&& std::ranges::none_of (layerTypes, [&config] (const char* key) { return config.contains (key); })) {
@@ -1665,7 +1685,9 @@ Render::CObject* CScene::createLayerFromConfig (JSON config) {
     try {
 	return this->buildLayer (config);
     } catch (const std::exception& e) {
-	sLog.error ("createLayer failed for ", config.dump (), ", falling back to an invisible placeholder: ", e.what ());
+	sLog.error (
+	    "createLayer failed for ", config.dump (), ", falling back to an invisible placeholder: ", e.what ()
+	);
 	return this->createPlaceholderLayer ();
     }
 }

+ 2 - 1
src/WallpaperEngine/Render/Wallpapers/CScene.h

@@ -92,7 +92,8 @@ public:
     [[nodiscard]] glm::ivec2 getOutputResolution () const;
     /** WE's world (scene units, y up from the bottom left in 2D) to clip space of the scene buffer */
     [[nodiscard]] glm::mat4 getWorldViewProjection () const;
-    /** Position of the active camera object in WE world units (y up, relative to the default camera), zero without one */
+    /** Position of the active camera object in WE world units (y up, relative to the default camera), zero without one
+     */
     [[nodiscard]] const glm::vec2& getCameraEye () const { return this->m_cameraEye; }
     [[nodiscard]] const CObject* getObject (int id) const;
     [[nodiscard]] CObject* getObject (int id);

+ 30 - 19
src/WallpaperEngine/Render/Wallpapers/CSplat.cpp

@@ -86,7 +86,8 @@ private:
 	std::vector<uint32_t> counts;
     };
 
-    void sort (const glm::vec3& position, const glm::vec3& forward, Scratch& scratch, std::vector<uint32_t>& out) const {
+    void
+    sort (const glm::vec3& position, const glm::vec3& forward, Scratch& scratch, std::vector<uint32_t>& out) const {
 	constexpr float nearPlane = 0.05f;
 	constexpr uint32_t bucketCount = 65536;
 	constexpr uint32_t skipped = 0xFFFFFFFFu;
@@ -189,7 +190,8 @@ private:
 } // namespace WallpaperEngine::Render::Wallpapers
 
 namespace {
-// Gaussian splat rasterizer: camera facing quad instances, the screen space ellipse comes from the projected 3D covariance (EWA splatting).
+// Gaussian splat rasterizer: camera facing quad instances, the screen space ellipse comes from the projected 3D
+// covariance (EWA splatting).
 const char* SPLAT_VERTEX_SHADER = R"(#version 330
 precision highp float;
 
@@ -571,9 +573,11 @@ void CSplat::loadCloud () {
 	const float verticalMin = quantile (verticalAngles, 0.02f);
 	const float verticalMax = quantile (verticalAngles, 0.98f);
 
-	this->m_profile.direction = glm::normalize (glm::vec3 (
-	    std::tan ((horizontalMin + horizontalMax) * 0.5f), std::tan ((verticalMin + verticalMax) * 0.5f), 1.0f
-	));
+	this->m_profile.direction = glm::normalize (
+	    glm::vec3 (
+		std::tan ((horizontalMin + horizontalMax) * 0.5f), std::tan ((verticalMin + verticalMax) * 0.5f), 1.0f
+	    )
+	);
 	this->m_profile.angularSize
 	    = { std::max (horizontalMax - horizontalMin, 0.01f), std::max (verticalMax - verticalMin, 0.01f) };
 	this->m_profile.depthMin = quantile (depths, 0.02f);
@@ -609,9 +613,7 @@ void CSplat::setupGL (
 
     // integer texture, a float one may flush denormals and eat some colors
     makeTexture (this->m_centerTexture);
-    glTexImage2D (
-	GL_TEXTURE_2D, 0, GL_RGBA32UI, width, height, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, centers.data ()
-    );
+    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA32UI, width, height, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, centers.data ());
 
     makeTexture (this->m_rotationTexture);
     glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, rotations.data ());
@@ -806,7 +808,9 @@ bool CSplat::updateClock (float aspect) {
 	this->m_clockTransitionStart = now;
     }
 
-    const float progress = std::clamp (std::chrono::duration<float> (now - this->m_clockTransitionStart).count () / TRANSITION_SECONDS, 0.0f, 1.0f);
+    const float progress = std::clamp (
+	std::chrono::duration<float> (now - this->m_clockTransitionStart).count () / TRANSITION_SECONDS, 0.0f, 1.0f
+    );
     const float rise = 1.0f - std::pow (1.0f - progress, 3.0f);
     const float sink = std::pow (1.0f - progress, 3.0f);
 
@@ -881,7 +885,8 @@ bool CSplat::updateClock (float aspect) {
     }
 
     const glm::vec4 params (active ? 1.0f : 0.0f, lift, radius, std::max (1.0f, aspect));
-    const bool changed = points != this->m_clockPoints || params != this->m_clockParams || distance != this->m_clockDistance;
+    const bool changed
+	= points != this->m_clockPoints || params != this->m_clockParams || distance != this->m_clockDistance;
 
     this->m_clockPoints = points;
     this->m_clockParams = params;
@@ -923,7 +928,8 @@ CSplat::BaseCamera CSplat::buildBaseCamera (int width, int height, float focusDe
     const float vertical = profile.angularSize.y;
     const float horizontalFit = width > height ? horizontal : 2.0f * std::atan (std::tan (horizontal * 0.5f) / aspect);
     const float verticalFit = width > height ? 2.0f * std::atan (std::tan (vertical * 0.5f) * aspect) : vertical;
-    const float fov = this->m_containFraming ? std::max (horizontalFit, verticalFit) : std::min (horizontalFit, verticalFit);
+    const float fov
+	= this->m_containFraming ? std::max (horizontalFit, verticalFit) : std::min (horizontalFit, verticalFit);
 
     base.fov = std::clamp (glm::degrees (fov) * 1.04f, 20.0f, 120.0f);
     base.viewport = { width, height };
@@ -981,9 +987,8 @@ bool CSplat::viewFitsFrontFace (const glm::vec3& position, const glm::vec3& targ
 
 // The viewer's camera: sits at the photo's origin and moves on a sphere around the focus point,
 // as far as the mouse asks without the view leaving the front face of the splat cloud.
-CSplat::Pose CSplat::posePlacement (
-    const BaseCamera& base, glm::vec2 tilt, glm::vec2 orbit, float parallaxStrength
-) const {
+CSplat::Pose
+CSplat::posePlacement (const BaseCamera& base, glm::vec2 tilt, glm::vec2 orbit, float parallaxStrength) const {
     const float strength = std::clamp (parallaxStrength, 0.0f, 0.16f);
     float parallaxMove = base.distance * strength * 2.4f;
 
@@ -1009,7 +1014,8 @@ CSplat::Pose CSplat::posePlacement (
 	    return std::nullopt;
 	}
 
-	const float frontDistance = std::sqrt (std::max (base.distance * base.distance - tangentDistance * tangentDistance, 0.0f));
+	const float frontDistance
+	    = std::sqrt (std::max (base.distance * base.distance - tangentDistance * tangentDistance, 0.0f));
 	const glm::vec3 position = base.target + base.front * frontDistance + offset;
 
 	if (!this->viewFitsFrontFace (position, base.target, base)) {
@@ -1092,7 +1098,8 @@ void CSplat::renderFrame (const glm::ivec4& viewport) {
 	const float normalizedX = std::clamp (static_cast<float> ((mouse.x - viewport.x) / viewport.z), 0.0f, 1.0f);
 	const float normalizedY = std::clamp (static_cast<float> ((mouse.y - viewport.y) / viewport.w), 0.0f, 1.0f);
 
-	targetTilt = glm::clamp (glm::vec2 (normalizedX * 2.0f - 1.0f, normalizedY * 2.0f - 1.0f) * sensitivity, -1.0f, 1.0f);
+	targetTilt
+	    = glm::clamp (glm::vec2 (normalizedX * 2.0f - 1.0f, normalizedY * 2.0f - 1.0f) * sensitivity, -1.0f, 1.0f);
     }
 
     // the viewer eases toward the target by 14% per 60Hz frame
@@ -1119,7 +1126,8 @@ void CSplat::renderFrame (const glm::ivec4& viewport) {
 	const float speed = static_cast<float> (this->numberProperty ("orbitSpeed", 0.12));
 	const float amount = static_cast<float> (this->numberProperty ("orbitAmount", 0.34));
 
-	this->m_orbitPhase = std::fmod (this->m_orbitPhase + delta * speed * glm::two_pi<float> (), glm::two_pi<float> ());
+	this->m_orbitPhase
+	    = std::fmod (this->m_orbitPhase + delta * speed * glm::two_pi<float> (), glm::two_pi<float> ());
 	orbit = { std::cos (this->m_orbitPhase) * amount, std::sin (this->m_orbitPhase) * amount * 0.47f };
     }
 
@@ -1179,7 +1187,8 @@ void CSplat::renderFrame (const glm::ivec4& viewport) {
 	this->m_drawCount = static_cast<uint32_t> (order.size ());
     }
 
-    const bool clockChanged = this->updateClock (static_cast<float> (this->m_width) / static_cast<float> (this->m_height));
+    const bool clockChanged
+	= this->updateClock (static_cast<float> (this->m_width) / static_cast<float> (this->m_height));
 
     // a still camera and clock produce the exact same frame, which is still sitting in the output texture
     if (this->m_hasDrawn && !haveNewOrder && !clockChanged && view == this->m_drawnView
@@ -1223,7 +1232,9 @@ void CSplat::renderFrame (const glm::ivec4& viewport) {
     glUniform2f (this->u_Viewport, static_cast<float> (this->m_width), static_cast<float> (this->m_height));
     glUniform2f (this->u_Focal, focal.x, focal.y);
     glUniform1i (this->u_TextureWidth, static_cast<GLint> (this->m_textureWidth));
-    glUniform4fv (this->u_ClockPoints, static_cast<GLsizei> (CLOCK_POINT_CAPACITY), glm::value_ptr (this->m_clockPoints[0]));
+    glUniform4fv (
+	this->u_ClockPoints, static_cast<GLsizei> (CLOCK_POINT_CAPACITY), glm::value_ptr (this->m_clockPoints[0])
+    );
     glUniform4fv (this->u_ClockParams, 1, glm::value_ptr (this->m_clockParams));
     glUniform4fv (this->u_ClockBounds, 1, glm::value_ptr (this->m_clockBounds));
     glUniform1f (this->u_ClockDistance, this->m_clockDistance);

+ 8 - 7
src/WallpaperEngine/Render/Wallpapers/CSplat.h

@@ -13,9 +13,10 @@
 namespace WallpaperEngine::Render::Wallpapers {
 class SplatSorter;
 
-// Native renderer for the "SOG depth wallpaper" family: a Gaussian splat cloud (PlayCanvas SOG format) viewed through a camera at
-// the photo's origin that sways with the mouse to fake depth. These items normally host a SuperSplat viewer in CEF, which drew
-// torn patches under offscreen rendering, so the same data is rendered natively with the camera model ported from the wallpaper's JS.
+// Native renderer for the "SOG depth wallpaper" family: a Gaussian splat cloud (PlayCanvas SOG format) viewed through a
+// camera at the photo's origin that sways with the mouse to fake depth. These items normally host a SuperSplat viewer
+// in CEF, which drew torn patches under offscreen rendering, so the same data is rendered natively with the camera
+// model ported from the wallpaper's JS.
 class CSplat : public CWallpaper {
 public:
     CSplat (
@@ -84,12 +85,12 @@ private:
     static constexpr size_t CLOCK_POINT_CAPACITY = 32;
 
     void loadCloud ();
-    void setupGL (const std::vector<float>& centers, const std::vector<float>& rotations, const std::vector<float>& scales);
+    void
+    setupGL (const std::vector<float>& centers, const std::vector<float>& rotations, const std::vector<float>& scales);
     void resizeOutput (int width, int height);
 
-    std::vector<ClockPoint> buildClockPoints (
-	const std::string& text, float aspect, float size, float originX, float originY
-    ) const;
+    std::vector<ClockPoint>
+    buildClockPoints (const std::string& text, float aspect, float size, float originX, float originY) const;
     // Advances the clock's animation and refreshes the uniform data below; true if anything changed
     bool updateClock (float aspect);
 

+ 20 - 17
src/WallpaperEngine/Render/Wallpapers/CWeb.cpp

@@ -5,18 +5,18 @@
 #include "WallpaperEngine/Application/WallpaperApplication.h"
 #include "WallpaperEngine/Logging/Log.h"
 
-#include "WallpaperEngine/Media/ThumbnailPalette.h"
-#include "WallpaperEngine/Render/RenderContext.h"
 #include "WallpaperEngine/Data/Model/Project.h"
 #include "WallpaperEngine/Data/Model/Wallpaper.h"
+#include "WallpaperEngine/Media/ThumbnailPalette.h"
+#include "WallpaperEngine/Render/RenderContext.h"
 
 #include <atomic>
+#include <cctype>
 #include <chrono>
 #include <cmath>
 #include <csignal>
 #include <cstdlib>
 #include <cstring>
-#include <cctype>
 #include <sys/prctl.h>
 #include <sys/syscall.h>
 #include <sys/wait.h>
@@ -65,15 +65,13 @@ CWeb::CWeb (
     auto* shm = this->m_shm;
 
     this->m_spectrumRecorder = &this->getAudioContext ().getRecorder ();
-    this->m_spectrumListenerId = this->m_spectrumRecorder->addSpectrumListener (
-	[shm] (const float* audio64) {
-	    for (std::size_t i = 0; i < WebHostSharedMemory::AUDIO_BANDS; i++) {
-		shm->audioBands[i].store (audio64[i], std::memory_order_relaxed);
-	    }
-
-	    shm->audioSeq.fetch_add (1, std::memory_order_release);
+    this->m_spectrumListenerId = this->m_spectrumRecorder->addSpectrumListener ([shm] (const float* audio64) {
+	for (std::size_t i = 0; i < WebHostSharedMemory::AUDIO_BANDS; i++) {
+	    shm->audioBands[i].store (audio64[i], std::memory_order_relaxed);
 	}
-    );
+
+	shm->audioSeq.fetch_add (1, std::memory_order_release);
+    });
 
     this->m_statsEnabled = std::getenv ("LWE_WEB_STATS") != nullptr;
 
@@ -207,8 +205,8 @@ void CWeb::renderFrame (const glm::ivec4& viewport) {
     // only reallocate when the size changed, glTexSubImage2D avoids a driver-side realloc/sync every frame
     if (frameWidth == this->m_uploadedTextureWidth && frameHeight == this->m_uploadedTextureHeight) {
 	glTexSubImage2D (
-	    GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei> (frameWidth), static_cast<GLsizei> (frameHeight),
-	    GL_BGRA_EXT, GL_UNSIGNED_BYTE, this->m_shm->frameBuffer (this->m_frontSlot)
+	    GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei> (frameWidth), static_cast<GLsizei> (frameHeight), GL_BGRA_EXT,
+	    GL_UNSIGNED_BYTE, this->m_shm->frameBuffer (this->m_frontSlot)
 	);
     } else {
 	glTexImage2D (
@@ -235,7 +233,8 @@ void CWeb::requestPageFrame () {
 
 	// a long gap is a pause or a stall, not the rate we're rendering at
 	if (interval < 0.1) {
-	    this->m_renderInterval = this->m_renderInterval == 0.0 ? interval : this->m_renderInterval * 0.95 + interval * 0.05;
+	    this->m_renderInterval
+		= this->m_renderInterval == 0.0 ? interval : this->m_renderInterval * 0.95 + interval * 0.05;
 
 	    // renders per 60Hz frame, only switching once clearly closer to another whole number so a rate sitting
 	    // between two (90Hz) doesn't flip back and forth
@@ -254,7 +253,9 @@ void CWeb::requestPageFrame () {
 
 	if (this->m_statsStart == std::chrono::steady_clock::time_point {}) {
 	    this->m_statsStart = now;
-	} else if (const double elapsed = std::chrono::duration<double> (now - this->m_statsStart).count (); elapsed >= 5.0) {
+	} else if (
+	    const double elapsed = std::chrono::duration<double> (now - this->m_statsStart).count (); elapsed >= 5.0
+	) {
 	    sLog.out (
 		"CWeb host ", this->m_hostPid, ": ", this->m_statsRenders / elapsed, " renders/s, ",
 		this->m_statsFrames / elapsed, " page frames/s, one page frame every ", this->m_frameRequestDivisor,
@@ -296,7 +297,8 @@ std::string coverPathFromUrl (const std::optional<std::string>& url) {
     std::string path;
 
     for (std::size_t i = 7; i < url->size (); i++) {
-	if ((*url)[i] == '%' && i + 2 < url->size () && std::isxdigit ((*url)[i + 1]) && std::isxdigit ((*url)[i + 2])) {
+	if ((*url)[i] == '%' && i + 2 < url->size () && std::isxdigit ((*url)[i + 1])
+	    && std::isxdigit ((*url)[i + 2])) {
 	    path += static_cast<char> (std::stoi (url->substr (i + 1, 2), nullptr, 16));
 	    i += 2;
 	} else {
@@ -411,7 +413,8 @@ CWeb::~CWeb () {
     if (this->m_statsEnabled) {
 	sLog.out (
 	    "CWeb host ", this->m_hostPid, exited ? " exited after " : " had to be killed after ",
-	    std::chrono::duration_cast<std::chrono::milliseconds> (std::chrono::steady_clock::now () - shutdownStart).count (),
+	    std::chrono::duration_cast<std::chrono::milliseconds> (std::chrono::steady_clock::now () - shutdownStart)
+		.count (),
 	    "ms"
 	);
     }

+ 34 - 25
src/WallpaperEngine/Scripting/Adapters/ScriptableObjectAdapter.cpp

@@ -9,9 +9,9 @@
 #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
 #include "WallpaperEngine/Logging/Log.h"
 #include "WallpaperEngine/Render/Objects/CImage.h"
-#include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
 #include "WallpaperEngine/Scripting/ScriptEngine.h"
 #include "WallpaperEngine/Scripting/ScriptableObject.h"
+#include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
 
 using namespace WallpaperEngine::Data::Model;
 using namespace WallpaperEngine::Data::Utils;
@@ -52,7 +52,8 @@ JSValue scriptableobject_hierarchy_call (
     int64_t engineAddress = 0;
     JS_ToInt64 (ctx, &objectAddress, func_data[0]);
     JS_ToInt64 (ctx, &engineAddress, func_data[1]);
-    auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
+    auto* object
+	= reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
     auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
     const auto& scene = engine->getScene ();
 
@@ -134,7 +135,8 @@ const std::vector<ImageEffectUniquePtr>* effectsOf (const Object& object) {
     return nullptr;
 }
 
-JSValue scriptableeffect_instantiate (JSContext* ctx, ImageEffect& effect, WallpaperEngine::Scripting::ScriptEngine& engine) {
+JSValue
+scriptableeffect_instantiate (JSContext* ctx, ImageEffect& effect, WallpaperEngine::Scripting::ScriptEngine& engine) {
     JSValue handle = JS_NewObject (ctx);
     JSValue data[] = {
 	JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&effect))),
@@ -159,7 +161,8 @@ JSValue scriptableobject_effect_call (
     int64_t engineAddress = 0;
     JS_ToInt64 (ctx, &objectAddress, func_data[0]);
     JS_ToInt64 (ctx, &engineAddress, func_data[1]);
-    auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
+    auto* object
+	= reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
     auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
     const auto* effects = effectsOf (object->getObject ());
 
@@ -199,7 +202,8 @@ JSValue scriptableobject_effect_call (
     return JS_UNDEFINED;
 }
 
-// video textures are only ever reached through the player's address, kept alive by the layer's texture for as long as the scene exists
+// video textures are only ever reached through the player's address, kept alive by the layer's texture for as long as
+// the scene exists
 JSValue videotexture_call (
     JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic, JSValueConst* func_data
 ) {
@@ -207,7 +211,8 @@ JSValue videotexture_call (
     int64_t engineAddress = 0;
     JS_ToInt64 (ctx, &playerAddress, func_data[0]);
     JS_ToInt64 (ctx, &engineAddress, func_data[1]);
-    auto* player = reinterpret_cast<WallpaperEngine::VideoPlayback::MPV::GLPlayer*> (static_cast<intptr_t> (playerAddress));
+    auto* player
+	= reinterpret_cast<WallpaperEngine::VideoPlayback::MPV::GLPlayer*> (static_cast<intptr_t> (playerAddress));
     auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
 
     switch (magic) {
@@ -233,15 +238,16 @@ JSValue videotexture_call (
 	    return JS_NewBool (ctx, player->isPaused () && player->getPlaybackPosition () < 0.001);
 	case 6:
 	    return JS_NewFloat64 (ctx, player->getPlaybackPosition ());
-	case 7: {
-	    double seconds = 0.0;
+	case 7:
+	    {
+		double seconds = 0.0;
 
-	    if (argc > 0 && JS_ToFloat64 (ctx, &seconds, argv[0]) == 0) {
-		player->seek (std::max (seconds, 0.0));
-	    }
+		if (argc > 0 && JS_ToFloat64 (ctx, &seconds, argv[0]) == 0) {
+		    player->seek (std::max (seconds, 0.0));
+		}
 
-	    return JS_UNDEFINED;
-	}
+		return JS_UNDEFINED;
+	    }
 	case 8:
 	    if (argc > 0) {
 		engine->addVideoEndedCallback (player, argv[0]);
@@ -260,22 +266,24 @@ JSValue videotexture_call (
 	    return JS_NewFloat64 (ctx, player->getDuration ());
 	case 12:
 	    return JS_NewFloat64 (ctx, player->getSpeed ());
-	case 13: {
-	    double rate = 0.0;
+	case 13:
+	    {
+		double rate = 0.0;
 
-	    if (argc > 0 && JS_ToFloat64 (ctx, &rate, argv[0]) == 0 && rate > 0.0) {
-		player->setSpeed (rate);
-	    }
+		if (argc > 0 && JS_ToFloat64 (ctx, &rate, argv[0]) == 0 && rate > 0.0) {
+		    player->setSpeed (rate);
+		}
 
-	    return JS_UNDEFINED;
-	}
+		return JS_UNDEFINED;
+	    }
 	default:
 	    return JS_UNDEFINED;
     }
 }
 
 JSValue videotexture_instantiate (
-    JSContext* ctx, WallpaperEngine::VideoPlayback::MPV::GLPlayer& player, WallpaperEngine::Scripting::ScriptEngine& engine
+    JSContext* ctx, WallpaperEngine::VideoPlayback::MPV::GLPlayer& player,
+    WallpaperEngine::Scripting::ScriptEngine& engine
 ) {
     JSValue handle = JS_NewObject (ctx);
     JSValue data[] = {
@@ -286,9 +294,9 @@ JSValue videotexture_instantiate (
 	const char* name;
 	int magic;
 	int length;
-    } calls[] = { { "play", 0, 0 },	       { "pause", 1, 0 },	     { "stop", 2, 0 },
-		  { "isPlaying", 3, 0 },       { "isPaused", 4, 0 },	     { "isStopped", 5, 0 },
-		  { "getCurrentTime", 6, 0 },  { "setCurrentTime", 7, 1 },   { "addEndedCallback", 8, 1 } };
+    } calls[] = { { "play", 0, 0 },           { "pause", 1, 0 },          { "stop", 2, 0 },
+		  { "isPlaying", 3, 0 },      { "isPaused", 4, 0 },       { "isStopped", 5, 0 },
+		  { "getCurrentTime", 6, 0 }, { "setCurrentTime", 7, 1 }, { "addEndedCallback", 8, 1 } };
 
     for (const auto& call : calls) {
 	JS_SetPropertyStr (
@@ -324,7 +332,8 @@ JSValue scriptableobject_video_texture_call (
     int64_t engineAddress = 0;
     JS_ToInt64 (ctx, &objectAddress, func_data[0]);
     JS_ToInt64 (ctx, &engineAddress, func_data[1]);
-    auto* object = reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
+    auto* object
+	= reinterpret_cast<WallpaperEngine::Scripting::ScriptableObject*> (static_cast<intptr_t> (objectAddress));
     auto* engine = reinterpret_cast<WallpaperEngine::Scripting::ScriptEngine*> (static_cast<intptr_t> (engineAddress));
 
     if (!object->is<WallpaperEngine::Render::Objects::CImage> ()) {

+ 7 - 7
src/WallpaperEngine/Scripting/AnimationSystem.cpp

@@ -143,9 +143,8 @@ float WallpaperEngine::Scripting::evaluateAnimationCurve (
     return sampleAnimationFrame (keys, first) * (1.0f - blend) + sampleAnimationFrame (keys, second) * blend;
 }
 
-AnimationClock::AnimationClock (
-    int id, DynamicValue& rootValue, std::shared_ptr<const PropertyAnimation> definition
-) : m_id (id), m_rootValue (&rootValue), m_definition (std::move (definition)) {
+AnimationClock::AnimationClock (int id, DynamicValue& rootValue, std::shared_ptr<const PropertyAnimation> definition) :
+    m_id (id), m_rootValue (&rootValue), m_definition (std::move (definition)) {
     m_playing = !m_definition->startPaused;
 }
 
@@ -186,9 +185,8 @@ void AnimationClock::applyBinding (Binding& binding) const {
 	    continue;
 	}
 
-	const float sampled = evaluateAnimationCurve (
-	    keys, m_frame, m_definition->fps, static_cast<int> (m_definition->length)
-	);
+	const float sampled
+	    = evaluateAnimationCurve (keys, m_frame, m_definition->fps, static_cast<int> (m_definition->length));
 
 	if (binding.data->relative) {
 	    // fold the offset in on top of whatever the value is now, so a script moving the base
@@ -400,7 +398,9 @@ void AnimationSystem::tick (float deltaSeconds) {
 
     for (const auto& clock : m_clocks) {
 	for (auto& event : clock->tick (deltaSeconds)) {
-	    m_events.push_back (PendingEvent { .clock = clock->getId (), .name = std::move (event.name), .frame = event.frame });
+	    m_events.push_back (
+		PendingEvent { .clock = clock->getId (), .name = std::move (event.name), .frame = event.frame }
+	    );
 	}
     }
 }

+ 11 - 14
src/WallpaperEngine/Scripting/EngineObject.cpp

@@ -26,9 +26,7 @@ std::map<uint32_t, EngineObject&> engineInstances;
 JSValue engine_set_value (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { return JS_UNDEFINED; }
 
 // rebuilt on every read so scripts always see the current values
-JSValue engine_get_user_properties (
-    JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic
-) {
+JSValue engine_get_user_properties (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
     JSValue result = JS_NewObject (ctx);
     const auto it = engineInstances.find (magic);
 
@@ -126,9 +124,7 @@ glm::vec2 engine_screen_size (EngineObject& engine) {
     return { camera.getWidth (), camera.getHeight () };
 }
 
-JSValue engine_get_screen_resolution (
-    JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic
-) {
+JSValue engine_get_screen_resolution (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv, int magic) {
     const auto it = engineInstances.find (magic);
 
     if (it == engineInstances.end ()) {
@@ -155,9 +151,7 @@ JSValue engine_query_orientation (JSContext* ctx, JSValueConst this_val, int arg
 
 // WE's version of this is the callback behind the stop functions setTimeout/setInterval return;
 // called straight off engine it has no timer bound to it and never stops anything
-JSValue engine_clear_timeout (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
-    return JS_FALSE;
-}
+JSValue engine_clear_timeout (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) { return JS_FALSE; }
 
 // layers are never destroyed from scripts here, so any layer handle is still valid
 JSValue engine_is_object_valid (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
@@ -165,7 +159,9 @@ JSValue engine_is_object_valid (JSContext* ctx, JSValueConst this_val, int argc,
 	return JS_FALSE;
     }
 
-    return JS_NewBool (ctx, WallpaperEngine::Scripting::Adapters::ScriptableObjectAdapter::getObject (argv[0]) != nullptr);
+    return JS_NewBool (
+	ctx, WallpaperEngine::Scripting::Adapters::ScriptableObjectAdapter::getObject (argv[0]) != nullptr
+    );
 }
 
 JSValue engine_get_frametime (JSContext* ctx, JSValueConst this_val, int argc, JSValueConst* argv) {
@@ -272,7 +268,9 @@ JSValue audio_buffer_get_values (
     const auto it = engineInstances.find (engineInstanceId);
 
     if (it == engineInstances.end ()) {
-	sLog.error ("registerAudioBuffers: no EngineObject found for instance ", engineInstanceId, " - returning zeros");
+	sLog.error (
+	    "registerAudioBuffers: no EngineObject found for instance ", engineInstanceId, " - returning zeros"
+	);
 	return result;
     }
 
@@ -453,7 +451,7 @@ EngineObject::EngineObject (ScriptEngine& engine, Render::Wallpapers::CScene& sc
 	int answer;
     } flags[] = {
 	{ "isRunningInEditor", 0 }, { "isDesktopDevice", 1 }, { "isMobileDevice", 0 },
-	{ "isWallpaper", 1 },	    { "isScreensaver", 0 },
+	{ "isWallpaper", 1 },       { "isScreensaver", 0 },
     };
     for (const auto& flag : flags) {
 	JS_DefinePropertyValueStr (
@@ -494,8 +492,7 @@ EngineObject::EngineObject (ScriptEngine& engine, Render::Wallpapers::CScene& sc
     );
     JS_DefinePropertyValueStr (
 	this->m_engine.getContext (), this->m_instance, "isObjectValid",
-	JS_NewCFunction (this->m_engine.getContext (), engine_is_object_valid, "isObjectValid", 1),
-	JS_PROP_ENUMERABLE
+	JS_NewCFunction (this->m_engine.getContext (), engine_is_object_valid, "isObjectValid", 1), JS_PROP_ENUMERABLE
     );
 }
 

+ 40 - 20
src/WallpaperEngine/Scripting/SceneObject.cpp

@@ -161,7 +161,8 @@ JSValue sound_layer_call (
     }
 }
 
-JSValue instantiate_sound_layer (JSContext* ctx, WallpaperEngine::Render::Wallpapers::CScene& scene, const Sound& sound) {
+JSValue
+instantiate_sound_layer (JSContext* ctx, WallpaperEngine::Render::Wallpapers::CScene& scene, const Sound& sound) {
     JSValue handle = JS_NewObject (ctx);
     JSValue data[] = { JS_NewInt64 (ctx, static_cast<int64_t> (reinterpret_cast<intptr_t> (&scene))),
 		       JS_NewInt32 (ctx, sound.id), JS_NewInt32 (ctx, sound.startsilent.value_or (false) ? 1 : 0) };
@@ -469,102 +470,121 @@ SceneObject::SceneObject (ScriptEngine& engine, Render::Wallpapers::CScene& scen
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "bloom"),
 	JS_NewCFunction (this->m_engine.getContext (), get_bloom, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 0), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 0),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "bloomstrength"),
 	JS_NewCFunction (this->m_engine.getContext (), get_bloomstrength, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 1), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 1),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "bloomthreshold"),
 	JS_NewCFunction (this->m_engine.getContext (), get_bloomthreshold, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 2), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 2),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "clearenabled"),
 	JS_NewCFunction (this->m_engine.getContext (), get_clearenabled, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 3), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 3),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "clearcolor"),
 	JS_NewCFunction (this->m_engine.getContext (), get_clearcolor, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 4), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 4),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "ambientcolor"),
 	JS_NewCFunction (this->m_engine.getContext (), get_ambientcolor, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 5), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 5),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "skylightcolor"),
 	JS_NewCFunction (this->m_engine.getContext (), get_skylightcolor, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 6), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 6),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "fov"),
 	JS_NewCFunction (this->m_engine.getContext (), get_fov, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 7), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 7),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "nearz"),
 	JS_NewCFunction (this->m_engine.getContext (), get_nearz, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 8), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 8),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "farz"),
 	JS_NewCFunction (this->m_engine.getContext (), get_farz, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 9), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 9),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "camerafade"),
 	JS_NewCFunction (this->m_engine.getContext (), get_camerafade, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 10), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 10),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "camerashake"),
 	JS_NewCFunction (this->m_engine.getContext (), get_camerashake, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 11), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 11),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "camerashakespeed"),
 	JS_NewCFunction (this->m_engine.getContext (), get_camerashakespeed, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 12), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 12),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance,
 	JS_NewAtom (this->m_engine.getContext (), "camerashakeamplitude"),
 	JS_NewCFunction (this->m_engine.getContext (), get_camerashakeamplitude, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 13), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 13),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance,
 	JS_NewAtom (this->m_engine.getContext (), "camerashakeroughness"),
 	JS_NewCFunction (this->m_engine.getContext (), get_camerashakeroughness, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 14), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 14),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance, JS_NewAtom (this->m_engine.getContext (), "cameraparallax"),
 	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallax, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 15), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 15),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance,
 	JS_NewAtom (this->m_engine.getContext (), "cameraparallaxamount"),
 	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallaxamount, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 16), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 16),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance,
 	JS_NewAtom (this->m_engine.getContext (), "cameraparallaxdelay"),
 	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallaxdelay, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 17), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 17),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyGetSet (
 	this->m_engine.getContext (), this->m_instance,
 	JS_NewAtom (this->m_engine.getContext (), "cameraparallaxmouseinfluence"),
 	JS_NewCFunction (this->m_engine.getContext (), get_cameraparallaxmouseinfluence, "get", 0),
-	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 18), JS_PROP_ENUMERABLE
+	JS_NewCFunctionMagic (this->m_engine.getContext (), scene_set_value, "set", 1, JS_CFUNC_generic_magic, 18),
+	JS_PROP_ENUMERABLE
     );
     JS_DefinePropertyValueStr (
 	this->m_engine.getContext (), this->m_instance, "getLayer",

+ 9 - 8
src/WallpaperEngine/Scripting/ScriptEngine.cpp

@@ -1,16 +1,16 @@
 #include "ScriptEngine.h"
 
-#include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
 #include "WallpaperEngine/Media/ThumbnailPalette.h"
+#include "WallpaperEngine/VideoPlayback/MPV/GLPlayer.h"
 
 #include "Adapters/ScriptableObjectAdapter.h"
 #include "ScriptPropertiesObject.h"
 #include "ScriptableObject.h"
 #include "WallpaperEngine/Audio/AudioContext.h"
 #include "WallpaperEngine/Audio/Drivers/Recorders/PlaybackRecorder.h"
-#include "WallpaperEngine/Desktop/UserShortcut.h"
 #include "WallpaperEngine/Data/Model/Property.h"
 #include "WallpaperEngine/Data/Utils/ScopeGuard.h"
+#include "WallpaperEngine/Desktop/UserShortcut.h"
 #include "WallpaperEngine/Logging/Log.h"
 #include "WallpaperEngine/Render/CObject.h"
 #include "WallpaperEngine/Render/Objects/CSound.h"
@@ -1095,7 +1095,9 @@ void ScriptEngine::dispatchAnimationEvents () {
 	    }
 
 	    JSValue eventObject = JS_NewObject (this->m_context);
-	    JS_SetPropertyStr (this->m_context, eventObject, "name", JS_NewString (this->m_context, event.name.c_str ()));
+	    JS_SetPropertyStr (
+		this->m_context, eventObject, "name", JS_NewString (this->m_context, event.name.c_str ())
+	    );
 	    JS_SetPropertyStr (this->m_context, eventObject, "frame", JS_NewFloat64 (this->m_context, event.frame));
 
 	    JSValue args[] = { eventObject, this->dynamicToJs (module.value) };
@@ -1161,8 +1163,8 @@ void ScriptEngine::initializeModule (const std::string& key, LoadedModule& modul
 }
 
 namespace {
-constexpr const char* CURSOR_HANDLERS[] = { "cursorEnter", "cursorLeave", "cursorMove",
-					    "cursorDown",  "cursorUp",	  "cursorClick" };
+constexpr const char* CURSOR_HANDLERS[]
+    = { "cursorEnter", "cursorLeave", "cursorMove", "cursorDown", "cursorUp", "cursorClick" };
 }
 
 bool ScriptEngine::hasCursorHandlers (const ScriptableObject& object) {
@@ -1314,9 +1316,8 @@ void ScriptEngine::tick () {
 	    if (++scaleDiagnosticCounter >= 300) {
 		scaleDiagnosticCounter = 0;
 		sLog.debug (
-		    "scale script '", key, "': update() returned tag=", JS_VALUE_GET_TAG (result),
-		    ", current vec3 = (", module.value.getVec3 ().x, ", ", module.value.getVec3 ().y, ", ",
-		    module.value.getVec3 ().z, ")"
+		    "scale script '", key, "': update() returned tag=", JS_VALUE_GET_TAG (result), ", current vec3 = (",
+		    module.value.getVec3 ().x, ", ", module.value.getVec3 ().y, ", ", module.value.getVec3 ().z, ")"
 		);
 	    }
 

+ 12 - 6
src/WallpaperEngine/Scripting/ScriptEngine.h

@@ -100,13 +100,16 @@ public:
      * @return The modified value from update(), or a copy of currentValue on error
      */
     void queueScript (
-	const std::string& key, DynamicValue& currentValue, ScriptableObject& object, const std::string& propertyName = {}
+	const std::string& key, DynamicValue& currentValue, ScriptableObject& object,
+	const std::string& propertyName = {}
     );
 
-    /** Stops a queued script module (by its queueScript() key) and frees it at the start of the next tick(), for when a later registerProperty() supersedes it */
+    /** Stops a queued script module (by its queueScript() key) and frees it at the start of the next tick(), for when a
+     * later registerProperty() supersedes it */
     void retireScript (const std::string& key);
 
-    /** Rebinds an already-running module under key to newValue in place when its script source is identical, so init() does not run twice. Returns false if nothing was rebound */
+    /** Rebinds an already-running module under key to newValue in place when its script source is identical, so init()
+     * does not run twice. Returns false if nothing was rebound */
     bool rebindScript (const std::string& key, DynamicValue& newValue);
 
     /**
@@ -165,13 +168,15 @@ public:
     [[nodiscard]] bool hasCursorHandlers (const ScriptableObject& object);
     /**
      * Calls `handler` (cursorEnter/cursorLeave/cursorMove/cursorDown/cursorUp/cursorClick) on every script running on
-     * the object with an event carrying worldPosition (scene coordinates) and localPosition (offset from the layer's center)
+     * the object with an event carrying worldPosition (scene coordinates) and localPosition (offset from the layer's
+     * center)
      */
     void dispatchCursorEvent (
 	const char* handler, ScriptableObject& object, const glm::vec2& worldPosition, const glm::vec2& localPosition
     );
 
-    /** Calls callback (once per playthrough) when player reaches the end of a non-looping video, for IVideoTexture.addEndedCallback() */
+    /** Calls callback (once per playthrough) when player reaches the end of a non-looping video, for
+     * IVideoTexture.addEndedCallback() */
     void addVideoEndedCallback (VideoPlayback::MPV::GLPlayer* player, JSValueConst callback);
 
     AnimationSystem& getAnimations () { return m_animations; }
@@ -179,7 +184,8 @@ public:
     [[nodiscard]] bool hasScript (const DynamicValue& value) const;
     const JSObjectAdapters& getAdapters () const { return m_adapters; }
     const Render::Wallpapers::CScene& getScene () const { return m_scene; }
-    /** Reads a file through the wallpaper's asset locator (project first, then the assets dir), nullopt if it is missing */
+    /** Reads a file through the wallpaper's asset locator (project first, then the assets dir), nullopt if it is
+     * missing */
     std::optional<std::string> readScriptAsset (const std::string& path) const;
 
 private:

+ 3 - 4
src/WallpaperEngine/Scripting/ScriptableObject.cpp

@@ -53,8 +53,7 @@ void ScriptableObject::registerEffectConstants (const std::vector<ImageEffectUni
 		}
 
 		this->registerProperty (
-		    prefix + constant, *setting->value,
-		    "obj" + std::to_string (this->getId ()) + "/" + prefix, constant
+		    prefix + constant, *setting->value, "obj" + std::to_string (this->getId ()) + "/" + prefix, constant
 		);
 	    }
 	}
@@ -89,8 +88,8 @@ void ScriptableObject::registerProperty (
 
     // Includes the DynamicValue's own address so two different registrations under the same
     // name (see above) never end up sharing a script engine key/module filename.
-    const std::string key = name + "_" + std::to_string (this->getId ()) + "_"
-	+ std::to_string (reinterpret_cast<uintptr_t> (&value));
+    const std::string key
+	= name + "_" + std::to_string (this->getId ()) + "_" + std::to_string (reinterpret_cast<uintptr_t> (&value));
 
     const auto inserted = this->m_properties.emplace (name, PropertyEntry { .key = key, .value = value });
 

+ 4 - 2
src/WallpaperEngine/Scripting/ScriptableObject.h

@@ -27,7 +27,8 @@ public:
 
     enum class Playback { Playing, Paused, Stopped };
 
-    /** thisLayer.play()/pause()/stop() from scripts, kept here since init() can call it before the derived object exists */
+    /** thisLayer.play()/pause()/stop() from scripts, kept here since init() can call it before the derived object
+     * exists */
     void setPlayback (Playback playback) { this->m_playback = playback; }
     [[nodiscard]] Playback getPlayback () const { return this->m_playback; }
     [[nodiscard]] virtual bool isPlaying () const { return this->m_playback == Playback::Playing; }
@@ -37,7 +38,8 @@ protected:
 	const std::string& name, DynamicValue& value, const std::string& animationGroup = {},
 	const std::string& animationKey = {}
     );
-    /** Effect pass constants (Radius, Area, ...) only matter to the script engine when they carry a script or animation */
+    /** Effect pass constants (Radius, Area, ...) only matter to the script engine when they carry a script or animation
+     */
     void registerEffectConstants (const std::vector<ImageEffectUniquePtr>& effects);
 
 private:

+ 30 - 5
src/WallpaperEngine/Splat/SogLoader.cpp

@@ -192,11 +192,36 @@ SplatCloud loadSog (const std::string& metaText, const FileReader& readFile) {
 	float x, y, z, w;
 
 	switch (quat[3] - 252) {
-	    case 0: x = a; y = b; z = c; w = d; break;
-	    case 1: x = d; y = b; z = c; w = a; break;
-	    case 2: x = b; y = d; z = c; w = a; break;
-	    case 3: x = b; y = c; z = d; w = a; break;
-	    default: x = 0.0f; y = 0.0f; z = 0.0f; w = 1.0f; break;
+	    case 0:
+		x = a;
+		y = b;
+		z = c;
+		w = d;
+		break;
+	    case 1:
+		x = d;
+		y = b;
+		z = c;
+		w = a;
+		break;
+	    case 2:
+		x = b;
+		y = d;
+		z = c;
+		w = a;
+		break;
+	    case 3:
+		x = b;
+		y = c;
+		z = d;
+		w = a;
+		break;
+	    default:
+		x = 0.0f;
+		y = 0.0f;
+		z = 0.0f;
+		w = 1.0f;
+		break;
 	}
 
 	const float length = std::sqrt (x * x + y * y + z * z + w * w);

+ 1 - 1
src/WallpaperEngine/Testing/Cases/Localization.cpp

@@ -79,7 +79,7 @@ TEST_CASE ("Property texts and combo labels of a whole project are localized") {
     };
 
     const auto localization = Localization (data.optional ("general"), "en-us");
-    (void) localization;
+    (void)localization;
 
     // ProjectParser needs an asset locator to build a Project, so the pieces are checked separately above and the
     // combo relabeling here through the property itself

+ 18 - 15
src/WallpaperEngine/Testing/Cases/ModuleNamespaceTiming.cpp

@@ -32,7 +32,8 @@ TEST_CASE ("QuickJS module top-level code sees external state set before JS_Eval
 
     const char* source = "export var x = probe();";
 
-    JSValue compiled = JS_Eval (ctx, source, strlen (source), "<test-module>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
+    JSValue compiled
+	= JS_Eval (ctx, source, strlen (source), "<test-module>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
 
     REQUIRE_FALSE (JS_IsException (compiled));
 
@@ -97,9 +98,7 @@ TEST_CASE ("Duplicating this_val before returning it from a chained method keeps
     JS_NewClass (rt, g_chainableClassId, &classDef);
 
     JSValue proto = JS_NewObject (ctx);
-    JS_DefinePropertyValueStr (
-	ctx, proto, "add", JS_NewCFunction (ctx, chainFixed, "add", 0), JS_PROP_ENUMERABLE
-    );
+    JS_DefinePropertyValueStr (ctx, proto, "add", JS_NewCFunction (ctx, chainFixed, "add", 0), JS_PROP_ENUMERABLE);
     JS_SetClassProto (ctx, g_chainableClassId, proto);
 
     JSValue globalObj = JS_GetGlobalObject (ctx);
@@ -107,9 +106,8 @@ TEST_CASE ("Duplicating this_val before returning it from a chained method keeps
     JS_FreeValue (ctx, globalObj);
 
     const char* source = "export var x = make().add().add().add().add();";
-    JSValue compiled = JS_Eval (
-	ctx, source, strlen (source), "<test-chain-fixed>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY
-    );
+    JSValue compiled
+	= JS_Eval (ctx, source, strlen (source), "<test-chain-fixed>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
 
     REQUIRE_FALSE (JS_IsException (compiled));
 
@@ -144,7 +142,9 @@ TEST_CASE ("A module-level throw does not surface via JS_IsException on JS_EvalF
     // call on that throws - but as a *module-level* throw, not a C-level JS_EXCEPTION.
     const char* source = "export var x = undefined.someMethod();";
 
-    JSValue compiled = JS_Eval (ctx, source, strlen (source), "<test-throwing-module>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
+    JSValue compiled = JS_Eval (
+	ctx, source, strlen (source), "<test-throwing-module>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY
+    );
 
     REQUIRE_FALSE (JS_IsException (compiled));
 
@@ -192,8 +192,10 @@ JSValue exoticGetOnlyX (JSContext* ctx, JSValueConst /*obj_val*/, JSAtom atom, J
 }
 } // namespace
 
-TEST_CASE ("An exotic get_property handler that only special-cases some names must still reach "
-	   "prototype methods for everything else") {
+TEST_CASE (
+    "An exotic get_property handler that only special-cases some names must still reach "
+    "prototype methods for everything else"
+) {
     JSRuntime* rt = JS_NewRuntime ();
     JSContext* ctx = JS_NewContext (rt);
 
@@ -206,9 +208,8 @@ TEST_CASE ("An exotic get_property handler that only special-cases some names mu
     JS_SetPropertyStr (
 	ctx, proto, "double",
 	JS_NewCFunction (
-	    ctx,
-	    [] (JSContext* ctx, JSValueConst, int, JSValueConst*) -> JSValue { return JS_NewInt32 (ctx, 84); }, "double",
-	    0
+	    ctx, [] (JSContext* ctx, JSValueConst, int, JSValueConst*) -> JSValue { return JS_NewInt32 (ctx, 84); },
+	    "double", 0
 	)
     );
     JS_SetClassProto (ctx, g_exoticProtoClassId, proto);
@@ -276,8 +277,10 @@ void ownedFinalizer (JSRuntime* /*rt*/, JSValueConst val) {
 }
 } // namespace
 
-TEST_CASE ("An owning adapter freed after JS_FreeRuntime sees its still-live instances finalized "
-	   "safely") {
+TEST_CASE (
+    "An owning adapter freed after JS_FreeRuntime sees its still-live instances finalized "
+    "safely"
+) {
     JSRuntime* rt = JS_NewRuntime ();
     JSContext* ctx = JS_NewContext (rt);
 

+ 4 - 2
src/WallpaperEngine/Testing/Cases/PropertyParser.cpp

@@ -33,7 +33,8 @@ TEST_CASE ("Directory properties are parsed as file-like properties") {
 }
 TEST_CASE ("Directory properties remember they are directories and what file type they hold") {
     const auto directory = PropertyParser::parse (
-	JSON { { "type", "directory" }, { "text", "Videos" }, { "fileType", "video" }, { "mode", "fetchall" } }, "vidDir"
+	JSON { { "type", "directory" }, { "text", "Videos" }, { "fileType", "video" }, { "mode", "fetchall" } },
+	"vidDir"
     );
     const auto file = PropertyParser::parse (JSON { { "type", "file" }, { "text", "Image" } }, "image");
 
@@ -65,7 +66,8 @@ TEST_CASE ("Combo properties can be given a value the wallpaper did not list") {
 	{ "type", "combo" },
 	{ "text", "Interval" },
 	{ "value", 1 },
-	{ "options", JSON::array ({ { { "label", "1 min" }, { "value", 1 } }, { { "label", "5 min" }, { "value", 5 } } }) },
+	{ "options",
+	  JSON::array ({ { { "label", "1 min" }, { "value", 1 } }, { { "label", "5 min" }, { "value", 5 } } }) },
     };
 
     const auto property = PropertyParser::parse (propertyData, "interval");

+ 2 - 1
src/WallpaperEngine/Testing/Cases/SpectrumPipeline.cpp

@@ -17,7 +17,8 @@ std::vector<float> stereoTone (int frames, float frequency, int rate, float left
     std::vector<float> samples (frames * 2);
 
     for (int i = 0; i < frames; i++) {
-	const float value = std::sin (2.0f * 3.1415927f * frequency * static_cast<float> (i) / static_cast<float> (rate));
+	const float value
+	    = std::sin (2.0f * 3.1415927f * frequency * static_cast<float> (i) / static_cast<float> (rate));
 
 	samples[i * 2] = value * leftAmplitude;
 	samples[i * 2 + 1] = value * rightAmplitude;

+ 0 - 1
src/WallpaperEngine/Testing/Cases/TextInputProperty.cpp

@@ -21,7 +21,6 @@ TEST_CASE ("Text input properties keep their string value without JSON quotes")
     CHECK (property->getString () == "HH:mm");
 }
 
-
 TEST_CASE ("Text input properties without a value default to an empty string") {
     const JSON propertyData = {
 	{ "type", "textinput" },

+ 2 - 1
src/WallpaperEngine/Testing/Cases/TextLayoutRules.cpp

@@ -79,7 +79,8 @@ TEST_CASE ("Text layout follows wallpaper64.exe's line rules") {
 
 	REQUIRE (justified.valid);
 	CHECK (justified.lines == wrapped.lines);
-	// wrapped lines span exactly maxwidth from their own left edge, the unwrapped last line may reach a pixel further
+	// wrapped lines span exactly maxwidth from their own left edge, the unwrapped last line may reach a pixel
+	// further
 	CHECK (width (justified) >= 300.0f);
 	CHECK (width (justified) <= 302.0f);
     }

+ 3 - 1
src/WallpaperEngine/Testing/Cases/ThumbnailPalette.cpp

@@ -7,7 +7,9 @@
 using WallpaperEngine::Media::computeThumbnailPalette;
 
 namespace {
-void fill (std::vector<uint8_t>& image, size_t width, size_t x0, size_t x1, size_t height, uint8_t r, uint8_t g, uint8_t b) {
+void fill (
+    std::vector<uint8_t>& image, size_t width, size_t x0, size_t x1, size_t height, uint8_t r, uint8_t g, uint8_t b
+) {
     for (size_t y = 0; y < height; y++) {
 	for (size_t x = x0; x < x1; x++) {
 	    uint8_t* pixel = &image[(y * width + x) * 4];

+ 2 - 3
src/WallpaperEngine/Testing/Cases/UserShortcut.cpp

@@ -63,9 +63,8 @@ TEST_CASE ("Command shortcuts run through the shell") {
     const auto dir = scratchDir ();
     const auto marker = dir / "ran";
 
-    const auto shortcut = UserShortcut::parse (
-	R"({"commandtype":"command","file":"touch","arguments":")" + marker.string () + R"("})"
-    );
+    const auto shortcut
+	= UserShortcut::parse (R"({"commandtype":"command","file":"touch","arguments":")" + marker.string () + R"("})");
 
     REQUIRE (shortcut.has_value ());
     CHECK (shortcut->launch ());

+ 4 - 3
src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.cpp

@@ -2,10 +2,10 @@
 
 #include "WallpaperEngine/Logging/Log.h"
 
-#include <mpv/render_gl.h>
-#include <mpv/stream_cb.h>
 #include <chrono>
 #include <cstdlib>
+#include <mpv/render_gl.h>
+#include <mpv/stream_cb.h>
 #include <string_view>
 #include <vector>
 
@@ -455,7 +455,8 @@ void GLPlayer::stop () {
     // drop ourselves from the active players list, but only if we're still the one registered
     // (a newer player for the same path may have already taken over the slot)
     if (this->m_file.has_value ()) {
-	if (const auto it = s_activePlayers.find (this->m_file.value ()); it != s_activePlayers.end () && it->second == this) {
+	if (const auto it = s_activePlayers.find (this->m_file.value ());
+	    it != s_activePlayers.end () && it->second == this) {
 	    s_activePlayers.erase (it);
 	}
     }

+ 2 - 1
src/WallpaperEngine/VideoPlayback/MPV/GLPlayer.h

@@ -47,7 +47,8 @@ public:
     void setPaused ();
     void clearPaused ();
     void setLoop (bool loop);
-    /** Jumps to a position in seconds, remembered and applied once the file has loaded if playback hasn't got that far */
+    /** Jumps to a position in seconds, remembered and applied once the file has loaded if playback hasn't got that far
+     */
     void seek (double seconds);
     /** Whether a non-looping video has played through to its end since the last seek */
     [[nodiscard]] bool hasEnded () const { return this->m_ended; }

+ 33 - 26
src/WallpaperEngine/WebBrowser/CEF/BrowserClient.cpp

@@ -17,38 +17,45 @@ nlohmann::json toPageValue (const DynamicValue& property) {
     std::ostringstream ss;
 
     switch (property.getType ()) {
-	case DynamicValue::Boolean: return property.getBool ();
-	case DynamicValue::Int: return property.getInt ();
-	case DynamicValue::Float: return property.getFloat ();
-	case DynamicValue::Vec2: ss << property.getVec2 ().x << " " << property.getVec2 ().y; return ss.str ();
+	case DynamicValue::Boolean:
+	    return property.getBool ();
+	case DynamicValue::Int:
+	    return property.getInt ();
+	case DynamicValue::Float:
+	    return property.getFloat ();
+	case DynamicValue::Vec2:
+	    ss << property.getVec2 ().x << " " << property.getVec2 ().y;
+	    return ss.str ();
 	case DynamicValue::Vec3:
 	case DynamicValue::Vec4:
 	    ss << property.getVec3 ().x << " " << property.getVec3 ().y << " " << property.getVec3 ().z;
 	    return ss.str ();
-	case DynamicValue::String: {
-	    const auto& text = property.getString ();
+	case DynamicValue::String:
+	    {
+		const auto& text = property.getString ();
 
-	    if (dynamic_cast<const PropertyCombo*> (&property) == nullptr) {
-		return text;
-	    }
+		if (dynamic_cast<const PropertyCombo*> (&property) == nullptr) {
+		    return text;
+		}
 
-	    int asInt = 0;
-	    const auto end = text.data () + text.size ();
+		int asInt = 0;
+		const auto end = text.data () + text.size ();
 
-	    if (!text.empty () && std::from_chars (text.data (), end, asInt).ptr == end) {
-		return asInt;
-	    }
+		if (!text.empty () && std::from_chars (text.data (), end, asInt).ptr == end) {
+		    return asInt;
+		}
 
-	    char* parsedEnd = nullptr;
-	    const double asNumber = std::strtod (text.c_str (), &parsedEnd);
+		char* parsedEnd = nullptr;
+		const double asNumber = std::strtod (text.c_str (), &parsedEnd);
 
-	    if (!text.empty () && parsedEnd == text.c_str () + text.size ()) {
-		return asNumber;
-	    }
+		if (!text.empty () && parsedEnd == text.c_str () + text.size ()) {
+		    return asNumber;
+		}
 
-	    return text;
-	}
-	default: return property.toString ();
+		return text;
+	    }
+	default:
+	    return property.toString ();
     }
 }
 } // namespace
@@ -67,8 +74,7 @@ CefRefPtr<CefLoadHandler> BrowserClient::GetLoadHandler () { return this; }
 void BrowserClient::OnBeforeClose (CefRefPtr<CefBrowser> browser) { this->m_closed = true; }
 
 bool BrowserClient::OnConsoleMessage (
-    CefRefPtr<CefBrowser> browser, cef_log_severity_t level, const CefString& message, const CefString& source,
-    int line
+    CefRefPtr<CefBrowser> browser, cef_log_severity_t level, const CefString& message, const CefString& source, int line
 ) {
     // some wallpapers log continuously, below warnings only the first few messages are forwarded
     static std::atomic<int> chatterBudget = 60;
@@ -118,7 +124,7 @@ void BrowserClient::OnLoadError (
     const CefString& failedUrl
 ) {
     std::cout << "[web load error] " << failedUrl.ToString () << ": " << errorText.ToString () << " (" << errorCode
-	       << ")" << std::endl;
+	      << ")" << std::endl;
 }
 
 void BrowserClient::OnLoadEnd (CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int httpStatusCode) {
@@ -138,7 +144,8 @@ void BrowserClient::OnLoadEnd (CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame
     // defined yet
     const std::string script = "if (window.__lweProperties) { window.__lweProperties(" + props.dump ()
 	+ "); } else if (window.wallpaperPropertyListener && window.wallpaperPropertyListener.applyUserProperties) { "
-	  "window.wallpaperPropertyListener.applyUserProperties(" + props.dump () + "); }";
+	  "window.wallpaperPropertyListener.applyUserProperties("
+	+ props.dump () + "); }";
 
     frame->ExecuteJavaScript (script, frame->GetURL (), 0);
     this->m_loaded = true;

+ 14 - 12
src/WallpaperEngine/WebBrowser/CEF/PageBridge.cpp

@@ -41,8 +41,8 @@ std::string lowerExtension (const std::filesystem::path& path) {
 
 // Same split Wallpaper Engine makes for directory properties: images unless the project asks for videos
 bool matchesFileType (const std::filesystem::path& path, const std::string& fileType) {
-    static const std::array images = { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tga", ".tif", ".tiff",
-				       ".avif", ".jfif", ".svg" };
+    static const std::array images
+	= { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tga", ".tif", ".tiff", ".avif", ".jfif", ".svg" };
     static const std::array videos = { ".mp4", ".webm", ".mkv", ".avi", ".mov", ".m4v", ".ogv", ".wmv", ".flv" };
 
     const std::string extension = lowerExtension (path);
@@ -234,12 +234,9 @@ void PageBridge::forwardMedia (CefRefPtr<CefFrame> frame) {
 	this->m_lastCoverVersion = coverVersion;
 
 	event["thumbnail"] = {
-	    { "thumbnail", coverDataUrl (coverPath) },
-	    { "primaryColor", cssColor (palette[0]) },
-	    { "secondaryColor", cssColor (palette[1]) },
-	    { "tertiaryColor", cssColor (palette[2]) },
-	    { "textColor", cssColor (palette[3]) },
-	    { "highContrastColor", cssColor (palette[4]) },
+	    { "thumbnail", coverDataUrl (coverPath) },   { "primaryColor", cssColor (palette[0]) },
+	    { "secondaryColor", cssColor (palette[1]) }, { "tertiaryColor", cssColor (palette[2]) },
+	    { "textColor", cssColor (palette[3]) },      { "highContrastColor", cssColor (palette[4]) },
 	};
     }
 
@@ -266,10 +263,13 @@ void PageBridge::scanDirectories (CefRefPtr<CefFrame> frame) {
 
 	if (!directory.empty () && !std::filesystem::is_directory (directory, error)) {
 	    if (this->m_reportedMissing.insert (name).second) {
-		sLog.error ("--web-host: directory property ", name, " points to a folder that does not exist: ", directory);
+		sLog.error (
+		    "--web-host: directory property ", name, " points to a folder that does not exist: ", directory
+		);
 	    }
 	} else if (!directory.empty ()) {
-	    for (std::filesystem::directory_iterator it (directory, error), end; !error && it != end; it.increment (error)) {
+	    for (std::filesystem::directory_iterator it (directory, error), end; !error && it != end;
+		 it.increment (error)) {
 		const auto& entry = *it;
 		std::error_code entryError;
 
@@ -309,14 +309,16 @@ void PageBridge::scanDirectories (CefRefPtr<CefFrame> frame) {
 
 	if (!changed.empty ()) {
 	    runScript (
-		frame, "window.__lweDirectory&&window.__lweDirectory('added'," + toJson (name) + "," + toJson (changed) + ");"
+		frame,
+		"window.__lweDirectory&&window.__lweDirectory('added'," + toJson (name) + "," + toJson (changed) + ");"
 	    );
 	}
 
 	if (!removed.empty ()) {
 	    runScript (
 		frame,
-		"window.__lweDirectory&&window.__lweDirectory('removed'," + toJson (name) + "," + toJson (removed) + ");"
+		"window.__lweDirectory&&window.__lweDirectory('removed'," + toJson (name) + "," + toJson (removed)
+		    + ");"
 	    );
 	}
     }

+ 5 - 3
src/WallpaperEngine/WebBrowser/CEF/SharedMemoryRenderHandler.cpp

@@ -8,7 +8,8 @@ using namespace WallpaperEngine::WebBrowser::IPC;
 SharedMemoryRenderHandler::SharedMemoryRenderHandler (WebHostSharedMemory* shm) : m_shm (shm) { }
 
 void SharedMemoryRenderHandler::GetViewRect (CefRefPtr<CefBrowser> browser, CefRect& rect) {
-    const uint32_t width = std::clamp (this->m_shm->desiredWidth.load (std::memory_order_relaxed), 1u, this->m_shm->maxWidth);
+    const uint32_t width
+	= std::clamp (this->m_shm->desiredWidth.load (std::memory_order_relaxed), 1u, this->m_shm->maxWidth);
     const uint32_t height
 	= std::clamp (this->m_shm->desiredHeight.load (std::memory_order_relaxed), 1u, this->m_shm->maxHeight);
 
@@ -35,8 +36,9 @@ void SharedMemoryRenderHandler::OnPaint (
     this->m_shm->slotWidth[this->m_backSlot].store (static_cast<uint32_t> (width), std::memory_order_relaxed);
     this->m_shm->slotHeight[this->m_backSlot].store (static_cast<uint32_t> (height), std::memory_order_relaxed);
 
-    const uint32_t previous
-	= this->m_shm->frameSlot.exchange (this->m_backSlot | WebHostSharedMemory::FRAME_DIRTY, std::memory_order_acq_rel);
+    const uint32_t previous = this->m_shm->frameSlot.exchange (
+	this->m_backSlot | WebHostSharedMemory::FRAME_DIRTY, std::memory_order_acq_rel
+    );
 
     this->m_backSlot = previous & WebHostSharedMemory::FRAME_SLOT_MASK;
 }

+ 2 - 2
src/WallpaperEngine/WebBrowser/CEF/SharedMemoryRenderHandler.h

@@ -19,8 +19,8 @@ public:
     void GetViewRect (CefRefPtr<CefBrowser> browser, CefRect& rect) override;
 
     void OnPaint (
-	CefRefPtr<CefBrowser> browser, PaintElementType type, const RectList& dirtyRects, const void* buffer,
-	int width, int height
+	CefRefPtr<CefBrowser> browser, PaintElementType type, const RectList& dirtyRects, const void* buffer, int width,
+	int height
     ) override;
 
     IMPLEMENT_REFCOUNTING (SharedMemoryRenderHandler);

+ 9 - 5
src/WallpaperEngine/WebBrowser/CEF/WPSchemeHandler.cpp

@@ -1,8 +1,8 @@
 #include "WPSchemeHandler.h"
 #include "WallpaperEngine/Assets/AssetLoadException.h"
-#include <filesystem>
 #include <algorithm>
 #include <cstdlib>
+#include <filesystem>
 #include <fstream>
 #include <iostream>
 
@@ -93,12 +93,14 @@ bool WPSchemeHandler::Open (CefRefPtr<CefRequest> request, bool& handle_request,
 	}
 
 	if (file.starts_with (LOCAL_FILE_PREFIX)) {
-	    // only files the user picked through (or that live in a folder picked through) this wallpaper's own properties
+	    // only files the user picked through (or that live in a folder picked through) this wallpaper's own
+	    // properties
 	    const std::filesystem::path local = "/" + file.substr (std::char_traits<char>::length (LOCAL_FILE_PREFIX));
 
 	    if (!isPropertyFile (this->m_project, local) || !std::filesystem::is_regular_file (local)) {
 		throw AssetLoadException (
-		    "Not a file or directory property of this wallpaper", local, std::make_error_code (std::errc::permission_denied)
+		    "Not a file or directory property of this wallpaper", local,
+		    std::make_error_code (std::errc::permission_denied)
 		);
 	    }
 
@@ -109,7 +111,8 @@ bool WPSchemeHandler::Open (CefRefPtr<CefRequest> request, bool& handle_request,
 	    this->m_remaining = this->m_totalSize;
 
 	    // "bytes=<start>-[<end>]", the only form media elements send
-	    if (const std::string range = request->GetHeaderByName ("Range").ToString (); range.starts_with ("bytes=")) {
+	    if (const std::string range = request->GetHeaderByName ("Range").ToString ();
+		range.starts_with ("bytes=")) {
 		const auto dash = range.find ('-');
 		const int64_t start = std::atoll (range.c_str () + 6);
 		const int64_t last = dash != std::string::npos && dash + 1 < range.size ()
@@ -159,7 +162,8 @@ void WPSchemeHandler::GetResponseHeaders (
 	    response->SetHeaderByName (
 		"Content-Range",
 		"bytes " + std::to_string (this->m_rangeStart) + "-"
-		    + std::to_string (this->m_rangeStart + this->m_remaining - 1) + "/" + std::to_string (this->m_totalSize),
+		    + std::to_string (this->m_rangeStart + this->m_remaining - 1) + "/"
+		    + std::to_string (this->m_totalSize),
 		true
 	    );
 	} else {

+ 1 - 1
src/WallpaperEngine/WebBrowser/CEF/WPSchemeHandlerFactory.cpp

@@ -44,7 +44,7 @@ CefRefPtr<CefResourceHandler> WPSchemeHandlerFactory::Create (
     }
 
     std::cout << "WPSchemeHandlerFactory: no loaded project for workshop id " << workshopId
-	       << " (request: " << request->GetURL ().ToString () << ")" << std::endl;
+	      << " (request: " << request->GetURL ().ToString () << ")" << std::endl;
 
     return nullptr;
 }

+ 3 - 4
src/WallpaperEngine/WebBrowser/IPC/WebHostSharedMemory.cpp

@@ -5,9 +5,9 @@
 #include <climits>
 #include <csignal>
 #include <cstring>
-#include <filesystem>
 #include <ctime>
 #include <fcntl.h>
+#include <filesystem>
 #include <linux/futex.h>
 #include <new>
 #include <sys/mman.h>
@@ -92,9 +92,8 @@ void WallpaperEngine::WebBrowser::IPC::removeStaleWebHostFiles () {
     }
 }
 
-WebHostSharedMemory* WallpaperEngine::WebBrowser::IPC::createSharedMemory (
-    std::string& outName, uint32_t width, uint32_t height
-) {
+WebHostSharedMemory*
+WallpaperEngine::WebBrowser::IPC::createSharedMemory (std::string& outName, uint32_t width, uint32_t height) {
     // once per engine process, before the first segment of this run exists
     static const bool swept = (removeStaleWebHostFiles (), true);
     (void)swept;

+ 3 - 3
src/WallpaperEngine/WebBrowser/WebBrowserContext.cpp

@@ -74,9 +74,9 @@ WebBrowserContext::WebBrowserContext (WallpaperEngine::Application::WallpaperApp
 
     CefSettings settings;
     // the pid in the name lets a later engine run tell this profile is stale if we get killed before cleaning up
-    this->m_cachePath
-	= (std::filesystem::temp_directory_path () / ("lwe-cef-" + std::to_string (getpid ()) + "-" + uuid::generate_uuid_v4 ()))
-	      .string ();
+    this->m_cachePath = (std::filesystem::temp_directory_path ()
+			 / ("lwe-cef-" + std::to_string (getpid ()) + "-" + uuid::generate_uuid_v4 ()))
+			    .string ();
     cef_string_utf8_to_utf16 (this->m_cachePath.c_str (), this->m_cachePath.length (), &settings.root_cache_path);
     settings.windowless_rendering_enabled = true;
     // Chromium's own ERROR-level chatter (cancelled requests and the like) is noise here, the page's console is

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است