summaryrefslogtreecommitdiff
path: root/py_modules
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-07-30 21:09:27 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-07-30 21:09:27 -0400
commiteb51ec80231cf13accf8af156fa7a4283e2a2f88 (patch)
tree95fd368b42efa29f28b924c8884a8f0d4c80d123 /py_modules
parent4390d600ffd35184c4c30bc64480f58e468627de (diff)
downloaddecky-lsfg-vk-feat/lsfg-v2-release.tar.gz
decky-lsfg-vk-feat/lsfg-v2-release.zip
fix: harden lsfg-v2 migration and flatpak supportfeat/lsfg-v2-release
Diffstat (limited to 'py_modules')
-rw-r--r--py_modules/lsfg_vk/config_schema.py6
-rw-r--r--py_modules/lsfg_vk/config_schema_generated.py8
-rw-r--r--py_modules/lsfg_vk/configuration.py7
-rw-r--r--py_modules/lsfg_vk/flatpak_service.py26
-rw-r--r--py_modules/lsfg_vk/installation.py27
5 files changed, 61 insertions, 13 deletions
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():