ソースを参照

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

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません