diff --git a/Backend/src/lib.rs b/Backend/src/lib.rs index 654d439..204c08c 100644 --- a/Backend/src/lib.rs +++ b/Backend/src/lib.rs @@ -9,7 +9,7 @@ use log::{error, warn}; use std::sync::Arc; use tauri::path::BaseDirectory; use tauri::WindowEvent; -use tauri::{Emitter, Manager}; +use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; use tauri_plugin_updater::UpdaterExt; use crate::core::BackendId; @@ -147,11 +147,24 @@ fn try_reopen_last_repo(app_handle: &tauri::AppHandle) { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { load_local_dotenv(); + let initial_config = settings::AppConfig::load_or_default(); workarounds::apply_linux_nvidia_workaround(); + workarounds::apply_gpu_acceleration_preference(&initial_config.performance); + #[cfg(target_os = "windows")] + let main_window_browser_args = + workarounds::main_window_browser_args(&initial_config.performance); // Initialize logging after startup-only process environment adjustments. logging::init(); - let app_state = state::AppState::new_with_config(); + log::info!( + "performance: GPU acceleration {} at startup", + if initial_config.performance.gpu_accel { + "enabled" + } else { + "disabled" + } + ); + let app_state = state::AppState::new_with_config(initial_config); monitoring::sync_backend_monitoring(&app_state.config()); println!("Running OpenVCS..."); @@ -197,6 +210,39 @@ pub fn run() { crate::plugin_paths::set_resource_dir(parent.to_path_buf()); } } + + if app.get_webview_window("main").is_none() { + #[cfg(target_os = "windows")] + let builder = { + let mut builder = WebviewWindowBuilder::new( + app, + "main", + WebviewUrl::App("index.html".into()), + ) + .title("OpenVCS") + .inner_size(1100.0, 600.0) + .min_inner_size(1100.0, 600.0) + .resizable(true); + if let Some(args) = main_window_browser_args.clone() { + builder = builder.additional_browser_args(args); + } + builder + }; + #[cfg(not(target_os = "windows"))] + let builder = WebviewWindowBuilder::new( + app, + "main", + WebviewUrl::App("index.html".into()), + ) + .title("OpenVCS") + .inner_size(1100.0, 600.0) + .min_inner_size(1100.0, 600.0) + .resizable(true); + if let Err(err) = builder.build() { + log::error!("failed to create main window: {}", err); + } + } + // Keep resource lookup state populated before resolving bundled Node // candidates. `bundled_node_candidate_paths()` uses both the generic // RESOURCE_DIR base and the exact Tauri-resolved `node-runtime` diff --git a/Backend/src/settings.rs b/Backend/src/settings.rs index 1f937c6..b6d373a 100644 --- a/Backend/src/settings.rs +++ b/Backend/src/settings.rs @@ -285,7 +285,7 @@ impl Default for Lfs { pub struct Performance { #[serde(default)] pub progressive_render: bool, - #[serde(default)] + #[serde(default = "default_true")] pub gpu_accel: bool, #[serde(default = "default_true")] pub animations: bool, diff --git a/Backend/src/state.rs b/Backend/src/state.rs index 325097a..05f3c74 100644 --- a/Backend/src/state.rs +++ b/Backend/src/state.rs @@ -43,12 +43,11 @@ pub struct AppState { } impl AppState { - /// Creates app state by loading persisted settings and recent repositories. + /// Creates app state from a preloaded settings snapshot and recent repositories. /// /// # Returns /// - A fully initialized [`AppState`] with config and recent repositories loaded. - pub fn new_with_config() -> Self { - let cfg = AppConfig::load_or_default(); // reads ~/.config/openvcs/openvcs.conf + pub fn new_with_config(cfg: AppConfig) -> Self { let s = Self { config: RwLock::new(cfg), repo_config: RwLock::new(RepoConfig::default()), diff --git a/Backend/src/workarounds.rs b/Backend/src/workarounds.rs index c635eee..086027b 100644 --- a/Backend/src/workarounds.rs +++ b/Backend/src/workarounds.rs @@ -1,5 +1,7 @@ // Copyright © 2025-2026 OpenVCS Contributors // SPDX-License-Identifier: GPL-3.0-or-later +use crate::settings::Performance; + #[cfg(target_os = "linux")] /// Applies a runtime workaround for NVIDIA + Wayland rendering issues. /// @@ -37,6 +39,38 @@ pub fn apply_linux_nvidia_workaround() { } } +#[cfg(target_os = "linux")] +/// Applies the stored GPU acceleration preference before the webview starts. +/// +/// When disabled, this forces WebKitGTK into a software/compositing-off path. +/// When enabled, any previously injected disable flags are removed so the host +/// can use the default accelerated path. +/// +/// # Parameters +/// - `performance`: Persisted performance settings. +/// +/// # Returns +/// - `()`. +pub fn apply_gpu_acceleration_preference(performance: &Performance) { + const COMPOSITING_KEY: &str = "WEBKIT_DISABLE_COMPOSITING_MODE"; + const WEBGL_KEY: &str = "WEBKIT_DISABLE_WEBGL"; + + if performance.gpu_accel { + eprintln!("Clearing GPU-disable env vars where present"); + unsafe { + std::env::remove_var(COMPOSITING_KEY); + std::env::remove_var(WEBGL_KEY); + } + return; + } + + eprintln!("Applying GPU-disable env vars: {COMPOSITING_KEY}=1, {WEBGL_KEY}=1"); + unsafe { + std::env::set_var(COMPOSITING_KEY, "1"); + std::env::set_var(WEBGL_KEY, "1"); + } +} + #[cfg(not(target_os = "linux"))] #[inline] /// No-op on non-Linux platforms. @@ -46,3 +80,33 @@ pub fn apply_linux_nvidia_workaround() { pub fn apply_linux_nvidia_workaround() { // no-op on non-Linux } + +#[cfg(not(target_os = "linux"))] +#[inline] +/// No-op on non-Linux platforms. +/// +/// # Parameters +/// - `performance`: Persisted performance settings. +/// +/// # Returns +/// - `()`. +pub fn apply_gpu_acceleration_preference(_performance: &Performance) { + // no-op on non-Linux +} + +#[cfg(target_os = "windows")] +/// Returns additional browser arguments for the main webview when GPU acceleration is disabled. +/// +/// # Parameters +/// - `performance`: Persisted performance settings. +/// +/// # Returns +/// - Browser argument string when GPU acceleration is disabled. +/// - `None` when no override is needed. +pub fn main_window_browser_args(performance: &Performance) -> Option { + if performance.gpu_accel { + return None; + } + + Some("--disable-gpu --disable-gpu-compositing".to_string()) +} diff --git a/Backend/tauri.conf.json b/Backend/tauri.conf.json index bb39db9..b939561 100644 --- a/Backend/tauri.conf.json +++ b/Backend/tauri.conf.json @@ -16,16 +16,7 @@ }, "app": { "withGlobalTauri": true, - "windows": [ - { - "title": "OpenVCS", - "width": 1100, - "height": 600, - "resizable": true, - "minWidth": 1100, - "minHeight": 600 - } - ], + "windows": [], "security": { "csp": null } diff --git a/Frontend/src/modals/settings.html b/Frontend/src/modals/settings.html index 199d68f..4ab8dd0 100644 --- a/Frontend/src/modals/settings.html +++ b/Frontend/src/modals/settings.html @@ -4,19 +4,19 @@
-
diff --git a/Frontend/src/scripts/features/settings.ts b/Frontend/src/scripts/features/settings.ts index 1ddcf7d..8110326 100644 --- a/Frontend/src/scripts/features/settings.ts +++ b/Frontend/src/scripts/features/settings.ts @@ -6,7 +6,7 @@ import { openModal, closeModal } from '../ui/modals'; import { toKebab } from '../lib/dom'; import { confirmBool } from '../lib/confirm'; import { notify } from '../lib/notify'; -import { setTheme, applyCommitSummaryRestriction } from '../ui/layout'; +import { setTheme, applyCommitSummaryRestriction, applyGpuAccelerationPreference } from '../ui/layout'; import { collectGeneralSettings, loadGeneralSettingsIntoForm } from './settingsGeneral'; import { DEFAULT_DARK_THEME_ID, DEFAULT_LIGHT_THEME_ID, DEFAULT_THEME_ID, getActiveThemeId, getAvailableThemes, refreshAvailableThemes, selectThemePack } from '../themes'; import { invokePluginAction, reloadPlugins } from '../plugins'; @@ -241,11 +241,17 @@ async function renderPluginMenus(modal: HTMLElement): Promise { const panelsScroll = modal.querySelector('#settings-panels-scroll'); if (!nav || !panelsScroll) return; - nav.querySelectorAll('[data-plugin-menu="true"]').forEach((node) => node.remove()); - nav.querySelectorAll('[data-plugin-menus-wrap="true"]').forEach((node) => node.remove()); + nav.querySelectorAll('[data-plugin-menu="true"]').forEach((node) => { + node.remove(); + }); + nav.querySelectorAll('[data-plugin-menus-wrap="true"]').forEach((node) => { + node.remove(); + }); panelsScroll .querySelectorAll('.panel-form[data-plugin-menu="true"]') - .forEach((node) => node.remove()); + .forEach((node) => { + node.remove(); + }); let menus: PluginMenuPayload[] = []; let pluginSummaries: PluginSummary[] = []; @@ -462,10 +468,10 @@ function activateSection(modal: HTMLElement, section: string) { })(); const btn = nav.querySelector(`[data-section="${safeSection}"]`); - nav.querySelectorAll('.seg-btn').forEach(b => { + nav.querySelectorAll('.seg-btn').forEach((b) => { b.classList.toggle('active', b === btn); }); - panels.querySelectorAll('.panel-form').forEach(p => { + panels.querySelectorAll('.panel-form').forEach((p) => { p.classList.toggle('hidden', p.getAttribute('data-panel') !== safeSection); }); @@ -575,7 +581,9 @@ export function wireSettings() { .filter((el): el is HTMLInputElement => !!el); const updateLfsDependentState = () => { const enabled = !!lfsToggle?.checked; - lfsDependents.forEach(input => input.disabled = !enabled); + lfsDependents.forEach((input) => { + input.disabled = !enabled; + }); }; updateLfsDependentState(); lfsToggle?.addEventListener('change', updateLfsDependentState); @@ -690,6 +698,14 @@ export function wireSettings() { } const next = collectSettingsFromForm(modal); + const previousCfg = (() => { + try { + return JSON.parse(String(modal.dataset.currentCfg || '{}')) as GlobalSettings; + } catch { + return {} as GlobalSettings; + } + })(); + const gpuChanged = previousCfg.performance?.gpu_accel !== next.performance?.gpu_accel; await TAURI.invoke('set_global_settings', { cfg: next }); await syncFrontendMonitoring(next); @@ -710,10 +726,11 @@ export function wireSettings() { if (mono) root.style.setProperty('--mono', mono); else root.style.removeProperty('--mono'); applyAnimationPreference(next?.performance?.animations); + applyGpuAccelerationPreference(next?.performance?.gpu_accel); applyCommitSummaryRestriction(next?.general?.restrict_commit_summary !== false); } catch {} - notify('Settings saved'); + notify(gpuChanged ? 'Settings saved. GPU changes apply after restart.' : 'Settings saved'); flashSavedState(settingsSave); } catch (e) { console.error('Failed to save settings:', e); @@ -766,6 +783,7 @@ export function wireSettings() { await TAURI.invoke('set_global_settings', { cfg: cur }); await syncFrontendMonitoring(cur); applyAnimationPreference(cur.performance?.animations); + applyGpuAccelerationPreference(cur.performance?.gpu_accel); applyCommitSummaryRestriction(cur.general?.restrict_commit_summary !== false); await loadSettingsIntoForm(modal); setTheme('system'); diff --git a/Frontend/src/scripts/main.ts b/Frontend/src/scripts/main.ts index 3574f0f..3017e8b 100644 --- a/Frontend/src/scripts/main.ts +++ b/Frontend/src/scripts/main.ts @@ -11,7 +11,7 @@ import { destroyOverlayScrollbarsFor, initOverlayScrollbarsFor, refreshOverlaySc import { prefs, state, hasRepo, resolveVcsActionLabel } from './state/state'; import { bindTabs, initResizer, refreshRepoActions, setRepoHeader, resetRepoHeader, setTab, setTheme, - bindLayoutActionState, applyCommitSummaryRestriction + bindLayoutActionState, applyCommitSummaryRestriction, applyGpuAccelerationPreference } from './ui/layout'; import { clearPluginMenubarMenus, initMenubar, refreshPluginMenubarMenus } from './ui/menubar'; import { closeAllModals } from './ui/modals'; @@ -132,18 +132,21 @@ async function boot() { const mono = String(cfg?.ux?.font_mono || '').trim(); if (mono) root.style.setProperty('--mono', mono); applyAnimationPreference(cfg?.performance?.animations); + applyGpuAccelerationPreference(cfg?.performance?.gpu_accel); applyCommitSummaryRestriction(cfg?.general?.restrict_commit_summary !== false); } catch { /* best-effort */ } } catch { try { await selectThemePack(DEFAULT_LIGHT_THEME_ID, { silent: true, mode: 'system' }); } catch {} setTheme(prefs.theme); applyAnimationPreference(true); + applyGpuAccelerationPreference(true); applyCommitSummaryRestriction(true); } })(); } else { setTheme(prefs.theme); applyAnimationPreference(true); + applyGpuAccelerationPreference(true); applyCommitSummaryRestriction(true); } wireRenderListCallbacks(); diff --git a/Frontend/src/scripts/ui/layout.test.ts b/Frontend/src/scripts/ui/layout.test.ts index b53be59..a274eab 100644 --- a/Frontend/src/scripts/ui/layout.test.ts +++ b/Frontend/src/scripts/ui/layout.test.ts @@ -70,6 +70,34 @@ describe('applyCommitSummaryRestriction', () => { }); }); +describe('applyGpuAccelerationPreference', () => { + beforeEach(() => { + vi.resetModules(); + Object.defineProperty(globalThis, 'matchMedia', { + value: createMatchMediaMock, + configurable: true, + writable: true, + }); + mountLayoutDom(); + }); + + it('stores the GPU acceleration preference on the document root', async () => { + const { applyGpuAccelerationPreference } = await import('./layout'); + + applyGpuAccelerationPreference(false); + expect(document.documentElement.dataset.gpuAcceleration).toBe('off'); + + applyGpuAccelerationPreference(true); + expect(document.documentElement.dataset.gpuAcceleration).toBe('on'); + + applyGpuAccelerationPreference(undefined); + expect(document.documentElement.dataset.gpuAcceleration).toBe('on'); + + applyGpuAccelerationPreference(null); + expect(document.documentElement.dataset.gpuAcceleration).toBe('on'); + }); +}); + describe('refreshRepoActions', () => { beforeEach(() => { vi.resetModules(); diff --git a/Frontend/src/scripts/ui/layout.ts b/Frontend/src/scripts/ui/layout.ts index 1d792e3..0fae345 100644 --- a/Frontend/src/scripts/ui/layout.ts +++ b/Frontend/src/scripts/ui/layout.ts @@ -54,6 +54,11 @@ export function setTheme(theme: 'dark'|'light'|'system') { savePrefs(); } +/** Applies or clears GPU compositor hints for the app shell. */ +export function applyGpuAccelerationPreference(enabled: boolean | undefined | null) { + document.documentElement.dataset.gpuAcceleration = enabled === false ? 'off' : 'on'; +} + /** Toggles between light and dark appearance modes. */ export function toggleTheme() { const next = (prefs.theme === 'dark' ? 'light' : 'dark'); diff --git a/Frontend/src/styles/layout.css b/Frontend/src/styles/layout.css index 306084a..9052fb5 100644 --- a/Frontend/src/styles/layout.css +++ b/Frontend/src/styles/layout.css @@ -85,6 +85,15 @@ box-sizing: border-box; overflow: hidden; } + +html[data-gpu-acceleration="on"] .menubar, +html[data-gpu-acceleration="on"] .titlebar, +html[data-gpu-acceleration="on"] .menubar .menu-list, +html[data-gpu-acceleration="on"] .modal .dialog.sheet { + backface-visibility: hidden; + transform: translateZ(0); +} + .title-actions { margin-left: auto; display: flex; align-items: center; gap: .45rem; } .plugin-title-actions { display: flex; align-items: center; gap: .45rem; }