From 71de2528b59e257e31285e4cf69b5d713a3224e4 Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 18:51:40 -0600 Subject: [PATCH 01/14] refactor: Replace mpvpaper with QtMultimedia for native video rendering --- .gitignore | 10 +- cli.sh | 16 +- install.sh | 7 +- .../wallpapers/MpvShaderGenerator.js | 93 -- .../dashboard/wallpapers/Wallpaper.qml | 1170 ++++++----------- .../widgets/dashboard/wallpapers/mpvpaper.sh | 35 - .../widgets/dashboard/wallpapers/palette.frag | 76 +- .../dashboard/wallpapers/palette.frag.qsb | Bin 2474 -> 3348 bytes .../widgets/dashboard/wallpapers/palette.vert | 2 +- .../dashboard/wallpapers/palette.vert.qsb | Bin 1580 -> 1331 bytes nix/packages/default.nix | 3 + nix/packages/media.nix | 9 +- scripts/thumbgen.py | 273 +++- 13 files changed, 717 insertions(+), 977 deletions(-) delete mode 100644 modules/widgets/dashboard/wallpapers/MpvShaderGenerator.js delete mode 100755 modules/widgets/dashboard/wallpapers/mpvpaper.sh diff --git a/.gitignore b/.gitignore index 969ce020..2d87792f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -.qmlls.ini -.ruff_cache/ -scripts/__pycache__ -.sisyphus +scripts/LUT 3D/Cargo.lock +/scripts/LUT 3D/target/release/.fingerprint +/scripts/LUT 3D/target/release/deps +/scripts/LUT 3D/target/release/build/zmij-cbe3f8201850d6e1 +/scripts/LUT 3D/target/release/build/av-scenechange-8559eb33c3ad0528 +scripts/LUT 3D/target/.rustc_info.json diff --git a/cli.sh b/cli.sh index 98274b12..8b756212 100755 --- a/cli.sh +++ b/cli.sh @@ -603,9 +603,21 @@ help | --help | -h) # Force Qt6CT export QT_QPA_PLATFORMTHEME=qt6ct + + # Use GStreamer backend for QtMultimedia + export QT_MEDIA_BACKEND=gstreamer - # Cache this script's PID before exec (for fast PID lookups in future CLI calls) - echo $$ >/tmp/ambxst.pid + # Enable VA-API hardware decoding if available + if gst-inspect-1.0 vaapi &>/dev/null; then + export GST_PLUGIN_FEATURE_RANK=vaapidecodebin:MAX,vaapipostproc:MAX + fi + + # Force OpenGL/EGL for better performance + export GST_GL_API=opengl + export GST_GL_PLATFORM=egl + + # Cache this script's PID before exec (for fast PID lookups in future CLI calls) + echo $$ >/tmp/ambxst.pid # Launch QuickShell with the main shell.qml # If NIXGL_BIN is set (NixOS/Nix setup), use it. Otherwise, just run qs directly. diff --git a/install.sh b/install.sh index 22c36696..bb42c0e8 100755 --- a/install.sh +++ b/install.sh @@ -50,7 +50,6 @@ declare -A BINARY_CHECK=( ["jq"]="jq" ["playerctl"]="playerctl" ["wtype"]="wtype" - ["mpvpaper"]="mpvpaper" ["gradia"]="gradia" ["pipx"]="pipx" ["python-pipx"]="pipx" @@ -117,6 +116,7 @@ install_dependencies() { local PKGS=( kitty tmux fuzzel network-manager-applet blueman + gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pipewire wireplumber easyeffects playerctl qt6-qtbase qt6-qtdeclarative qt6-qtwayland qt6-qtsvg qt6-qttools qt6-qtimageformats qt6-qtmultimedia qt6-qtshadertools @@ -128,7 +128,7 @@ install_dependencies() { tesseract-langpack-chi_sim tesseract-langpack-chi_tra tesseract-langpack-kor tesseract-langpack-lat google-roboto-fonts google-roboto-mono-fonts dejavu-sans-fonts liberation-fonts google-noto-fonts-common google-noto-cjk-fonts google-noto-emoji-fonts - mpvpaper matugen R-CRAN-phosphoricons adw-gtk3-theme quickshell unzip curl + matugen R-CRAN-phosphoricons adw-gtk3-theme quickshell unzip curl ) log_info "Installing dependencies..." @@ -165,6 +165,7 @@ install_dependencies() { local PKGS=( kitty tmux fuzzel network-manager-applet blueman + gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pipewire wireplumber pavucontrol easyeffects ffmpeg x264 playerctl qt6-base qt6-declarative qt6-wayland qt6-svg qt6-tools qt6-imageformats qt6-multimedia qt6-shadertools libwebp libavif syntax-highlighting breeze-icons hicolor-icon-theme @@ -175,7 +176,7 @@ install_dependencies() { tesseract-data-chi_sim tesseract-data-chi_tra tesseract-data-kor tesseract-data-lat ttf-roboto ttf-roboto-mono ttf-dejavu ttf-liberation noto-fonts noto-fonts-cjk noto-fonts-emoji ttf-nerd-fonts-symbols - matugen gpu-screen-recorder wl-clip-persist mpvpaper gradia + matugen gpu-screen-recorder wl-clip-persist gradia quickshell ttf-phosphor-icons ttf-league-gothic adw-gtk-theme ) diff --git a/modules/widgets/dashboard/wallpapers/MpvShaderGenerator.js b/modules/widgets/dashboard/wallpapers/MpvShaderGenerator.js deleted file mode 100644 index 9b87795f..00000000 --- a/modules/widgets/dashboard/wallpapers/MpvShaderGenerator.js +++ /dev/null @@ -1,93 +0,0 @@ -.pragma library - -function generate(paletteColors) { - // Safety check - if (!paletteColors || paletteColors.length === 0) { - // Return a passthrough shader if no palette - return `//!HOOK MAIN -//!BIND HOOKED -//!DESC Ambxst Passthrough -void main() { - HOOKED_col = HOOKED_tex(HOOKED_pos); -}`; - } - - let unrolledLogic = ""; - - // Unroll the loop to ensure compatibility with all GLES drivers - for (let i = 0; i < paletteColors.length; i++) { - let color = paletteColors[i]; - - let r = (typeof color.r === 'number' ? color.r : 0.0).toFixed(5); - let g = (typeof color.g === 'number' ? color.g : 0.0).toFixed(5); - let b = (typeof color.b === 'number' ? color.b : 0.0).toFixed(5); - - unrolledLogic += ` - { - vec3 pColor = vec3(${r}, ${g}, ${b}); - vec3 diff = color - pColor; - - // Perceptual weighting (Red: 0.299, Green: 0.587, Blue: 0.114) - // This makes the distance match human perception better than raw Euclidean - vec3 weightedDiff = diff * vec3(0.55, 0.77, 0.34); // Sqrt of standard luma weights roughly - float distSq = dot(weightedDiff, weightedDiff); - - // Track closest color for fallback - if (distSq < minDistSq) { - minDistSq = distSq; - closestColor = pColor; - } - - float weight = exp(-distributionSharpness * distSq); - accumulatedColor += pColor * weight; - totalWeight += weight; - } -`; - } - - return `//!HOOK MAIN -//!BIND HOOKED -//!DESC Ambxst Palette Tint - -// Simple dithering function -float noise_random(vec2 uv) { - return fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453); -} - -vec4 hook() { - vec4 tex = HOOKED_tex(HOOKED_pos); - vec3 color = tex.rgb; - - // Add slight dithering to input to break banding before quantization - float noise = (noise_random(HOOKED_pos * 100.0 + sin(HOOKED_pos.x)) - 0.5) / 64.0; - color += noise; - - vec3 accumulatedColor = vec3(0.0); - float totalWeight = 0.0; - float minDistSq = 1000.0; - vec3 closestColor = vec3(0.0); - - // Increased sharpness for cleaner separation (was 20.0) - // 40.0 makes it stick tighter to palette colors - float distributionSharpness = 40.0; - - // Unrolled palette comparison - ${unrolledLogic} - - vec3 finalColor; - - // If we have a decent match blend, use it. - // Otherwise snap to closest to avoid "holes" or dark spots. - if (totalWeight > 0.0001) { - finalColor = accumulatedColor / totalWeight; - } else { - finalColor = closestColor; - } - - // Mix in the closest color slightly to reinforce structure if the blend is too muddy - // finalColor = mix(finalColor, closestColor, 0.2); - - return vec4(finalColor, tex.a); -} -`; -} diff --git a/modules/widgets/dashboard/wallpapers/Wallpaper.qml b/modules/widgets/dashboard/wallpapers/Wallpaper.qml index 852359ef..6b3470b3 100644 --- a/modules/widgets/dashboard/wallpapers/Wallpaper.qml +++ b/modules/widgets/dashboard/wallpapers/Wallpaper.qml @@ -1,11 +1,11 @@ import QtQuick +import QtMultimedia import Quickshell import Quickshell.Wayland import Quickshell.Io import qs.modules.globals import qs.modules.theme import qs.config -import "MpvShaderGenerator.js" as ShaderGenerator PanelWindow { id: wallpaper @@ -28,6 +28,19 @@ PanelWindow { property var wallpaperPaths: [] property var subfolderFilters: [] property var allSubdirs: [] + + // Custom palette loaded from JSON file + property var customPalette: [] + property int customPaletteSize: 0 + + // Default palette (optimizedPalette) as fallback + readonly property var fallbackPalette: optimizedPalette + readonly property int fallbackPaletteSize: optimizedPalette.length + + // Effective palette that will be used in the shader + readonly property var effectivePalette: customPaletteSize > 0 ? customPalette : fallbackPalette + readonly property int effectivePaletteSize: customPaletteSize > 0 ? customPaletteSize : fallbackPaletteSize + property int currentIndex: 0 property string currentWallpaper: initialLoadCompleted && wallpaperPaths.length > 0 ? wallpaperPaths[currentIndex] : "" property bool initialLoadCompleted: false @@ -40,14 +53,18 @@ PanelWindow { property alias tintEnabled: wallpaperAdapter.tintEnabled property int thumbnailsVersion: 0 - // QUICKSHELL-GIT: property string mpvShaderDir: Quickshell.cacheDir + "/mpv_shaders_" + (currentScreenName ? currentScreenName : "ALL") - property string mpvShaderDir: Quickshell.env("HOME") + "/.cache/ambxst/mpv_shaders_" + (currentScreenName ? currentScreenName : "ALL") - property string mpvShaderPath: "" - property bool mpvShaderReady: false - - readonly property var optimizedPalette: ["background", "overBackground", "shadow", "surface", "surfaceBright", "surfaceDim", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "surfaceContainerLow", "surfaceContainerLowest", "primary", "secondary", "tertiary", "red", "lightRed", "green", "lightGreen", "blue", "lightBlue", "yellow", "lightYellow", "cyan", "lightCyan", "magenta", "lightMagenta"] - - // Sync state from the primary wallpaper manager to secondary instances + // Optimized palette color names (used as fallback) + readonly property var optimizedPalette: [ + "background", "overBackground", "shadow", "surface", "surfaceBright", "surfaceDim", + "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", + "surfaceContainerLow", "surfaceContainerLowest", "primary", "secondary", "tertiary", + "red", "lightRed", "green", "lightGreen", "blue", "lightBlue", "yellow", "lightYellow", + "cyan", "lightCyan", "magenta", "lightMagenta" + ] + + // ------------------------------------------------------------------- + // Bindings to sync state from primary wallpaper manager + // ------------------------------------------------------------------- Binding { target: wallpaper property: "wallpaperPaths" @@ -76,6 +93,9 @@ PanelWindow { when: GlobalStates.wallpaperManager !== null && GlobalStates.wallpaperManager !== wallpaper } + // ------------------------------------------------------------------- + // Color presets + // ------------------------------------------------------------------- property string colorPresetsDir: Quickshell.env("HOME") + "/.config/ambxst/colors" property string officialColorPresetsDir: decodeURIComponent(Qt.resolvedUrl("../../../../assets/colors").toString().replace("file://", "")) onColorPresetsDirChanged: console.log("Color Presets Directory:", colorPresetsDir) @@ -83,7 +103,6 @@ PanelWindow { onColorPresetsChanged: console.log("Color Presets Updated:", colorPresets) property string activeColorPreset: wallpaperConfig.adapter.activeColorPreset || "" - // React to light/dark mode changes property bool isLightMode: Config.theme.lightMode onIsLightModeChanged: { if (activeColorPreset) { @@ -106,19 +125,14 @@ PanelWindow { } function applyColorPreset() { - if (!activeColorPreset) - return; + if (!activeColorPreset) return; var mode = Config.theme.lightMode ? "light.json" : "dark.json"; - var officialFile = officialColorPresetsDir + "/" + activeColorPreset + "/" + mode; var userFile = colorPresetsDir + "/" + activeColorPreset + "/" + mode; - // QUICKSHELL-GIT: var dest = Quickshell.cachePath("colors.json"); var dest = Quickshell.env("HOME") + "/.cache/ambxst/colors.json"; - // Try official first, then user. Use bash conditional. var cmd = "if [ -f '" + officialFile + "' ]; then cp '" + officialFile + "' '" + dest + "'; else cp '" + userFile + "' '" + dest + "'; fi"; - console.log("Applying color preset:", activeColorPreset); applyPresetProcess.command = ["bash", "-c", cmd]; applyPresetProcess.running = true; @@ -126,10 +140,11 @@ PanelWindow { function setColorPreset(name) { wallpaperConfig.adapter.activeColorPreset = name; - // activeColorPreset property will update automatically via binding to adapter } - // Funciones utilitarias para tipos de archivo + // ------------------------------------------------------------------- + // Utility functions for file types + // ------------------------------------------------------------------- function getFileType(path) { var extension = path.toLowerCase().split('.').pop(); if (['jpg', 'jpeg', 'png', 'webp', 'tif', 'tiff', 'bmp'].includes(extension)) { @@ -143,68 +158,39 @@ PanelWindow { } function getThumbnailPath(filePath) { - // Compute relative path from wallpaperDir var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; var relativePath = filePath.replace(basePath, ""); - - // Replace the filename with .jpg extension var pathParts = relativePath.split('/'); var fileName = pathParts.pop(); var thumbnailName = fileName + ".jpg"; var relativeDir = pathParts.join('/'); - - // Build the proxy path - // QUICKSHELL-GIT: var thumbnailPath = Quickshell.cacheDir + "/thumbnails/" + relativeDir + "/" + thumbnailName; - var thumbnailPath = Quickshell.env("HOME") + "/.cache/ambxst" + "/thumbnails/" + relativeDir + "/" + thumbnailName; - return thumbnailPath; + return Quickshell.env("HOME") + "/.cache/ambxst/thumbnails/" + relativeDir + "/" + thumbnailName; } function getDisplaySource(filePath) { var fileType = getFileType(filePath); - - // Para el display (WallpapersTab), siempre usar thumbnails si están disponibles if (fileType === 'video' || fileType === 'image' || fileType === 'gif') { - var thumbnailPath = getThumbnailPath(filePath); - // Verificar si el thumbnail existe (esto es solo para debugging, QML manejará el fallback) - return thumbnailPath; + return getThumbnailPath(filePath); } - - // Fallback al archivo original si no es un tipo soportado return filePath; } function getColorSource(filePath) { var fileType = getFileType(filePath); - - // Para generación de colores: solo videos usan thumbnails if (fileType === 'video') { return getThumbnailPath(filePath); } - - // Imágenes y GIFs usan el archivo original para colores return filePath; } function getLockscreenFramePath(filePath) { - if (!filePath) { - return ""; - } - + if (!filePath) return ""; var fileType = getFileType(filePath); - - // Para imágenes estáticas, usar el archivo original - if (fileType === 'image') { - return filePath; - } - - // Para videos y GIFs, usar el frame cacheado + if (fileType === 'image') return filePath; if (fileType === 'video' || fileType === 'gif') { var fileName = filePath.split('/').pop(); - // QUICKSHELL-GIT: var cachePath = Quickshell.cacheDir + "/lockscreen/" + fileName + ".jpg"; - var cachePath = Quickshell.env("HOME") + "/.cache/ambxst" + "/lockscreen/" + fileName + ".jpg"; - return cachePath; + return Quickshell.env("HOME") + "/.cache/ambxst/lockscreen/" + fileName + ".jpg"; } - return filePath; } @@ -213,15 +199,10 @@ PanelWindow { console.warn("generateLockscreenFrame: empty filePath"); return; } - console.log("Generating lockscreen frame for:", filePath); - var scriptPath = decodeURIComponent(Qt.resolvedUrl("../../../../scripts/lockwall.py").toString().replace("file://", "")); - // QUICKSHELL-GIT: var dataPath = Quickshell.cacheDir; var dataPath = Quickshell.env("HOME") + "/.cache/ambxst"; - lockscreenWallpaperScript.command = ["python3", scriptPath, filePath, dataPath]; - lockscreenWallpaperScript.running = true; } @@ -229,58 +210,89 @@ PanelWindow { var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; var relativePath = filePath.replace(basePath, ""); var parts = relativePath.split("/"); - if (parts.length > 1) { - return parts[0]; - } + if (parts.length > 1) return parts[0]; return ""; } + // ------------------------------------------------------------------- + // Palette loading + // ------------------------------------------------------------------- + function loadCustomPalette(filePath) { + if (!filePath) return; + var palettePath = getPalettePath(filePath); + var xhr = new XMLHttpRequest(); + xhr.open("GET", "file://" + palettePath, true); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var data = JSON.parse(xhr.responseText); + customPalette = data.colors; + customPaletteSize = data.size; + console.log("Palette loaded:", customPaletteSize, "colors - First:", customPalette[0]); + } catch (e) { + console.warn("Failed to parse palette:", palettePath, e); + fallbackToDefaultPalette(); + } + } else { + console.warn("Palette file not found (status " + xhr.status + "):", palettePath); + fallbackToDefaultPalette(); + } + } + }; + xhr.send(); + } + + function fallbackToDefaultPalette() { + customPalette = []; + customPaletteSize = 0; + } + + function getPalettePath(filePath) { + var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; + var relativePath = filePath.replace(basePath, ""); + return Quickshell.env("HOME") + "/.cache/ambxst/palettes/" + relativePath + ".json"; + } + function scanSubfolders() { - if (!wallpaperDir) - return; - // Explicitly update command with current wallpaperDir + if (!wallpaperDir) return; var cmd = ["find", wallpaperDir, "-mindepth", "1", "-name", ".*", "-prune", "-o", "-type", "d", "-print"]; scanSubfoldersProcess.command = cmd; scanSubfoldersProcess.running = true; } - // Update directory watcher when wallpaperDir changes onWallpaperDirChanged: { - // Skip initial spurious changes before config is loaded - if (!_wallpaperDirInitialized) - return; - - // Only the primary wallpaper manager should handle directory changes - if (GlobalStates.wallpaperManager !== wallpaper) - return; + if (!_wallpaperDirInitialized) return; + if (GlobalStates.wallpaperManager !== wallpaper) return; console.log("Wallpaper directory changed to:", wallpaperDir); usingFallback = false; - - // Clear current lists to reflect change immediately wallpaperPaths = []; subfolderFilters = []; - directoryWatcher.path = wallpaperDir; - // Force update scan command - var cmd = ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; + var cmd = ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; scanWallpapers.command = cmd; scanWallpapers.running = true; - scanSubfolders(); - // Regenerate thumbnails for the new directory (delayed) if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); else delayedThumbnailGen.start(); } - onCurrentWallpaperChanged: - // Matugen se ejecuta manualmente en las funciones de cambio - {} + onCurrentWallpaperChanged: { + // Matugen is executed manually in change functions + } + // ------------------------------------------------------------------- + // Wallpaper control functions + // ------------------------------------------------------------------- function setWallpaper(path, targetScreen = null) { if (GlobalStates.wallpaperManager && GlobalStates.wallpaperManager !== wallpaper) { GlobalStates.wallpaperManager.setWallpaper(path, targetScreen); @@ -292,29 +304,28 @@ PanelWindow { var pathIndex = wallpaperPaths.indexOf(path); if (pathIndex !== -1) { if (targetScreen) { - // If targeting a specific screen, save to perScreenWallpapers instead of currentWall let perScreen = Object.assign({}, wallpaperConfig.adapter.perScreenWallpapers || {}); perScreen[targetScreen] = path; wallpaperConfig.adapter.perScreenWallpapers = perScreen; - - // If this targetScreen is the primary screen, it must update currentWall - // because currentWall is exactly the primary monitor fallback. + let isPrimary = false; if (GlobalStates.wallpaperManager && GlobalStates.wallpaperManager.screen) { isPrimary = (targetScreen === GlobalStates.wallpaperManager.screen.name); } - if (isPrimary || !wallpaperConfig.adapter.currentWall) { currentIndex = pathIndex; wallpaperConfig.adapter.currentWall = path; currentWallpaper = path; + loadCustomPalette(path); + generateLockscreenFrame(path); runMatugenForCurrentWallpaper(); } } else { - // Global fallback target currentIndex = pathIndex; wallpaperConfig.adapter.currentWall = path; currentWallpaper = path; + loadCustomPalette(path); + generateLockscreenFrame(path); runMatugenForCurrentWallpaper(); } generateLockscreenFrame(path); @@ -328,7 +339,6 @@ PanelWindow { GlobalStates.wallpaperManager.clearPerScreenWallpaper(targetScreen); return; } - console.log("Clearing per-screen wallpaper for:", targetScreen); let perScreen = Object.assign({}, wallpaperConfig.adapter.perScreenWallpapers || {}); if (perScreen[targetScreen]) { @@ -342,9 +352,7 @@ PanelWindow { GlobalStates.wallpaperManager.nextWallpaper(); return; } - - if (wallpaperPaths.length === 0) - return; + if (wallpaperPaths.length === 0) return; initialLoadCompleted = true; currentIndex = (currentIndex + 1) % wallpaperPaths.length; currentWallpaper = wallpaperPaths[currentIndex]; @@ -358,9 +366,7 @@ PanelWindow { GlobalStates.wallpaperManager.previousWallpaper(); return; } - - if (wallpaperPaths.length === 0) - return; + if (wallpaperPaths.length === 0) return; initialLoadCompleted = true; currentIndex = currentIndex === 0 ? wallpaperPaths.length - 1 : currentIndex - 1; currentWallpaper = wallpaperPaths[currentIndex]; @@ -374,7 +380,6 @@ PanelWindow { GlobalStates.wallpaperManager.setWallpaperByIndex(index); return; } - if (index >= 0 && index < wallpaperPaths.length) { initialLoadCompleted = true; currentIndex = index; @@ -385,10 +390,8 @@ PanelWindow { } } - // Función para re-ejecutar Matugen con el wallpaper actual function setMatugenScheme(scheme) { wallpaperConfig.adapter.matugenScheme = scheme; - if (wallpaperConfig.adapter.activeColorPreset) { console.log("Switching to Matugen scheme, clearing preset"); wallpaperConfig.adapter.activeColorPreset = ""; @@ -397,294 +400,61 @@ PanelWindow { } } - // property string mpvSocket: "/tmp/ambxst_mpv_socket" - property string mpvSocket: "/tmp/ambxst_mpv_socket_" + (currentScreenName ? currentScreenName : "ALL") - function runMatugenForCurrentWallpaper() { if (activeColorPreset) { console.log("Skipping Matugen because color preset is active:", activeColorPreset); return; } - if (currentWallpaper && initialLoadCompleted) { console.log("Running Matugen for current wallpaper:", currentWallpaper); - var fileType = getFileType(currentWallpaper); var matugenSource = getColorSource(currentWallpaper); - console.log("Using source for matugen:", matugenSource, "(type:", fileType + ")"); - // Stop existing processes if running to prioritize new request - if (matugenProcessWithConfig.running) { - matugenProcessWithConfig.running = false; - } - if (matugenProcessNormal.running) { - matugenProcessNormal.running = false; - } + if (matugenProcessWithConfig.running) matugenProcessWithConfig.running = false; + if (matugenProcessNormal.running) matugenProcessNormal.running = false; - // Ejecutar matugen con configuración específica - var commandWithConfig = ["matugen", "image", matugenSource, "--source-color-index", "0", "-c", decodeURIComponent(Qt.resolvedUrl("../../../../assets/matugen/config.toml").toString().replace("file://", "")), "-t", wallpaperConfig.adapter.matugenScheme]; - if (Config.theme.lightMode) { - commandWithConfig.push("-m", "light"); - } + var commandWithConfig = ["matugen", "image", matugenSource, "--source-color-index", "0", + "-c", decodeURIComponent(Qt.resolvedUrl("../../../../assets/matugen/config.toml").toString().replace("file://", "")), + "-t", wallpaperConfig.adapter.matugenScheme]; + if (Config.theme.lightMode) commandWithConfig.push("-m", "light"); matugenProcessWithConfig.command = commandWithConfig; matugenProcessWithConfig.running = true; - // Ejecutar matugen normal en paralelo - var commandNormal = ["matugen", "image", matugenSource, "--source-color-index", "0", "-t", wallpaperConfig.adapter.matugenScheme]; - if (Config.theme.lightMode) { - commandNormal.push("-m", "light"); - } + var commandNormal = ["matugen", "image", matugenSource, "--source-color-index", "0", + "-t", wallpaperConfig.adapter.matugenScheme]; + if (Config.theme.lightMode) commandNormal.push("-m", "light"); matugenProcessNormal.command = commandNormal; matugenProcessNormal.running = true; } } - function updateMpvRuntime(enable) { - var cmdString; - if (enable) { - // Since we are using unique filenames, we can just set the new path. - // MPV will handle the switch smoothly and won't use cached versions. - var setCmd = JSON.stringify({ - "command": ["set_property", "glsl-shaders", mpvShaderPath] - }); - cmdString = "echo '" + setCmd + "' | socat - " + mpvSocket; - } else { - // Clear shaders - var jsonCmd = JSON.stringify({ - "command": ["set_property", "glsl-shaders", ""] - }); - cmdString = "echo '" + jsonCmd + "' | socat - " + mpvSocket; - } - - mpvIpcProcess.command = ["bash", "-c", cmdString]; - mpvIpcProcess.running = true; - } - - function requestVideoSync() { - if (GlobalStates.wallpaperManager !== wallpaper) { - if (GlobalStates.wallpaperManager) { - GlobalStates.wallpaperManager.requestVideoSync(); - } - return; - } - videoSyncTimer.restart(); - } - - Timer { - id: videoSyncTimer - interval: 1200 // give mpvpaper processes time to spawn and initialize - repeat: false - onTriggered: { - console.log("Broadcasting video sync to all mpvpaper sockets..."); - mpvSyncProcess.running = true; - } - } - - Process { - id: mpvSyncProcess - running: false - command: ["bash", "-c", "for sock in /tmp/ambxst_mpv_socket_*; do echo '{ \"command\": [\"set_property\", \"time-pos\", 0] }' | socat - \"$sock\" 2>/dev/null; done"] - onExited: code => { - console.log("Video sync broadcast completed with code:", code); - } - } - - function updateMpvShader() { - if (getFileType(effectiveWallpaper) !== "video") { - return; - } - if (!wallpaperAdapter.tintEnabled) { - updateMpvRuntime(false); - return; - } - - var colors = []; - // Log the first color to see if it changed - var firstColorRaw = Colors[optimizedPalette[0]]; - console.log("Generating MPV shader. First palette color (" + optimizedPalette[0] + "):", firstColorRaw); - - for (var i = 0; i < optimizedPalette.length; i++) { - var rawColor = Colors[optimizedPalette[i]]; - if (rawColor) { - var c = Qt.darker(rawColor, 1.0); - if (c && !isNaN(c.r) && !isNaN(c.g) && !isNaN(c.b)) { - colors.push({ - r: c.r, - g: c.g, - b: c.b - }); - } - } - } - - if (colors.length === 0) { - console.warn("MpvShaderGenerator: No valid colors found for palette! Aborting."); - return; - } - - var shaderContent = ShaderGenerator.generate(colors); - - // Generate a unique filename in a dedicated directory - var timestamp = Date.now(); - var currentShaderPath = mpvShaderDir + "/tint_" + timestamp + ".glsl"; - - // Store the current active path so updateMpvRuntime knows which one to use - wallpaper.mpvShaderPath = currentShaderPath; - - var cmd = ["python3", "-c", "import sys, os, pathlib; " + "d = pathlib.Path(sys.argv[1]); " + "d.mkdir(parents=True, exist_ok=True); " + "[f.unlink() for f in d.iterdir() if f.is_file()]; " + "pathlib.Path(sys.argv[2]).write_text(sys.argv[3]); " + "print('Wrote shader to ' + sys.argv[2]); " + "legacy_dir = os.path.dirname(sys.argv[1]); " + "[pathlib.Path(legacy_dir, f).unlink(missing_ok=True) for f in ['mpv_tint_0.glsl', 'mpv_tint_1.glsl', 'mpv_tint.glsl']]", mpvShaderDir, currentShaderPath, shaderContent]; - - mpvShaderWriter.command = cmd; - mpvShaderWriter.running = true; - } - - property int ipcRetryCount: 0 - - Timer { - id: ipcRetryTimer - interval: 200 - repeat: false - onTriggered: { - // Retry the last command (which is currently set in mpvIpcProcess) - mpvIpcProcess.running = true; - } - } - - Process { - id: mpvIpcProcess - running: false - onExited: code => { - if (code !== 0) { - console.warn("MPV IPC failed (is mpvpaper running?) Code:", code); - if (ipcRetryCount < 10) { - ipcRetryCount++; - console.log("Retrying IPC (" + ipcRetryCount + "/10)..."); - ipcRetryTimer.restart(); - } - } else { - ipcRetryCount = 0; - } - } - } - - Process { - id: mpvShaderWriter - running: false - command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("mpvShaderWriter stdout:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("mpvShaderWriter stderr:", text); - } - } - } - - onExited: code => { - if (code === 0) { - console.log("MPV tint shader generated at:", mpvShaderPath); - mpvShaderReady = true; - // Apply immediately via IPC - updateMpvRuntime(true); - } else { - console.warn("Failed to generate MPV shader"); - } - } - } - - // Trigger update when colors change - Timer { - id: shaderUpdateDebounce - interval: 500 - onTriggered: { - console.log("Shader debounce triggered, updating MPV..."); - updateMpvShader(); - } - } - - Connections { - target: Colors - // Watch for file reload (theme change) - function onFileChanged() { - console.log("Colors file changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - // Watch for background change (OLED mode often affects this first/only) - function onBackgroundChanged() { - console.log("Colors background changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - // Fallback - function onPrimaryChanged() { - console.log("Colors primary changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - } - - Connections { - target: Config - function onOledModeChanged() { - console.log("Config OLED mode changed, scheduling update..."); - shaderUpdateDebounce.restart(); - } - } - - onTintEnabledChanged: { - console.log("Tint enabled changed to", tintEnabled); - updateMpvShader(); - } - - onEffectiveWallpaperChanged: { - if (getFileType(effectiveWallpaper) === "video") { - shaderUpdateDebounce.restart(); - } - } - Component.onCompleted: { - // Only the first Wallpaper instance should manage scanning - // Other instances (for other screens) share the same data via GlobalStates if (GlobalStates.wallpaperManager !== null) { - // Another instance already registered, skip initialization _wallpaperDirInitialized = true; return; } - GlobalStates.wallpaperManager = wallpaper; - // Verificar si existe wallpapers.json, si no, crear con fallback checkWallpapersJson.running = true; - - // Initial scans - do these once after config is loaded scanColorPresets(); - // Start directory monitoring presetsWatcher.reload(); officialPresetsWatcher.reload(); - // Load initial wallpaper config - this will trigger onWallPathChanged which does the actual scan wallpaperConfig.reload(); - // Generate lockscreen frame for initial wallpaper after a short delay Qt.callLater(function () { if (currentWallpaper) { generateLockscreenFrame(currentWallpaper); - } - // Force shader generation on startup if enabled - if (tintEnabled) { - updateMpvShader(); + loadCustomPalette(currentWallpaper); } }); } + // ------------------------------------------------------------------- + // Configuration file handling + // ------------------------------------------------------------------- FileView { id: wallpaperConfig - // QUICKSHELL-GIT: path: Quickshell.cachePath("wallpapers.json") path: Quickshell.env("HOME") + "/.cache/ambxst/wallpapers.json" watchChanges: true @@ -697,11 +467,9 @@ PanelWindow { onFileChanged: reload() onAdapterUpdated: { - // Ensure matugenScheme has a default value if (!wallpaperConfig.adapter.matugenScheme) { wallpaperConfig.adapter.matugenScheme = "scheme-tonal-spot"; } - // Update the currentMatugenScheme property to trigger UI updates currentMatugenScheme = Qt.binding(function () { return wallpaperConfig.adapter.matugenScheme; }); @@ -724,17 +492,9 @@ PanelWindow { } onCurrentWallChanged: { - // Skip during initial load - scanWallpapers handles this - if (!wallpaper._wallpaperDirInitialized) - return; - - // Siempre actualizar si es diferente al actual + if (!wallpaper._wallpaperDirInitialized) return; if (currentWall && currentWall !== wallpaper.currentWallpaper) { - // If paths are not loaded yet, wait for scanWallpapers to finish - if (wallpaper.wallpaperPaths.length === 0) { - return; - } - + if (wallpaper.wallpaperPaths.length === 0) return; var pathIndex = wallpaper.wallpaperPaths.indexOf(currentWall); if (pathIndex !== -1) { wallpaper.currentIndex = pathIndex; @@ -751,22 +511,19 @@ PanelWindow { onWallPathChanged: { if (wallPath) { console.log("Config wallPath updated:", wallPath); - - // Initialize scanning on first valid wallPath load if (!wallpaper._wallpaperDirInitialized && GlobalStates.wallpaperManager === wallpaper) { wallpaper._wallpaperDirInitialized = true; - - // Set up directory watcher directoryWatcher.path = wallPath; directoryWatcher.reload(); - // Perform initial wallpaper scan - var cmd = ["find", wallPath, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; + var cmd = ["find", wallPath, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"]; scanWallpapers.command = cmd; scanWallpapers.running = true; wallpaper.scanSubfolders(); - - // Start thumbnail generation delayedThumbnailGen.start(); } } @@ -774,12 +531,13 @@ PanelWindow { } } + // ------------------------------------------------------------------- + // External processes + // ------------------------------------------------------------------- Process { id: checkWallpapersJson running: false - // QUICKSHELL-GIT: command: ["test", "-f", Quickshell.cachePath("wallpapers.json")] command: ["test", "-f", Quickshell.env("HOME") + "/.cache/ambxst/wallpapers.json"] - onExited: function (exitCode) { if (exitCode !== 0) { console.log("wallpapers.json does not exist, creating with fallbackDir"); @@ -794,77 +552,28 @@ PanelWindow { id: matugenProcessWithConfig running: false command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Matugen (with config) output:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Matugen (with config) error:", text); - } - } - } - - onExited: { - console.log("Matugen with config finished"); - } + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Matugen (with config) output:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Matugen (with config) error:", text); } } + onExited: { console.log("Matugen with config finished"); } } Process { id: matugenProcessNormal running: false command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Matugen (normal) output:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Matugen (normal) error:", text); - } - } - } - - onExited: { - console.log("Matugen normal finished"); - } + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Matugen (normal) output:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Matugen (normal) error:", text); } } + onExited: { console.log("Matugen normal finished"); } } - // Proceso para generar thumbnails de videos Process { id: thumbnailGeneratorScript running: false - // QUICKSHELL-GIT: command: ["python3", decodeURIComponent(Qt.resolvedUrl("../../../../scripts/thumbgen.py").toString().replace("file://", "")), Quickshell.cacheDir + "/wallpapers.json", Quickshell.cacheDir, fallbackDir] - command: ["python3", decodeURIComponent(Qt.resolvedUrl("../../../../scripts/thumbgen.py").toString().replace("file://", "")), Quickshell.env("HOME") + "/.cache/ambxst" + "/wallpapers.json", Quickshell.env("HOME") + "/.cache/ambxst", fallbackDir] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Thumbnail Generator:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Thumbnail Generator Error:", text); - } - } - } - + command: ["python3", decodeURIComponent(Qt.resolvedUrl("../../../../scripts/thumbgen.py").toString().replace("file://", "")), + Quickshell.env("HOME") + "/.cache/ambxst/wallpapers.json", + Quickshell.env("HOME") + "/.cache/ambxst", fallbackDir] + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Thumbnail Generator:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Thumbnail Generator Error:", text); } } onExited: function (exitCode) { if (exitCode === 0) { console.log("✅ Video thumbnails generated successfully"); @@ -877,39 +586,20 @@ PanelWindow { Timer { id: delayedThumbnailGen - interval: 2000 // Delay 2 seconds after change to not block + interval: 2000 repeat: false onTriggered: thumbnailGeneratorScript.running = true } - // Proceso para generar frame de lockscreen con el script de Python Process { id: lockscreenWallpaperScript running: false command: [] - - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("Lockscreen Wallpaper Generator:", text); - } - } - } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Lockscreen Wallpaper Generator Error:", text); - } - } - } - + stdout: StdioCollector { onStreamFinished: { if (text.length > 0) console.log("Lockscreen Wallpaper Generator:", text); } } + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Lockscreen Wallpaper Generator Error:", text); } } onExited: function (exitCode) { - if (exitCode === 0) { - console.log("✅ Lockscreen wallpaper ready"); - } else { - console.warn("⚠️ Lockscreen wallpaper generation failed with code:", exitCode); - } + if (exitCode === 0) console.log("✅ Lockscreen wallpaper ready"); + else console.warn("⚠️ Lockscreen wallpaper generation failed with code:", exitCode); } } @@ -917,18 +607,12 @@ PanelWindow { id: scanSubfoldersProcess running: false command: wallpaperDir ? ["find", wallpaperDir, "-mindepth", "1", "-name", ".*", "-prune", "-o", "-type", "d", "-print"] : [] - stdout: StdioCollector { onStreamFinished: { console.log("scanSubfolders stdout:", text); - var rawPaths = text.trim().split("\n").filter(function (f) { - return f.length > 0; - }); - + var rawPaths = text.trim().split("\n").filter(function (f) { return f.length > 0; }); allSubdirs = rawPaths; - var basePath = wallpaperDir.endsWith("/") ? wallpaperDir : wallpaperDir + "/"; - var topLevelFolders = rawPaths.filter(function (path) { var relative = path.replace(basePath, ""); return relative.indexOf("/") === -1; @@ -937,58 +621,38 @@ PanelWindow { }).filter(function (name) { return name.length > 0 && !name.startsWith("."); }); - topLevelFolders.sort(); subfolderFilters = topLevelFolders; - subfolderFiltersChanged(); // Emitir señal manualmente console.log("Updated subfolderFilters:", subfolderFilters); } } - - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("Error scanning subfolders:", text); - } - } - } - + stderr: StdioCollector { onStreamFinished: { if (text.length > 0) console.warn("Error scanning subfolders:", text); } } onRunningChanged: { - if (running) { - console.log("Starting scanSubfolders for directory:", wallpaperDir); - } else { - console.log("Finished scanSubfolders"); - } + if (running) console.log("Starting scanSubfolders for directory:", wallpaperDir); + else console.log("Finished scanSubfolders"); } } - // Directory watcher using FileView to monitor the wallpaper directory + // ------------------------------------------------------------------- + // Directory watchers + // ------------------------------------------------------------------- FileView { id: directoryWatcher path: wallpaperDir watchChanges: true printErrors: false - onFileChanged: { - if (wallpaperDir === "") - return; + if (wallpaperDir === "") return; console.log("Wallpaper directory changed, rescanning..."); scanWallpapers.running = true; scanSubfoldersProcess.running = true; - // Regenerar thumbnails si hay nuevos videos (delayed) - if (delayedThumbnailGen.running) - delayedThumbnailGen.restart(); - else - delayedThumbnailGen.start(); + if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); + else delayedThumbnailGen.start(); } - - // Remove onLoadFailed to prevent premature fallback activation } - // Recursive directory watchers for subfolders Instantiator { model: allSubdirs - delegate: FileView { path: modelData watchChanges: true @@ -997,36 +661,28 @@ PanelWindow { console.log("Subdirectory content changed (" + path + "), rescanning..."); scanWallpapers.running = true; scanSubfoldersProcess.running = true; - - // Regenerar thumbnails (delayed) - if (delayedThumbnailGen.running) - delayedThumbnailGen.restart(); - else - delayedThumbnailGen.start(); + if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); + else delayedThumbnailGen.start(); } } } - // Directory watcher for user color presets FileView { id: presetsWatcher path: colorPresetsDir watchChanges: true printErrors: false - onFileChanged: { console.log("User color presets directory changed, rescanning..."); scanPresetsProcess.running = true; } } - // Directory watcher for official color presets FileView { id: officialPresetsWatcher path: officialColorPresetsDir watchChanges: true printErrors: false - onFileChanged: { console.log("Official color presets directory changed, rescanning..."); scanPresetsProcess.running = true; @@ -1036,41 +692,34 @@ PanelWindow { Process { id: scanWallpapers running: false - command: wallpaperDir ? ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] : [] - + command: wallpaperDir ? ["find", wallpaperDir, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] : [] onRunningChanged: { if (running && wallpaperDir === "") { console.log("Blocking scanWallpapers because wallpaperDir is empty"); running = false; } } - stdout: StdioCollector { onStreamFinished: { - var files = text.trim().split("\n").filter(function (f) { - return f.length > 0; - }); + var files = text.trim().split("\n").filter(function (f) { return f.length > 0; }); if (files.length === 0) { console.log("No wallpapers found in main directory, using fallback"); usingFallback = true; scanFallback.running = true; } else { usingFallback = false; - // Only update if the list has actually changed var newFiles = files.sort(); var listChanged = JSON.stringify(newFiles) !== JSON.stringify(wallpaperPaths); if (listChanged) { console.log("Wallpaper directory updated. Found", newFiles.length, "images"); wallpaperPaths = newFiles; - - // Always try to load the saved wallpaper when list changes if (wallpaperPaths.length > 0) { - // Trigger thumbnail generation if list changed - if (delayedThumbnailGen.running) - delayedThumbnailGen.restart(); - else - delayedThumbnailGen.start(); - + if (delayedThumbnailGen.running) delayedThumbnailGen.restart(); + else delayedThumbnailGen.start(); if (wallpaperConfig.adapter.currentWall) { var savedIndex = wallpaperPaths.indexOf(wallpaperConfig.adapter.currentWall); if (savedIndex !== -1) { @@ -1083,25 +732,21 @@ PanelWindow { } else { currentIndex = 0; } - if (!initialLoadCompleted) { if (!wallpaperConfig.adapter.currentWall) { wallpaperConfig.adapter.currentWall = wallpaperPaths[0]; } initialLoadCompleted = true; - // runMatugenForCurrentWallpaper() will be called by onCurrentWallChanged } } } } } } - stderr: StdioCollector { onStreamFinished: { if (text.length > 0) { console.warn("Error scanning wallpaper directory:", text); - // Only fallback if we don't already have wallpapers loaded AND we have a valid directory that failed if (wallpaperPaths.length === 0 && wallpaperDir !== "") { console.log("Directory scan failed for " + wallpaperDir + ", using fallback"); usingFallback = true; @@ -1115,38 +760,30 @@ PanelWindow { Process { id: scanFallback running: false - command: ["find", fallbackDir, "-name", ".*", "-prune", "-o", "-type", "f", "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] - + command: ["find", fallbackDir, "-name", ".*", "-prune", "-o", "-type", "f", + "(", "-name", "*.jpg", "-o", "-name", "*.jpeg", "-o", "-name", "*.png", + "-o", "-name", "*.webp", "-o", "-name", "*.tif", "-o", "-name", "*.tiff", + "-o", "-name", "*.gif", "-o", "-name", "*.mp4", "-o", "-name", "*.webm", + "-o", "-name", "*.mov", "-o", "-name", "*.avi", "-o", "-name", "*.mkv", ")", "-print"] stdout: StdioCollector { onStreamFinished: { - var files = text.trim().split("\n").filter(function (f) { - return f.length > 0; - }); + var files = text.trim().split("\n").filter(function (f) { return f.length > 0; }); console.log("Using fallback wallpapers. Found", files.length, "images"); - - // Only use fallback if we don't already have main wallpapers loaded if (usingFallback) { wallpaperPaths = files.sort(); - - // Initialize fallback wallpaper selection if (wallpaperPaths.length > 0) { if (wallpaperConfig.adapter.currentWall) { var savedIndex = wallpaperPaths.indexOf(wallpaperConfig.adapter.currentWall); - if (savedIndex !== -1) { - currentIndex = savedIndex; - } else { - currentIndex = 0; - } + if (savedIndex !== -1) currentIndex = savedIndex; + else currentIndex = 0; } else { currentIndex = 0; } - if (!initialLoadCompleted) { if (!wallpaperConfig.adapter.currentWall) { wallpaperConfig.adapter.currentWall = wallpaperPaths[0]; } initialLoadCompleted = true; - // runMatugenForCurrentWallpaper() will be called by onCurrentWallChanged } } } @@ -1157,9 +794,7 @@ PanelWindow { Process { id: scanPresetsProcess running: false - // Scan both directories. find will complain to stderr if one is missing but still output what it finds. command: ["find", officialColorPresetsDir, colorPresetsDir, "-mindepth", "1", "-maxdepth", "1", "-type", "d"] - stdout: StdioCollector { onStreamFinished: { console.log("Scan Presets Output:", text); @@ -1167,286 +802,329 @@ PanelWindow { var uniqueNames = []; for (var i = 0; i < rawLines.length; i++) { var line = rawLines[i].trim(); - if (line.length === 0) - continue; + if (line.length === 0) continue; var name = line.split('/').pop(); - // Deduplicate - if (uniqueNames.indexOf(name) === -1) { - uniqueNames.push(name); - } + if (uniqueNames.indexOf(name) === -1) uniqueNames.push(name); } uniqueNames.sort(); console.log("Found color presets:", uniqueNames); colorPresets = uniqueNames; } } - - stderr: StdioCollector { - onStreamFinished: { - // Suppress common "No such file or directory" if one dir is missing - // console.warn("Scan Presets Error:", text); - } - } + stderr: StdioCollector { onStreamFinished: { /* suppress errors */ } } } Process { id: applyPresetProcess running: false command: [] - onExited: code => { - if (code === 0) - console.log("Color preset applied successfully"); - else - console.warn("Failed to apply color preset, code:", code); + if (code === 0) console.log("Color preset applied successfully"); + else console.warn("Failed to apply color preset, code:", code); } } - Rectangle { - id: background - anchors.fill: parent - color: "black" - focus: true - - Keys.onLeftPressed: { - if (wallpaper.wallpaperPaths.length > 0) { - wallpaper.previousWallpaper(); - } - } - - Keys.onRightPressed: { - if (wallpaper.wallpaperPaths.length > 0) { - wallpaper.nextWallpaper(); - } - } + // ------------------------------------------------------------------- + // Reusable shader effect for palette tinting + // ------------------------------------------------------------------- + component PaletteShaderEffect: ShaderEffect { + id: effect + property var source: null + property var paletteTexture: null + property real paletteSize: 0 + property real texWidth: 1 + property real texHeight: 1 + + vertexShader: "palette.vert.qsb" + fragmentShader: "palette.frag.qsb" + } - WallpaperImage { - id: wallImage + // ------------------------------------------------------------------- + // Component for static images (jpg, png, webp, etc.) + // ------------------------------------------------------------------- + Component { + id: staticImageComponent + Item { + id: staticImageRoot anchors.fill: parent - source: wallpaper.effectiveWallpaper - } - } + property string sourceFile + property bool tint: wallpaper.tintEnabled - component WallpaperImage: Item { - property string source - property string previousSource + onSourceFileChanged: console.log("staticImageComponent: sourceFile =", sourceFile) + onTintChanged: console.log("staticImageComponent: tint =", tint) - Process { - id: killMpvpaperProcess - running: false - command: ["pkill", "-f", wallpaper.mpvSocket] + // Hidden item that builds a 1D texture from the effective palette + Item { + id: paletteSourceItem + visible: true + width: wallpaper.effectivePaletteSize + height: 1 + opacity: 0 - onExited: function (exitCode) { - console.log("Killed mpvpaper processes on socket", wallpaper.mpvSocket, ", exit code:", exitCode); + Row { + anchors.fill: parent + Repeater { + model: wallpaper.effectivePalette + Rectangle { + width: 1 + height: 1 + color: { + if (typeof modelData === "string") { + if (modelData.charAt(0) === '#') return modelData; + else return Colors[modelData] || "black"; + } + return modelData; + } + } + } + } + + Component.onCompleted: { if (width > 0) paletteTextureSource.scheduleUpdate(); } + onWidthChanged: { if (width > 0) paletteTextureSource.scheduleUpdate(); } } - } - // Trigger animation when source changes - onSourceChanged: { - if (previousSource !== "" && source !== previousSource) { - if (Config.animDuration > 0) { - transitionAnimation.restart(); - } + ShaderEffectSource { + id: paletteTextureSource + sourceItem: paletteSourceItem + hideSource: true + visible: false + smooth: false + recursive: false } - previousSource = source; - // Kill mpvpaper if switching to a static image - if (source) { - var fileType = getFileType(source); - if (fileType === 'image') { - killMpvpaperProcess.running = true; + // Force palette texture update when effective palette changes + Connections { + target: wallpaper + function onEffectivePaletteChanged() { + paletteTextureSource.scheduleUpdate(); } } - } - SequentialAnimation { - id: transitionAnimation + Item { + id: mediaContainer + anchors.fill: parent + clip: true - ParallelAnimation { - NumberAnimation { - target: wallImage - property: "scale" - to: 1.01 - duration: Config.animDuration - easing.type: Easing.OutCubic - } - NumberAnimation { - target: wallImage - property: "opacity" - to: 0.5 - duration: Config.animDuration - easing.type: Easing.OutCubic + Image { + id: rawImage + anchors.fill: parent + source: staticImageRoot.sourceFile ? "file://" + staticImageRoot.sourceFile : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + smooth: true + mipmap: true + opacity: staticImageRoot.tint ? 0.0 : 1.0 + visible: true + + onStatusChanged: { + if (status === Image.Ready) { + console.log("rawImage ready, scheduling texture update"); + mediaTextureSource.scheduleUpdate(); + } + } } - } - ParallelAnimation { - NumberAnimation { - target: wallImage - property: "scale" - to: 1.0 - duration: Config.animDuration - easing.type: Easing.OutCubic + ShaderEffectSource { + id: mediaTextureSource + sourceItem: rawImage + hideSource: staticImageRoot.tint + live: false + smooth: true + recursive: false } - NumberAnimation { - target: wallImage - property: "opacity" - to: 1.0 - duration: Config.animDuration - easing.type: Easing.OutCubic + + PaletteShaderEffect { + anchors.fill: parent + visible: staticImageRoot.tint && wallpaper.effectivePaletteSize > 0 + source: mediaTextureSource + paletteTexture: paletteTextureSource + paletteSize: wallpaper.effectivePaletteSize + texWidth: Math.max(rawImage.width, 1) + texHeight: Math.max(rawImage.height, 1) } } } + } - Loader { + // ------------------------------------------------------------------- + // Component for videos and GIFs (animated content) + // ------------------------------------------------------------------- + Component { + id: videoComponent + Item { + id: videoRoot anchors.fill: parent - sourceComponent: { - if (!parent.source) - return null; + property string sourceFile + property bool tint: wallpaper.tintEnabled - var fileType = getFileType(parent.source); - if (fileType === 'image') { - return staticImageComponent; - } else if (fileType === 'gif' || fileType === 'video') { - return mpvpaperComponent; + onSourceFileChanged: console.log("videoComponent: sourceFile =", sourceFile) + onTintChanged: console.log("videoComponent: tint =", tint) + + Item { + id: paletteSourceItem + visible: true + width: wallpaper.effectivePaletteSize + height: 1 + opacity: 0 + + Row { + anchors.fill: parent + Repeater { + model: wallpaper.effectivePalette + Rectangle { + width: 1 + height: 1 + color: { + if (typeof modelData === "string") { + if (modelData.charAt(0) === '#') return modelData; + else return Colors[modelData] || "black"; + } + return modelData; + } + } + } } - return staticImageComponent; // fallback + + Component.onCompleted: { if (width > 0) paletteTextureSource.scheduleUpdate(); } + onWidthChanged: { if (width > 0) paletteTextureSource.scheduleUpdate(); } } - property string sourceFile: parent.source - } + ShaderEffectSource { + id: paletteTextureSource + sourceItem: paletteSourceItem + hideSource: true + visible: false + smooth: false + recursive: false + } + + Connections { + target: wallpaper + function onEffectivePaletteChanged() { + paletteTextureSource.scheduleUpdate(); + } + } - Component { - id: staticImageComponent Item { - id: staticImageRoot - width: parent.width - height: parent.height - property string sourceFile: parent.sourceFile - property bool tint: wallpaper.tintEnabled - - // Subset of colors for optimization (approx 25 colors vs 98) - readonly property var optimizedPalette: ["background", "overBackground", "shadow", "surface", "surfaceBright", "surfaceDim", "surfaceContainer", "surfaceContainerHigh", "surfaceContainerHighest", "surfaceContainerLow", "surfaceContainerLowest", "primary", "secondary", "tertiary", "red", "lightRed", "green", "lightGreen", "blue", "lightBlue", "yellow", "lightYellow", "cyan", "lightCyan", "magenta", "lightMagenta"] - - // Palette generation for the shader - Item { - id: paletteSourceItem - // Must be visible for ShaderEffectSource to capture it, - // but we hide it visually by placing it behind or expecting ShaderEffectSource hideSource behavior. + id: mediaContainer + anchors.fill: parent + clip: true + + Video { + id: videoPlayer + anchors.fill: parent + source: videoRoot.sourceFile ? "file://" + videoRoot.sourceFile : "" + loops: MediaPlayer.Infinite + autoPlay: true + muted: true + fillMode: VideoOutput.PreserveAspectCrop + opacity: videoRoot.tint ? 0.0 : 1.0 visible: true - width: staticImageRoot.optimizedPalette.length - height: 1 - opacity: 0 // Make invisible to eye but maintain presence for capture if needed (though hideSource usually handles this) - - Row { - anchors.fill: parent - Repeater { - model: staticImageRoot.optimizedPalette - Rectangle { - width: 1 - height: 1 - color: Colors[modelData] - } - } - } } ShaderEffectSource { - id: paletteTextureSource - sourceItem: paletteSourceItem - hideSource: true - visible: false // The source object itself doesn't need to be visible in the scene graph - smooth: false + id: mediaTextureSource + sourceItem: videoPlayer + hideSource: videoRoot.tint + live: true + smooth: true recursive: false } - Image { - mipmap: true - id: rawImage + PaletteShaderEffect { anchors.fill: parent - source: parent.sourceFile ? "file://" + parent.sourceFile : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - smooth: true - sourceSize.width: wallpaper.width - sourceSize.height: wallpaper.height - layer.enabled: parent.tint - layer.effect: ShaderEffect { - property var paletteTexture: paletteTextureSource - property real paletteSize: staticImageRoot.optimizedPalette.length - property real texWidth: rawImage.width - property real texHeight: rawImage.height - - vertexShader: "palette.vert.qsb" - fragmentShader: "palette.frag.qsb" - } + visible: videoRoot.tint && wallpaper.effectivePaletteSize > 0 + source: mediaTextureSource + paletteTexture: paletteTextureSource + paletteSize: wallpaper.effectivePaletteSize + texWidth: Math.max(videoPlayer.width, 1) + texHeight: Math.max(videoPlayer.height, 1) } } } + } - Component { - id: mpvpaperComponent - Item { - property string sourceFile: parent.sourceFile - property string scriptPath: decodeURIComponent(Qt.resolvedUrl("mpvpaper.sh").toString().replace("file://", "")) - - Timer { - id: mpvpaperRestartTimer - interval: 100 - onTriggered: { - if (sourceFile) { - console.log("Restarting mpvpaper for:", sourceFile); - mpvpaperProcess.running = true; - wallpaper.requestVideoSync(); - } - } - } + // ------------------------------------------------------------------- + // Main wallpaper display area + // ------------------------------------------------------------------- + Rectangle { + id: background + anchors.fill: parent + color: "black" + focus: true - onSourceFileChanged: { - if (sourceFile) { - console.log("Source file changed to:", sourceFile); - mpvpaperProcess.running = false; - mpvpaperRestartTimer.restart(); - } - } + Keys.onLeftPressed: { + if (wallpaper.wallpaperPaths.length > 0) wallpaper.previousWallpaper(); + } - Component.onCompleted: { - if (sourceFile) { - console.log("Initial mpvpaper run for:", sourceFile); - mpvpaperProcess.running = true; - wallpaper.requestVideoSync(); - } + Keys.onRightPressed: { + if (wallpaper.wallpaperPaths.length > 0) wallpaper.nextWallpaper(); + } + + // Container that handles source changes, transitions, and palette loading + Item { + id: wallImageContainer + anchors.fill: parent + property string source: wallpaper.effectiveWallpaper + property string previousSource: "" + + onSourceChanged: { + console.log("wallImageContainer source changed to:", source); + if (previousSource !== "" && source !== previousSource) { + if (Config.animDuration > 0) transitionAnimation.restart(); } + previousSource = source; + if (source) wallpaper.loadCustomPalette(source); + } - Component.onDestruction: - // mpvpaper script handles killing previous instances - {} + SequentialAnimation { + id: transitionAnimation + ParallelAnimation { + NumberAnimation { target: wallImageContainer; property: "scale"; to: 1.01; duration: Config.animDuration; easing.type: Easing.OutCubic } + NumberAnimation { target: wallImageContainer; property: "opacity"; to: 0.5; duration: Config.animDuration; easing.type: Easing.OutCubic } + } + ParallelAnimation { + NumberAnimation { target: wallImageContainer; property: "scale"; to: 1.0; duration: Config.animDuration; easing.type: Easing.OutCubic } + NumberAnimation { target: wallImageContainer; property: "opacity"; to: 1.0; duration: Config.animDuration; easing.type: Easing.OutCubic } + } + } - Process { - id: mpvpaperProcess - running: false - command: sourceFile && wallpaper.currentScreenName ? ["bash", scriptPath, sourceFile, (wallpaper.tintEnabled ? wallpaper.mpvShaderPath : ""), wallpaper.currentScreenName] : [] + Loader { + id: wallImageLoader + anchors.fill: parent + sourceComponent: { + if (!wallImageContainer.source) return null; + var fileType = wallpaper.getFileType(wallImageContainer.source); + console.log("Loader: fileType =", fileType, "source =", wallImageContainer.source); + if (fileType === 'image') return staticImageComponent; + else if (fileType === 'gif' || fileType === 'video') return videoComponent; + return staticImageComponent; + } - stdout: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.log("mpvpaper output:", text); - } - } - } + onLoaded: { + console.log("Loader: item loaded, assigning sourceFile =", wallImageContainer.source); + if (item) item.sourceFile = wallImageContainer.source; + } - stderr: StdioCollector { - onStreamFinished: { - if (text.length > 0) { - console.warn("mpvpaper error:", text); - } - } - } + // Bind sourceFile directly to wallImageContainer.source + Binding { + target: wallImageLoader.item + property: "sourceFile" + value: wallImageContainer.source + when: wallImageLoader.item !== null + } + } - onExited: function (exitCode) { - console.log("mpvpaper process exited with code:", exitCode); + // Fallback in case Binding doesn't trigger + Connections { + target: wallImageContainer + function onSourceChanged() { + if (wallImageLoader.item) { + console.log("Connections: updating sourceFile to", wallImageContainer.source); + wallImageLoader.item.sourceFile = wallImageContainer.source; } } } } } -} +} \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/mpvpaper.sh b/modules/widgets/dashboard/wallpapers/mpvpaper.sh deleted file mode 100755 index edcb0dd4..00000000 --- a/modules/widgets/dashboard/wallpapers/mpvpaper.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -if [ -z "$1" ]; then - echo "Use: $0 /path/to/wallpaper [shader_path] [monitor_target]" - exit 1 -fi - -WALLPAPER="$1" -SHADER="$2" -MONITOR="${3:-ALL}" - -# When a specific monitor is targeted, we don't kill all mpvpaper instances, -# just the one for that monitor if possible. However mpvpaper doesn't -# natively support killing by monitor easily via pkill. -# For now, we'll kill by checking the command line args if MONITOR != ALL. -# We must avoid killing this script itself, so we filter by the exact executable name. -if [ "$MONITOR" = "ALL" ]; then - pkill -x "mpvpaper" 2>/dev/null -else - pgrep -x mpvpaper | while read -r pid; do - if ps -p "$pid" -o args= | grep -q "$MONITOR"; then - kill "$pid" 2>/dev/null - fi - done -fi -SOCKET="/tmp/ambxst_mpv_socket_${MONITOR}" - -MPV_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 load-scripts=no input-ipc-server=$SOCKET" - -# Si el shader no está vacío y el archivo existe, agregarlo a MPV_OPTS -if [ -n "$SHADER" ] && [ -f "$SHADER" ]; then - MPV_OPTS="$MPV_OPTS glsl-shaders=$SHADER" -fi - -nohup mpvpaper -o "$MPV_OPTS" "$MONITOR" "$WALLPAPER" >/tmp/mpvpaper.log 2>&1 & diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index eabcd2eb..7f07673c 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -1,6 +1,7 @@ #version 440 -layout(location = 0) in vec2 qt_TexCoord0; -layout(location = 0) out vec4 fragColor; + +layout(location = 0) in mediump vec2 qt_TexCoord0; +layout(location = 0) out mediump vec4 fragColor; layout(binding = 1) uniform sampler2D source; layout(binding = 2) uniform sampler2D paletteTexture; @@ -13,42 +14,47 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; +const float SHARPNESS = 20.0; +const float EPSILON = 1e-5; + +// Fast rational approximation of exp(-x) for x >= 0. +float fastExpNeg(float x) { + x = clamp(x, 0.0, 10.0); + return 1.0 / (1.0 + x + 0.5 * x * x); +} + void main() { - vec4 tex = texture(source, qt_TexCoord0); - vec3 color = tex.rgb; - - vec3 accumulatedColor = vec3(0.0); - float totalWeight = 0.0; - - int size = int(ubuf.paletteSize); - - // "Sharpness" factor. - // Higher value = colors stick closer to the palette (more posterized). - // Lower value = colors blend more (more washed out/grey). - // 15.0 - 20.0 is a good sweet spot for keeping identity while allowing gradients. - float distributionSharpness = 20.0; - - for (int i = 0; i < 128; i++) { + mediump vec4 tex = texture(source, qt_TexCoord0); + mediump vec3 color = tex.rgb; + + int size = int(clamp(ubuf.paletteSize, 0.0, 128.0)); + if (size <= 0) { + fragColor = tex * ubuf.qt_Opacity; + return; + } + + // Precompute UV step to avoid divisions inside loop + float stepU = 1.0 / float(size); + float u = 0.5 * stepU; + + mediump vec3 accumulated = vec3(0.0); + mediump float totalWeight = 0.0; + + for (int i = 0; i < 128; ++i) { if (i >= size) break; - - float u = (float(i) + 0.5) / ubuf.paletteSize; - vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; - - vec3 diff = color - pColor; - // Euclidean squared distance - float distSq = dot(diff, diff); - - // Gaussian Weighting function: e^(-k * d^2) - // This creates a smooth bell curve of influence around each palette color. - float weight = exp(-distributionSharpness * distSq); - - accumulatedColor += pColor * weight; + + mediump vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; + u += stepU; + + mediump vec3 diff = color - pColor; + mediump float distSq = diff.x*diff.x + diff.y*diff.y + diff.z*diff.z; + + mediump float weight = fastExpNeg(SHARPNESS * distSq); + + accumulated += pColor * weight; totalWeight += weight; } - // Normalize - vec3 finalColor = accumulatedColor / (totalWeight + 0.00001); // Avoid div by zero - - // Pre-multiply alpha for proper blending in Qt Quick + mediump vec3 finalColor = accumulated / (totalWeight + EPSILON); fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; -} +} \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 1b3548ee882691a50e7a129a9b0b8fc3287768da..3619e2fc805b5b2ac701bfacfcb1a5cedab4656f 100644 GIT binary patch literal 3348 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 literal 2474 zcmV;b303w004Q2`ob6g|ZyU!EUQv=&(@x?zj_uTGa*pysN;E}Lq9mJ^qExaYr*fp2 zs+Cqz*b{k_&L(;E@n}(Ug(fHhYpfjLhOhJi3{-~dg}4I!8I%ms1@EOBLOLQX`Mox0>`4keMS$? zL1azTNT@&}fjEHCMZLQ4bzU-;DGmYsI3h=4hxAlp%l*X8Nam*4B=#bW_aLy+Pv9&B zR_M1&VcR2GiLCY$(HC#XX;G&bHz*JVxwsf2JkU$v^^i|$mjfwo3P&Di^=YpYcoy4G z^sj@F(GgzUDca*Bpf8g>UhXLp;pc~|xz%JnC?=;foOa?xF(~A@d9(EL~6lcuNX{zCd45g9{nQL(+RV|_Cu zSAdV2pUcx9|MK1}ucM5(j$qwPSAcPhJj6)rW4ea<8Oq;?XxA&oz9nQBx;T$c8oGJk z&WGsN44U`nCkFi{Y@73Aq4`8;emJyyFl1*TtLZuH{hy%gDtH+GC#^M$wXj~EyC(Nc>Ai+E zYCBm&?*i<2h|~MPz9c&*WK0#dXvFuYJ>t6r{AQp=*Lw*&Q=Kn;f z^P=fHpnnISbKtoH`E_90$Da#zUVj7nE-*d6Db)G<3i>_;596l^VeA+1@e=0Q&IWM3 zu3y40t#?bPbNmhb{0ux>hRn85wdYIld=Bh0(LQgV8#?pQsp(${RjYK~*ue6leMTAM zJmhopEuK0LcZABrFG!y0BCvvJpT(l+Jc}t~{UxkluV**39v9f2jIrv8_kipBYxsU2 z`tHHD15DfE7cR#5JaCPCAXE*j8TNhPI_7oA`G#E3L#_e*6Ri6oWQBq40~;`~Ca{R~ zZ$zG#1IX`Vz9HIY_)w@bTrg}u0{#uo+pnSH5#mq*_8+o5r`~pk@cub~?ZGS1&{v}m z9l!2+RckVpiq?Fm;7V%G-QBBMrLt!S*)G_v5*V!qexndrJB?EGQPiTVVh7XKgCKv~ z4t%$nm6+7P;9AWtxWR#m*6gwq1WwL9bWAvKnwxGh*b8AR4%sHVM`XHDbxWRKVcS-N zei@KjVco9O%8s92w(4HPFOdFz^H{`&(+nCu@kJln{()QFwH`W!G%uWHmORfdrg+YC zixx*R7KhKoHO;HD=J_|PNH52f%|wTEJgfPqtb$ked?k@=9vm7ocA?OyG|G116qmrn zcr2CF3J?!3u*;h=BE&+oYLYg_s#7=#qhX9glQg2-p`z&02D&?qfc7=FXZy9PQ?HX? zx}}ZcWyM&f%Qmv~xNc3R=V&lK9+w;{2GVkgSgnP5X+GY9+YEf|_>O%)+k%c-BekLi znTg3^%#B;)`Kot$E`8wGsL_0ew@Mq@5Z3kU^i2MIQMZp z+JTj>mE5Xb))DHmFrl)mLzQ(kc~xGs)3Z~vbF-PrnW;=BGdZ0}&rHqCPR`9aS5h-r z$Zk1*)3E#M!r>p zNQ`XV_e!O@6T}LgM@X|`IU(NZZeAk=!_3KpZgySAx_A$J@~ZmQ#k?O^C2vEw)a%OE z1=Ke)G66f7Q)Mm}&^}vv#gs?wGnwnm+^x0q@N{UO?9$%J+|vpRQLC^6x1byjS6Oqe zv(%!&>b$nwUGPnXu#?kuNN*m~$#YXl)BhFQ{(yPMyz z{aq)33R&@)A>UEmunoG%Mv-#LoQ0m(Y^3T!> zO3zI*DCusH@7WCywpKsr%+A*4^iJ9Y@(GQi6z&vI{=@7QaPCRn;#oRHDQ@Je(Js{S zd2++ZhZ;urNmg%TT~)%O!m{h+T^{Q*+fgg#O^#$|QPQi$kyk(u(@KLU3B_b0$qt9x zl}+|8hqA94QgCGUL;k=mkQaaLV%s)lJbifM&ZCrMIg#rAfB&Uux{stneeIsgCw diff --git a/modules/widgets/dashboard/wallpapers/palette.vert b/modules/widgets/dashboard/wallpapers/palette.vert index fbecaa9d..9567262f 100644 --- a/modules/widgets/dashboard/wallpapers/palette.vert +++ b/modules/widgets/dashboard/wallpapers/palette.vert @@ -14,4 +14,4 @@ layout(std140, binding = 0) uniform buf { void main() { qt_TexCoord0 = qt_MultiTexCoord0; gl_Position = ubuf.qt_Matrix * qt_Vertex; -} +} \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.vert.qsb b/modules/widgets/dashboard/wallpapers/palette.vert.qsb index a1bd0d0b32f1dd3a8dd4b094cad0369d1d4ad195..5ec668ccdee0b43623d9d06dc8c645387cacd0df 100644 GIT binary patch literal 1331 zcmV-31{R!Y1(`^-GE&&=Lg25(G#(6BU^$POFc^XsZ^Xtu>%}S^{mD;CNU0gz8L~xngl1Tcl@@Rg6D|8jW z0{Nq8Y%~Y2r+IDSqkt>iHXA6JIjkxtwm7Fm;L^y4Yz2-pKZ;W{^Q4amKW_;!ld>ge zPi#51J+f?hwmr6N1-5Niwj$d$`K&hqELvx$^<8VdU}=k*1)(Z0jyN%kE}M~Zim<5Aw%QN$(YqVX}WFEjoYiblIBwk>cS zk{C10Im`T4m~)oYmuEl$-=R*{X?zH|ACBypx=W?C# zl*28y)0#N$F#k^|nYCcd9gefe{c@G#5YJu4{e~PqkFR=>?_ZAZIN$%=cT0yd2$k=N zm1@;-LLEeHEm~36dF==xMBmjl@lrRpTpg%k-PpQ4-_>N?9=L6#k8JC}?MbcWjyjgM zSuX_9NrBv~0r zBR;YzP8=sC`p|mzTB6_oU-Z9E^xQX==r2d2KX~xYiS9>wT$lIUBR|sRp5J!){42{s zc@uj+i?1g5fBgpJezuD7(j_|^*KPp56FX0zJlk!q2Ywhf zpLl~vv-)Wdu_~T__3irh_Or*_36n@IDiSzTb4W_(F_k+N1c}d02 p_HE(r^y8+at}K~k_{Eiad5wPEFuyZpTQ{?x*wQ#N{s4CyfgmlaqG|vD literal 1580 zcmV+{2GjWf03)S%ob6g$PuxZnp1lCU(sVb_?FNyi%9bb*sX`!0aw=r8F@)t)nRY|r|FnpO&sFv#nfGvB$NGxh?E zv3@2;0j@Fjig8wF3(RM2m|5%vGubYiWe)tftjVf?%EOiWn+8i6!z5+}m|d8m2*!%I zbvRp{7^N;`22F7F9ma-$E5w*(?xA{7KrdSdpIb0H1NUIQq@a2f#}=3YcNgM=jtvmv zk%Ar!CYOk1k!=A}16VlQBc+dmsKm#J0Jp&zF2c7AZ2NGRY)`V-1}WAr0p5i9CNa!q zF$@#K9DG`A0~k25%xAIu*O8-pAoMKVn-Qe&dm%sGhtL7Y!Z=ZfAc4p{06D_@)A{B3 zipQIk>3f)6c3xDjRB(Z+8I^Pl%f@*>lNq`Q*A3){-F})gS`W}#$Om~tOqR_YWPONm zL6!qrj%YcBmM2<)j6u>lO!|jN=P;A?P#_(bNJoxz zToQCSuE2VPWEO>vJ`vXu(xG0c>k7qUlyu?xsuG`%!Z^zHb<%r|@Yh*Xi@1N2?8N;q z!M{9sgMi->p_P&Eu}$;QG4X(q3yi zjE=#l^;f*UU~t#ktqEGI?HC-1i(5v+;(ID}%V?XNo6FX|sgk+5`_gLi%@}RYv^F+* zO*^P*nr~Yz$L*kQ-G>h#Z5!^MWpC))W@8HVy)buY9mj1}Yns8qo^_v_;pT$h=2jL$ z_^@KSpcCafR#V4-l*;6$DqNpQ6Njjcc74h5ERNxt5k^+R(CBwm6$hG@5p=k6zkPr~ zk9MRXb=U(Q(|srTh@mU{fDbJIsJge4mcCsU4z*{dK9uwwNzVj9kM+z46Z8|_GjEih zc^akf-my!c3oa6*WS!w*i`3z=y$kpu6UV z<#E$3tycr{U1z7>F}`

Fo6Gw6LxoS@!^P)A+$@wLFuT8r8CziU3LpjbW#gSS8JI zaAhwE57M8&?x$j9StP(aWmzdur3SYe_+m!lZdFk2nmHMpf zcwYUPz2$R?ew+hLfAP(?vx|$*pHvkGQFRbZLtR&B0JFTJ1nMB@ z0_cBU|KF|9@!O@){mEGEl|_9gJ(3-m2&xWw*f8rB&J~+tftyibl?)@Jj3nS{qbf0w zgcMsPyCW77q*5x~;pnQ#1U4DqnAxt$ebTAirKC>gfmLMD-?{Mo+8?*L)rnN!ta&?UE5Mt=#ZS10^PcW1_q_M zwl=9Z92=IlNWpEeIV^~k_BejS#9JbDmU>GE}7M)qUv;H zQDRm02xDSp3D42|AE#w`{K_)z;ijVQzt6=RV-k-yK&el-IArHvm3{0>X82zTjQ%$y e_KKOvxb12vV diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 6d620894..40e133be 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -59,6 +59,9 @@ let export QML2_IMPORT_PATH="${envAmbxst}/lib/qt-6/qml:$QML2_IMPORT_PATH" export QML_IMPORT_PATH="$QML2_IMPORT_PATH" + # Ensure QtMultimedia uses GStreamer backend + export QT_MEDIA_BACKEND=gstreamer + # Make bundled fonts available to fontconfig export FONTCONFIG_PATH="${fontconfigConf}/etc/fonts:''${FONTCONFIG_PATH:-}" diff --git a/nix/packages/media.nix b/nix/packages/media.nix index 56074e79..2b3e303e 100644 --- a/nix/packages/media.nix +++ b/nix/packages/media.nix @@ -3,7 +3,14 @@ with pkgs; [ gpu-screen-recorder - mpvpaper + + # GStreamer backend for QtMultimedia + gst_all_1.gstreamer + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good + gst_all_1.gst-plugins-bad + gst_all_1.gst-plugins-ugly + gst_all_1.gst-libav ffmpeg x264 diff --git a/scripts/thumbgen.py b/scripts/thumbgen.py index 5064f991..31556341 100755 --- a/scripts/thumbgen.py +++ b/scripts/thumbgen.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 """ -Thumbnail Generator for Ambxst Wallpaper System -Generates thumbnails for video files, images, and GIFs using FFmpeg and ImageMagick with multithreading. +Thumbnail & Palette Generator for Ambxst Wallpaper System +Generates thumbnails for video files, images, and GIFs using FFmpeg and ImageMagick. +Also generates a dynamic color palette (dominant colors) for each media file. +Uses only Python standard library + system tools (FFmpeg, ImageMagick). """ import json @@ -10,6 +12,7 @@ import sys import threading import time +from collections import Counter from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path from typing import List, Optional, Tuple @@ -22,6 +25,11 @@ # Default thumbnail size THUMBNAIL_SIZE = "140x140" +# Palette sample size (resize image to this square before counting colors) +PALETTE_SAMPLE_SIZE = 128 +# Maximum number of colors in the palette +MAX_PALETTE_COLORS = 25 + class ThumbnailGenerator: def __init__( @@ -35,6 +43,7 @@ def __init__( self.fallback_path = Path(fallback_path).expanduser() if fallback_path else None self.wall_path: Optional[Path] = None self.thumbnails_dir: Optional[Path] = None + self.palettes_dir: Optional[Path] = None self.files_to_process = [] self.total_files = 0 self.processed_count = 0 @@ -66,12 +75,16 @@ def load_config(self) -> bool: print(f"ERROR: Wallpaper directory not found: {self.wall_path}") return False - # Setup thumbnails directory using provided cache base + # Setup directories self.thumbnails_dir = self.cache_base_path / "thumbnails" self.thumbnails_dir.mkdir(parents=True, exist_ok=True) + self.palettes_dir = self.cache_base_path / "palettes" + self.palettes_dir.mkdir(parents=True, exist_ok=True) + print(f"✓ Config loaded: {self.wall_path}") print(f"✓ Thumbnails cache: {self.thumbnails_dir}") + print(f"✓ Palettes cache: {self.palettes_dir}") return True except Exception as e: @@ -87,7 +100,6 @@ def find_files(self) -> List[Path]: return [] try: - # Recursively find all files in wallpaper directory and subdirectories for file_path in self.wall_path.rglob("*"): if file_path.is_file() and not file_path.name.startswith("."): # Check if any parent directory is hidden @@ -103,9 +115,7 @@ def find_files(self) -> List[Path]: ): files.append(file_path) - # Sort for consistent ordering files.sort() - print(f"✓ Found {len(files)} media files") return files @@ -118,29 +128,36 @@ def get_thumbnail_path(self, file_path: Path) -> Path: if self.wall_path is None or self.thumbnails_dir is None: raise RuntimeError("Paths not initialized") - # Get relative path from wall_path try: relative_path = file_path.relative_to(self.wall_path) except ValueError: raise ValueError(f"File {file_path} is not within {self.wall_path}") - # Create thumbnail name with .jpg extension thumbnail_name = file_path.name + ".jpg" - - # Build the proxy path thumbnail_path = self.thumbnails_dir / relative_path.parent / thumbnail_name - return thumbnail_path + def get_palette_path(self, file_path: Path) -> Path: + """Get palette JSON path for a media file.""" + if self.wall_path is None or self.palettes_dir is None: + raise RuntimeError("Paths not initialized") + + try: + relative_path = file_path.relative_to(self.wall_path) + except ValueError: + raise ValueError(f"File {file_path} is not within {self.wall_path}") + + palette_name = file_path.name + ".json" + palette_path = self.palettes_dir / relative_path.parent / palette_name + return palette_path + def needs_thumbnail(self, file_path: Path) -> bool: """Check if file needs thumbnail generation.""" thumbnail_path = self.get_thumbnail_path(file_path) - # If thumbnail doesn't exist, needs generation if not thumbnail_path.exists(): return True - # If file is newer than thumbnail, needs regeneration try: file_mtime = file_path.stat().st_mtime thumbnail_mtime = thumbnail_path.stat().st_mtime @@ -148,39 +165,50 @@ def needs_thumbnail(self, file_path: Path) -> bool: except: return True + def needs_palette(self, file_path: Path) -> bool: + """Check if file needs palette generation.""" + palette_path = self.get_palette_path(file_path) + + if not palette_path.exists(): + return True + + try: + file_mtime = file_path.stat().st_mtime + palette_mtime = palette_path.stat().st_mtime + return file_mtime > palette_mtime + except: + return True + def generate_video_thumbnail(self, video_path: Path) -> Tuple[bool, str]: """Generate thumbnail for a video file using FFmpeg.""" thumbnail_path = self.get_thumbnail_path(video_path) try: - # Ensure parent directory exists thumbnail_path.parent.mkdir(parents=True, exist_ok=True) - # FFmpeg command for high-quality thumbnail cmd = [ "ffmpeg", "-y", "-i", str(video_path), "-ss", - "00:00:01", # Skip first second to avoid black frames + "00:00:01", "-vframes", - "1", # Extract only 1 frame + "1", "-vf", f"scale=140:140:force_original_aspect_ratio=increase,crop=140:140", "-q:v", - "2", # High quality + "2", "-f", - "image2", # Force image format + "image2", str(thumbnail_path), ] - # Run FFmpeg with error suppression result = subprocess.run( cmd, capture_output=True, text=True, - timeout=30, # 30 second timeout per video + timeout=30, ) if result.returncode == 0 and thumbnail_path.exists(): @@ -199,30 +227,27 @@ def generate_image_thumbnail(self, image_path: Path) -> Tuple[bool, str]: thumbnail_path = self.get_thumbnail_path(image_path) try: - # Ensure parent directory exists thumbnail_path.parent.mkdir(parents=True, exist_ok=True) - # ImageMagick command for high-quality thumbnail cmd = [ "convert", str(image_path), "-resize", - "140x140^", # Force resize to exact dimensions + "140x140^", "-gravity", - "center", # Center the crop + "center", "-extent", - "140x140", # Crop to exact size + "140x140", "-quality", - "85", # High quality JPEG + "85", str(thumbnail_path), ] - # Run ImageMagick result = subprocess.run( cmd, capture_output=True, text=True, - timeout=15, # 15 second timeout per image + timeout=15, ) if result.returncode == 0 and thumbnail_path.exists(): @@ -237,36 +262,33 @@ def generate_image_thumbnail(self, image_path: Path) -> Tuple[bool, str]: return False, str(e) def generate_gif_thumbnail(self, gif_path: Path) -> Tuple[bool, str]: - """Generate thumbnail for a GIF file using FFmpeg (extract first frame).""" + """Generate thumbnail for a GIF file using FFmpeg.""" thumbnail_path = self.get_thumbnail_path(gif_path) try: - # Ensure parent directory exists thumbnail_path.parent.mkdir(parents=True, exist_ok=True) - # FFmpeg command to extract first frame from GIF cmd = [ "ffmpeg", "-y", "-i", str(gif_path), "-vframes", - "1", # Extract only the first frame + "1", "-vf", f"scale=140:140:force_original_aspect_ratio=increase,crop=140:140", "-q:v", - "2", # High quality + "2", "-f", - "image2", # Force image format + "image2", str(thumbnail_path), ] - # Run FFmpeg result = subprocess.run( cmd, capture_output=True, text=True, - timeout=15, # 15 second timeout per GIF + timeout=15, ) if result.returncode == 0 and thumbnail_path.exists(): @@ -280,6 +302,122 @@ def generate_gif_thumbnail(self, gif_path: Path) -> Tuple[bool, str]: except Exception as e: return False, str(e) + def generate_palette(self, file_path: Path) -> Tuple[bool, str]: + """ + Generate a color palette (dominant colors) for a media file. + Uses ImageMagick to create a small PPM image, then counts colors in pure Python. + Returns (success, message). + """ + palette_path = self.get_palette_path(file_path) + + try: + palette_path.parent.mkdir(parents=True, exist_ok=True) + + # Step 1: Use ImageMagick to resize and convert to PPM (binary P6 format) + # PPM is simple to parse without external libraries. + # We'll request a sample size of PALETTE_SAMPLE_SIZE x PALETTE_SAMPLE_SIZE. + cmd = [ + "convert", + str(file_path), + "-resize", + f"{PALETTE_SAMPLE_SIZE}x{PALETTE_SAMPLE_SIZE}^", + "-gravity", + "center", + "-extent", + f"{PALETTE_SAMPLE_SIZE}x{PALETTE_SAMPLE_SIZE}", + "-strip", # Remove metadata + "ppm:-", # Output PPM to stdout + ] + + result = subprocess.run( + cmd, + capture_output=True, + timeout=20, + ) + + if result.returncode != 0: + error_msg = result.stderr.decode("utf-8", errors="ignore").strip() + return False, f"ImageMagick failed: {error_msg}" + + ppm_data = result.stdout + + # Step 2: Parse PPM P6 format + # Format: "P6\n \n\n" + header_end = ppm_data.find(b"\n", 0, 100) # first newline after magic + if header_end == -1 or not ppm_data.startswith(b"P6"): + return False, "Invalid PPM header (not P6)" + + # Find second newline (dimensions) + dims_start = header_end + 1 + dims_end = ppm_data.find(b"\n", dims_start, dims_start + 30) + if dims_end == -1: + return False, "Invalid PPM dimensions" + + dims_line = ppm_data[dims_start:dims_end].decode("ascii") + try: + width_str, height_str = dims_line.split() + width = int(width_str) + height = int(height_str) + except: + return False, "Could not parse PPM dimensions" + + # Maxval line + maxval_start = dims_end + 1 + maxval_end = ppm_data.find(b"\n", maxval_start, maxval_start + 10) + if maxval_end == -1: + return False, "Invalid PPM maxval" + + maxval_line = ppm_data[maxval_start:maxval_end].decode("ascii") + try: + maxval = int(maxval_line) + except: + return False, "Could not parse PPM maxval" + + data_start = maxval_end + 1 + expected_bytes = width * height * 3 + if len(ppm_data) - data_start < expected_bytes: + return False, f"Incomplete PPM data: expected {expected_bytes}, got {len(ppm_data)-data_start}" + + # Step 3: Read RGB triples and count frequencies + # We'll use a tuple (r,g,b) as key, but we can reduce bit depth to group similar colors. + # For simplicity, we'll use the raw 8-bit per channel (after scaling if maxval != 255). + pixel_data = ppm_data[data_start:data_start + expected_bytes] + + # If maxval is not 255, we need to scale values to 0-255 range + scale = 255.0 / maxval if maxval != 255 else 1.0 + + color_counter = Counter() + idx = 0 + for _ in range(width * height): + r = int(pixel_data[idx] * scale) + g = int(pixel_data[idx+1] * scale) + b = int(pixel_data[idx+2] * scale) + idx += 3 + # Round to nearest integer and clamp + color_counter[(r, g, b)] += 1 + + # Step 4: Get most common colors up to MAX_PALETTE_COLORS + most_common = color_counter.most_common(MAX_PALETTE_COLORS) + palette_hex = [] + for (r, g, b), _ in most_common: + hex_color = "#{:02x}{:02x}{:02x}".format(r, g, b) + palette_hex.append(hex_color) + + # Step 5: Save as JSON + palette_data = { + "colors": palette_hex, + "size": len(palette_hex), + } + with open(palette_path, "w") as f: + json.dump(palette_data, f, indent=2) + + return True, f"Palette generated with {len(palette_hex)} colors" + + except subprocess.TimeoutExpired: + return False, "Timeout" + except Exception as e: + return False, str(e) + def generate_single_thumbnail(self, file_path: Path) -> Tuple[bool, str]: """Generate thumbnail for a single file based on its type.""" try: @@ -308,7 +446,7 @@ def generate_single_thumbnail(self, file_path: Path) -> Tuple[bool, str]: return False, str(e) def process_files(self, max_workers: int = 4) -> None: - """Process files with multithreading.""" + """Process files with multithreading for both thumbnails and palettes.""" all_files = self.files_to_process if not all_files: @@ -319,73 +457,94 @@ def process_files(self, max_workers: int = 4) -> None: start_time = time.time() failed_files = [] + palette_failed = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: - # Submit all jobs + # Submit thumbnail jobs future_to_file = { executor.submit(self.generate_single_thumbnail, file_path): file_path for file_path in all_files } - # Process completed jobs + # Also submit palette jobs (if needed) + palette_futures = {} + for file_path in all_files: + if self.needs_palette(file_path): + future = executor.submit(self.generate_palette, file_path) + palette_futures[future] = file_path + + # Process thumbnail results for future in as_completed(future_to_file): file_path = future_to_file[future] try: success, message = future.result() if not success: failed_files.append((file_path, message)) - except Exception as e: failed_files.append((file_path, str(e))) + # Process palette results + for future in as_completed(palette_futures): + file_path = palette_futures[future] + try: + success, message = future.result() + if not success: + palette_failed.append((file_path, message)) + except Exception as e: + palette_failed.append((file_path, str(e))) + elapsed = time.time() - start_time - success_count = self.total_files - len(failed_files) + total_files = len(all_files) + success_count = total_files - len(failed_files) print(f"\n🏁 Processing complete in {elapsed:.1f}s") - print(f"✅ Success: {success_count}/{self.total_files}") + print(f"✅ Thumbnails success: {success_count}/{total_files}") if failed_files: - print(f"❌ Failed: {len(failed_files)}") - for file_path, error in failed_files[:3]: # Show first 3 errors + print(f"❌ Thumbnails failed: {len(failed_files)}") + for file_path, error in failed_files[:3]: print(f" • {file_path.name}: {error}") if len(failed_files) > 3: print(f" ... and {len(failed_files) - 3} more") + if palette_failed: + print(f"⚠️ Palettes failed: {len(palette_failed)}") + for file_path, error in palette_failed[:3]: + print(f" • {file_path.name}: {error}") + if len(palette_failed) > 3: + print(f" ... and {len(palette_failed) - 3} more") + def run(self) -> int: """Main execution function.""" - print("🖼️ Ambxst Thumbnail Generator") + print("🖼️ Ambxst Thumbnail & Palette Generator") print("=" * 40) - # Load configuration if not self.load_config(): return 1 - # Find all files files = self.find_files() if not files: print("ℹ️ No media files found") return 0 - # Filter files that need thumbnails + # Filter files that need thumbnails OR palettes for file_path in files: - if self.needs_thumbnail(file_path): + if self.needs_thumbnail(file_path) or self.needs_palette(file_path): self.files_to_process.append(file_path) self.total_files = len(self.files_to_process) if self.total_files == 0: - print("✓ All thumbnails are up to date") + print("✓ All thumbnails and palettes are up to date") return 0 - print(f"📋 {self.total_files} files need thumbnail generation") + print(f"📋 {self.total_files} files need processing (thumbnails or palettes)") - # Determine optimal worker count max_workers = min(4, os.cpu_count() or 1, self.total_files) - # Process files try: self.process_files(max_workers) - print("🎉 Thumbnail generation complete!") + print("🎉 Generation complete!") return 0 except KeyboardInterrupt: print("\n⚠️ Interrupted by user") @@ -399,7 +558,7 @@ def main(): """Entry point.""" if len(sys.argv) < 3 or len(sys.argv) > 4: print( - "Usage: python3 generate_thumbnails.py [fallback_wall_path]" + "Usage: python3 thumbgen.py [fallback_wall_path]" ) return 1 @@ -412,4 +571,4 @@ def main(): if __name__ == "__main__": - sys.exit(main()) + sys.exit(main()) \ No newline at end of file From b25694be78002423aaa8a7e0b57fce674cd1fa66 Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 18:55:22 -0600 Subject: [PATCH 02/14] Update .gitignore --- .gitignore | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 2d87792f..969ce020 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ -scripts/LUT 3D/Cargo.lock -/scripts/LUT 3D/target/release/.fingerprint -/scripts/LUT 3D/target/release/deps -/scripts/LUT 3D/target/release/build/zmij-cbe3f8201850d6e1 -/scripts/LUT 3D/target/release/build/av-scenechange-8559eb33c3ad0528 -scripts/LUT 3D/target/.rustc_info.json +.qmlls.ini +.ruff_cache/ +scripts/__pycache__ +.sisyphus From 7d850517e41da714fc6b60732abc3eae28984197 Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 18:57:45 -0600 Subject: [PATCH 03/14] Update thumbgen.py --- scripts/thumbgen.py | 273 +++++++++----------------------------------- 1 file changed, 57 insertions(+), 216 deletions(-) diff --git a/scripts/thumbgen.py b/scripts/thumbgen.py index 31556341..5064f991 100755 --- a/scripts/thumbgen.py +++ b/scripts/thumbgen.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 """ -Thumbnail & Palette Generator for Ambxst Wallpaper System -Generates thumbnails for video files, images, and GIFs using FFmpeg and ImageMagick. -Also generates a dynamic color palette (dominant colors) for each media file. -Uses only Python standard library + system tools (FFmpeg, ImageMagick). +Thumbnail Generator for Ambxst Wallpaper System +Generates thumbnails for video files, images, and GIFs using FFmpeg and ImageMagick with multithreading. """ import json @@ -12,7 +10,6 @@ import sys import threading import time -from collections import Counter from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path from typing import List, Optional, Tuple @@ -25,11 +22,6 @@ # Default thumbnail size THUMBNAIL_SIZE = "140x140" -# Palette sample size (resize image to this square before counting colors) -PALETTE_SAMPLE_SIZE = 128 -# Maximum number of colors in the palette -MAX_PALETTE_COLORS = 25 - class ThumbnailGenerator: def __init__( @@ -43,7 +35,6 @@ def __init__( self.fallback_path = Path(fallback_path).expanduser() if fallback_path else None self.wall_path: Optional[Path] = None self.thumbnails_dir: Optional[Path] = None - self.palettes_dir: Optional[Path] = None self.files_to_process = [] self.total_files = 0 self.processed_count = 0 @@ -75,16 +66,12 @@ def load_config(self) -> bool: print(f"ERROR: Wallpaper directory not found: {self.wall_path}") return False - # Setup directories + # Setup thumbnails directory using provided cache base self.thumbnails_dir = self.cache_base_path / "thumbnails" self.thumbnails_dir.mkdir(parents=True, exist_ok=True) - self.palettes_dir = self.cache_base_path / "palettes" - self.palettes_dir.mkdir(parents=True, exist_ok=True) - print(f"✓ Config loaded: {self.wall_path}") print(f"✓ Thumbnails cache: {self.thumbnails_dir}") - print(f"✓ Palettes cache: {self.palettes_dir}") return True except Exception as e: @@ -100,6 +87,7 @@ def find_files(self) -> List[Path]: return [] try: + # Recursively find all files in wallpaper directory and subdirectories for file_path in self.wall_path.rglob("*"): if file_path.is_file() and not file_path.name.startswith("."): # Check if any parent directory is hidden @@ -115,7 +103,9 @@ def find_files(self) -> List[Path]: ): files.append(file_path) + # Sort for consistent ordering files.sort() + print(f"✓ Found {len(files)} media files") return files @@ -128,36 +118,29 @@ def get_thumbnail_path(self, file_path: Path) -> Path: if self.wall_path is None or self.thumbnails_dir is None: raise RuntimeError("Paths not initialized") + # Get relative path from wall_path try: relative_path = file_path.relative_to(self.wall_path) except ValueError: raise ValueError(f"File {file_path} is not within {self.wall_path}") + # Create thumbnail name with .jpg extension thumbnail_name = file_path.name + ".jpg" - thumbnail_path = self.thumbnails_dir / relative_path.parent / thumbnail_name - return thumbnail_path - - def get_palette_path(self, file_path: Path) -> Path: - """Get palette JSON path for a media file.""" - if self.wall_path is None or self.palettes_dir is None: - raise RuntimeError("Paths not initialized") - try: - relative_path = file_path.relative_to(self.wall_path) - except ValueError: - raise ValueError(f"File {file_path} is not within {self.wall_path}") + # Build the proxy path + thumbnail_path = self.thumbnails_dir / relative_path.parent / thumbnail_name - palette_name = file_path.name + ".json" - palette_path = self.palettes_dir / relative_path.parent / palette_name - return palette_path + return thumbnail_path def needs_thumbnail(self, file_path: Path) -> bool: """Check if file needs thumbnail generation.""" thumbnail_path = self.get_thumbnail_path(file_path) + # If thumbnail doesn't exist, needs generation if not thumbnail_path.exists(): return True + # If file is newer than thumbnail, needs regeneration try: file_mtime = file_path.stat().st_mtime thumbnail_mtime = thumbnail_path.stat().st_mtime @@ -165,50 +148,39 @@ def needs_thumbnail(self, file_path: Path) -> bool: except: return True - def needs_palette(self, file_path: Path) -> bool: - """Check if file needs palette generation.""" - palette_path = self.get_palette_path(file_path) - - if not palette_path.exists(): - return True - - try: - file_mtime = file_path.stat().st_mtime - palette_mtime = palette_path.stat().st_mtime - return file_mtime > palette_mtime - except: - return True - def generate_video_thumbnail(self, video_path: Path) -> Tuple[bool, str]: """Generate thumbnail for a video file using FFmpeg.""" thumbnail_path = self.get_thumbnail_path(video_path) try: + # Ensure parent directory exists thumbnail_path.parent.mkdir(parents=True, exist_ok=True) + # FFmpeg command for high-quality thumbnail cmd = [ "ffmpeg", "-y", "-i", str(video_path), "-ss", - "00:00:01", + "00:00:01", # Skip first second to avoid black frames "-vframes", - "1", + "1", # Extract only 1 frame "-vf", f"scale=140:140:force_original_aspect_ratio=increase,crop=140:140", "-q:v", - "2", + "2", # High quality "-f", - "image2", + "image2", # Force image format str(thumbnail_path), ] + # Run FFmpeg with error suppression result = subprocess.run( cmd, capture_output=True, text=True, - timeout=30, + timeout=30, # 30 second timeout per video ) if result.returncode == 0 and thumbnail_path.exists(): @@ -227,27 +199,30 @@ def generate_image_thumbnail(self, image_path: Path) -> Tuple[bool, str]: thumbnail_path = self.get_thumbnail_path(image_path) try: + # Ensure parent directory exists thumbnail_path.parent.mkdir(parents=True, exist_ok=True) + # ImageMagick command for high-quality thumbnail cmd = [ "convert", str(image_path), "-resize", - "140x140^", + "140x140^", # Force resize to exact dimensions "-gravity", - "center", + "center", # Center the crop "-extent", - "140x140", + "140x140", # Crop to exact size "-quality", - "85", + "85", # High quality JPEG str(thumbnail_path), ] + # Run ImageMagick result = subprocess.run( cmd, capture_output=True, text=True, - timeout=15, + timeout=15, # 15 second timeout per image ) if result.returncode == 0 and thumbnail_path.exists(): @@ -262,33 +237,36 @@ def generate_image_thumbnail(self, image_path: Path) -> Tuple[bool, str]: return False, str(e) def generate_gif_thumbnail(self, gif_path: Path) -> Tuple[bool, str]: - """Generate thumbnail for a GIF file using FFmpeg.""" + """Generate thumbnail for a GIF file using FFmpeg (extract first frame).""" thumbnail_path = self.get_thumbnail_path(gif_path) try: + # Ensure parent directory exists thumbnail_path.parent.mkdir(parents=True, exist_ok=True) + # FFmpeg command to extract first frame from GIF cmd = [ "ffmpeg", "-y", "-i", str(gif_path), "-vframes", - "1", + "1", # Extract only the first frame "-vf", f"scale=140:140:force_original_aspect_ratio=increase,crop=140:140", "-q:v", - "2", + "2", # High quality "-f", - "image2", + "image2", # Force image format str(thumbnail_path), ] + # Run FFmpeg result = subprocess.run( cmd, capture_output=True, text=True, - timeout=15, + timeout=15, # 15 second timeout per GIF ) if result.returncode == 0 and thumbnail_path.exists(): @@ -302,122 +280,6 @@ def generate_gif_thumbnail(self, gif_path: Path) -> Tuple[bool, str]: except Exception as e: return False, str(e) - def generate_palette(self, file_path: Path) -> Tuple[bool, str]: - """ - Generate a color palette (dominant colors) for a media file. - Uses ImageMagick to create a small PPM image, then counts colors in pure Python. - Returns (success, message). - """ - palette_path = self.get_palette_path(file_path) - - try: - palette_path.parent.mkdir(parents=True, exist_ok=True) - - # Step 1: Use ImageMagick to resize and convert to PPM (binary P6 format) - # PPM is simple to parse without external libraries. - # We'll request a sample size of PALETTE_SAMPLE_SIZE x PALETTE_SAMPLE_SIZE. - cmd = [ - "convert", - str(file_path), - "-resize", - f"{PALETTE_SAMPLE_SIZE}x{PALETTE_SAMPLE_SIZE}^", - "-gravity", - "center", - "-extent", - f"{PALETTE_SAMPLE_SIZE}x{PALETTE_SAMPLE_SIZE}", - "-strip", # Remove metadata - "ppm:-", # Output PPM to stdout - ] - - result = subprocess.run( - cmd, - capture_output=True, - timeout=20, - ) - - if result.returncode != 0: - error_msg = result.stderr.decode("utf-8", errors="ignore").strip() - return False, f"ImageMagick failed: {error_msg}" - - ppm_data = result.stdout - - # Step 2: Parse PPM P6 format - # Format: "P6\n \n\n" - header_end = ppm_data.find(b"\n", 0, 100) # first newline after magic - if header_end == -1 or not ppm_data.startswith(b"P6"): - return False, "Invalid PPM header (not P6)" - - # Find second newline (dimensions) - dims_start = header_end + 1 - dims_end = ppm_data.find(b"\n", dims_start, dims_start + 30) - if dims_end == -1: - return False, "Invalid PPM dimensions" - - dims_line = ppm_data[dims_start:dims_end].decode("ascii") - try: - width_str, height_str = dims_line.split() - width = int(width_str) - height = int(height_str) - except: - return False, "Could not parse PPM dimensions" - - # Maxval line - maxval_start = dims_end + 1 - maxval_end = ppm_data.find(b"\n", maxval_start, maxval_start + 10) - if maxval_end == -1: - return False, "Invalid PPM maxval" - - maxval_line = ppm_data[maxval_start:maxval_end].decode("ascii") - try: - maxval = int(maxval_line) - except: - return False, "Could not parse PPM maxval" - - data_start = maxval_end + 1 - expected_bytes = width * height * 3 - if len(ppm_data) - data_start < expected_bytes: - return False, f"Incomplete PPM data: expected {expected_bytes}, got {len(ppm_data)-data_start}" - - # Step 3: Read RGB triples and count frequencies - # We'll use a tuple (r,g,b) as key, but we can reduce bit depth to group similar colors. - # For simplicity, we'll use the raw 8-bit per channel (after scaling if maxval != 255). - pixel_data = ppm_data[data_start:data_start + expected_bytes] - - # If maxval is not 255, we need to scale values to 0-255 range - scale = 255.0 / maxval if maxval != 255 else 1.0 - - color_counter = Counter() - idx = 0 - for _ in range(width * height): - r = int(pixel_data[idx] * scale) - g = int(pixel_data[idx+1] * scale) - b = int(pixel_data[idx+2] * scale) - idx += 3 - # Round to nearest integer and clamp - color_counter[(r, g, b)] += 1 - - # Step 4: Get most common colors up to MAX_PALETTE_COLORS - most_common = color_counter.most_common(MAX_PALETTE_COLORS) - palette_hex = [] - for (r, g, b), _ in most_common: - hex_color = "#{:02x}{:02x}{:02x}".format(r, g, b) - palette_hex.append(hex_color) - - # Step 5: Save as JSON - palette_data = { - "colors": palette_hex, - "size": len(palette_hex), - } - with open(palette_path, "w") as f: - json.dump(palette_data, f, indent=2) - - return True, f"Palette generated with {len(palette_hex)} colors" - - except subprocess.TimeoutExpired: - return False, "Timeout" - except Exception as e: - return False, str(e) - def generate_single_thumbnail(self, file_path: Path) -> Tuple[bool, str]: """Generate thumbnail for a single file based on its type.""" try: @@ -446,7 +308,7 @@ def generate_single_thumbnail(self, file_path: Path) -> Tuple[bool, str]: return False, str(e) def process_files(self, max_workers: int = 4) -> None: - """Process files with multithreading for both thumbnails and palettes.""" + """Process files with multithreading.""" all_files = self.files_to_process if not all_files: @@ -457,94 +319,73 @@ def process_files(self, max_workers: int = 4) -> None: start_time = time.time() failed_files = [] - palette_failed = [] with ThreadPoolExecutor(max_workers=max_workers) as executor: - # Submit thumbnail jobs + # Submit all jobs future_to_file = { executor.submit(self.generate_single_thumbnail, file_path): file_path for file_path in all_files } - # Also submit palette jobs (if needed) - palette_futures = {} - for file_path in all_files: - if self.needs_palette(file_path): - future = executor.submit(self.generate_palette, file_path) - palette_futures[future] = file_path - - # Process thumbnail results + # Process completed jobs for future in as_completed(future_to_file): file_path = future_to_file[future] try: success, message = future.result() if not success: failed_files.append((file_path, message)) - except Exception as e: - failed_files.append((file_path, str(e))) - # Process palette results - for future in as_completed(palette_futures): - file_path = palette_futures[future] - try: - success, message = future.result() - if not success: - palette_failed.append((file_path, message)) except Exception as e: - palette_failed.append((file_path, str(e))) + failed_files.append((file_path, str(e))) elapsed = time.time() - start_time - total_files = len(all_files) - success_count = total_files - len(failed_files) + success_count = self.total_files - len(failed_files) print(f"\n🏁 Processing complete in {elapsed:.1f}s") - print(f"✅ Thumbnails success: {success_count}/{total_files}") + print(f"✅ Success: {success_count}/{self.total_files}") if failed_files: - print(f"❌ Thumbnails failed: {len(failed_files)}") - for file_path, error in failed_files[:3]: + print(f"❌ Failed: {len(failed_files)}") + for file_path, error in failed_files[:3]: # Show first 3 errors print(f" • {file_path.name}: {error}") if len(failed_files) > 3: print(f" ... and {len(failed_files) - 3} more") - if palette_failed: - print(f"⚠️ Palettes failed: {len(palette_failed)}") - for file_path, error in palette_failed[:3]: - print(f" • {file_path.name}: {error}") - if len(palette_failed) > 3: - print(f" ... and {len(palette_failed) - 3} more") - def run(self) -> int: """Main execution function.""" - print("🖼️ Ambxst Thumbnail & Palette Generator") + print("🖼️ Ambxst Thumbnail Generator") print("=" * 40) + # Load configuration if not self.load_config(): return 1 + # Find all files files = self.find_files() if not files: print("ℹ️ No media files found") return 0 - # Filter files that need thumbnails OR palettes + # Filter files that need thumbnails for file_path in files: - if self.needs_thumbnail(file_path) or self.needs_palette(file_path): + if self.needs_thumbnail(file_path): self.files_to_process.append(file_path) self.total_files = len(self.files_to_process) if self.total_files == 0: - print("✓ All thumbnails and palettes are up to date") + print("✓ All thumbnails are up to date") return 0 - print(f"📋 {self.total_files} files need processing (thumbnails or palettes)") + print(f"📋 {self.total_files} files need thumbnail generation") + # Determine optimal worker count max_workers = min(4, os.cpu_count() or 1, self.total_files) + # Process files try: self.process_files(max_workers) - print("🎉 Generation complete!") + print("🎉 Thumbnail generation complete!") return 0 except KeyboardInterrupt: print("\n⚠️ Interrupted by user") @@ -558,7 +399,7 @@ def main(): """Entry point.""" if len(sys.argv) < 3 or len(sys.argv) > 4: print( - "Usage: python3 thumbgen.py [fallback_wall_path]" + "Usage: python3 generate_thumbnails.py [fallback_wall_path]" ) return 1 @@ -571,4 +412,4 @@ def main(): if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) From f48cdaa7a5e9e7867f7828b5e4b88697da1a8d15 Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 19:16:08 -0600 Subject: [PATCH 04/14] Shaders enhanced --- .../widgets/dashboard/wallpapers/palette.frag | 24 +++++++++++++----- .../dashboard/wallpapers/palette.frag.qsb | Bin 3348 -> 3340 bytes .../widgets/dashboard/wallpapers/palette.vert | 1 + 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index 7f07673c..2f11eb11 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -14,13 +14,16 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; -const float SHARPNESS = 20.0; -const float EPSILON = 1e-5; +const float SHARPNESS = 20.0; // Controls how strictly colors snap to the palette +const float EPSILON = 1e-5; // Small value to avoid division by zero -// Fast rational approximation of exp(-x) for x >= 0. +// Rational Padé [2,2] approximation of exp(-x) for x >= 0. +// Extremely close to the real exponential for typical distance values, +// but computed without the expensive native exp() function. float fastExpNeg(float x) { x = clamp(x, 0.0, 10.0); - return 1.0 / (1.0 + x + 0.5 * x * x); + float x2 = x * x; + return (1.0 - 0.5 * x + 0.1 * x2) / (1.0 + 0.5 * x + 0.1 * x2); } void main() { @@ -29,17 +32,20 @@ void main() { int size = int(clamp(ubuf.paletteSize, 0.0, 128.0)); if (size <= 0) { + // No palette defined: pass the original color through fragColor = tex * ubuf.qt_Opacity; return; } - // Precompute UV step to avoid divisions inside loop + // Precompute horizontal step for iterating through the palette texture float stepU = 1.0 / float(size); - float u = 0.5 * stepU; + float u = 0.5 * stepU; // Sample at pixel centers of the 1D palette texture mediump vec3 accumulated = vec3(0.0); mediump float totalWeight = 0.0; + // Soft blending: each palette color contributes according to its + // similarity to the source color, using a Gaussian-like weight. for (int i = 0; i < 128; ++i) { if (i >= size) break; @@ -47,14 +53,18 @@ void main() { u += stepU; mediump vec3 diff = color - pColor; - mediump float distSq = diff.x*diff.x + diff.y*diff.y + diff.z*diff.z; + mediump float distSq = dot(diff, diff); // Squared Euclidean distance + // Weight = exp(-SHARPNESS * distSq) (approximated) mediump float weight = fastExpNeg(SHARPNESS * distSq); accumulated += pColor * weight; totalWeight += weight; } + // Normalize the weighted sum mediump vec3 finalColor = accumulated / (totalWeight + EPSILON); + + // Preserve the original alpha and apply global opacity fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 3619e2fc805b5b2ac701bfacfcb1a5cedab4656f..469ca85857c422a45dc301866fa7679fd483b250 100644 GIT binary patch literal 3340 zcmV+n4fFB<06UF%ob6ldb6eFBU)ypXN`Md`ywj6pCP=ZPNPZ?(6hdQ2fCL8|5~ym# zwR9z4BIznuSFxPbgp~3uC8eb;g;JobSRdWh&YIz>L^=os15qyyxVOCA*{NkqLwM4#fJE)Of% zNJa9=)b{3~)0< zrl>|1<;fwR7GQM0nO*qVq?yZPlTSX`G))ejAsb?o%ZY8!%uPB$4i(6!IS3qDPGBPh z4v|d`m1vH9h#X!{#5}wq>#0sIHOQm9o?HwbW(6J3U7+iITNI$A@&1@%Vi)ze)^tMvE5!_$Wob-$1p8Rf8}>#0xk@z@QtTKj}Q70anI zotchk*Y{~U8$L*V6odYQ6rn*P+5#O0-`X1Q(>e?dL1X!`}N7qm%1>jUkOphZACj5Y1U`bpq&{YS*uktB6SH zPejKZf0(v~BD4lD(*`J_;p?E!;MZ$R-DmKdHU8Ya2EQd>Z{fES(ceD#By%f8Xgl;r zVE-Q2V*2g0mf{!>rz?ZsNqw|~Nb|A$0O(9h1Lw2M58F#XUoZG~L01C&j3+^3{$cR< zfqw+^WBM%=q22IZ561Ukoawhwgwj|GuWvK0)@Lsbo$|x78O$r-3GjWOvF}*ty<+bB z0kgdE?SWva2Mqr~to>%{)#sn(4r4vrVTbV}pc%O-tZ9%|>9u|r`))plkE_CS!q+}Hy?+icVZ($x|)UVfO>KoJkNxeExrV%GG_&SY!85KD1 z!-T;3`&bBnA|#&;jn9PiJr&}wgz)*0{MitGE`&cHlD_~vLlGUnI}y8#b9|-{n|BQ?y3!;W%ifF7f{MLUt76_ffx&tNRdp_d*}<-M2wsPir(j z2^|9%yAJVJMQq#;9@akrJcF?Zus?sGejS$&g2wUs59*_bF~@&XM2|g0q-h)j4~aEC zf;F0XAH}?<#JnE`{dM^5F~~iNxg7!RKh&f3KP=|?IOgd>*KyEZrhbi|K)yQ;UB^H( zwx2}Yy#W62ARj&n{wF|VIE!<}@C^LP@Kf;1zo?gT@Bx4Sog!or(JXw$<9Uqd#TvN5 zVjaJSuZy7B@Gs*}6KP#6`?QcPLDt}N@W=0<%K^_E_U)wDHy(S2NYhyFGnk{Xb5g{* zi`-%SR>S!`O8q+aPl0xeh<^`qr$mg`LHD}Ecm?CehL625cmp=iW3Ntw=8L^L4Su%8 zy~(J+xj$hW&r`p~&wytE^7Eh>e(qJ~1-=OUP1t`HJd2P&3!0fP_bz8eY<(a3_B{BX z2W=0=e?UZ0N0 z_ewLu=dXeGsyGL)A#P?7H?txhe}wpaj`}tJI%rek?7SX2JC>L~_hDHvpEpAD`6*~z zw~nF~z6rYda4ul_&!F#3*uDUovBkaJ1>`Eu^S?yydP~@U8+7CIcOdt+ko#2^xpzT- z6!Yf3(DaOk_8w?Gg7!XWd`6DaYJILgfc$$He^=z!-*m~ZAA)Z3>qpS>p~$ZvA;0dX z9$nj4Ar|@J9&?=f^xD>9kJgDgxk1!LmRk=#Q-^L4b!a{E=Jl|>7UM?tM#vg`BXDzu zZUWCn$Zr76@NWw7--?{H3Fn?^x1lE7D&+3KI*i<%=!5SNv33_^?-X)(hwN>Jy}O0n zw_tCxkW(RhTVPKKxvh}hBI?;T%!9}NOT8NBJ>ymk@6$afA85f>{bk!}+p9ZnRi%>2 zXw9?pj>hJk(p*gy%dX|8JIGGePe-G5-)rQ3HQOjg&qkF}s$%&g>Xe^5V)>raOlz9f zz{IJVm3RCFNv&CB+xP8h=Zr1MzTG_G6#Tgm?U3!1=KOSYF&&LIs!q}MDr{RdW*bF8 z3T9ZhDz&oh4Na=L+wk&sdf9PI;+Wm^8=jqxMo(Mbf>SN2id}FTm6|$j=ZAO!$L!_- z*Yyg?bTk@Fs%X{y`{86Bx(y$5ZN}k`rpl;%*{alH%|Vq+BnMS0nMlUvx|%~O zqnc`mg1bE1Z+KM|i=`4twNu5CiBTSm$JGEMsbuh^M)o9Asj*aQ&+zV%@v+gdAk}9=HtQbaXKqJ?%OL#nBQo>kyt8;hj(!{>kndnv4bh9c=RumMG+gRo*STULcuh zE}RKCoT{(t&KX-}7>otZ;tfrR*f72u+MP)99JOFk#kAnwj7r8eDVQrirE=~8&nit? zzLibR1UA(U9Vm%#BrZ8Uz!$~l)P1}5xXLJY=dOUJjkfJzLuFLpXwBYbWXlVVF@U^o%xu5Xo3=s?$Mld$A^D#pHYn3`6O+RN@pD`#LJyma9lA>D&lgm8R#IVX4 z=eZwFN24ehLz8=hweCaRk)H8+fzv^$nf5K;R=I#Fhx#L#>&)D#wY@&IX-{=%Z)fgm zg@vMZ-}0S&u;B3fjvQ-$zdA{}TtMrXHJ2 z25#%3W2)hYlnAqGLQNmf9kaZW?L)=oy=E1I_pIWIU$lyN(<-j;ZK)V~*DB&=>r&;H zUbol@xwdyMcqe%43PLFnL_jPQSzTYkwD_gIhRI{r@+7~96|dk$EO(i=ZN-aTx3L?= zofDSae&fcANHm+x&%AIee%Uu}m}={Fto<;z_PP5RAH?y)Ra|i;cX2TIh26##zXI2B z@WE%41?)9w>^I4tx9p-2RPNTQ?*qtXZgP8%r<;Gmy5-$^1QWB&jo|=cxO%a=RDgg z1lG2}0L6@l`IaD?HRo$)W>DqbYTdW0I(oM2ldZDZpo`WU zrH{RC_zFS8*Xkm(S#uk=3}2`(T;fk@e$)B|>ZU8TGKUDL&}n$uRfTz3U+eErS}W&I zMx9+uFWZ*{^0*V$N(YrwO=E488(=L& zCa6Ld<;bCc<}m3VV_o!FrC=nv{37Z4 zN>}H`UN&wut)x#uy@5KlxvOb~{@#CZ@?f$WZT1ubSHEed_!ZrOY1Pa3@^%d(cJ>>6L7zPw+MI;9y~zZ z8aA1GfO;BuFY;5;HsagRIzZbaXq}*q3tAUw2L!Dfw1ZgFFxF23m+K!BV`Gu^j|=#) zfF}i<6Y_b?)hTc}S6RTWfIR_M1bkA!H382GxGvz+0$vdClLCH9z|RQytbor6_&EWe z7w`)Leo4SD3-}cQ|3tvA3ivev|5U)Q3;2S7e=guR1^jCPzboMP1pK~$e=Fb*1pJ|Z zKLUIcb?TVjNnJYU7+yzRw1S9~{&efS<`2{Eq;6UXm}z~~t>J5+&*0Z;OzkyzShvsI zWAK|o_IqxjZn_Qnx-s9kp_}Qq(ki+G%7x*{BKBnJH-IT;!9T?w=ai*`QZW_Q^czw51uinc8&?!GGI|RE1 zVGHBKpt0{*=ZLU<8(@|ty}uT67c-}vy2$qmM<5!|$GgP#%W<@G$Tx!-Bz(l3bU)4$w3YWUxV-P@=~ z%bEVlw7*lQuG4Ac)$NE)8gc9sIL9p|aQ?nOfbj>LQoY;9>nK;2DhVLVW&6Jvxu?0gdzf zZ`4J5VdFokTaR&%-HRO9Bi1;MH5$9`h28sv-TOg*8Gbtex&5$h477hyht|JW*!ex! z=|R^)(4MCrjX!{Tco4eo2hGeqfxJ5h{zIsj6X1UkG=>jjUot!ie=>Xoe)$J=(!=lp zfB%)b=@=0mhp%}25sW_~*6@Ag?jPXmAAmLm|1y45=T#_sRLGuytid0JKYkBgS@1lH zxLG1@JoXrorm^0~V56DG61i@p))>F>%_8(okIwxfXg7)cpMhLaOgjW5Z!H*|FGdlQ;cT(v15&Fyv;C~Uc9T@*H5yen{dHg(RCXO#5 zjz&M970;u7@c1j>d0EKtx$!V;X8cvqUJ-lyRiX1W=rlN=DJR96UkB|qQ4e0n{yC2Q zb6o7ppJAV#p&pID0op!ML*9tgkYmDrKCd1Y_PiOf=a-;yzdnRB#9N@757$Jd{|frv z!rT`?Gjs77_=3obcTmIM7W2Ogy7BpYkb76i{icoF`=B3&-F$vFXKO?I0JIK4`w%p) z6^E!-*O!kV{{hC|7k%b;ZTif|pqoDP33Plc`phTjGux>{pK(?o7y03{_$YPhwXH&o zRtx=WKsWvOI?;dEpkG{zxmID^$ld^16VDsbw{8$TYr$jo(aos0YX#4`2+u9xTPJj{ zhyGhcpS%_G@z}qqQ{(IdKH}j$K?hm@t^zBb+jz?MtB&WYfmAA1@$H&KjYFcYC2u$|&ekd7^+W3ig+6g%<>Xe-utmkh=TqreEHtWMRfB!? z%7b<>?zmnpfUWfe{83jKl`C20O1$2$Qpr@m8b~EmiEv%M9n^eR4J1=)tBS|@yHC{< zs!zq^spN>-%n1KTq+<)Q*eTD+D~@5@XczVkvwu{EfAS&*$K&Cu`#6oUJ<-C&T zhmy(q+prDHn%wAvJA@_ZF%UpY)oD@W~Occhim*B4zpd&W__Gup4= zU^4?h)4pvzvFLzs(Jh8o#uj6SXgB9@zfReBjUzOYNG!&cpPK5+NT0N*&pU+z=dGS( z3ubHO4Ly^0s=?$*l~M6H%OyFWA)pgkgL8&9mslju8{&LoDc#wDp;48=6pKvZ;7lc( z^D1-T;jbu1#qzDP%BaCqGNm?4fhcEoU^t~RayM()B@LT3nZSaJOfsSRGAfQZZ3e$k zC-bQdUlRFQ;1F@YdBb{dvpu)gt}PDbjKqd?EGuPoh5mxlx~ohnHP_Mlqds+&rIoGio5A zmaf??+=K17Tk3e*ihZ@XUoGy-_6&ZtxL+;qZHv44n-=qHi}ktbTA^V3x~A?_zFl;x zf$hhqQ|18Zd$ZZH^|x`tZQ$!QSsGIRk3nUc!fgM4&u4yE2?2dABgY{xAKba z5M@rzwX&P8(ZW05mN)DsEzGlg`c21TDEWiqyTY~ZM(2>833!2%VY8SFtiV>;kSU7} zBAKhq+^V&?!!&6hXwlxx+;$cgid6$EaB|^-qjwHD)_li6JD)ss-$O^UW4>3dX76(= zwE)p?qfPBpNA?{bo0xcLJQcdFjgE<05K$tgRXf$>(d-e+FWLcAT;5fu5Z-kPEtj2w zx$P91t~-Uc_Z_w?+jQxH!Qr(hjBYXvN<0$mwznWH{(0{~GV>dY-G9)_RGRo-7sNlqh&VluST5PZ@Yf%2)xZIT*~V#Ot>sw zXcfK&Z?xdA&$upK&Q+<95UD6OOSsoBeq(CdN(-@AYqbksoU~L@RSm47opl&C1*WN< zW+X?lxo{h6QJ+FWo2f$$o<>3@Nl7*>c(MA6qS>rqL7o}9@iz*1dgnG)e5+WtxD>lZ zm3OS7>s7hGI7QdV{ZsqvPgEwY;r%mF>VPWvfBH znyOW8pYNQm6xVIsaZ8SCt8CT^0^gag1$H(Yk6Wc#Yp$9N2NQ|b zmKx0Xwv`WO+tl1tnQQ0HFX}BKd!TH)!b2exH|yrh^VF2d`V%MmRnBv(f#vF$ZqoZr zO-+SIQ|&&z8TBbDAD*HbN3*FZbLUxtFLKyga~3U diff --git a/modules/widgets/dashboard/wallpapers/palette.vert b/modules/widgets/dashboard/wallpapers/palette.vert index 9567262f..d7116d36 100644 --- a/modules/widgets/dashboard/wallpapers/palette.vert +++ b/modules/widgets/dashboard/wallpapers/palette.vert @@ -1,4 +1,5 @@ #version 440 + layout(location = 0) in vec4 qt_Vertex; layout(location = 1) in vec2 qt_MultiTexCoord0; layout(location = 0) out vec2 qt_TexCoord0; From 1a7f7c8b41ca3754395f372a9db9a24cebad1ba6 Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 19:19:07 -0600 Subject: [PATCH 05/14] Update palette.frag --- .../widgets/dashboard/wallpapers/palette.frag | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index 2f11eb11..b8968e5e 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -14,16 +14,15 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; -const float SHARPNESS = 20.0; // Controls how strictly colors snap to the palette -const float EPSILON = 1e-5; // Small value to avoid division by zero +// Adjusted sharpness to compensate for the approximation's overestimation. +// This value makes the visual result nearly identical to using exp(-20*distSq). +const float SHARPNESS = 17.0; +const float EPSILON = 1e-5; -// Rational Padé [2,2] approximation of exp(-x) for x >= 0. -// Extremely close to the real exponential for typical distance values, -// but computed without the expensive native exp() function. +// Original fast approximation of exp(-x) for x >= 0. float fastExpNeg(float x) { x = clamp(x, 0.0, 10.0); - float x2 = x * x; - return (1.0 - 0.5 * x + 0.1 * x2) / (1.0 + 0.5 * x + 0.1 * x2); + return 1.0 / (1.0 + x + 0.5 * x * x); } void main() { @@ -32,20 +31,16 @@ void main() { int size = int(clamp(ubuf.paletteSize, 0.0, 128.0)); if (size <= 0) { - // No palette defined: pass the original color through fragColor = tex * ubuf.qt_Opacity; return; } - // Precompute horizontal step for iterating through the palette texture float stepU = 1.0 / float(size); - float u = 0.5 * stepU; // Sample at pixel centers of the 1D palette texture + float u = 0.5 * stepU; mediump vec3 accumulated = vec3(0.0); mediump float totalWeight = 0.0; - // Soft blending: each palette color contributes according to its - // similarity to the source color, using a Gaussian-like weight. for (int i = 0; i < 128; ++i) { if (i >= size) break; @@ -53,18 +48,14 @@ void main() { u += stepU; mediump vec3 diff = color - pColor; - mediump float distSq = dot(diff, diff); // Squared Euclidean distance + mediump float distSq = dot(diff, diff); - // Weight = exp(-SHARPNESS * distSq) (approximated) mediump float weight = fastExpNeg(SHARPNESS * distSq); accumulated += pColor * weight; totalWeight += weight; } - // Normalize the weighted sum mediump vec3 finalColor = accumulated / (totalWeight + EPSILON); - - // Preserve the original alpha and apply global opacity fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file From 184d93a53f1eb646ec5b10737546891d4d27d824 Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 19:21:53 -0600 Subject: [PATCH 06/14] Update palette.frag --- .../widgets/dashboard/wallpapers/palette.frag | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index b8968e5e..b57fb9ec 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -14,15 +14,19 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; -// Adjusted sharpness to compensate for the approximation's overestimation. -// This value makes the visual result nearly identical to using exp(-20*distSq). -const float SHARPNESS = 17.0; +const float SHARPNESS = 20.0; // Keep original sharpness const float EPSILON = 1e-5; -// Original fast approximation of exp(-x) for x >= 0. +// Accurate Padé [2,2] approximation of exp(-x) for x >= 0. +// Very close to the real exponential curve. float fastExpNeg(float x) { - x = clamp(x, 0.0, 10.0); - return 1.0 / (1.0 + x + 0.5 * x * x); + // Clamp to a safe range to prevent extreme values + x = clamp(x, 0.0, 20.0); + float x2 = x * x; + float num = 1.0 - 0.5 * x + 0.1 * x2; + float den = 1.0 + 0.5 * x + 0.1 * x2; + // Ensure we never return a negative value (prevents color inversion) + return max(num / den, 0.0); } void main() { From c51152d95aa49af49cbd3cdcd49a0d4aaa2f5e3b Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 19:53:07 -0600 Subject: [PATCH 07/14] Matriz de dithering Bayer 8x8 normalizada --- .../widgets/dashboard/wallpapers/palette.frag | 96 ++++++++++-------- .../dashboard/wallpapers/palette.frag.qsb | Bin 3340 -> 4743 bytes .../widgets/dashboard/wallpapers/palette.vert | 1 - 3 files changed, 54 insertions(+), 43 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index b57fb9ec..fdf2eb17 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -1,7 +1,6 @@ #version 440 - -layout(location = 0) in mediump vec2 qt_TexCoord0; -layout(location = 0) out mediump vec4 fragColor; +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; layout(binding = 1) uniform sampler2D source; layout(binding = 2) uniform sampler2D paletteTexture; @@ -14,52 +13,65 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; -const float SHARPNESS = 20.0; // Keep original sharpness -const float EPSILON = 1e-5; - -// Accurate Padé [2,2] approximation of exp(-x) for x >= 0. -// Very close to the real exponential curve. -float fastExpNeg(float x) { - // Clamp to a safe range to prevent extreme values - x = clamp(x, 0.0, 20.0); - float x2 = x * x; - float num = 1.0 - 0.5 * x + 0.1 * x2; - float den = 1.0 + 0.5 * x + 0.1 * x2; - // Ensure we never return a negative value (prevents color inversion) - return max(num / den, 0.0); -} +// Matriz de dithering Bayer 8x8 normalizada (valores 0..1) +const float ditherMatrix[64] = float[64]( + 0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0, + 48.0/64.0, 16.0/64.0, 56.0/64.0, 24.0/64.0, 50.0/64.0, 18.0/64.0, 58.0/64.0, 26.0/64.0, + 12.0/64.0, 44.0/64.0, 4.0/64.0, 36.0/64.0, 14.0/64.0, 46.0/64.0, 6.0/64.0, 38.0/64.0, + 60.0/64.0, 28.0/64.0, 52.0/64.0, 20.0/64.0, 62.0/64.0, 30.0/64.0, 54.0/64.0, 22.0/64.0, + 3.0/64.0, 35.0/64.0, 11.0/64.0, 43.0/64.0, 1.0/64.0, 33.0/64.0, 9.0/64.0, 41.0/64.0, + 51.0/64.0, 19.0/64.0, 59.0/64.0, 27.0/64.0, 49.0/64.0, 17.0/64.0, 57.0/64.0, 25.0/64.0, + 15.0/64.0, 47.0/64.0, 7.0/64.0, 39.0/64.0, 13.0/64.0, 45.0/64.0, 5.0/64.0, 37.0/64.0, + 63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0 +); void main() { - mediump vec4 tex = texture(source, qt_TexCoord0); - mediump vec3 color = tex.rgb; - - int size = int(clamp(ubuf.paletteSize, 0.0, 128.0)); - if (size <= 0) { - fragColor = tex * ubuf.qt_Opacity; + vec4 tex = texture(source, qt_TexCoord0); + if (tex.a < 0.001) { + fragColor = vec4(0.0); return; } - - float stepU = 1.0 / float(size); - float u = 0.5 * stepU; - - mediump vec3 accumulated = vec3(0.0); - mediump float totalWeight = 0.0; - + + // Obtener el valor de dithering para este píxel + ivec2 fragCoord = ivec2(gl_FragCoord.xy); + int index = (fragCoord.x & 7) + ((fragCoord.y & 7) << 3); + float dither = ditherMatrix[index] - 0.5; // Rango -0.5 .. 0.5 + + // Aplicar dithering al color de entrada (sutil, escala reducida) + vec3 color = tex.rgb + dither * 0.03; // 0.03 ≈ 7/255, apenas perceptible pero rompe bandas + + int size = int(ubuf.paletteSize); + const float sharpness = 20.0; + const float weightThreshold = 0.001; + const float maxDistSq = 0.3454; // -ln(0.001)/20.0 + + vec3 accumulatedColor = vec3(0.0); + float totalWeight = 0.0; + + float invSize = 1.0 / float(size); + float halfInv = 0.5 * invSize; + + // Bucle con early skip + aproximación de exp for (int i = 0; i < 128; ++i) { if (i >= size) break; - - mediump vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; - u += stepU; - - mediump vec3 diff = color - pColor; - mediump float distSq = dot(diff, diff); - - mediump float weight = fastExpNeg(SHARPNESS * distSq); - - accumulated += pColor * weight; + + float u = float(i) * invSize + halfInv; + vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; + + vec3 diff = color - pColor; + float distSq = diff.x*diff.x + diff.y*diff.y + diff.z*diff.z; + + if (distSq > maxDistSq) continue; + + float x = sharpness * distSq; + float weight = 1.0 / (1.0 + x * (1.0 + 0.5 * x)); + + accumulatedColor += pColor * weight; totalWeight += weight; } - - mediump vec3 finalColor = accumulated / (totalWeight + EPSILON); + + vec3 finalColor = accumulatedColor / (totalWeight + 1e-5); + + // Re-multiplicar alpha y opacidad global fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 469ca85857c422a45dc301866fa7679fd483b250..86401d8d0d201932a8d79d23b692b02c11ce6abf 100644 GIT binary patch literal 4743 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 Date: Mon, 20 Apr 2026 20:04:08 -0600 Subject: [PATCH 08/14] Mejoras de rendimiento del shader --- .../widgets/dashboard/wallpapers/palette.frag | 41 ++++++++---------- .../dashboard/wallpapers/palette.frag.qsb | Bin 4743 -> 3004 bytes 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index fdf2eb17..417de4fb 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -13,65 +13,60 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; -// Matriz de dithering Bayer 8x8 normalizada (valores 0..1) -const float ditherMatrix[64] = float[64]( - 0.0/64.0, 32.0/64.0, 8.0/64.0, 40.0/64.0, 2.0/64.0, 34.0/64.0, 10.0/64.0, 42.0/64.0, - 48.0/64.0, 16.0/64.0, 56.0/64.0, 24.0/64.0, 50.0/64.0, 18.0/64.0, 58.0/64.0, 26.0/64.0, - 12.0/64.0, 44.0/64.0, 4.0/64.0, 36.0/64.0, 14.0/64.0, 46.0/64.0, 6.0/64.0, 38.0/64.0, - 60.0/64.0, 28.0/64.0, 52.0/64.0, 20.0/64.0, 62.0/64.0, 30.0/64.0, 54.0/64.0, 22.0/64.0, - 3.0/64.0, 35.0/64.0, 11.0/64.0, 43.0/64.0, 1.0/64.0, 33.0/64.0, 9.0/64.0, 41.0/64.0, - 51.0/64.0, 19.0/64.0, 59.0/64.0, 27.0/64.0, 49.0/64.0, 17.0/64.0, 57.0/64.0, 25.0/64.0, - 15.0/64.0, 47.0/64.0, 7.0/64.0, 39.0/64.0, 13.0/64.0, 45.0/64.0, 5.0/64.0, 37.0/64.0, - 63.0/64.0, 31.0/64.0, 55.0/64.0, 23.0/64.0, 61.0/64.0, 29.0/64.0, 53.0/64.0, 21.0/64.0 -); - void main() { vec4 tex = texture(source, qt_TexCoord0); + // Salida inmediata para píxeles transparentes (ahorro masivo) if (tex.a < 0.001) { fragColor = vec4(0.0); return; } - // Obtener el valor de dithering para este píxel - ivec2 fragCoord = ivec2(gl_FragCoord.xy); - int index = (fragCoord.x & 7) + ((fragCoord.y & 7) << 3); - float dither = ditherMatrix[index] - 0.5; // Rango -0.5 .. 0.5 - - // Aplicar dithering al color de entrada (sutil, escala reducida) - vec3 color = tex.rgb + dither * 0.03; // 0.03 ≈ 7/255, apenas perceptible pero rompe bandas - + vec3 color = tex.rgb; int size = int(ubuf.paletteSize); + + // Factor de nitidez (ajustable, mismo valor que original) const float sharpness = 20.0; + // Umbral de peso mínimo: cuando exp(-sharpness*distSq) < 0.001, ignoramos const float weightThreshold = 0.001; - const float maxDistSq = 0.3454; // -ln(0.001)/20.0 + // distSq máxima correspondiente: d² = -ln(threshold)/sharpness ≈ 0.3454 + const float maxDistSq = 0.3454; vec3 accumulatedColor = vec3(0.0); float totalWeight = 0.0; + // Precalcular constantes para el cálculo de coordenada U float invSize = 1.0 / float(size); float halfInv = 0.5 * invSize; - // Bucle con early skip + aproximación de exp + // Bucle con límite estático 128 (máximo esperado) for (int i = 0; i < 128; ++i) { if (i >= size) break; + // Coordenada U optimizada: i*invSize + 0.5*invSize float u = float(i) * invSize + halfInv; vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; + // Diferencia manual (evita función built-in) vec3 diff = color - pColor; float distSq = diff.x*diff.x + diff.y*diff.y + diff.z*diff.z; + // Early skip si el peso sería insignificante if (distSq > maxDistSq) continue; + // Aproximación rápida de exp(-sharpness * distSq) usando Padé [1/2] + // f(x) = 1 / (1 + x + 0.5*x^2) con x = sharpness * distSq + // Evaluada con Horner para reducir multiplicaciones float x = sharpness * distSq; float weight = 1.0 / (1.0 + x * (1.0 + 0.5 * x)); + // Acumulación (puede ser FMA en hardware moderno) accumulatedColor += pColor * weight; totalWeight += weight; } + // Normalización segura con épsilon vec3 finalColor = accumulatedColor / (totalWeight + 1e-5); - // Re-multiplicar alpha y opacidad global + // Pre‑multiplicación de alpha y opacidad global fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 86401d8d0d201932a8d79d23b692b02c11ce6abf..6e26db3f5c26fe4806a3d6bf15a87b188becee6e 100644 GIT binary patch literal 3004 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 Date: Mon, 20 Apr 2026 20:31:55 -0600 Subject: [PATCH 09/14] Translated comments --- .../widgets/dashboard/wallpapers/palette.frag | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index 417de4fb..b47fbca0 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -15,7 +15,7 @@ layout(std140, binding = 0) uniform buf { void main() { vec4 tex = texture(source, qt_TexCoord0); - // Salida inmediata para píxeles transparentes (ahorro masivo) + // Early exit for fully transparent pixels (massive savings) if (tex.a < 0.001) { fragColor = vec4(0.0); return; @@ -24,49 +24,49 @@ void main() { vec3 color = tex.rgb; int size = int(ubuf.paletteSize); - // Factor de nitidez (ajustable, mismo valor que original) + // Sharpness factor (adjustable, same as original) const float sharpness = 20.0; - // Umbral de peso mínimo: cuando exp(-sharpness*distSq) < 0.001, ignoramos + // Minimum weight threshold: when exp(-sharpness*distSq) < 0.001, we skip const float weightThreshold = 0.001; - // distSq máxima correspondiente: d² = -ln(threshold)/sharpness ≈ 0.3454 + // Corresponding maximum squared distance: d² = -ln(threshold)/sharpness ≈ 0.3454 const float maxDistSq = 0.3454; vec3 accumulatedColor = vec3(0.0); float totalWeight = 0.0; - // Precalcular constantes para el cálculo de coordenada U + // Precompute constants for U coordinate calculation float invSize = 1.0 / float(size); float halfInv = 0.5 * invSize; - // Bucle con límite estático 128 (máximo esperado) + // Loop with static bound 128 (maximum expected) for (int i = 0; i < 128; ++i) { if (i >= size) break; - // Coordenada U optimizada: i*invSize + 0.5*invSize + // Optimized U coordinate: i*invSize + 0.5*invSize float u = float(i) * invSize + halfInv; vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; - // Diferencia manual (evita función built-in) + // Manual difference (avoids built-in function) vec3 diff = color - pColor; float distSq = diff.x*diff.x + diff.y*diff.y + diff.z*diff.z; - // Early skip si el peso sería insignificante + // Early skip if weight would be negligible if (distSq > maxDistSq) continue; - // Aproximación rápida de exp(-sharpness * distSq) usando Padé [1/2] - // f(x) = 1 / (1 + x + 0.5*x^2) con x = sharpness * distSq - // Evaluada con Horner para reducir multiplicaciones + // Fast approximation of exp(-sharpness * distSq) using Padé [1/2] + // f(x) = 1 / (1 + x + 0.5*x^2) with x = sharpness * distSq + // Evaluated using Horner's method to reduce multiplications float x = sharpness * distSq; float weight = 1.0 / (1.0 + x * (1.0 + 0.5 * x)); - // Acumulación (puede ser FMA en hardware moderno) + // Accumulation (may become FMA on modern hardware) accumulatedColor += pColor * weight; totalWeight += weight; } - // Normalización segura con épsilon + // Safe normalization with epsilon vec3 finalColor = accumulatedColor / (totalWeight + 1e-5); - // Pre‑multiplicación de alpha y opacidad global + // Pre-multiplied alpha and global opacity fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file From 45d62b3c8a5a21d445a92bb29a360c0dc3109b06 Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 21:18:46 -0600 Subject: [PATCH 10/14] Fix (animation not working) --- .../dashboard/wallpapers/Wallpaper.qml | 120 +++++++----------- 1 file changed, 48 insertions(+), 72 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/Wallpaper.qml b/modules/widgets/dashboard/wallpapers/Wallpaper.qml index 6b3470b3..27f1ce3b 100644 --- a/modules/widgets/dashboard/wallpapers/Wallpaper.qml +++ b/modules/widgets/dashboard/wallpapers/Wallpaper.qml @@ -219,6 +219,9 @@ PanelWindow { // ------------------------------------------------------------------- function loadCustomPalette(filePath) { if (!filePath) return; + // Vaciar paleta actual para usar fallback mientras se carga la nueva + customPalette = []; + customPaletteSize = 0; var palettePath = getPalettePath(filePath); var xhr = new XMLHttpRequest(); xhr.open("GET", "file://" + palettePath, true); @@ -900,47 +903,30 @@ PanelWindow { } } - Item { - id: mediaContainer + // Image with layer effect for tinting + Image { + id: rawImage anchors.fill: parent - clip: true - - Image { - id: rawImage - anchors.fill: parent - source: staticImageRoot.sourceFile ? "file://" + staticImageRoot.sourceFile : "" - fillMode: Image.PreserveAspectCrop - asynchronous: true - smooth: true - mipmap: true - opacity: staticImageRoot.tint ? 0.0 : 1.0 - visible: true - - onStatusChanged: { - if (status === Image.Ready) { - console.log("rawImage ready, scheduling texture update"); - mediaTextureSource.scheduleUpdate(); - } - } - } - - ShaderEffectSource { - id: mediaTextureSource - sourceItem: rawImage - hideSource: staticImageRoot.tint - live: false - smooth: true - recursive: false - } + source: staticImageRoot.sourceFile ? "file://" + staticImageRoot.sourceFile : "" + fillMode: Image.PreserveAspectCrop + asynchronous: true + smooth: true + mipmap: true + visible: true - PaletteShaderEffect { - anchors.fill: parent - visible: staticImageRoot.tint && wallpaper.effectivePaletteSize > 0 - source: mediaTextureSource + // Layer effect for palette tinting + layer.enabled: staticImageRoot.tint && wallpaper.effectivePaletteSize > 0 + layer.effect: PaletteShaderEffect { paletteTexture: paletteTextureSource paletteSize: wallpaper.effectivePaletteSize - texWidth: Math.max(rawImage.width, 1) - texHeight: Math.max(rawImage.height, 1) + texWidth: rawImage.width + texHeight: rawImage.height + } + + onStatusChanged: { + if (status === Image.Ready) { + console.log("rawImage ready"); + } } } } @@ -1005,40 +991,23 @@ PanelWindow { } } - Item { - id: mediaContainer + Video { + id: videoPlayer anchors.fill: parent - clip: true - - Video { - id: videoPlayer - anchors.fill: parent - source: videoRoot.sourceFile ? "file://" + videoRoot.sourceFile : "" - loops: MediaPlayer.Infinite - autoPlay: true - muted: true - fillMode: VideoOutput.PreserveAspectCrop - opacity: videoRoot.tint ? 0.0 : 1.0 - visible: true - } - - ShaderEffectSource { - id: mediaTextureSource - sourceItem: videoPlayer - hideSource: videoRoot.tint - live: true - smooth: true - recursive: false - } + source: videoRoot.sourceFile ? "file://" + videoRoot.sourceFile : "" + loops: MediaPlayer.Infinite + autoPlay: true + muted: true + fillMode: VideoOutput.PreserveAspectCrop + visible: true - PaletteShaderEffect { - anchors.fill: parent - visible: videoRoot.tint && wallpaper.effectivePaletteSize > 0 - source: mediaTextureSource + // Layer effect for palette tinting + layer.enabled: videoRoot.tint && wallpaper.effectivePaletteSize > 0 + layer.effect: PaletteShaderEffect { paletteTexture: paletteTextureSource paletteSize: wallpaper.effectivePaletteSize - texWidth: Math.max(videoPlayer.width, 1) - texHeight: Math.max(videoPlayer.height, 1) + texWidth: videoPlayer.width + texHeight: videoPlayer.height } } } @@ -1070,11 +1039,8 @@ PanelWindow { onSourceChanged: { console.log("wallImageContainer source changed to:", source); - if (previousSource !== "" && source !== previousSource) { - if (Config.animDuration > 0) transitionAnimation.restart(); - } - previousSource = source; if (source) wallpaper.loadCustomPalette(source); + // Animation will be triggered after loader finishes loading } SequentialAnimation { @@ -1092,6 +1058,7 @@ PanelWindow { Loader { id: wallImageLoader anchors.fill: parent + asynchronous: true sourceComponent: { if (!wallImageContainer.source) return null; var fileType = wallpaper.getFileType(wallImageContainer.source); @@ -1103,7 +1070,16 @@ PanelWindow { onLoaded: { console.log("Loader: item loaded, assigning sourceFile =", wallImageContainer.source); - if (item) item.sourceFile = wallImageContainer.source; + if (item) { + item.sourceFile = wallImageContainer.source; + } + // Trigger animation after new content is loaded + if (wallImageContainer.previousSource !== "" && + wallImageContainer.source !== wallImageContainer.previousSource && + Config.animDuration > 0) { + transitionAnimation.restart(); + } + wallImageContainer.previousSource = wallImageContainer.source; } // Bind sourceFile directly to wallImageContainer.source From c46df44ec49ab41616a670e3a6684e31669c33ea Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 21:30:18 -0600 Subject: [PATCH 11/14] Blue color fix --- .../widgets/dashboard/wallpapers/palette.frag | 37 +++++++++--------- .../dashboard/wallpapers/palette.frag.qsb | Bin 3004 -> 3030 bytes 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index b47fbca0..7848b57b 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -15,7 +15,8 @@ layout(std140, binding = 0) uniform buf { void main() { vec4 tex = texture(source, qt_TexCoord0); - // Early exit for fully transparent pixels (massive savings) + + // Early exit for fully transparent pixels if (tex.a < 0.001) { fragColor = vec4(0.0); return; @@ -24,49 +25,49 @@ void main() { vec3 color = tex.rgb; int size = int(ubuf.paletteSize); - // Sharpness factor (adjustable, same as original) + // Guard against invalid palette size + if (size <= 0) { + fragColor = tex * ubuf.qt_Opacity; + return; + } + const float sharpness = 20.0; - // Minimum weight threshold: when exp(-sharpness*distSq) < 0.001, we skip const float weightThreshold = 0.001; - // Corresponding maximum squared distance: d² = -ln(threshold)/sharpness ≈ 0.3454 + // maxDistSq = -ln(threshold) / sharpness const float maxDistSq = 0.3454; vec3 accumulatedColor = vec3(0.0); float totalWeight = 0.0; - // Precompute constants for U coordinate calculation + // Precompute U coordinate stepping float invSize = 1.0 / float(size); float halfInv = 0.5 * invSize; - // Loop with static bound 128 (maximum expected) + // Maximum loop count matches original bound (128) for (int i = 0; i < 128; ++i) { if (i >= size) break; - // Optimized U coordinate: i*invSize + 0.5*invSize + // Palette texture coordinate float u = float(i) * invSize + halfInv; vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; - // Manual difference (avoids built-in function) + // Squared Euclidean distance vec3 diff = color - pColor; - float distSq = diff.x*diff.x + diff.y*diff.y + diff.z*diff.z; + float distSq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; - // Early skip if weight would be negligible + // Skip if weight would be negligible (optimization) if (distSq > maxDistSq) continue; - // Fast approximation of exp(-sharpness * distSq) using Padé [1/2] - // f(x) = 1 / (1 + x + 0.5*x^2) with x = sharpness * distSq - // Evaluated using Horner's method to reduce multiplications - float x = sharpness * distSq; - float weight = 1.0 / (1.0 + x * (1.0 + 0.5 * x)); + // Exact Gaussian weight (guarantees color fidelity) + float weight = exp(-sharpness * distSq); - // Accumulation (may become FMA on modern hardware) accumulatedColor += pColor * weight; totalWeight += weight; } - // Safe normalization with epsilon + // Normalize with epsilon to avoid division by zero vec3 finalColor = accumulatedColor / (totalWeight + 1e-5); - // Pre-multiplied alpha and global opacity + // Premultiplied alpha and global opacity fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 6e26db3f5c26fe4806a3d6bf15a87b188becee6e..1b96a102f19897fb8d8f5f050374b57b3e0e17ac 100644 GIT binary patch literal 3030 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 YyiD^auu^YXPW{k1C@-)70xpNBd==Uzw*UYD literal 3004 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 Date: Mon, 20 Apr 2026 21:40:46 -0600 Subject: [PATCH 12/14] Fix Quality and dark zones --- .../widgets/dashboard/wallpapers/palette.frag | 77 +++++++++++------- .../dashboard/wallpapers/palette.frag.qsb | Bin 3030 -> 3575 bytes 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index 7848b57b..e9584fca 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -13,61 +13,78 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; +// Fast exp(-k*x) approximation using Padé [1/2] +// f(x) = 1 / (1 + x + 0.5*x^2) with x = k * distSq +float fastExpWeight(float k, float distSq) { + float x = k * distSq; + return 1.0 / (1.0 + x * (1.0 + 0.5 * x)); +} + void main() { vec4 tex = texture(source, qt_TexCoord0); - + vec3 color = tex.rgb; + // Early exit for fully transparent pixels if (tex.a < 0.001) { fragColor = vec4(0.0); return; } - - vec3 color = tex.rgb; + int size = int(ubuf.paletteSize); - - // Guard against invalid palette size if (size <= 0) { fragColor = tex * ubuf.qt_Opacity; return; } - - const float sharpness = 20.0; - const float weightThreshold = 0.001; - // maxDistSq = -ln(threshold) / sharpness + + const float distributionSharpness = 20.0; // Same as original + const float weightThreshold = 0.001; // Skip negligible contributions + // Precomputed max squared distance: -ln(threshold)/sharpness ≈ 0.3454 const float maxDistSq = 0.3454; - + vec3 accumulatedColor = vec3(0.0); float totalWeight = 0.0; - - // Precompute U coordinate stepping + + // Nearest neighbor fallback for colors far from any palette entry + float minDistSq = 1e10; + vec3 closestColor = vec3(0.0); + float invSize = 1.0 / float(size); float halfInv = 0.5 * invSize; - - // Maximum loop count matches original bound (128) - for (int i = 0; i < 128; ++i) { + + for (int i = 0; i < 128; i++) { if (i >= size) break; - - // Palette texture coordinate + float u = float(i) * invSize + halfInv; vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; - - // Squared Euclidean distance + vec3 diff = color - pColor; float distSq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; - - // Skip if weight would be negligible (optimization) + + // Track closest color (for fallback) + if (distSq < minDistSq) { + minDistSq = distSq; + closestColor = pColor; + } + + // Skip if contribution would be below threshold if (distSq > maxDistSq) continue; - - // Exact Gaussian weight (guarantees color fidelity) - float weight = exp(-sharpness * distSq); - + + // Use fast approximate Gaussian weight + float weight = fastExpWeight(distributionSharpness, distSq); + accumulatedColor += pColor * weight; totalWeight += weight; } - - // Normalize with epsilon to avoid division by zero - vec3 finalColor = accumulatedColor / (totalWeight + 1e-5); - - // Premultiplied alpha and global opacity + + vec3 finalColor; + // Fallback: if total weight is near zero, snap to nearest palette color + // Prevents bright saturated colors from becoming dark + if (totalWeight < 0.001) { + finalColor = closestColor; + } else { + finalColor = accumulatedColor / (totalWeight + 0.00001); + } + + // Pre-multiply alpha for proper blending in Qt Quick fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 1b96a102f19897fb8d8f5f050374b57b3e0e17ac..2a0bd142c96ad55fe4c1d67513df622cbe67eb1c 100644 GIT binary patch literal 3575 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 literal 3030 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 YyiD^auu^YXPW{k1C@-)70xpNBd==Uzw*UYD From 59bc1157d210548ff875f06b007b59b588c9987e Mon Sep 17 00:00:00 2001 From: leriart Date: Mon, 20 Apr 2026 21:47:58 -0600 Subject: [PATCH 13/14] optimize palette mapping with fast exp approx and early culling - Fast exp(-k*x) approximation avoids transcendental function calls - Skip palette entries with negligible weight (<0.001) based on squared distance - Nearest-neighbor fallback ensures bright colors stay vivid --- .../widgets/dashboard/wallpapers/palette.frag | 72 ++++++++++-------- .../dashboard/wallpapers/palette.frag.qsb | Bin 3575 -> 3666 bytes 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index e9584fca..df013786 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -1,4 +1,11 @@ #version 440 + +// Precision statement for ES compatibility (ignored by desktop GLSL) +#ifdef GL_ES +precision highp float; +precision mediump int; +#endif + layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) out vec4 fragColor; @@ -13,11 +20,11 @@ layout(std140, binding = 0) uniform buf { float texHeight; } ubuf; -// Fast exp(-k*x) approximation using Padé [1/2] -// f(x) = 1 / (1 + x + 0.5*x^2) with x = k * distSq +// Fast exp(-k*x) approximation: 1 / (1 + x + 0.5*x^2) float fastExpWeight(float k, float distSq) { float x = k * distSq; - return 1.0 / (1.0 + x * (1.0 + 0.5 * x)); + float x2 = x * x; + return 1.0 / (1.0 + x + 0.5 * x2); } void main() { @@ -36,55 +43,56 @@ void main() { return; } - const float distributionSharpness = 20.0; // Same as original - const float weightThreshold = 0.001; // Skip negligible contributions - // Precomputed max squared distance: -ln(threshold)/sharpness ≈ 0.3454 - const float maxDistSq = 0.3454; + const float sharpness = 20.0; + const float weightThresh = 0.001; + const float maxDistSq = 0.3454; // -ln(0.001)/20.0 + const float epsilon = 1e-5; - vec3 accumulatedColor = vec3(0.0); - float totalWeight = 0.0; + vec3 accum = vec3(0.0); + float sumW = 0.0; - // Nearest neighbor fallback for colors far from any palette entry - float minDistSq = 1e10; - vec3 closestColor = vec3(0.0); + float minDist = 1e10; + vec3 nearest = vec3(0.0); + // Precompute stepping factors to avoid division inside loop float invSize = 1.0 / float(size); - float halfInv = 0.5 * invSize; + float uStep = invSize; + float uStart = 0.5 * invSize; - for (int i = 0; i < 128; i++) { + for (int i = 0; i < 128; ++i) { if (i >= size) break; - float u = float(i) * invSize + halfInv; + float u = float(i) * uStep + uStart; vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; - vec3 diff = color - pColor; - float distSq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z; + // Difference and squared distance (unrolled for clarity, compiler optimizes) + float dx = color.x - pColor.x; + float dy = color.y - pColor.y; + float dz = color.z - pColor.z; + float distSq = dx*dx + dy*dy + dz*dz; - // Track closest color (for fallback) - if (distSq < minDistSq) { - minDistSq = distSq; - closestColor = pColor; + // Nearest neighbor tracking (for fallback) + if (distSq < minDist) { + minDist = distSq; + nearest = pColor; } - // Skip if contribution would be below threshold + // Skip if contribution < threshold if (distSq > maxDistSq) continue; - // Use fast approximate Gaussian weight - float weight = fastExpWeight(distributionSharpness, distSq); + // Weight using fast approximation + float w = fastExpWeight(sharpness, distSq); - accumulatedColor += pColor * weight; - totalWeight += weight; + accum += pColor * w; + sumW += w; } vec3 finalColor; - // Fallback: if total weight is near zero, snap to nearest palette color - // Prevents bright saturated colors from becoming dark - if (totalWeight < 0.001) { - finalColor = closestColor; + if (sumW < weightThresh) { + finalColor = nearest; } else { - finalColor = accumulatedColor / (totalWeight + 0.00001); + finalColor = accum / (sumW + epsilon); } - // Pre-multiply alpha for proper blending in Qt Quick fragColor = vec4(finalColor * tex.a, tex.a) * ubuf.qt_Opacity; } \ No newline at end of file diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 2a0bd142c96ad55fe4c1d67513df622cbe67eb1c..4fff71af6440e96776560a1a78da9f5e796a654f 100644 GIT binary patch literal 3666 zcmV-Y4z2M307-p#ob6osms?dG|IXfqy@gVi1D94I=}gniOfpN;l-5pZ!NRmmPYVtu zyh&a%k7n!3%S>j<$WoC-MNkn@P*7x%MW8AOBFg3h3L+x-yZ?fM=b)bNeV;qIdC9au z7eVsQdCmKN?svQ2yM6C{uR}!hiKvx`nuusI?IMTbw3SRM(>lsio@`1|h=`hri2j3% z>RhZ~0j0<$gVItsBgI|y`xR}cF)C4th~`mg_Ib0$)g}$N9-dDIIb@SXWg;-jGaMkG!M@T@1#~qpCC0}b+)1xyC%*)ky`0=$h1PIfGp!D(R}Godz~(- z=u@ag)_Do!Kc&Sr3Yy!JFSd6U^fiOu4_#-0pXmTF=I;W33;37AKgLg>R_aDP`051R z1AbNC2odEM>{)p{HB%q-tOU>Lu*-DRBex2AyQo9*#enHI`*1dVXoBuF@R9Mw)JmU$ z9ggv7)FJx{W9^=sWrq-(b?}Sn^}yIK)_IN=zYTb9*UC3(@nCujeCwii8Mp18I;i@7 zj)>lEK4G(x%W$vJ+VKicocDO>8(~M$w`hK|{s*}(Z#}wN(T_>#yVffDcSQ8ii)Z!s zQJa)g^C`Eq7XoMe}O}_)Tbwt&khh>bX^`=P>YX80-9e z5ym<{UyQNN&!gZ`_1b}Yk(;F;8TI5p{RPa= zdo;V(0#`g=#GI(}TnBs)>UKTku7jOxfGHhcf}I;R9bX2nc)kKVI?qkOm7T9b?k3o| z0hrS9HQ2dD)A4oSisu`!qx0MfT-o_1#adn?IuDI-8B8l-hx>wV4KlG}(?S7PTKYal0pys*G{sN5W_|4c4y$Sg@Q2x4>$NaTU9`hD( zmB+jd9dBuQ%-hIg2B?X6AJDFy7xDhKpIT(y=4;PB#PCQh-#yBc{~o91yR7qQ=uq@A z(4*$JW5II_d^-x5;$Hw8ie3m>$sZ4%g^)iEnBrdqe$}5&L|(N><2ea_E1f4}T|P;Z zI|Z^QYdPAf(8Xo{re;ZVJb6onbC@QiTG+;Sytj11w2M|gC%Qu+U%@sLmZZk5^msv} zGI_&^*1%TIiTZp+$1WutF;+_XcKd`7B5OEdvCD~{Z#cG9j!H~wU~sr#BrIo2M+-*A zbR2WUnl^RVG0WSnq%-cpHkwv?+==>TqCQ_KXQlFXmTimDSSh7Ju7^bCe5+Yrm(SbDP}JvhokYMTTCh}HNaHIN_tqC&`=0J%U|I4-M8S=Z;@2aXuz_m4DCVE98pUK;< zM6f(H?Uq<6;b&k{tP-JMDAXNtuW)~Ous?hQqH2L{8TaL-`hKz}44gmh-PYm#eKOoaApn4_^w z_K={r3)Xi+#>7Y@N?F(KUd?T{#fqiub`j&S=xEl;4OvA;#CRB~OR-3$In%IB$x&CW zm7CyNvI5!m5|uTrVk?a}W*Pw3#%FU8naDfXC;ts@HAgUOt=HXZw0so*lq5`u9Fo(8JfbnN+(3Otsa$K zaxnT!U0k<7pwdw*7L(lVWoHhS#d4?&mTTCiX4$4HY*RIC)3a>T6}DQVay)Zg{Yma< zNq3|sr}%j);1bhJOlw`A0}RPks^iQR=SsiQ=MHsM`jqQoy#~>&trnqRZ#WX>z8Q)1 z4-5{5`y-*j{=P_GPh>6+68W5CB5pNS zxU+!kv=|Hy%Bw%z-`hXXAL;JvjYJ~d;Yd$kZ(o1+z<{|t)ECgL)SqHCL(AlhKH25| z1nSL#Gd`auG2rt9Tpce~5`&)5M|H8Edzo)6@Qnq&vA{PL_{IX?Sl}BAJQnz-^Rxy2 z&DjOMxA*^Rfwyw?Ht!i@DqnK^nS8=!Ny{Mg^jAxC@^R~Onhs$AUBR-o$h?SyXBzERgsiNul6V!JDCCB%j*j7y<58lS6)VNaw)j@VPMZ!?9LD3A zlsb<|=?5~YniCmzH(qrtgL=3JGj5{?-L~?3`as=t8YzBoPHIy7IyMOfL#bf7TrPjW zqnh|ZIjYe{`@ZZW80s?&<#YY7FjDX1GYp+a{}Q8~=Bf`dQV0AcM(ShpDF&i5dt_5R zx6F3t`cn=DqWVT7)%Zx`kbI<(IyhfxU_5+eKGQ(gJFpKmQim!<`#|YgDo~kFYI8Jq zmDqolRy9%1_mPs4i);ShmlkXzoi#)vW8~5zX&LEUzQ_rSmCogDGbxrV z-=4LKMJtzHUa<2{-kBgH(9xNRCqo|=k|YxYde z*y_QD>vbB3D{JPoaJo-^6(l|k9353dWXFy!*H0rSJHVN8aEj4UcVi@DEk~}*;gfC- zKf5s)9aV2V=HTb_pvNJhb_%cN))orxbS%t@pl>c@OeCN3Zqw_1@9zhbN(R$%FA_aZN@$hw4x0 z*TwruY0yWRLG#PW?w_ue`ke3*pgMy(|__#sB~S literal 3575 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 From 79010e6e5b4102019eaa1c971160dfc367b63c42 Mon Sep 17 00:00:00 2001 From: leriart <92061397+leriart@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:06:16 -0600 Subject: [PATCH 14/14] =?UTF-8?q?optimize=20palette=20mapping=20with=20low?= =?UTF-8?q?=E2=80=91level=20GPU=20techniques?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace texture() with texelFetch() to bypass UV computation and filtering - Use dot() for squared distance - Apply mediump/lowp precision to accumulators and palette colors - Add #pragma unroll to hint loop unrolling - Simplify fastExpWeight polynomial to reduce operations - Keep identical visual output while reducing ALU and bandwidth usage --- .../widgets/dashboard/wallpapers/palette.frag | 29 ++++++++++-------- .../dashboard/wallpapers/palette.frag.qsb | Bin 3666 -> 3674 bytes .../dashboard/wallpapers/palette.vert.qsb | Bin 1331 -> 1642 bytes 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/modules/widgets/dashboard/wallpapers/palette.frag b/modules/widgets/dashboard/wallpapers/palette.frag index df013786..2d1a672c 100644 --- a/modules/widgets/dashboard/wallpapers/palette.frag +++ b/modules/widgets/dashboard/wallpapers/palette.frag @@ -21,10 +21,10 @@ layout(std140, binding = 0) uniform buf { } ubuf; // Fast exp(-k*x) approximation: 1 / (1 + x + 0.5*x^2) +// Slightly rewritten to minimize operations float fastExpWeight(float k, float distSq) { float x = k * distSq; - float x2 = x * x; - return 1.0 / (1.0 + x + 0.5 * x2); + return 1.0 / (1.0 + x * (1.0 + 0.5 * x)); } void main() { @@ -48,28 +48,31 @@ void main() { const float maxDistSq = 0.3454; // -ln(0.001)/20.0 const float epsilon = 1e-5; - vec3 accum = vec3(0.0); - float sumW = 0.0; + // Use mediump for accumulators to save registers/cycles + mediump vec3 accum = vec3(0.0); + mediump float sumW = 0.0; - float minDist = 1e10; - vec3 nearest = vec3(0.0); + mediump float minDist = 1e10; + lowp vec3 nearest = vec3(0.0); // lowp sufficient for palette colors // Precompute stepping factors to avoid division inside loop float invSize = 1.0 / float(size); float uStep = invSize; float uStart = 0.5 * invSize; + // Hint to compiler to unroll the loop for better pipelining + #pragma unroll for (int i = 0; i < 128; ++i) { if (i >= size) break; float u = float(i) * uStep + uStart; - vec3 pColor = texture(paletteTexture, vec2(u, 0.5)).rgb; + // Use texelFetch instead of texture() – avoids UV filtering & derivative overhead + // Assumes paletteTexture is a 2D texture with height=1. + lowp vec3 pColor = texelFetch(paletteTexture, ivec2(i, 0), 0).rgb; - // Difference and squared distance (unrolled for clarity, compiler optimizes) - float dx = color.x - pColor.x; - float dy = color.y - pColor.y; - float dz = color.z - pColor.z; - float distSq = dx*dx + dy*dy + dz*dz; + // Difference and squared distance (dot product is hardware accelerated) + mediump vec3 diff = color - pColor; + mediump float distSq = dot(diff, diff); // Nearest neighbor tracking (for fallback) if (distSq < minDist) { @@ -81,7 +84,7 @@ void main() { if (distSq > maxDistSq) continue; // Weight using fast approximation - float w = fastExpWeight(sharpness, distSq); + mediump float w = fastExpWeight(sharpness, distSq); accum += pColor * w; sumW += w; diff --git a/modules/widgets/dashboard/wallpapers/palette.frag.qsb b/modules/widgets/dashboard/wallpapers/palette.frag.qsb index 4fff71af6440e96776560a1a78da9f5e796a654f..db987734131f9ae0d2ebb3f011f6dc7ceca8d588 100644 GIT binary patch literal 3674 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 literal 3666 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 diff --git a/modules/widgets/dashboard/wallpapers/palette.vert.qsb b/modules/widgets/dashboard/wallpapers/palette.vert.qsb index 5ec668ccdee0b43623d9d06dc8c645387cacd0df..48996f69381ee9fb662ada606bfbd751e9b09d57 100644 GIT binary patch literal 1642 zcmV-w29@~$04MHvob6gmZyQw<{`^ckp|7+}TWHgprW7X)#%TjZF{xUmh!iDC)l@0S zimZt}P6ljGGIJ-dDofb2V8@OwuET;<;ah}_U05vkpqJQbj`dU6s6t|m1!j8*46wi~ zEZA7Z8YGL%^%8m6iUOTb#>#vYMa# zn6itLs&WGZr?{HQNW-+7v_8%mItyTkrAaE8lf78$jMmgSHa_!I^> z(~==xiuGJ*=}D_~SVx2p={~L&ofn=x*I8}=g~eZ`w7-ZIEr@ zulTj1VQiZ2j@?`pn^xr(*?VGb&p3`-Efft?O4nZTrPbP;_v_N`g-~Z$vRr9xlU~QJ z3JOSWl#i(^7t`I;5o)zwe&l$zq;O3uC-be)h%w#7o?-L|dICuNdvlC665V>{e*0#?{=XeEbpLY(D7xr?A6U3$CtUfQ!zPW z6Qjaz_RM7*UkLH9y`5V5p!M%Z`Ot$E&)Xf9w>PryzSQnj8|^&bK5%36Ff?753|jwo zl#I8t<9^XP?)z4fCl6l7b#0Zy(1u}#X#M9=h7L(@(u?h_c5kM$l@-5Mvs~2@Q^K`Y zZBJTmZlw^q?>bxMhWU--28W&8o>bnI+uluyb+huVQ>%HF%vB1bdIN?~ZsQ1cGLF@n zI||P4#NlDn$FRFyvB5BifwxA3`Cxd`ly-$rsz-gi5+M~P~M*KuU2gZUbJZY|4Wt-NTPm#b)>#%$eC)*i` z!SzPfVh}L8_!@{iiU>eDC*K*4j+%^N;{lGi?OM`zn-PFMg*zz>}!$cqdFNsW)?ArTO=FzLL8mcC%9VtCskzVM()I z_GGnguiQ1-e9vxFKcYYRwY!Gld-O|X(`;DYx>>P=k}7t2QE5E@y^@y|HczK4(8>2{ zrBccFESDZnn}yNk<(}m0G{N<{}ol4G16{2H1*=?W zo1UjjrLZ)Ym&ZlLX?oIZsubK%?I}v7;CWSf5jIX+BYmdDv%SDSCthH7vsW7t_&E>Z zfw10GT%Ar(lvI@&&BYrL%G2%5Znkd-Vk`#P`cGva odC6@3dz1P{v-l%sY(Fy5_ucO%uWkcf^t9f3Bl7C{7rf-F9E$`{1poj5 literal 1331 zcmV-31{R!Y1(`^-GE&&=Lg25(G#(6BU^$POFc^XsZ^Xtu>%}S^{mD;CNU0gz8L~xngl1Tcl@@Rg6D|8jW z0{Nq8Y%~Y2r+IDSqkt>iHXA6JIjkxtwm7Fm;L^y4Yz2-pKZ;W{^Q4amKW_;!ld>ge zPi#51J+f?hwmr6N1-5Niwj$d$`K&hqELvx$^<8VdU}=k*1)(Z0jyN%kE}M~Zim<5Aw%QN$(YqVX}WFEjoYiblIBwk>cS zk{C10Im`T4m~)oYmuEl$-=R*{X?zH|ACBypx=W?C# zl*28y)0#N$F#k^|nYCcd9gefe{c@G#5YJu4{e~PqkFR=>?_ZAZIN$%=cT0yd2$k=N zm1@;-LLEeHEm~36dF==xMBmjl@lrRpTpg%k-PpQ4-_>N?9=L6#k8JC}?MbcWjyjgM zSuX_9NrBv~0r zBR;YzP8=sC`p|mzTB6_oU-Z9E^xQX==r2d2KX~xYiS9>wT$lIUBR|sRp5J!){42{s zc@uj+i?1g5fBgpJezuD7(j_|^*KPp56FX0zJlk!q2Ywhf zpLl~vv-)Wdu_~T__3irh_Or*_36n@IDiSzTb4W_(F_k+N1c}d02 p_HE(r^y8+at}K~k_{Eiad5wPEFuyZpTQ{?x*wQ#N{s4CyfgmlaqG|vD