From eb51ec80231cf13accf8af156fa7a4283e2a2f88 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 30 Jul 2026 21:09:27 -0400 Subject: fix: harden lsfg-v2 migration and flatpak support --- py_modules/lsfg_vk/config_schema.py | 6 +++++- py_modules/lsfg_vk/config_schema_generated.py | 8 +++++++- py_modules/lsfg_vk/configuration.py | 7 +++---- py_modules/lsfg_vk/flatpak_service.py | 26 ++++++++++++++++++++++++++ py_modules/lsfg_vk/installation.py | 27 ++++++++++++++++++++------- 5 files changed, 61 insertions(+), 13 deletions(-) (limited to 'py_modules') diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 258b9dd..f8a9fe6 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -259,9 +259,13 @@ class ConfigurationManager: if normalized in profile_data["profiles"]: raise ValueError(f"Profile '{normalized}' already exists") source = source_profile if source_profile in profile_data["profiles"] else profile_data["current_profile"] + new_profile = dict(profile_data["profiles"][source]) + # A cloned profile may inherit Active In values that collide with its source. + # Force the newly selected profile until the user explicitly enables native matching. + new_profile["use_native_matching"] = False return ProfileData( current_profile=profile_data["current_profile"], - profiles={**profile_data["profiles"], normalized: dict(profile_data["profiles"][source])}, + profiles={**profile_data["profiles"], normalized: new_profile}, global_config=dict(profile_data["global_config"]), ) diff --git a/py_modules/lsfg_vk/config_schema_generated.py b/py_modules/lsfg_vk/config_schema_generated.py index 4c37c90..9cfda51 100644 --- a/py_modules/lsfg_vk/config_schema_generated.py +++ b/py_modules/lsfg_vk/config_schema_generated.py @@ -20,6 +20,7 @@ FLOW_SCALE = "flow_scale" PERFORMANCE_MODE = "performance_mode" PACING = "pacing" ACTIVE_IN = "active_in" +USE_NATIVE_MATCHING = "use_native_matching" GPU = "gpu" DISABLE_LSFGVK = "disable_lsfgvk" DXVK_FRAME_RATE = "dxvk_frame_rate" @@ -41,6 +42,7 @@ class ConfigurationData(TypedDict): performance_mode: bool pacing: str active_in: str + use_native_matching: bool gpu: str disable_lsfgvk: bool dxvk_frame_rate: int @@ -68,6 +70,8 @@ def get_script_parsing_logic(): value = value.strip() # Auto-generated parsing logic: + if key == "DECKY_LSFGVK_AUTO_PROFILE": + script_values["use_native_matching"] = value == "1" if key == "DISABLE_LSFGVK": script_values["disable_lsfgvk"] = value == "1" if key == "DXVK_FRAME_RATE": @@ -102,6 +106,8 @@ def get_script_generation_logic(): """Return the script generation logic as a callable""" def generate_script_lines(config): lines = [] + if config.get("use_native_matching", False): + lines.append("export DECKY_LSFGVK_AUTO_PROFILE=1") if config.get("disable_lsfgvk", False): lines.append("export DISABLE_LSFGVK=1") dxvk_frame_rate = config.get("dxvk_frame_rate", 0) @@ -127,4 +133,4 @@ def get_script_generation_logic(): return generate_script_lines -ALL_FIELDS = ['dll', 'allow_fp16', 'multiplier', 'flow_scale', 'performance_mode', 'pacing', 'active_in', 'gpu', 'disable_lsfgvk', 'dxvk_frame_rate', 'enable_wow64', 'disable_steamdeck_mode', 'mangohud_workaround', 'disable_vkbasalt', 'force_enable_vkbasalt', 'enable_wsi', 'enable_zink'] +ALL_FIELDS = ['dll', 'allow_fp16', 'multiplier', 'flow_scale', 'performance_mode', 'pacing', 'active_in', 'use_native_matching', 'gpu', 'disable_lsfgvk', 'dxvk_frame_rate', 'enable_wow64', 'disable_steamdeck_mode', 'mangohud_workaround', 'disable_vkbasalt', 'force_enable_vkbasalt', 'enable_wsi', 'enable_zink'] diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index 446ac3a..6511745 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -18,10 +18,9 @@ class ConfigurationService(BaseService): @staticmethod def _profile_selection_lines(profile_name: str, config: ConfigurationData) -> list[str]: - """Use native matching when a selected profile declares active_in.""" - active_in = str(config.get("active_in", "")).strip() - if active_in: - return ["# active_in is configured; lsfg-vk will select a matching profile automatically."] + """Force the selected profile unless the user explicitly opts into native matching.""" + if config.get("use_native_matching", False): + return ["# lsfg-vk will select a matching profile from Active In."] return [f"export LSFGVK_PROFILE={shlex.quote(profile_name)}"] def get_config(self) -> ConfigurationResponse: diff --git a/py_modules/lsfg_vk/flatpak_service.py b/py_modules/lsfg_vk/flatpak_service.py index 7f7ef07..80dcf5d 100644 --- a/py_modules/lsfg_vk/flatpak_service.py +++ b/py_modules/lsfg_vk/flatpak_service.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Dict, Any, List, Optional from .base_service import BaseService +from .config_schema import ConfigurationManager from .types import BaseResponse @@ -66,6 +67,21 @@ class FlatpakService(BaseService): self.log.debug("Could not read configured DLL path for Flatpak override: %s", error) return config_path, dll_directory + def _remove_legacy_app_overrides(self, app_id: str) -> list[str]: + """Remove only the v1 overrides previously created by this plugin.""" + legacy_dll_path = self.user_home / ".local/share/Steam/steamapps/common/Lossless Scaling/Lossless.dll" + legacy_overrides = [ + ["override", "--user", "--unset-env=LSFG_CONFIG", app_id], + ["override", "--user", f"--nofilesystem={legacy_dll_path}", app_id], + ["override", "--user", f"--nofilesystem={self.lsfg_launch_script_path}", app_id], + ] + errors = [] + for args in legacy_overrides: + result = self._run_flatpak_command(args, capture_output=True, text=True) + if result.returncode != 0: + errors.append(f"{' '.join(args[2:-1])}: {result.stderr}") + return errors + def _get_clean_env(self): """Get a clean environment without PyInstaller's bundled libraries""" env = os.environ.copy() @@ -382,6 +398,14 @@ class FlatpakService(BaseService): return self._error_response(FlatpakOverrideResponse, error_msg, app_id=app_id, operation="set") + legacy_errors = self._remove_legacy_app_overrides(app_id) + if legacy_errors: + self.log.warning( + "Applied v2 overrides for %s but could not fully remove v1 overrides: %s", + app_id, + "; ".join(legacy_errors), + ) + self.log.info(f"Successfully set lsfg-vk overrides for {app_id}") return self._success_response(FlatpakOverrideResponse, f"lsfg-vk overrides set for {app_id}", @@ -427,6 +451,8 @@ class FlatpakService(BaseService): if result.returncode != 0: removal_errors.append(f"unset-env: {result.stderr}") + removal_errors.extend(self._remove_legacy_app_overrides(app_id)) + if removal_errors: self.log.warning(f"Some override removals had issues for {app_id}: {'; '.join(removal_errors)}") diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py index cb6b161..7eee848 100644 --- a/py_modules/lsfg_vk/installation.py +++ b/py_modules/lsfg_vk/installation.py @@ -32,6 +32,7 @@ class InstallationService(BaseService): self.json_file = self.local_share_dir / JSON_FILENAME self.legacy_lib_file = self.local_lib_dir / LEGACY_LIB_FILENAME self.legacy_json_file = self.local_share_dir / LEGACY_JSON_FILENAME + self._config_recovery_backup: Path | None = None def install(self) -> InstallationResponse: try: @@ -49,8 +50,11 @@ class InstallationService(BaseService): self._create_lsfg_launch_script(profile_data) self._remove_legacy_layer_files() + message = "lsfg-vk v2 installed successfully" + if self._config_recovery_backup is not None: + message += f"; unsupported config backed up to {self._config_recovery_backup.name}" self.log.info("lsfg-vk v2 installed successfully from %s", archive_name) - return self._success_response(InstallationResponse, "lsfg-vk v2 installed successfully") + return self._success_response(InstallationResponse, message) except (OSError, tarfile.TarError, shutil.Error) as error: self.log.error("Error installing lsfg-vk: %s", error) return self._error_response(InstallationResponse, str(error), message="") @@ -136,13 +140,19 @@ class InstallationService(BaseService): dll_service = DllDetectionService(self.log) profile_data: ProfileData was_legacy = False + self._config_recovery_backup = None if self.config_file_path.exists(): content = self.config_file_path.read_text(encoding="utf-8") was_legacy = ConfigurationManager.is_legacy_v1(content) try: profile_data = ConfigurationManager.parse_toml_content_multi_profile(content) except (ValueError, KeyError, TypeError) as error: - self.log.warning("Could not parse existing config, using defaults: %s", error) + self._config_recovery_backup = self._backup_config(content, "unrecognized") + self.log.warning( + "Could not parse existing config; saved it to %s and using defaults: %s", + self._config_recovery_backup, + error, + ) default = ConfigurationManager.get_defaults_with_dll_detection(dll_service) profile_data = ProfileData( current_profile="decky-lsfg-vk", @@ -172,12 +182,15 @@ class InstallationService(BaseService): ) return profile_data + def _backup_config(self, content: str, label: str) -> Path: + backup_path = self.config_file_path.with_name(f"{self.config_file_path.name}.{label}.bak") + if not backup_path.exists(): + self._write_file(backup_path, content, 0o644) + self.log.info("Backed up %s configuration to %s", label, backup_path) + return backup_path + def _backup_legacy_config(self, content: str) -> None: - backup_path = self.config_file_path.with_name(f"{self.config_file_path.name}.v1.bak") - if backup_path.exists(): - return - self._write_file(backup_path, content, 0o644) - self.log.info("Backed up v1 configuration to %s", backup_path) + self._backup_config(content, "v1") def _read_script_values(self) -> Dict[str, Any]: if not self.lsfg_launch_script_path.exists(): -- cgit v1.2.3