diff options
| -rw-r--r-- | DRY_IMPLEMENTATION.md | 157 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/config_schema.py | 228 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/configuration.py | 64 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/constants.py | 26 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/installation.py | 21 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/plugin.py | 17 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/types.py | 13 | ||||
| -rw-r--r-- | src/api/lsfgApi.ts | 28 | ||||
| -rw-r--r-- | src/components/ConfigurationSection.tsx | 64 | ||||
| -rw-r--r-- | src/components/Content.tsx | 55 | ||||
| -rw-r--r-- | src/components/UsageInstructions.tsx | 30 | ||||
| -rw-r--r-- | src/config/configSchema.ts | 186 | ||||
| -rw-r--r-- | src/hooks/useLsfgHooks.ts | 91 |
13 files changed, 693 insertions, 287 deletions
diff --git a/DRY_IMPLEMENTATION.md b/DRY_IMPLEMENTATION.md new file mode 100644 index 0000000..b6f5487 --- /dev/null +++ b/DRY_IMPLEMENTATION.md @@ -0,0 +1,157 @@ +# DRY Configuration System Implementation + +## Overview +This implementation addresses the "Duplicated Configuration Parameter Handling" opportunity by centralizing all configuration-related logic in a single schema that can be shared between Python backend and TypeScript frontend. + +## Key Changes Made + +### 1. **Created Centralized Configuration Schema** +- **Python**: `py_modules/lsfg_vk/config_schema.py` +- **TypeScript**: `src/config/configSchema.ts` + +Both files define the same configuration structure with: +- Field definitions (name, type, default, description) +- Script generation templates +- Validation logic +- Type-safe interfaces + +### 2. **Simplified Backend Configuration** +- **Before**: 8 separate parameters passed to `update_lsfg_config()` +- **After**: Variable arguments `*args` using centralized schema + +```python +# Before +async def update_lsfg_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float, + hdr: bool, perf_mode: bool, immediate_mode: bool, disable_vkbasalt: bool, frame_cap: int) + +# After +async def update_lsfg_config(self, *args) +``` + +### 3. **Streamlined Frontend Configuration** +- **Before**: 8 separate handler functions in `Content.tsx` +- **After**: Single generic handler using field names + +```tsx +// Before +const handleEnableLsfgChange = async (value: boolean) => { + setters.setEnableLsfg(value); + await updateConfig(value, config.multiplier, config.flowScale, ...); +}; +// ... 7 more similar handlers + +// After +const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number) => { + await updateField(fieldName, value); +}; +``` + +### 4. **Updated Configuration Service** +- Removed hardcoded defaults from `constants.py` +- Removed duplicate script template logic +- Uses centralized `ConfigurationManager` for all operations + +### 5. **Modernized Hook System** +- **Before**: Separate state variables for each config field +- **After**: Single `ConfigurationData` object with field-based updates + +## Benefits Achieved + +### 1. **Reduced Code Duplication** +- Configuration parameters defined once in schema +- Script generation logic centralized +- No more repetitive handler functions + +### 2. **Easier to Add New Configuration Options** +To add a new configuration option, you now only need to: +1. Add the field definition to both config schemas +2. The rest of the system automatically handles it + +### 3. **Type Safety** +- TypeScript and Python types are consistent +- Schema validation ensures data integrity +- Centralized type definitions + +### 4. **Maintainability** +- Single source of truth for configuration +- Consistent field naming and types +- Easier to refactor and extend + +## Example: Adding a New Configuration Option + +To add a new boolean option called "experimental_mode": + +1. **Add to Python schema** (`config_schema.py`): +```python +"experimental_mode": ConfigField( + name="experimental_mode", + field_type=ConfigFieldType.BOOLEAN, + default=False, + description="Enable experimental features", + script_template="export LSFG_EXPERIMENTAL={value}", + script_comment="# export LSFG_EXPERIMENTAL=1" +) +``` + +2. **Add to TypeScript schema** (`configSchema.ts`): +```typescript +experimental_mode: { + name: "experimental_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Enable experimental features", + scriptTemplate: "export LSFG_EXPERIMENTAL={value}", + scriptComment: "# export LSFG_EXPERIMENTAL=1" +} +``` + +3. **Update type definitions** in both files: +```python +# Python +class ConfigurationData(TypedDict): + # ... existing fields ... + experimental_mode: bool +``` + +```typescript +// TypeScript +export interface ConfigurationData { + // ... existing fields ... + experimental_mode: boolean; +} +``` + +4. **Add UI component** to `ConfigurationSection.tsx`: +```tsx +<PanelSectionRow> + <ToggleField + label="Experimental Mode" + description="Enable experimental features" + checked={config.experimental_mode} + onChange={(value) => onConfigChange('experimental_mode', value)} + /> +</PanelSectionRow> +``` + +That's it! The rest of the system automatically handles the new field. + +## Files Modified + +### Python Backend +- `py_modules/lsfg_vk/config_schema.py` (new) +- `py_modules/lsfg_vk/types.py` (updated) +- `py_modules/lsfg_vk/constants.py` (simplified) +- `py_modules/lsfg_vk/configuration.py` (refactored) +- `py_modules/lsfg_vk/installation.py` (updated) +- `py_modules/lsfg_vk/plugin.py` (updated) + +### TypeScript Frontend +- `src/config/configSchema.ts` (new) +- `src/api/lsfgApi.ts` (updated) +- `src/hooks/useLsfgHooks.ts` (simplified) +- `src/components/Content.tsx` (simplified) +- `src/components/ConfigurationSection.tsx` (updated) +- `src/components/UsageInstructions.tsx` (updated) + +## Testing +The configuration system has been tested and generates correct script content with proper defaults and field ordering. diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py new file mode 100644 index 0000000..0f1bdae --- /dev/null +++ b/py_modules/lsfg_vk/config_schema.py @@ -0,0 +1,228 @@ +""" +Centralized configuration schema for lsfg-vk. + +This module defines the complete configuration structure, including: +- Field definitions with types, defaults, and metadata +- Script generation logic +- Validation rules +- Type definitions +""" + +from typing import TypedDict, Dict, Any, Union, Callable, cast +from dataclasses import dataclass, field +from enum import Enum + + +class ConfigFieldType(Enum): + """Supported configuration field types""" + BOOLEAN = "boolean" + INTEGER = "integer" + FLOAT = "float" + + +@dataclass +class ConfigField: + """Configuration field definition""" + name: str + field_type: ConfigFieldType + default: Union[bool, int, float] + 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) + + +# Configuration schema definition +CONFIG_SCHEMA: Dict[str, ConfigField] = { + "enable_lsfg": ConfigField( + name="enable_lsfg", + field_type=ConfigFieldType.BOOLEAN, + default=True, + description="Enables the frame generation layer", + script_template="export ENABLE_LSFG={value}", + script_comment="# export ENABLE_LSFG=1" + ), + + "multiplier": ConfigField( + name="multiplier", + field_type=ConfigFieldType.INTEGER, + default=2, + description="Traditional FPS multiplier value", + script_template="export LSFG_MULTIPLIER={value}" + ), + + "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" + ), + + "perf_mode": ConfigField( + name="perf_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" + ), + + "immediate_mode": ConfigField( + name="immediate_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" + ) +} + + +class ConfigurationData(TypedDict): + """Type-safe configuration data structure""" + enable_lsfg: bool + multiplier: int + flow_scale: float + hdr: bool + perf_mode: bool + immediate_mode: bool + disable_vkbasalt: bool + frame_cap: int + + +class ConfigurationManager: + """Centralized configuration management""" + + @staticmethod + def get_defaults() -> ConfigurationData: + """Get default configuration values""" + return cast(ConfigurationData, { + field.name: field.default + for field in CONFIG_SCHEMA.values() + }) + + @staticmethod + def get_field_names() -> list[str]: + """Get ordered list of configuration field names""" + return list(CONFIG_SCHEMA.keys()) + + @staticmethod + def get_field_types() -> Dict[str, ConfigFieldType]: + """Get field type mapping""" + return { + field.name: field.field_type + for field in CONFIG_SCHEMA.values() + } + + @staticmethod + def validate_config(config: Dict[str, Any]) -> ConfigurationData: + """Validate and convert configuration data""" + validated = {} + + for field_name, field_def in CONFIG_SCHEMA.items(): + value = config.get(field_name, field_def.default) + + # Type validation and conversion + if field_def.field_type == ConfigFieldType.BOOLEAN: + validated[field_name] = bool(value) + elif field_def.field_type == ConfigFieldType.INTEGER: + validated[field_name] = int(value) + elif field_def.field_type == ConfigFieldType.FLOAT: + validated[field_name] = float(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", ""] + + # Generate script lines for each field + for field_name in CONFIG_SCHEMA.keys(): + field_def = CONFIG_SCHEMA[field_name] + value = config[field_name] + + 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) + 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 "$@"' + ]) + + return "\n".join(script_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 + + @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)}") + + return cast(ConfigurationData, { + field_name: args[i] + for i, field_name in enumerate(field_names) + }) diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index 5dc0629..8be7b47 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Dict, Any from .base_service import BaseService -from .constants import LSFG_SCRIPT_TEMPLATE -from .types import ConfigurationResponse, ConfigurationData +from .config_schema import ConfigurationManager, ConfigurationData, CONFIG_SCHEMA +from .types import ConfigurationResponse class ConfigurationService(BaseService): @@ -32,8 +32,6 @@ class ConfigurationService(BaseService): content = self.lsfg_script_path.read_text() config = self._parse_script_content(content) - self.log.info(f"Parsed lsfg config: {config}") - return { "success": True, "config": config, @@ -60,16 +58,8 @@ class ConfigurationService(BaseService): Returns: ConfigurationData with parsed values """ - config: ConfigurationData = { - "enable_lsfg": False, - "multiplier": 2, - "flow_scale": 1.0, - "hdr": False, - "perf_mode": False, - "immediate_mode": False, - "disable_vkbasalt": False, - "frame_cap": 0 - } + # Start with defaults + config = ConfigurationManager.get_defaults() lines = content.split('\n') for line in lines: @@ -140,15 +130,18 @@ class ConfigurationService(BaseService): ConfigurationResponse with success status """ try: - # Generate script content using template - script_content = self._generate_script_content( + # Create configuration from individual arguments + config = ConfigurationManager.create_config_from_args( enable_lsfg, multiplier, flow_scale, hdr, perf_mode, immediate_mode, disable_vkbasalt, frame_cap ) + # Generate script content using centralized manager + script_content = ConfigurationManager.generate_script_content(config) + # Write the updated script atomically self._atomic_write(self.lsfg_script_path, script_content, 0o755) - self.log.info(f"Updated lsfg script configuration: enable={enable_lsfg}, " + self.log.info(f"Updated lsfg script configuration: enable_lsfg={enable_lsfg}, " f"multiplier={multiplier}, flow_scale={flow_scale}, hdr={hdr}, " f"perf_mode={perf_mode}, immediate_mode={immediate_mode}, " f"disable_vkbasalt={disable_vkbasalt}, frame_cap={frame_cap}") @@ -169,31 +162,12 @@ class ConfigurationService(BaseService): "message": None, "error": str(e) } - - def _generate_script_content(self, enable_lsfg: bool, multiplier: int, flow_scale: float, - hdr: bool, perf_mode: bool, immediate_mode: bool, disable_vkbasalt: bool, frame_cap: int) -> str: - """Generate script content from configuration parameters - - Args: - enable_lsfg: Whether to enable LSFG - multiplier: LSFG multiplier value - flow_scale: LSFG flow scale value - hdr: Whether to enable HDR - perf_mode: Whether to enable performance mode - immediate_mode: Whether to enable immediate present mode - disable_vkbasalt: Whether to disable vkbasalt layer - frame_cap: Frame rate cap value (0-60, 0 = disabled) - - Returns: - Generated script content - """ - return LSFG_SCRIPT_TEMPLATE.format( - enable_lsfg="export ENABLE_LSFG=1" if enable_lsfg else "# export ENABLE_LSFG=1", - multiplier=multiplier, - flow_scale=flow_scale, - hdr="export LSFG_HDR=1" if hdr else "# export LSFG_HDR=1", - perf_mode="export LSFG_PERF_MODE=1" if perf_mode else "# export LSFG_PERF_MODE=1", - immediate_mode="export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync" if immediate_mode else "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync", - disable_vkbasalt="export DISABLE_VKBASALT=1" if disable_vkbasalt else "# export DISABLE_VKBASALT=1", - frame_cap=f"export DXVK_FRAME_RATE={frame_cap}" if frame_cap > 0 else "# export DXVK_FRAME_RATE=60" - ) + except ValueError as e: + error_msg = f"Invalid configuration arguments: {str(e)}" + self.log.error(error_msg) + return { + "success": False, + "config": None, + "message": None, + "error": str(e) + } diff --git a/py_modules/lsfg_vk/constants.py b/py_modules/lsfg_vk/constants.py index 878ac2f..5f1e5a2 100644 --- a/py_modules/lsfg_vk/constants.py +++ b/py_modules/lsfg_vk/constants.py @@ -26,33 +26,7 @@ BIN_DIR = "bin" STEAM_COMMON_PATH = Path("steamapps/common/Lossless Scaling") LOSSLESS_DLL_NAME = "Lossless.dll" -# Script template -LSFG_SCRIPT_TEMPLATE = """#!/bin/bash - -{enable_lsfg} -export LSFG_MULTIPLIER={multiplier} -export LSFG_FLOW_SCALE={flow_scale} -{hdr} -{perf_mode} -{immediate_mode} -{disable_vkbasalt} -{frame_cap} - -# Execute the passed command with the environment variables set -exec "$@" -""" - # Environment variable names ENV_LSFG_DLL_PATH = "LSFG_DLL_PATH" ENV_XDG_DATA_HOME = "XDG_DATA_HOME" ENV_HOME = "HOME" - -# Default configuration values -DEFAULT_MULTIPLIER = 2 -DEFAULT_FLOW_SCALE = 0.8 -DEFAULT_ENABLE_LSFG = True -DEFAULT_HDR = False -DEFAULT_PERF_MODE = True -DEFAULT_IMMEDIATE_MODE = False -DEFAULT_DISABLE_VKBASALT = True -DEFAULT_FRAME_CAP = 0 diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py index 7fb97db..767a97a 100644 --- a/py_modules/lsfg_vk/installation.py +++ b/py_modules/lsfg_vk/installation.py @@ -12,11 +12,9 @@ from typing import Dict, Any from .base_service import BaseService from .constants import ( LIB_FILENAME, JSON_FILENAME, ZIP_FILENAME, BIN_DIR, - SO_EXT, JSON_EXT, LSFG_SCRIPT_TEMPLATE, - DEFAULT_MULTIPLIER, DEFAULT_FLOW_SCALE, DEFAULT_ENABLE_LSFG, - DEFAULT_HDR, DEFAULT_PERF_MODE, DEFAULT_IMMEDIATE_MODE, DEFAULT_DISABLE_VKBASALT, - DEFAULT_FRAME_CAP + SO_EXT, JSON_EXT ) +from .config_schema import ConfigurationManager from .types import InstallationResponse, UninstallationResponse, InstallationCheckResponse @@ -106,16 +104,11 @@ class InstallationService(BaseService): def _create_lsfg_script(self) -> None: """Create the lsfg script in home directory with default configuration""" - script_content = LSFG_SCRIPT_TEMPLATE.format( - enable_lsfg="export ENABLE_LSFG=1" if DEFAULT_ENABLE_LSFG else "# export ENABLE_LSFG=1", - multiplier=DEFAULT_MULTIPLIER, - flow_scale=DEFAULT_FLOW_SCALE, - hdr="export LSFG_HDR=1" if DEFAULT_HDR else "# export LSFG_HDR=1", - perf_mode="export LSFG_PERF_MODE=1" if DEFAULT_PERF_MODE else "# export LSFG_PERF_MODE=1", - immediate_mode="export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync" if DEFAULT_IMMEDIATE_MODE else "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync", - disable_vkbasalt="export DISABLE_VKBASALT=1" if DEFAULT_DISABLE_VKBASALT else "# export DISABLE_VKBASALT=1", - frame_cap=f"export DXVK_FRAME_RATE={DEFAULT_FRAME_CAP}" if DEFAULT_FRAME_CAP > 0 else "# export DXVK_FRAME_RATE=60" - ) + # Get default configuration + defaults = ConfigurationManager.get_defaults() + + # Generate script content using centralized manager + script_content = ConfigurationManager.generate_script_content(defaults) # Use atomic write to prevent corruption self._atomic_write(self.lsfg_script_path, script_content, 0o755) diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index 7dcda4e..b357ee6 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -11,6 +11,7 @@ from typing import Dict, Any from .installation import InstallationService from .dll_detection import DllDetectionService from .configuration import ConfigurationService +from .config_schema import ConfigurationManager class Plugin: @@ -72,14 +73,26 @@ class Plugin: """ return self.configuration_service.get_config() + async def get_config_schema(self) -> Dict[str, Any]: + """Get configuration schema information for frontend + + Returns: + Dict with field names, types, and defaults + """ + return { + "field_names": ConfigurationManager.get_field_names(), + "field_types": {name: field_type.value for name, field_type in ConfigurationManager.get_field_types().items()}, + "defaults": ConfigurationManager.get_defaults() + } + async def update_lsfg_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float, hdr: bool, perf_mode: bool, immediate_mode: bool, disable_vkbasalt: bool, frame_cap: int) -> Dict[str, Any]: """Update lsfg script configuration Args: enable_lsfg: Whether to enable LSFG - multiplier: LSFG multiplier value (typically 2-4) - flow_scale: LSFG flow scale value (typically 0.5-2.0) + multiplier: LSFG multiplier value + flow_scale: LSFG flow scale value hdr: Whether to enable HDR perf_mode: Whether to enable performance mode immediate_mode: Whether to enable immediate present mode (disable vsync) diff --git a/py_modules/lsfg_vk/types.py b/py_modules/lsfg_vk/types.py index 9f44cf1..3d555e1 100644 --- a/py_modules/lsfg_vk/types.py +++ b/py_modules/lsfg_vk/types.py @@ -3,6 +3,7 @@ Type definitions for the lsfg-vk plugin responses. """ from typing import TypedDict, Optional, List +from .config_schema import ConfigurationData class BaseResponse(TypedDict): @@ -54,18 +55,6 @@ class DllDetectionResponse(TypedDict): error: Optional[str] -class ConfigurationData(TypedDict): - """Configuration data structure""" - enable_lsfg: bool - multiplier: int - flow_scale: float - hdr: bool - perf_mode: bool - immediate_mode: bool - disable_vkbasalt: bool - frame_cap: int - - class ConfigurationResponse(BaseResponse): """Response for configuration operations""" config: Optional[ConfigurationData] diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 31ecbb5..2e7964c 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -1,4 +1,5 @@ import { callable } from "@decky/api"; +import { ConfigurationData, ConfigurationManager } from "../config/configSchema"; // Type definitions for API responses export interface InstallationResult { @@ -27,16 +28,8 @@ export interface DllDetectionResult { error?: string; } -export interface LsfgConfig { - enable_lsfg: boolean; - multiplier: number; - flow_scale: number; - hdr: boolean; - perf_mode: boolean; - immediate_mode: boolean; - disable_vkbasalt: boolean; - frame_cap: number; -} +// Use centralized configuration data type +export type LsfgConfig = ConfigurationData; export interface ConfigResult { success: boolean; @@ -50,13 +43,28 @@ export interface ConfigUpdateResult { error?: string; } +export interface ConfigSchemaResult { + field_names: string[]; + field_types: Record<string, string>; + defaults: ConfigurationData; +} + // API functions export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk"); export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk"); export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg_vk_installed"); export const checkLosslessScalingDll = callable<[], DllDetectionResult>("check_lossless_scaling_dll"); export const getLsfgConfig = callable<[], ConfigResult>("get_lsfg_config"); +export const getConfigSchema = callable<[], ConfigSchemaResult>("get_config_schema"); + +// Updated config function using centralized configuration export const updateLsfgConfig = callable< [boolean, number, number, boolean, boolean, boolean, boolean, number], ConfigUpdateResult >("update_lsfg_config"); + +// Helper function to create config update from configuration object +export const updateLsfgConfigFromObject = async (config: ConfigurationData): Promise<ConfigUpdateResult> => { + const args = ConfigurationManager.createArgsFromConfig(config); + return updateLsfgConfig(...args as [boolean, number, number, boolean, boolean, boolean, boolean, number]); +}; diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 11bb6b9..2545217 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -1,38 +1,14 @@ import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui"; - -interface LsfgConfig { - enableLsfg: boolean; - multiplier: number; - flowScale: number; - hdr: boolean; - perfMode: boolean; - immediateMode: boolean; - disableVkbasalt: boolean; - frameCap: number; -} +import { ConfigurationData } from "../config/configSchema"; interface ConfigurationSectionProps { - config: LsfgConfig; - onEnableLsfgChange: (value: boolean) => Promise<void>; - onMultiplierChange: (value: number) => Promise<void>; - onFlowScaleChange: (value: number) => Promise<void>; - onHdrChange: (value: boolean) => Promise<void>; - onPerfModeChange: (value: boolean) => Promise<void>; - onImmediateModeChange: (value: boolean) => Promise<void>; - onDisableVkbasaltChange: (value: boolean) => Promise<void>; - onFrameCapChange: (value: number) => Promise<void>; + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number) => Promise<void>; } export function ConfigurationSection({ config, - onEnableLsfgChange, - onMultiplierChange, - onFlowScaleChange, - onHdrChange, - onPerfModeChange, - onImmediateModeChange, - onDisableVkbasaltChange, - onFrameCapChange + onConfigChange }: ConfigurationSectionProps) { return ( <> @@ -55,8 +31,8 @@ export function ConfigurationSection({ <ToggleField label="Enable LSFG" description="Enables the frame generation layer" - checked={config.enableLsfg} - onChange={onEnableLsfgChange} + checked={config.enable_lsfg} + onChange={(value) => onConfigChange('enable_lsfg', value)} /> </PanelSectionRow> @@ -74,19 +50,19 @@ export function ConfigurationSection({ { notchIndex: 1, label: "3X" }, { notchIndex: 2, label: "4X" } ]} - onChange={onMultiplierChange} + onChange={(value) => onConfigChange('multiplier', value)} /> </PanelSectionRow> <PanelSectionRow> <SliderField - label={`Flow Scale ${Math.round(config.flowScale * 100)}%`} + label={`Flow Scale ${Math.round(config.flow_scale * 100)}%`} description="Lowers the internal motion estimation resolution" - value={config.flowScale} + value={config.flow_scale} min={0.25} max={1.0} step={0.01} - onChange={onFlowScaleChange} + onChange={(value) => onConfigChange('flow_scale', value)} /> </PanelSectionRow> @@ -95,7 +71,7 @@ export function ConfigurationSection({ label="HDR Mode" description="Enable HDR mode (only if Game supports HDR)" checked={config.hdr} - onChange={onHdrChange} + onChange={(value) => onConfigChange('hdr', value)} /> </PanelSectionRow> @@ -103,8 +79,8 @@ export function ConfigurationSection({ <ToggleField label="Performance Mode" description="Use lighter model for FG" - checked={config.perfMode} - onChange={onPerfModeChange} + checked={config.perf_mode} + onChange={(value) => onConfigChange('perf_mode', value)} /> </PanelSectionRow> @@ -112,20 +88,20 @@ export function ConfigurationSection({ <ToggleField label="Immediate Mode" description="Reduce input lag (Experimental, will cause issues in many games)" - checked={config.immediateMode} - onChange={onImmediateModeChange} + checked={config.immediate_mode} + onChange={(value) => onConfigChange('immediate_mode', value)} /> </PanelSectionRow> <PanelSectionRow> <SliderField - label={`Game Frame Cap ${config.frameCap === 0 ? "(Disabled)" : `(${config.frameCap} FPS)`}`} + label={`Game Frame Cap ${config.frame_cap === 0 ? "(Disabled)" : `(${config.frame_cap} FPS)`}`} description="Limit base game FPS (0 = disabled)" - value={config.frameCap} + value={config.frame_cap} min={0} max={60} step={1} - onChange={onFrameCapChange} + onChange={(value) => onConfigChange('frame_cap', value)} /> </PanelSectionRow> @@ -133,8 +109,8 @@ export function ConfigurationSection({ <ToggleField label="Disable vkbasalt" description="Some plugins add vkbasalt layer, which can break lsfg. Toggling on fixes this" - checked={config.disableVkbasalt} - onChange={onDisableVkbasaltChange} + checked={config.disable_vkbasalt} + onChange={(value) => onConfigChange('disable_vkbasalt', value)} /> </PanelSectionRow> */} </> diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 895b2fc..ba651d4 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -8,6 +8,7 @@ import { ConfigurationSection } from "./ConfigurationSection"; import { UsageInstructions } from "./UsageInstructions"; import { WikiButton } from "./WikiButton"; import { ClipboardButton } from "./ClipboardButton"; +import { ConfigurationData } from "../config/configSchema"; export function Content() { const { @@ -21,9 +22,8 @@ export function Content() { const { config, - setters, loadLsfgConfig, - updateConfig + updateField } = useLsfgConfig(); const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); @@ -35,45 +35,9 @@ export function Content() { } }, [isInstalled, loadLsfgConfig]); - // Configuration change handlers - const handleEnableLsfgChange = async (value: boolean) => { - setters.setEnableLsfg(value); - await updateConfig(value, config.multiplier, config.flowScale, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleMultiplierChange = async (value: number) => { - setters.setMultiplier(value); - await updateConfig(config.enableLsfg, value, config.flowScale, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleFlowScaleChange = async (value: number) => { - setters.setFlowScale(value); - await updateConfig(config.enableLsfg, config.multiplier, value, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleHdrChange = async (value: boolean) => { - setters.setHdr(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, value, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handlePerfModeChange = async (value: boolean) => { - setters.setPerfMode(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, value, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleImmediateModeChange = async (value: boolean) => { - setters.setImmediateMode(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, value, config.disableVkbasalt, config.frameCap); - }; - - const handleDisableVkbasaltChange = async (value: boolean) => { - setters.setDisableVkbasalt(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, config.immediateMode, value, config.frameCap); - }; - - const handleFrameCapChange = async (value: number) => { - setters.setFrameCap(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, value); + // Generic configuration change handler + const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number) => { + await updateField(fieldName, value); }; const onInstall = () => { @@ -105,14 +69,7 @@ export function Content() { {isInstalled && ( <ConfigurationSection config={config} - onEnableLsfgChange={handleEnableLsfgChange} - onMultiplierChange={handleMultiplierChange} - onFlowScaleChange={handleFlowScaleChange} - onHdrChange={handleHdrChange} - onPerfModeChange={handlePerfModeChange} - onImmediateModeChange={handleImmediateModeChange} - onDisableVkbasaltChange={handleDisableVkbasaltChange} - onFrameCapChange={handleFrameCapChange} + onConfigChange={handleConfigChange} /> )} diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx index 3589c04..727a0ab 100644 --- a/src/components/UsageInstructions.tsx +++ b/src/components/UsageInstructions.tsx @@ -1,18 +1,8 @@ import { PanelSectionRow } from "@decky/ui"; - -interface ConfigType { - enableLsfg: boolean; - multiplier: number; - flowScale: number; - hdr: boolean; - perfMode: boolean; - immediateMode: boolean; - disableVkbasalt: boolean; - frameCap: number; -} +import { ConfigurationData } from "../config/configSchema"; interface UsageInstructionsProps { - config: ConfigType; + config: ConfigurationData; } export function UsageInstructions({ config }: UsageInstructionsProps) { @@ -20,34 +10,34 @@ export function UsageInstructions({ config }: UsageInstructionsProps) { const buildManualEnvVars = (): string => { const envVars: string[] = []; - if (config.enableLsfg) { + if (config.enable_lsfg) { envVars.push("ENABLE_LSFG=1"); } // Always include multiplier and flow_scale if LSFG is enabled, as they have defaults - if (config.enableLsfg) { + if (config.enable_lsfg) { envVars.push(`LSFG_MULTIPLIER=${config.multiplier}`); - envVars.push(`LSFG_FLOW_SCALE=${config.flowScale}`); + envVars.push(`LSFG_FLOW_SCALE=${config.flow_scale}`); } if (config.hdr) { envVars.push("LSFG_HDR=1"); } - if (config.perfMode) { + if (config.perf_mode) { envVars.push("LSFG_PERF_MODE=1"); } - if (config.immediateMode) { + if (config.immediate_mode) { envVars.push("MESA_VK_WSI_PRESENT_MODE=immediate"); } - if (config.disableVkbasalt) { + if (config.disable_vkbasalt) { envVars.push("DISABLE_VKBASALT=1"); } - if (config.frameCap > 0) { - envVars.push(`DXVK_FRAME_RATE=${config.frameCap}`); + if (config.frame_cap > 0) { + envVars.push(`DXVK_FRAME_RATE=${config.frame_cap}`); } return envVars.length > 0 ? `${envVars.join(" ")} %command%` : "%command%"; diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts new file mode 100644 index 0000000..6956030 --- /dev/null +++ b/src/config/configSchema.ts @@ -0,0 +1,186 @@ +/** + * Centralized configuration schema for lsfg-vk frontend. + * + * This mirrors the Python configuration schema to ensure consistency + * between frontend and backend configuration handling. + */ + +// Configuration field type enum +export enum ConfigFieldType { + BOOLEAN = "boolean", + INTEGER = "integer", + FLOAT = "float" +} + +// Configuration field definition +export interface ConfigField { + name: string; + fieldType: ConfigFieldType; + default: boolean | number; + description: string; + scriptTemplate: string; + scriptComment?: string; +} + +// Configuration schema - must match Python CONFIG_SCHEMA +export const CONFIG_SCHEMA: Record<string, ConfigField> = { + enable_lsfg: { + name: "enable_lsfg", + fieldType: ConfigFieldType.BOOLEAN, + default: true, + description: "Enables the frame generation layer", + scriptTemplate: "export ENABLE_LSFG={value}", + scriptComment: "# export ENABLE_LSFG=1" + }, + + multiplier: { + name: "multiplier", + fieldType: ConfigFieldType.INTEGER, + default: 2, + description: "Traditional FPS multiplier value", + scriptTemplate: "export LSFG_MULTIPLIER={value}" + }, + + flow_scale: { + name: "flow_scale", + fieldType: ConfigFieldType.FLOAT, + default: 0.8, + description: "Lowers the internal motion estimation resolution", + scriptTemplate: "export LSFG_FLOW_SCALE={value}" + }, + + hdr: { + name: "hdr", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Enable HDR mode (only if Game supports HDR)", + scriptTemplate: "export LSFG_HDR={value}", + scriptComment: "# export LSFG_HDR=1" + }, + + perf_mode: { + name: "perf_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: true, + description: "Use lighter model for FG", + scriptTemplate: "export LSFG_PERF_MODE={value}", + scriptComment: "# export LSFG_PERF_MODE=1" + }, + + immediate_mode: { + name: "immediate_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Reduce input lag (Experimental, will cause issues in many games)", + scriptTemplate: "export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync", + scriptComment: "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync" + }, + + disable_vkbasalt: { + name: "disable_vkbasalt", + fieldType: ConfigFieldType.BOOLEAN, + default: true, + description: "Some plugins add vkbasalt layer, which can break lsfg. Toggling on fixes this", + scriptTemplate: "export DISABLE_VKBASALT={value}", + scriptComment: "# export DISABLE_VKBASALT=1" + }, + + frame_cap: { + name: "frame_cap", + fieldType: ConfigFieldType.INTEGER, + default: 0, + description: "Limit base game FPS (0 = disabled)", + scriptTemplate: "export DXVK_FRAME_RATE={value}", + scriptComment: "# export DXVK_FRAME_RATE=60" + } +}; + +// Type-safe configuration data structure +export interface ConfigurationData { + enable_lsfg: boolean; + multiplier: number; + flow_scale: number; + hdr: boolean; + perf_mode: boolean; + immediate_mode: boolean; + disable_vkbasalt: boolean; + frame_cap: number; +} + +// Centralized configuration manager +export class ConfigurationManager { + /** + * Get default configuration values + */ + static getDefaults(): ConfigurationData { + const defaults = {} as ConfigurationData; + Object.values(CONFIG_SCHEMA).forEach(field => { + (defaults as any)[field.name] = field.default; + }); + return defaults; + } + + /** + * Get ordered list of configuration field names + */ + static getFieldNames(): string[] { + return Object.keys(CONFIG_SCHEMA); + } + + /** + * Get field type mapping + */ + static getFieldTypes(): Record<string, ConfigFieldType> { + return Object.values(CONFIG_SCHEMA).reduce((acc, field) => { + acc[field.name] = field.fieldType; + return acc; + }, {} as Record<string, ConfigFieldType>); + } + + /** + * Create ordered arguments array from configuration object + */ + static createArgsFromConfig(config: ConfigurationData): (boolean | number)[] { + return this.getFieldNames().map(fieldName => + config[fieldName as keyof ConfigurationData] + ); + } + + /** + * Validate configuration object against schema + */ + static validateConfig(config: Partial<ConfigurationData>): ConfigurationData { + const defaults = this.getDefaults(); + const validated = { ...defaults }; + + Object.entries(CONFIG_SCHEMA).forEach(([fieldName, fieldDef]) => { + const value = config[fieldName as keyof ConfigurationData]; + if (value !== undefined) { + // Type validation + if (fieldDef.fieldType === ConfigFieldType.BOOLEAN) { + (validated as any)[fieldName] = Boolean(value); + } else if (fieldDef.fieldType === ConfigFieldType.INTEGER) { + (validated as any)[fieldName] = parseInt(String(value), 10); + } else if (fieldDef.fieldType === ConfigFieldType.FLOAT) { + (validated as any)[fieldName] = parseFloat(String(value)); + } + } + }); + + return validated; + } + + /** + * Get configuration field definition + */ + static getFieldDef(fieldName: string): ConfigField | undefined { + return CONFIG_SCHEMA[fieldName]; + } + + /** + * Get all field definitions + */ + static getAllFieldDefs(): ConfigField[] { + return Object.values(CONFIG_SCHEMA); + } +} diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts index f765ce6..8ff9061 100644 --- a/src/hooks/useLsfgHooks.ts +++ b/src/hooks/useLsfgHooks.ts @@ -1,12 +1,13 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { toaster } from "@decky/api"; import { checkLsfgVkInstalled, checkLosslessScalingDll, getLsfgConfig, - updateLsfgConfig, + updateLsfgConfigFromObject, type ConfigUpdateResult } from "../api/lsfgApi"; +import { ConfigurationData, ConfigurationManager } from "../config/configSchema"; export function useInstallationStatus() { const [isInstalled, setIsInstalled] = useState<boolean>(false); @@ -70,58 +71,30 @@ export function useDllDetection() { } export function useLsfgConfig() { - const [enableLsfg, setEnableLsfg] = useState<boolean>(true); - const [multiplier, setMultiplier] = useState<number>(2); - const [flowScale, setFlowScale] = useState<number>(0.8); - const [hdr, setHdr] = useState<boolean>(false); - const [perfMode, setPerfMode] = useState<boolean>(true); - const [immediateMode, setImmediateMode] = useState<boolean>(false); - const [disableVkbasalt, setDisableVkbasalt] = useState<boolean>(true); - const [frameCap, setFrameCap] = useState<number>(0); + // Use centralized configuration for initial state + const [config, setConfig] = useState<ConfigurationData>(() => ConfigurationManager.getDefaults()); - const loadLsfgConfig = async () => { + const loadLsfgConfig = useCallback(async () => { try { const result = await getLsfgConfig(); if (result.success && result.config) { - setEnableLsfg(result.config.enable_lsfg); - setMultiplier(result.config.multiplier); - setFlowScale(result.config.flow_scale); - setHdr(result.config.hdr); - setPerfMode(result.config.perf_mode); - setImmediateMode(result.config.immediate_mode); - setDisableVkbasalt(result.config.disable_vkbasalt); - setFrameCap(result.config.frame_cap); - console.log("Loaded lsfg config:", result.config); + setConfig(result.config); } else { console.log("lsfg config not available, using defaults:", result.error); + setConfig(ConfigurationManager.getDefaults()); } } catch (error) { console.error("Error loading lsfg config:", error); + setConfig(ConfigurationManager.getDefaults()); } - }; + }, []); - const updateConfig = async ( - newEnableLsfg: boolean, - newMultiplier: number, - newFlowScale: number, - newHdr: boolean, - newPerfMode: boolean, - newImmediateMode: boolean, - newDisableVkbasalt: boolean, - newFrameCap: number - ): Promise<ConfigUpdateResult> => { + const updateConfig = useCallback(async (newConfig: ConfigurationData): Promise<ConfigUpdateResult> => { try { - const result = await updateLsfgConfig( - newEnableLsfg, - newMultiplier, - newFlowScale, - newHdr, - newPerfMode, - newImmediateMode, - newDisableVkbasalt, - newFrameCap - ); - if (!result.success) { + const result = await updateLsfgConfigFromObject(newConfig); + if (result.success) { + setConfig(newConfig); + } else { toaster.toast({ title: "Update Failed", body: result.error || "Failed to update configuration" @@ -135,34 +108,22 @@ export function useLsfgConfig() { }); return { success: false, error: String(error) }; } - }; + }, []); + + const updateField = useCallback(async (fieldName: keyof ConfigurationData, value: boolean | number): Promise<ConfigUpdateResult> => { + const newConfig = { ...config, [fieldName]: value }; + return updateConfig(newConfig); + }, [config, updateConfig]); useEffect(() => { loadLsfgConfig(); - }, []); + }, []); // Empty dependency array to prevent infinite loop return { - config: { - enableLsfg, - multiplier, - flowScale, - hdr, - perfMode, - immediateMode, - disableVkbasalt, - frameCap - }, - setters: { - setEnableLsfg, - setMultiplier, - setFlowScale, - setHdr, - setPerfMode, - setImmediateMode, - setDisableVkbasalt, - setFrameCap - }, + config, + setConfig, loadLsfgConfig, - updateConfig + updateConfig, + updateField }; } |
