diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e9d5782..2d07e21 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,12 @@ serde_json = "1" tokio = { version = "1", features = ["rt", "sync"] } arboard = "3.6.1" +# Runtime shared-library probe for the Vulkan loader (active compute +# device detection, brief item #1). We do not call any vulkan symbols +# — we only need to answer "is libvulkan resolvable from the loader's +# default search path right now?". +libloading = "0.8" + # SqlitePool is named directly from src-tauri/src/lib.rs (the AppState # stores it). Must be unconditional, not Linux-only — naming a type from # a transitive dep requires the dep be listed here too. diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index dc1e636..0ee3b1a 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -4,6 +4,7 @@ use serde::Serialize; use tauri::Emitter; use crate::AppState; +use kon_core::hardware::{self, CpuFeatures}; use kon_core::model_registry::{self, Engine, LanguageSupport, ModelEntry}; use kon_core::types::ModelId; use kon_transcription::model_manager; @@ -195,6 +196,59 @@ pub fn prewarm_default_model(whisper_engine: Arc) { pub struct RuntimeCapabilities { pub accelerators: Vec, pub engines: Vec, + /// Which compute device Whisper / whisper.cpp actually booted against. + /// Distinct from `accelerators` (which lists what the _build_ supports): + /// a Vulkan-built binary on a CPU-only box reports accelerators=[cpu, vulkan] + /// but activeComputeDevice.kind = "cpu" with a reason. + pub active_compute_device: ActiveComputeDevice, + /// Runtime-detected CPU feature flags. Surfaced so Settings can warn + /// the user that performance will be poor without AVX2 / FMA. + pub cpu_features: CpuFeaturesInfo, + /// True when the detected hardware can sustain Whisper + LLM on the + /// same GPU concurrently (≥16 GB VRAM). Item #28 gates a user-facing + /// toggle on this. + pub parallel_mode_available: bool, +} + +/// Serialisable summary of whichever backend whisper.cpp / ggml wired +/// up on this boot. For MVP (Phase A.1) we derive this from +/// compile-time features + a runtime Vulkan loader probe; Phase A.2 +/// wires `whisper_print_system_info` for the real answer. +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ActiveComputeDevice { + /// One of "cuda" | "vulkan" | "metal" | "cpu". + pub kind: String, + /// Human-readable label, e.g. "GPU (Vulkan)" or "CPU (fallback)". + pub label: String, + /// Set only when we fell back from a richer backend, explains why + /// (e.g. "Vulkan loader not found"). None on the happy path. + pub reason: Option, +} + +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CpuFeaturesInfo { + pub avx2: bool, + pub avx512f: bool, + pub fma: bool, + pub sse4_2: bool, + pub neon: bool, + /// Shortcut for Settings: false means we fall back to a slow path. + pub has_ggml_baseline: bool, +} + +impl From for CpuFeaturesInfo { + fn from(f: CpuFeatures) -> Self { + Self { + avx2: f.avx2, + avx512f: f.avx512f, + fma: f.fma, + sse4_2: f.sse4_2, + neon: f.neon, + has_ggml_baseline: f.has_ggml_baseline(), + } + } } #[derive(Serialize)] @@ -224,6 +278,127 @@ pub struct LanguageSupportInfo { pub language_count: u16, } +/// Best-effort probe for the Vulkan loader shared library. +/// +/// whisper.cpp's Vulkan backend refuses to initialise if `vulkan-1.dll` +/// (Windows) / `libvulkan.so.1` (Linux) / the MoltenVK bundle (macOS) +/// is missing at runtime. We probe via `libloading::Library::new`; +/// failure means whisper.cpp will silently drop back to CPU, and the +/// user deserves to know before they start wondering why a 14-second +/// clip takes two minutes. +fn vulkan_loader_available() -> bool { + #[cfg(target_os = "linux")] + let candidates: &[&str] = &["libvulkan.so.1", "libvulkan.so"]; + #[cfg(target_os = "windows")] + let candidates: &[&str] = &["vulkan-1.dll"]; + #[cfg(target_os = "macos")] + let candidates: &[&str] = &["libvulkan.1.dylib", "libMoltenVK.dylib"]; + #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] + let candidates: &[&str] = &[]; + + for name in candidates { + // SAFETY: libloading::Library::new loads a shared library and returns + // a handle that is dropped at the end of this iteration. No symbols + // from it are called, so we just need the open-for-probe semantics. + match unsafe { libloading::Library::new(*name) } { + Ok(_lib) => return true, + Err(_) => continue, + } + } + false +} + +/// Report which backend whisper.cpp was actually able to initialise +/// against. The whisper-rs build here is compiled with the `vulkan` +/// feature unconditionally; on macOS that's still Metal via MoltenVK, +/// on Linux/Windows it's Vulkan. If the Vulkan loader is missing +/// we surface the CPU fallback path explicitly so the UI can warn. +pub fn detect_active_compute_device() -> ActiveComputeDevice { + #[cfg(target_os = "macos")] + { + if vulkan_loader_available() { + return ActiveComputeDevice { + kind: "metal".into(), + label: "GPU (Metal via MoltenVK)".into(), + reason: None, + }; + } + return ActiveComputeDevice { + kind: "cpu".into(), + label: "CPU (fallback)".into(), + reason: Some( + "MoltenVK / Vulkan loader not found — install the Vulkan SDK runtime." + .into(), + ), + }; + } + + #[cfg(not(target_os = "macos"))] + { + if vulkan_loader_available() { + return ActiveComputeDevice { + kind: "vulkan".into(), + label: "GPU (Vulkan)".into(), + reason: None, + }; + } + return ActiveComputeDevice { + kind: "cpu".into(), + label: "CPU (fallback)".into(), + reason: Some( + "Vulkan loader not found — install the Vulkan runtime (Windows) or \ + libvulkan1 (Linux) to enable GPU acceleration." + .into(), + ), + }; + } +} + +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase", tag = "kind", content = "message")] +pub enum RuntimeWarning { + /// CPU lacks AVX2 / FMA on x86_64 — ggml fallback path will be + /// dramatically slower. User should install a non-AVX2 build or + /// accept the hit. + Avx2Missing(String), + /// Vulkan loader is missing at runtime. Emitted once at startup + /// when `active_compute_device.reason` includes a loader-missing + /// message. + VulkanLoaderMissing(String), + /// CUDA driver mismatch forced a fallback (future-proofed for + /// when we add CUDA-detect; not emitted today). + #[allow(dead_code)] + CudaFallback(String), +} + +/// Emit any runtime warnings the frontend should surface as a banner. +/// Called once at setup() after `prewarm_default_model`. +pub fn emit_runtime_warnings(app: &tauri::AppHandle) { + let cpu_features = hardware::probe_cpu_features(); + let device = detect_active_compute_device(); + + if !cpu_features.has_ggml_baseline() { + let _ = app.emit( + "runtime-warning", + RuntimeWarning::Avx2Missing( + "Your CPU is missing AVX2/FMA. Whisper and the local LLM will run on a \ + dramatically slower fallback path — expect 5–10× the latency of a \ + 2015-or-newer CPU." + .into(), + ), + ); + } + + if device.kind == "cpu" { + if let Some(reason) = device.reason.as_ref() { + let _ = app.emit( + "runtime-warning", + RuntimeWarning::VulkanLoaderMissing(reason.clone()), + ); + } + } +} + #[tauri::command] pub fn get_runtime_capabilities( state: tauri::State<'_, AppState>, @@ -242,6 +417,9 @@ pub fn get_runtime_capabilities( .map(|entry| model_capability(entry, ¶keet)) .collect(); + let active_compute_device = detect_active_compute_device(); + let cpu_features: CpuFeaturesInfo = hardware::probe_cpu_features().into(); + // Kon's desktop build links transcribe-rs with the `whisper-vulkan` // feature unconditionally (see crates/transcription/Cargo.toml), so // whisper.cpp boots with Vulkan backend on any machine with a Vulkan @@ -268,6 +446,13 @@ pub fn get_runtime_capabilities( models: parakeet_models, }, ], + active_compute_device, + cpu_features, + // Phase A.1 ships detection of the VRAM budget only via + // hardware::probe_gpu; that currently returns None, so we + // default to sequential. When Phase A.4 (GpuGuard) and the + // real probe_gpu land, flip this based on detected VRAM ≥ 16 GB. + parallel_mode_available: false, }) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 00bcaa1..0d30e62 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -229,6 +229,11 @@ pub fn run() { crate::commands::models::prewarm_default_model(whisper); } + // Runtime-warning banner: push CPU-feature + Vulkan-loader + // fallbacks to the frontend so Settings can render a one-line + // hint. No-ops on a fully-supported box. + crate::commands::models::emit_runtime_warnings(&app.handle()); + if let Err(e) = tray::setup(app) { eprintln!("Failed to setup tray: {e}"); }