From 6b701637ad308513b678c80baceec6c79e339ce9 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 17 Jul 2025 14:22:56 -0400 Subject: initial conf FE and BE hooks --- py_modules/lsfg_vk/config_schema.py | 230 ++++++++++++++++++------------------ 1 file changed, 118 insertions(+), 112 deletions(-) (limited to 'py_modules/lsfg_vk/config_schema.py') diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 0f1bdae..f054abf 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -1,16 +1,18 @@ """ Centralized configuration schema for lsfg-vk. -This module defines the complete configuration structure, including: +This module defines the complete configuration structure for TOML-based config files, including: - Field definitions with types, defaults, and metadata -- Script generation logic +- TOML generation logic - Validation rules - Type definitions """ -from typing import TypedDict, Dict, Any, Union, Callable, cast -from dataclasses import dataclass, field +import re +from typing import TypedDict, Dict, Any, Union, cast +from dataclasses import dataclass from enum import Enum +from pathlib import Path class ConfigFieldType(Enum): @@ -18,6 +20,7 @@ class ConfigFieldType(Enum): BOOLEAN = "boolean" INTEGER = "integer" FLOAT = "float" + STRING = "string" @dataclass @@ -25,110 +28,72 @@ class ConfigField: """Configuration field definition""" name: str field_type: ConfigFieldType - default: Union[bool, int, float] + default: Union[bool, int, float, str] description: str - script_template: str # Template for script generation - script_comment: str = "" # Comment to add when disabled - def get_script_line(self, value: Union[bool, int, float]) -> str: - """Generate script line for this field""" - if self.field_type == ConfigFieldType.BOOLEAN: - if value: - return self.script_template.format(value=1) - else: - return f"# {self.script_template.format(value=1)}" - else: - return self.script_template.format(value=value) + def get_toml_value(self, value: Union[bool, int, float, str]) -> Union[bool, int, float, str]: + """Get the value for TOML output""" + return value # Configuration schema definition CONFIG_SCHEMA: Dict[str, ConfigField] = { - "enable_lsfg": ConfigField( - name="enable_lsfg", + "enable": ConfigField( + name="enable", field_type=ConfigFieldType.BOOLEAN, default=True, - description="Enables the frame generation layer", - script_template="export ENABLE_LSFG={value}", - script_comment="# export ENABLE_LSFG=1" + description="enable/disable lsfg on every game" + ), + + "dll": ConfigField( + name="dll", + field_type=ConfigFieldType.STRING, + default="/games/Lossless Scaling/Lossless.dll", + description="specify where Lossless.dll is stored" ), "multiplier": ConfigField( name="multiplier", field_type=ConfigFieldType.INTEGER, default=2, - description="Traditional FPS multiplier value", - script_template="export LSFG_MULTIPLIER={value}" + description="change the fps multiplier" ), "flow_scale": ConfigField( name="flow_scale", field_type=ConfigFieldType.FLOAT, default=0.8, - description="Lowers the internal motion estimation resolution", - script_template="export LSFG_FLOW_SCALE={value}" - ), - - "hdr": ConfigField( - name="hdr", - field_type=ConfigFieldType.BOOLEAN, - default=False, - description="Enable HDR mode (only if Game supports HDR)", - script_template="export LSFG_HDR={value}", - script_comment="# export LSFG_HDR=1" + description="change the flow scale (lower = faster)" ), - "perf_mode": ConfigField( - name="perf_mode", + "performance_mode": ConfigField( + name="performance_mode", field_type=ConfigFieldType.BOOLEAN, default=True, - description="Use lighter model for FG", - script_template="export LSFG_PERF_MODE={value}", - script_comment="# export LSFG_PERF_MODE=1" + description="toggle performance mode (2x-8x performance increase)" ), - "immediate_mode": ConfigField( - name="immediate_mode", + "hdr_mode": ConfigField( + name="hdr_mode", field_type=ConfigFieldType.BOOLEAN, default=False, - description="Reduce input lag (Experimental, will cause issues in many games)", - script_template="export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync", - script_comment="# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync" - ), - - "disable_vkbasalt": ConfigField( - name="disable_vkbasalt", - field_type=ConfigFieldType.BOOLEAN, - default=True, - description="Some plugins add vkbasalt layer, which can break lsfg. Toggling on fixes this", - script_template="export DISABLE_VKBASALT={value}", - script_comment="# export DISABLE_VKBASALT=1" - ), - - "frame_cap": ConfigField( - name="frame_cap", - field_type=ConfigFieldType.INTEGER, - default=0, - description="Limit base game FPS (0 = disabled)", - script_template="export DXVK_FRAME_RATE={value}", - script_comment="# export DXVK_FRAME_RATE=60" + description="enable hdr mode (doesn't support scrgb)" ) } class ConfigurationData(TypedDict): """Type-safe configuration data structure""" - enable_lsfg: bool + enable: bool + dll: str multiplier: int flow_scale: float - hdr: bool - perf_mode: bool - immediate_mode: bool - disable_vkbasalt: bool - frame_cap: int + performance_mode: bool + hdr_mode: bool class ConfigurationManager: - """Centralized configuration management""" + """Centralized configuration management for TOML-based config""" @staticmethod def get_defaults() -> ConfigurationData: @@ -166,63 +131,104 @@ class ConfigurationManager: validated[field_name] = int(value) elif field_def.field_type == ConfigFieldType.FLOAT: validated[field_name] = float(value) + elif field_def.field_type == ConfigFieldType.STRING: + validated[field_name] = str(value) else: validated[field_name] = value return cast(ConfigurationData, validated) @staticmethod - def generate_script_content(config: ConfigurationData) -> str: - """Generate lsfg script content from configuration""" - script_lines = ["#!/bin/bash", ""] + def generate_toml_content(config: ConfigurationData) -> str: + """Generate TOML configuration file content""" + lines = ["[global]"] - # Generate script lines for each field - for field_name in CONFIG_SCHEMA.keys(): - field_def = CONFIG_SCHEMA[field_name] + for field_name, field_def in CONFIG_SCHEMA.items(): value = config[field_name] + lines.append(f"# {field_def.description}") - if field_def.field_type == ConfigFieldType.BOOLEAN: - if value: - script_lines.append(field_def.script_template.format(value=1)) - else: - script_lines.append(field_def.script_comment) + # Format value based on type + if isinstance(value, bool): + lines.append(f"{field_name} = {str(value).lower()}") + elif isinstance(value, str): + lines.append(f'{field_name} = "{value}"') else: - # For frame_cap, special handling for 0 value - if field_name == "frame_cap" and value == 0: - script_lines.append(field_def.script_comment) - else: - script_lines.append(field_def.script_template.format(value=value)) - - # Add script footer - script_lines.extend([ - "", - "# Execute the passed command with the environment variables set", - 'exec "$@"' - ]) + lines.append(f"{field_name} = {value}") + + lines.append("") # Empty line for readability - return "\n".join(script_lines) + return "\n".join(lines) @staticmethod - def get_update_signature() -> list[tuple[str, type]]: - """Get the function signature for update_config method""" - signature = [] - for field_name, field_def in CONFIG_SCHEMA.items(): - if field_def.field_type == ConfigFieldType.BOOLEAN: - signature.append((field_name, bool)) - elif field_def.field_type == ConfigFieldType.INTEGER: - signature.append((field_name, int)) - elif field_def.field_type == ConfigFieldType.FLOAT: - signature.append((field_name, float)) - return signature + def parse_toml_content(content: str) -> ConfigurationData: + """Parse TOML content into configuration data using simple regex parsing""" + config = ConfigurationManager.get_defaults() + + try: + # Look for [global] section + lines = content.split('\n') + in_global_section = False + + for line in lines: + line = line.strip() + + # Skip comments and empty lines + if not line or line.startswith('#'): + continue + + # Check for section headers + if line.startswith('[') and line.endswith(']'): + section = line[1:-1].strip() + in_global_section = (section == 'global') + continue + + # Only parse lines in the global section + if not in_global_section: + continue + + # Parse key = value lines + if '=' in line: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + + # Remove quotes from string values + if value.startswith('"') and value.endswith('"'): + value = value[1:-1] + elif value.startswith("'") and value.endswith("'"): + value = value[1:-1] + + # Convert to appropriate type based on field definition + if key in CONFIG_SCHEMA: + field_def = CONFIG_SCHEMA[key] + try: + if field_def.field_type == ConfigFieldType.BOOLEAN: + config[key] = value.lower() in ('true', '1', 'yes', 'on') + elif field_def.field_type == ConfigFieldType.INTEGER: + config[key] = int(value) + elif field_def.field_type == ConfigFieldType.FLOAT: + config[key] = float(value) + elif field_def.field_type == ConfigFieldType.STRING: + config[key] = value + except (ValueError, TypeError): + # If conversion fails, keep default value + pass + + return config + + except Exception: + # If parsing fails completely, return defaults + return ConfigurationManager.get_defaults() @staticmethod - def create_config_from_args(*args) -> ConfigurationData: - """Create configuration from ordered arguments""" - field_names = ConfigurationManager.get_field_names() - if len(args) != len(field_names): - raise ValueError(f"Expected {len(field_names)} arguments, got {len(args)}") - + def create_config_from_args(enable: bool, dll: str, multiplier: int, flow_scale: float, + performance_mode: bool, hdr_mode: bool) -> ConfigurationData: + """Create configuration from individual arguments""" return cast(ConfigurationData, { - field_name: args[i] - for i, field_name in enumerate(field_names) + "enable": enable, + "dll": dll, + "multiplier": multiplier, + "flow_scale": flow_scale, + "performance_mode": performance_mode, + "hdr_mode": hdr_mode }) -- cgit v1.2.3 From 57bd1cd9fcbbc8d4cd1d2f45106fc29d00bb6918 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 17 Jul 2025 14:35:06 -0400 Subject: write dll path when discovered --- py_modules/lsfg_vk/config_schema.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'py_modules/lsfg_vk/config_schema.py') diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index f054abf..8c5f58c 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -48,7 +48,7 @@ CONFIG_SCHEMA: Dict[str, ConfigField] = { "dll": ConfigField( name="dll", field_type=ConfigFieldType.STRING, - default="/games/Lossless Scaling/Lossless.dll", + default="", # Will be populated dynamically based on detection description="specify where Lossless.dll is stored" ), @@ -103,6 +103,34 @@ class ConfigurationManager: for field in CONFIG_SCHEMA.values() }) + @staticmethod + def get_defaults_with_dll_detection(dll_detection_service=None) -> ConfigurationData: + """Get default configuration values with DLL path detection + + Args: + dll_detection_service: Optional DLL detection service instance + + Returns: + ConfigurationData with detected DLL path if available + """ + defaults = ConfigurationManager.get_defaults() + + # Try to detect DLL path if service provided + if dll_detection_service: + try: + dll_result = dll_detection_service.check_lossless_scaling_dll() + if dll_result.get("detected") and dll_result.get("path"): + defaults["dll"] = dll_result["path"] + except Exception: + # If detection fails, keep empty default + pass + + # If DLL path is still empty, use a reasonable fallback + if not defaults["dll"]: + defaults["dll"] = "/home/deck/.local/share/Steam/steamapps/common/Lossless Scaling/Lossless.dll" + + return defaults + @staticmethod def get_field_names() -> list[str]: """Get ordered list of configuration field names""" -- cgit v1.2.3 From ad0ba0fc61f83e2aaf22192e7d0ad05dde9ffd62 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 17 Jul 2025 14:53:18 -0400 Subject: fix descriptions and cleanup UI --- py_modules/lsfg_vk/config_schema.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'py_modules/lsfg_vk/config_schema.py') diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 8c5f58c..1604f5d 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -63,21 +63,21 @@ CONFIG_SCHEMA: Dict[str, ConfigField] = { name="flow_scale", field_type=ConfigFieldType.FLOAT, default=0.8, - description="change the flow scale (lower = faster)" + description="change the flow scale" ), "performance_mode": ConfigField( name="performance_mode", field_type=ConfigFieldType.BOOLEAN, default=True, - description="toggle performance mode (2x-8x performance increase)" + description="toggle performance mode" ), "hdr_mode": ConfigField( name="hdr_mode", field_type=ConfigFieldType.BOOLEAN, default=False, - description="enable hdr mode (doesn't support scrgb)" + description="enable hdr mode" ) } @@ -93,7 +93,7 @@ class ConfigurationData(TypedDict): class ConfigurationManager: - """Centralized configuration management for TOML-based config""" + """Centralized configuration management""" @staticmethod def get_defaults() -> ConfigurationData: -- cgit v1.2.3 From 0670041467ca5625d93e3e4dbc2f738da24d88b4 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 17 Jul 2025 23:23:03 -0400 Subject: add experimental toggles --- py_modules/lsfg_vk/config_schema.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'py_modules/lsfg_vk/config_schema.py') diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 1604f5d..42ac640 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -78,6 +78,20 @@ CONFIG_SCHEMA: Dict[str, ConfigField] = { field_type=ConfigFieldType.BOOLEAN, default=False, description="enable hdr mode" + ), + + "experimental_present_mode": ConfigField( + name="experimental_present_mode", + field_type=ConfigFieldType.STRING, + default="", + description="experimental: override vulkan present mode (empty/fifo/vsync/mailbox/immediate)" + ), + + "experimental_fps_limit": ConfigField( + name="experimental_fps_limit", + field_type=ConfigFieldType.INTEGER, + default=0, + description="experimental: base framerate cap for dxvk games, before frame multiplier (0 = disabled)" ) } @@ -90,6 +104,8 @@ class ConfigurationData(TypedDict): flow_scale: float performance_mode: bool hdr_mode: bool + experimental_present_mode: str + experimental_fps_limit: int class ConfigurationManager: @@ -250,7 +266,9 @@ class ConfigurationManager: @staticmethod def create_config_from_args(enable: bool, dll: str, multiplier: int, flow_scale: float, - performance_mode: bool, hdr_mode: bool) -> ConfigurationData: + performance_mode: bool, hdr_mode: bool, + experimental_present_mode: str = "", + experimental_fps_limit: int = 0) -> ConfigurationData: """Create configuration from individual arguments""" return cast(ConfigurationData, { "enable": enable, @@ -258,5 +276,7 @@ class ConfigurationManager: "multiplier": multiplier, "flow_scale": flow_scale, "performance_mode": performance_mode, - "hdr_mode": hdr_mode + "hdr_mode": hdr_mode, + "experimental_present_mode": experimental_present_mode, + "experimental_fps_limit": experimental_fps_limit }) -- cgit v1.2.3 From 48ee73dae1bdecec47ccbaf5456be8c5937cb0fd Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 18 Jul 2025 12:00:31 -0400 Subject: new profile method workaround for sudo global use --- py_modules/lsfg_vk/config_schema.py | 100 ++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 27 deletions(-) (limited to 'py_modules/lsfg_vk/config_schema.py') diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 42ac640..ed82d97 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -184,19 +184,44 @@ class ConfigurationManager: @staticmethod def generate_toml_content(config: ConfigurationData) -> str: - """Generate TOML configuration file content""" - lines = ["[global]"] + """Generate TOML configuration file content using the new game-specific format""" + lines = ["version = 1"] + lines.append("") + # Add global section with DLL path only (if specified) + if config.get("dll"): + lines.append("[global]") + lines.append(f"# specify where Lossless.dll is stored") + lines.append(f'dll = "{config["dll"]}"') + lines.append("") + + # Add game section with process name for LSFG_PROCESS approach + lines.append("[[game]]") + lines.append("# Plugin-managed game entry (uses LSFG_PROCESS=decky-lsfg-vk)") + lines.append('exe = "decky-lsfg-vk"') + lines.append("") + + # Add all configuration fields to the game section for field_name, field_def in CONFIG_SCHEMA.items(): + # Skip dll and enable fields - dll goes in global, enable is handled via multiplier + if field_name in ["dll", "enable"]: + continue + value = config[field_name] - lines.append(f"# {field_def.description}") + + # Handle enable field by setting multiplier to 1 when disabled + if field_name == "multiplier" and not config.get("enable", True): + value = 1 + lines.append(f"# LSFG disabled via plugin - multiplier set to 1") + else: + lines.append(f"# {field_def.description}") # Format value based on type if isinstance(value, bool): lines.append(f"{field_name} = {str(value).lower()}") - elif isinstance(value, str): + elif isinstance(value, str) and value: # Only add non-empty strings lines.append(f'{field_name} = "{value}"') - else: + elif isinstance(value, (int, float)) and value != 0: # Only add non-zero numbers lines.append(f"{field_name} = {value}") lines.append("") # Empty line for readability @@ -209,9 +234,11 @@ class ConfigurationManager: config = ConfigurationManager.get_defaults() try: - # Look for [global] section + # Look for both [global] and [[game]] sections lines = content.split('\n') in_global_section = False + in_game_section = False + current_game_exe = None for line in lines: line = line.strip() @@ -222,12 +249,16 @@ class ConfigurationManager: # Check for section headers if line.startswith('[') and line.endswith(']'): - section = line[1:-1].strip() - in_global_section = (section == 'global') - continue - - # Only parse lines in the global section - if not in_global_section: + if line == '[global]': + in_global_section = True + in_game_section = False + elif line == '[[game]]': + in_global_section = False + in_game_section = True + current_game_exe = None + else: + in_global_section = False + in_game_section = False continue # Parse key = value lines @@ -242,21 +273,36 @@ class ConfigurationManager: elif value.startswith("'") and value.endswith("'"): value = value[1:-1] - # Convert to appropriate type based on field definition - if key in CONFIG_SCHEMA: - field_def = CONFIG_SCHEMA[key] - try: - if field_def.field_type == ConfigFieldType.BOOLEAN: - config[key] = value.lower() in ('true', '1', 'yes', 'on') - elif field_def.field_type == ConfigFieldType.INTEGER: - config[key] = int(value) - elif field_def.field_type == ConfigFieldType.FLOAT: - config[key] = float(value) - elif field_def.field_type == ConfigFieldType.STRING: - config[key] = value - except (ValueError, TypeError): - # If conversion fails, keep default value - pass + # Handle global section (dll only) + if in_global_section and key == "dll": + config["dll"] = value + + # Handle game section + elif in_game_section: + # Track the exe for this game section + if key == "exe": + current_game_exe = value + # Only parse config for our plugin-managed game entry + elif current_game_exe == "decky-lsfg-vk" and key in CONFIG_SCHEMA: + field_def = CONFIG_SCHEMA[key] + try: + if field_def.field_type == ConfigFieldType.BOOLEAN: + config[key] = value.lower() in ('true', '1', 'yes', 'on') + elif field_def.field_type == ConfigFieldType.INTEGER: + parsed_value = int(value) + # Handle enable field via multiplier + if key == "multiplier": + config[key] = parsed_value + config["enable"] = parsed_value != 1 + else: + config[key] = parsed_value + elif field_def.field_type == ConfigFieldType.FLOAT: + config[key] = float(value) + elif field_def.field_type == ConfigFieldType.STRING: + config[key] = value + except (ValueError, TypeError): + # If conversion fails, keep default value + pass return config -- cgit v1.2.3