From 80ae56bca04a53b4c32a21914adb1671fce8ad44 Mon Sep 17 00:00:00 2001 From: Joep Talboom Date: Sat, 6 Jun 2026 19:50:43 +0200 Subject: [PATCH 1/2] add local ai gpu hardware acceleration Introduces an opt-in toggle in the settings to enable Apple's CoreML Hardware Acceleration (Neural Engine/GPU) for the local Denoise model. The default safely remains pure CPU execution to ensure maximum numerical precision and stability, allowing users to test hardware acceleration when needed. --- src-tauri/src/ai_processing.rs | 48 +++++++++++++++++++++++++- src-tauri/src/app_settings.rs | 3 ++ src/components/panel/SettingsPanel.tsx | 17 +++++++++ src/components/ui/AppProperties.tsx | 1 + src/i18n/locales/de.json | 4 +++ src/i18n/locales/en.json | 4 +++ 6 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/ai_processing.rs b/src-tauri/src/ai_processing.rs index ca2fdd06c..24c5cd269 100644 --- a/src-tauri/src/ai_processing.rs +++ b/src-tauri/src/ai_processing.rs @@ -379,7 +379,7 @@ pub async fn get_or_init_denoise_model( let _ = ort::init().with_name("AI-Denoise").commit(); let model_path = models_dir.join(DENOISE_FILENAME); - let session = Session::builder()?.commit_from_file(model_path)?; + let session = create_session(app_handle, model_path)?; let denoise_model = Arc::new(Mutex::new(session)); crate::register_exit_handler(); @@ -1508,3 +1508,49 @@ pub struct AiDepthMaskParameters { #[serde(default)] pub orientation_steps: Option, } + +fn create_session>( + app_handle: &tauri::AppHandle, + model_path: P, +) -> Result { + let settings = crate::app_settings::load_settings(app_handle.clone()).unwrap_or_default(); + let enable_hardware_acceleration = settings.enable_denoise_hardware_acceleration; + let path_ref = model_path.as_ref(); + + #[cfg(target_os = "macos")] + if enable_hardware_acceleration { + use ort::execution_providers::{ + CoreMLExecutionProvider, + coreml::{CoreMLComputeUnits, CoreMLModelFormat}, + }; + + // Attempt CoreML compilation + if let Ok(builder) = Session::builder() { + let provider = CoreMLExecutionProvider::default() + .with_subgraphs(true) + .with_model_format(CoreMLModelFormat::MLProgram) + .with_compute_units(CoreMLComputeUnits::All) + .build(); + + if let Ok(builder_with_ep) = builder.with_execution_providers([provider]) { + match builder_with_ep.commit_from_file(path_ref) { + Ok(session) => return Ok(session), + Err(e) => { + log::warn!( + "Hardware acceleration failed for {:?}, falling back to CPU. Error: {}", + path_ref.file_name().unwrap_or_default(), + e + ); + } + } + } + } + } + + #[cfg(not(target_os = "macos"))] + let _ = enable_hardware_acceleration; + + // Fallback to CPU execution + let builder = Session::builder()?; + builder.commit_from_file(path_ref) +} diff --git a/src-tauri/src/app_settings.rs b/src-tauri/src/app_settings.rs index da6e1ac50..8a178926f 100644 --- a/src-tauri/src/app_settings.rs +++ b/src-tauri/src/app_settings.rs @@ -320,6 +320,8 @@ pub struct AppSettings { #[serde(default)] pub use_full_dpi_rendering: Option, #[serde(default)] + pub enable_denoise_hardware_acceleration: bool, + #[serde(default)] pub high_res_zoom_multiplier: Option, #[serde(default)] pub enable_live_previews: Option, @@ -427,6 +429,7 @@ impl Default for AppSettings { editor_preview_resolution: Some(1920), enable_zoom_hifi: Some(true), use_full_dpi_rendering: Some(false), + enable_denoise_hardware_acceleration: false, enable_live_previews: Some(true), live_preview_quality: Some("high".to_string()), sort_criteria: None, diff --git a/src/components/panel/SettingsPanel.tsx b/src/components/panel/SettingsPanel.tsx index bdf13da79..06a283cfd 100644 --- a/src/components/panel/SettingsPanel.tsx +++ b/src/components/panel/SettingsPanel.tsx @@ -534,6 +534,7 @@ export default function SettingsPanel({ thumbnailResolution: appSettings?.thumbnailResolution || 720, rawHighlightCompression: appSettings?.rawHighlightCompression ?? 2.5, processingBackend: appSettings?.processingBackend || 'auto', + enableDenoiseHardwareAcceleration: appSettings?.enableDenoiseHardwareAcceleration ?? false, linuxGpuOptimization: appSettings?.linuxGpuOptimization ?? false, highResZoomMultiplier: appSettings?.highResZoomMultiplier || 1.0, useFullDpiRendering: appSettings?.useFullDpiRendering ?? false, @@ -642,6 +643,7 @@ export default function SettingsPanel({ thumbnailResolution: appSettings?.thumbnailResolution || 720, rawHighlightCompression: appSettings?.rawHighlightCompression ?? 2.5, processingBackend: appSettings?.processingBackend || 'auto', + enableDenoiseHardwareAcceleration: appSettings?.enableDenoiseHardwareAcceleration ?? false, linuxGpuOptimization: appSettings?.linuxGpuOptimization ?? false, highResZoomMultiplier: appSettings?.highResZoomMultiplier || 1.0, useFullDpiRendering: appSettings?.useFullDpiRendering ?? false, @@ -675,6 +677,7 @@ export default function SettingsPanel({ if ( key === 'processingBackend' || + key === 'enableDenoiseHardwareAcceleration' || key === 'linuxGpuOptimization' || key === 'useWgpuRenderer' || key === 'thumbnailWorkerThreads' @@ -1890,6 +1893,20 @@ export default function SettingsPanel({ /> + {osPlatform === 'macos' && ( + + handleProcessingSettingChange('enableDenoiseHardwareAcceleration', checked)} + /> + + )} + {osPlatform !== 'macos' && osPlatform !== 'windows' && ( Date: Sat, 6 Jun 2026 19:50:43 +0200 Subject: [PATCH 2/2] add denoise elapsed timer Adds an elapsed execution timer within the Denoise Modal. Shows running elapsed seconds during active processing and displays the final completion duration once the process finishes. --- src/components/modals/DenoiseModal.tsx | 48 ++++++++++++++++++++++++++ src/i18n/locales/de.json | 1 + src/i18n/locales/en.json | 1 + 3 files changed, 50 insertions(+) diff --git a/src/components/modals/DenoiseModal.tsx b/src/components/modals/DenoiseModal.tsx index 1dfc24e0a..593925ded 100644 --- a/src/components/modals/DenoiseModal.tsx +++ b/src/components/modals/DenoiseModal.tsx @@ -235,6 +235,38 @@ export default function DenoiseModal({ const isBatch = targetPaths.length > 1; const mouseDownTarget = useRef(null); + const [elapsedTime, setElapsedTime] = useState(0); + const startTimeRef = useRef(null); + const timerIntervalRef = useRef(null); + + useEffect(() => { + if (isProcessing) { + startTimeRef.current = Date.now(); + setElapsedTime(0); + timerIntervalRef.current = setInterval(() => { + if (startTimeRef.current !== null) { + setElapsedTime(Date.now() - startTimeRef.current); + } + }, 50); + } else { + if (startTimeRef.current !== null) { + const duration = Date.now() - startTimeRef.current; + setElapsedTime(duration); + startTimeRef.current = null; + } + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + timerIntervalRef.current = null; + } + } + + return () => { + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + } + }; + }, [isProcessing]); + const methodOptions = useMemo>( () => [ { label: t('modals.denoise.methodAi'), value: 'ai' }, @@ -264,6 +296,7 @@ export default function DenoiseModal({ setMethod(isRaw ? 'ai' : 'bm3d'); setIntensity(isRaw ? 50 : 15); setIsMounted(true); + setElapsedTime(0); const timer = setTimeout(() => setShow(true), 10); return () => clearTimeout(timer); } else { @@ -352,6 +385,15 @@ export default function DenoiseModal({ return (
+ {elapsedTime > 0 && ( + +
+ + {t('modals.denoise.completedIn', { time: (elapsedTime / 1000).toFixed(2) })} + +
+
+ )} {savedPath && ( {currentStatusText} + {elapsedTime > 0 && ( + + {t('modals.denoise.elapsedTime', { time: (elapsedTime / 1000).toFixed(2) })} + + )} +