summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>2025-07-18 16:01:47 -0400
committerGitHub <noreply@github.com>2025-07-18 16:01:47 -0400
commit1d296606babfb0ceb02068e852582ade7adc4d98 (patch)
treeb29330427adf1b27487a9d2a4b59942a71f585e3
parentfa328306e3393a787d7c4f5855ecc23177eaa480 (diff)
parent3b60286fc360704eb6faeb72edbec602c624bd51 (diff)
downloaddecky-lsfg-vk-1d296606babfb0ceb02068e852582ade7adc4d98.tar.gz
decky-lsfg-vk-1d296606babfb0ceb02068e852582ade7adc4d98.zip
Merge pull request #34 from xXJSONDeruloXx/conf-per-game
implement conf file and real time changes
-rw-r--r--LICENSE89
-rw-r--r--README.md79
-rw-r--r--justfile4
-rw-r--r--package.json16
-rw-r--r--py_modules/lsfg_vk/base_service.py45
-rw-r--r--py_modules/lsfg_vk/config_schema.py312
-rw-r--r--py_modules/lsfg_vk/configuration.py197
-rw-r--r--py_modules/lsfg_vk/constants.py2
-rw-r--r--py_modules/lsfg_vk/installation.py84
-rw-r--r--py_modules/lsfg_vk/plugin.py96
-rw-r--r--src/api/lsfgApi.ts11
-rw-r--r--src/components/ConfigurationSection.tsx103
-rw-r--r--src/components/Content.tsx2
-rw-r--r--src/components/LaunchOptionInfo.tsx25
-rw-r--r--src/components/PluginUpdateChecker.tsx121
-rw-r--r--src/components/UsageInstructions.tsx151
-rw-r--r--src/components/index.ts3
-rw-r--r--src/config/configSchema.ts89
-rw-r--r--src/hooks/useLsfgHooks.ts2
-rwxr-xr-xsrc/index.tsx2
20 files changed, 896 insertions, 537 deletions
diff --git a/LICENSE b/LICENSE
index b7e00f0..0bf1806 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,31 +1,94 @@
BSD 3-Clause License
Copyright (c) 2025, Kurt Himebauch
-Original Copyright (c) 2022-2024, Steam Deck Homebrew
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+
+This project includes third-party components under the following licenses:
+
+---
+
+MIT License
+
+Includes components from lsfg-vk licensed under the MIT License:
+
+Copyright (c) 2025 lsfg-vk
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
+
+BSD 3-Clause License
+
+Includes components originally developed by Steam Deck Homebrew licensed under the BSD 3-Clause License:
+Copyright (c) 2022-2024, Steam Deck Homebrew
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-1. Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
+1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
- contributors may be used to endorse or promote products derived from
- this software without specific prior written permission.
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 1abcd7f..dbab5bd 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,13 @@
# Lossless Scaling for Steam Deck
-**Unofficial Community Plugin**: This Decky Loader plugin is an independent project and is **not officially supported** by the creators of Lossless Scaling or lsfg-vk. Support is provided separately via the [Decky Lossless Discord Channel](https://discord.gg/SFhFy2Sd).
+[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B71HZTAX)
+
+<p align="center">
+ <img src="assets/decky-lossless-logo.png" alt="Lossless Scaling for Steam Deck Logo" width="200"/>
+</p>
+
+> **Note:**
+> This is an **unofficial community plugin**. It is independently developed and **not officially supported** by the creators of Lossless Scaling or lsfg-vk. For support, please use the [Decky Lossless Discord Channel](https://discord.gg/SFhFy2Sd).
## What is this?
@@ -8,7 +15,7 @@ A Decky plugin that streamlines the installation of **lsfg-vk** ([Lossless Scali
## Installation
-**Note:** This plugin is not yet available on the Decky Plugin Store, it is in an experimental state, and likely to change drastically pending a full store release.
+**Note:** This plugin is currently in active development. While functional, some features may change before the official Decky Plugin Store release.
1. **Download the plugin** from the [latest release](https://github.com/xXJSONDeruloXx/decky-lossless-scaling-vk/releases)
- Download the "Lossless Scaling.zip" file to your Steam Deck
@@ -23,28 +30,68 @@ A Decky plugin that streamlines the installation of **lsfg-vk** ([Lossless Scali
1. **Purchase and install** [Lossless Scaling](https://store.steampowered.com/app/993090/Lossless_Scaling/) from Steam
2. **Open the plugin** from the Decky menu
3. **Click "Install lsfg-vk"** to automatically set up the lsfg-vk vulkan layer
-4. **Configure settings** using the plugin's UI.
-5. **Apply launch commands** to the game you want to use frame generation with:
- - **Option 1 (Recommended)**: `~/lsfg %command%` - Uses your plugin configuration
- - **Option 2**: Manual environment variables like `ENABLE_LSFG=1 LSFG_MULTIPLIER=2 %COMMAND%`
- - See the [LSFG-VK WIKI](https://github.com/PancakeTAS/lsfg-vk/wiki/Configuring-lsfg%E2%80%90vk) for more information on each available environment variable
+4. **Configure settings** using the plugin's UI - adjust FPS multiplier, flow scale, performance mode, HDR settings, and experimental features
+5. **Apply launch option** to games you want to use frame generation with:
+ - Add `~/lsfg %command%` to your game's launch options in Steam Properties
+ - Or use the "Launch Option Clipboard" button in the plugin to copy the command
+6. **Launch your game** - frame generation will activate automatically using your plugin configuration
+
+**Note**: Configuration changes are applied in real-time and will take effect immediately without restarting your game.
+
+## Configuration Options
+
+The plugin provides several configuration options to optimize frame generation for your games:
+
+### Core Settings
+- **FPS Multiplier**: Choose between 2x, 3x, or 4x frame generation
+- **Flow Scale**: Adjust motion estimation quality (lower = better performance, higher = better quality)
+- **Performance Mode**: Uses a lighter processing model - recommended for most games
+- **HDR Mode**: Enable for games that support HDR output
+
+### Experimental Features
+- **Present Mode Override**: Force specific Vulkan presentation modes for compatibility
+- **Base FPS Limit**: Set a base framerate cap before the multiplier is applied (useful for DirectX games)
+
+All settings (except base FPS limit) are saved automatically and can be changed while games are running.
## Feedback and Support
For per-game feedback and community support, please join the [Decky Lossless Discord Channel](https://discord.gg/SFhFy2Sd)
+## Troubleshooting
+
+**Frame generation not working?**
+- Ensure you've added `LSFG_PROCESS=decky-lsfg-vk %command%` to your game's launch options
+- Check that the Lossless Scaling DLL was detected correctly in the plugin
+- Try enabling Performance Mode if you're experiencing crashes
+- Make sure your game is running in fullscreen mode for best results
+
+**Performance issues?**
+- Lower the Flow Scale setting for better performance
+- Enable Performance Mode (recommended for most games)
+- Try reducing the FPS multiplier from 4x to 2x or 3x
+- Consider using the experimental FPS limit feature for DirectX games
+
## What it does
The plugin:
-- Extracts the lsfg-vk library to `~/.local/lib/`
-- Installs the Vulkan layer configuration to `~/.local/share/vulkan/implicit_layer.d/`
-- Creates an executable `lsfg` script in the home directory with configurable settings
-- Provides a user-friendly interface to configure LSFG settings (enable/disable, multiplier, flow scale, HDR, immediate mode)
-- Automatically updates the `lsfg` script when settings are changed
-- Provides easy uninstallation by removing these files when no longer needed
+- Automatically downloads and installs the latest lsfg-vk Vulkan layer to `~/.local/lib/`
+- Configures the Vulkan layer in `~/.local/share/vulkan/implicit_layer.d/`
+- Creates a TOML configuration file in `~/.config/lsfg-vk/conf.toml` with your settings
+- Automatically detects your Lossless Scaling DLL installation
+- Provides an easy-to-use interface to configure frame generation settings:
+ - **FPS Multiplier**: Choose 2x, 3x, or 4x frame generation
+ - **Flow Scale**: Adjust motion estimation quality vs performance
+ - **Performance Mode**: Use lighter processing for better performance
+ - **HDR Mode**: Enable for HDR-compatible games
+ - **Experimental Features**: Override present mode and set FPS limits
+- **Hot-reloading**: Configuration changes apply immediately without restarting games
+- Easy uninstallation that removes all installed files when no longer needed
## Credits
-[PancakeTAS](https://github.com/PancakeTAS/lsfg-vk) for creating the lsfg-vk compatibility layer.
-
-Special thanks to <a href="https://www.youtube.com/@DeckWizard" target="_blank">Deck Wizard</a> for the video tutorial.
+- **[PancakeTAS](https://github.com/PancakeTAS/lsfg-vk)** for creating the lsfg-vk Vulkan compatibility layer
+- **[Lossless Scaling](https://store.steampowered.com/app/993090/Lossless_Scaling/)** developers for the original frame generation technology
+- **[Deck Wizard](https://www.youtube.com/@DeckWizard)** for the helpful video tutorial
+- The **Decky Loader** team for the plugin framework
+- Community contributors and testers for feedback and bug reports
diff --git a/justfile b/justfile
index 7641cea..9923416 100644
--- a/justfile
+++ b/justfile
@@ -2,10 +2,10 @@ default:
echo "Available recipes: build, test, clean"
build:
- rm -rf node_modules && .vscode/build.sh
+ sudo rm -rf node_modules && .vscode/build.sh
test:
- scp "/Users/kurt/Developer/decky-lossless-scaling-vk/out/Lossless Scaling.zip" deck@192.168.0.6:~/Desktop
+ scp "out/Lossless Scaling.zip" deck@192.168.0.6:~/Desktop
clean:
rm -rf node_modules dist \ No newline at end of file
diff --git a/package.json b/package.json
index 9c8553b..7def6f5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lossless-scaling-vk",
- "version": "0.4.0",
+ "version": "0.5.5",
"description": "Use Lossless Scaling on the Steam Deck using the lsfg-vk vulkan layer",
"type": "module",
"scripts": {
@@ -15,9 +15,12 @@
"keywords": [
"decky",
"plugin",
- "plugin-template",
"steam-deck",
- "deck"
+ "deck",
+ "lossless-scaling",
+ "frame-generation",
+ "lsfg-vk",
+ "framegen"
],
"author": "Kurt Himebauch <dkh4830@gmail.com>",
"license": "BSD-3-Clause",
@@ -43,9 +46,10 @@
"remote_binary": [
{
"name": "lsfg-vk_archlinux.zip",
- "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/upstream-16274840875/lsfg-vk_archlinux.zip",
- "sha256hash": "7409f91a717d17d77c90eec161652dcc26a6fab333b253a9e095e451ad81bbab"
- }
+ "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/upstream-16378130046/lsfg-vk_archlinux.zip",
+ "sha256hash": "779f2081a7b3096a116f3395a0d12f436bccacdee0a05b9dcf0e11c61f51b2ad"
+ }
+
],
"pnpm": {
"peerDependencyRules": {
diff --git a/py_modules/lsfg_vk/base_service.py b/py_modules/lsfg_vk/base_service.py
index b547759..b595b07 100644
--- a/py_modules/lsfg_vk/base_service.py
+++ b/py_modules/lsfg_vk/base_service.py
@@ -4,11 +4,10 @@ Base service class with common functionality.
import os
import shutil
-import tempfile
from pathlib import Path
from typing import Any, Optional
-from .constants import LOCAL_LIB, LOCAL_SHARE_BASE, VULKAN_LAYER_DIR, SCRIPT_NAME
+from .constants import LOCAL_LIB, LOCAL_SHARE_BASE, VULKAN_LAYER_DIR, SCRIPT_NAME, CONFIG_DIR, CONFIG_FILENAME
class BaseService:
@@ -31,12 +30,16 @@ class BaseService:
self.local_lib_dir = self.user_home / LOCAL_LIB
self.local_share_dir = self.user_home / VULKAN_LAYER_DIR
self.lsfg_script_path = self.user_home / SCRIPT_NAME
+ self.lsfg_launch_script_path = self.user_home / SCRIPT_NAME # ~/lsfg launch script
+ self.config_dir = self.user_home / CONFIG_DIR
+ self.config_file_path = self.config_dir / CONFIG_FILENAME
def _ensure_directories(self) -> None:
"""Create necessary directories if they don't exist"""
self.local_lib_dir.mkdir(parents=True, exist_ok=True)
self.local_share_dir.mkdir(parents=True, exist_ok=True)
- self.log.info(f"Ensured directories exist: {self.local_lib_dir}, {self.local_share_dir}")
+ self.config_dir.mkdir(parents=True, exist_ok=True)
+ self.log.info(f"Ensured directories exist: {self.local_lib_dir}, {self.local_share_dir}, {self.config_dir}")
def _remove_if_exists(self, path: Path) -> bool:
"""Remove a file if it exists
@@ -62,8 +65,8 @@ class BaseService:
self.log.info(f"File not found: {path}")
return False
- def _atomic_write(self, path: Path, content: str, mode: int = 0o644) -> None:
- """Write content to a file atomically
+ def _write_file(self, path: Path, content: str, mode: int = 0o644) -> None:
+ """Write content to a file
Args:
path: Target file path
@@ -73,31 +76,17 @@ class BaseService:
Raises:
OSError: If write fails
"""
- # Create temporary file in the same directory to ensure atomic move
- temp_path = None
try:
- with tempfile.NamedTemporaryFile(
- mode='w',
- dir=path.parent,
- delete=False,
- prefix=f'.{path.name}.',
- suffix='.tmp'
- ) as temp_file:
- temp_file.write(content)
- temp_path = Path(temp_file.name)
+ # Write directly to the file
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(content)
+ f.flush() # Ensure data is written to disk
+ os.fsync(f.fileno()) # Force filesystem sync
- # Set permissions before moving
- temp_path.chmod(mode)
-
- # Atomic move
- temp_path.replace(path)
- self.log.info(f"Atomically wrote to {path}")
+ # Set permissions
+ path.chmod(mode)
+ self.log.info(f"Wrote to {path}")
except Exception:
- # Clean up temp file if something went wrong
- if temp_path and temp_path.exists():
- try:
- temp_path.unlink()
- except OSError:
- pass # Best effort cleanup
+ self.log.error(f"Failed to write to {path}")
raise
diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py
index 0f1bdae..ed82d97 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,106 +28,84 @@ 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="", # Will be populated dynamically based on detection
+ 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}"
+ description="change the flow scale"
),
- "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",
+ "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"
),
- "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"
+ description="enable hdr mode"
),
- "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"
+ "experimental_present_mode": ConfigField(
+ name="experimental_present_mode",
+ field_type=ConfigFieldType.STRING,
+ default="",
+ description="experimental: override vulkan present mode (empty/fifo/vsync/mailbox/immediate)"
),
- "frame_cap": ConfigField(
- name="frame_cap",
+ "experimental_fps_limit": ConfigField(
+ name="experimental_fps_limit",
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="experimental: base framerate cap for dxvk games, before frame multiplier (0 = disabled)"
)
}
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
+ experimental_present_mode: str
+ experimental_fps_limit: int
class ConfigurationManager:
@@ -139,6 +120,34 @@ class ConfigurationManager:
})
@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"""
return list(CONFIG_SCHEMA.keys())
@@ -166,63 +175,154 @@ 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 using the new game-specific format"""
+ lines = ["version = 1"]
+ lines.append("")
- # Generate script lines for each field
- for field_name in CONFIG_SCHEMA.keys():
- field_def = CONFIG_SCHEMA[field_name]
+ # 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]
- 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)
+ # 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:
- # 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_def.description}")
+
+ # Format value based on type
+ if isinstance(value, bool):
+ lines.append(f"{field_name} = {str(value).lower()}")
+ elif isinstance(value, str) and value: # Only add non-empty strings
+ lines.append(f'{field_name} = "{value}"')
+ 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
- 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 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()
+
+ # Skip comments and empty lines
+ if not line or line.startswith('#'):
+ continue
+
+ # Check for section headers
+ if line.startswith('[') and line.endswith(']'):
+ 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
+ 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]
+
+ # 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
+
+ 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,
+ experimental_present_mode: str = "",
+ experimental_fps_limit: int = 0) -> 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,
+ "experimental_present_mode": experimental_present_mode,
+ "experimental_fps_limit": experimental_fps_limit
})
diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py
index 8be7b47..255092a 100644
--- a/py_modules/lsfg_vk/configuration.py
+++ b/py_modules/lsfg_vk/configuration.py
@@ -1,8 +1,7 @@
"""
-Configuration service for lsfg script management.
+Configuration service for TOML-based lsfg configuration management.
"""
-import re
from pathlib import Path
from typing import Dict, Any
@@ -12,25 +11,29 @@ from .types import ConfigurationResponse
class ConfigurationService(BaseService):
- """Service for managing lsfg script configuration"""
+ """Service for managing TOML-based lsfg configuration"""
def get_config(self) -> ConfigurationResponse:
- """Read current lsfg script configuration
+ """Read current TOML configuration
Returns:
ConfigurationResponse with current configuration or error
"""
try:
- if not self.lsfg_script_path.exists():
+ if not self.config_file_path.exists():
+ # Return default configuration with DLL detection if file doesn't exist
+ from .dll_detection import DllDetectionService
+ dll_service = DllDetectionService(self.log)
+ config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
return {
- "success": False,
- "config": None,
- "message": None,
- "error": "lsfg script not found"
+ "success": True,
+ "config": config,
+ "message": "Using default configuration (config file not found)",
+ "error": None
}
- content = self.lsfg_script_path.read_text()
- config = self._parse_script_content(content)
+ content = self.config_file_path.read_text(encoding='utf-8')
+ config = ConfigurationManager.parse_toml_content(content)
return {
"success": True,
@@ -48,83 +51,35 @@ class ConfigurationService(BaseService):
"message": None,
"error": str(e)
}
+ except Exception as e:
+ error_msg = f"Error parsing config file: {str(e)}"
+ self.log.error(error_msg)
+ # Return defaults with DLL detection if parsing fails
+ from .dll_detection import DllDetectionService
+ dll_service = DllDetectionService(self.log)
+ config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
+ return {
+ "success": True,
+ "config": config,
+ "message": f"Using default configuration due to parse error: {str(e)}",
+ "error": None
+ }
- def _parse_script_content(self, content: str) -> ConfigurationData:
- """Parse script content to extract configuration values
-
- Args:
- content: Script file content
-
- Returns:
- ConfigurationData with parsed values
- """
- # Start with defaults
- config = ConfigurationManager.get_defaults()
-
- lines = content.split('\n')
- for line in lines:
- line = line.strip()
-
- # Parse ENABLE_LSFG
- if match := re.match(r'^(#\s*)?export\s+ENABLE_LSFG=(\d+)', line):
- config["enable_lsfg"] = not bool(match.group(1)) and match.group(2) == '1'
-
- # Parse LSFG_MULTIPLIER
- elif match := re.match(r'^export\s+LSFG_MULTIPLIER=(\d+)', line):
- try:
- config["multiplier"] = int(match.group(1))
- except ValueError:
- pass
-
- # Parse LSFG_FLOW_SCALE
- elif match := re.match(r'^export\s+LSFG_FLOW_SCALE=([0-9]*\.?[0-9]+)', line):
- try:
- config["flow_scale"] = float(match.group(1))
- except ValueError:
- pass
-
- # Parse LSFG_HDR
- elif match := re.match(r'^(#\s*)?export\s+LSFG_HDR=(\d+)', line):
- config["hdr"] = not bool(match.group(1)) and match.group(2) == '1'
-
- # Parse LSFG_PERF_MODE
- elif match := re.match(r'^(#\s*)?export\s+LSFG_PERF_MODE=(\d+)', line):
- config["perf_mode"] = not bool(match.group(1)) and match.group(2) == '1'
-
- # Parse MESA_VK_WSI_PRESENT_MODE
- elif match := re.match(r'^(#\s*)?export\s+MESA_VK_WSI_PRESENT_MODE=([^\s#]+)', line):
- config["immediate_mode"] = not bool(match.group(1)) and match.group(2) == 'immediate'
-
- # Parse DISABLE_VKBASALT
- elif match := re.match(r'^(#\s*)?export\s+DISABLE_VKBASALT=(\d+)', line):
- config["disable_vkbasalt"] = not bool(match.group(1)) and match.group(2) == '1'
-
- # Parse DXVK_FRAME_RATE
- elif match := re.match(r'^(#\s*)?export\s+DXVK_FRAME_RATE=(\d+)', line):
- if not bool(match.group(1)): # Not commented out
- try:
- config["frame_cap"] = int(match.group(2))
- except ValueError:
- pass
- else:
- # If it's commented out, frame cap is disabled (0)
- config["frame_cap"] = 0
-
- return config
-
- def update_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float,
- hdr: bool, perf_mode: bool, immediate_mode: bool, disable_vkbasalt: bool, frame_cap: int) -> ConfigurationResponse:
- """Update lsfg script configuration
+ def update_config(self, enable: bool, dll: str, multiplier: int, flow_scale: float,
+ performance_mode: bool, hdr_mode: bool,
+ experimental_present_mode: str = "",
+ experimental_fps_limit: int = 0) -> ConfigurationResponse:
+ """Update TOML configuration
Args:
- enable_lsfg: Whether to enable LSFG
+ enable: Whether to enable LSFG
+ dll: Path to Lossless.dll
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)
- disable_vkbasalt: Whether to disable vkbasalt layer
- frame_cap: Frame rate cap value (0-60, 0 = disabled)
+ performance_mode: Whether to enable performance mode
+ hdr_mode: Whether to enable HDR mode
+ experimental_present_mode: Experimental Vulkan present mode override
+ experimental_fps_limit: Experimental FPS limit for DXVK games
Returns:
ConfigurationResponse with success status
@@ -132,23 +87,28 @@ class ConfigurationService(BaseService):
try:
# 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
+ enable, dll, multiplier, flow_scale, performance_mode, hdr_mode,
+ experimental_present_mode, experimental_fps_limit
)
- # Generate script content using centralized manager
- script_content = ConfigurationManager.generate_script_content(config)
+ # Generate TOML content using centralized manager
+ toml_content = ConfigurationManager.generate_toml_content(config)
- # Write the updated script atomically
- self._atomic_write(self.lsfg_script_path, script_content, 0o755)
+ # Ensure config directory exists
+ self.config_dir.mkdir(parents=True, exist_ok=True)
- 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}")
+ # Write the updated config directly to preserve inode for file watchers
+ self._write_file(self.config_file_path, toml_content, 0o644)
+
+ self.log.info(f"Updated lsfg TOML configuration: enable={enable}, "
+ f"dll='{dll}', multiplier={multiplier}, flow_scale={flow_scale}, "
+ f"performance_mode={performance_mode}, hdr_mode={hdr_mode}, "
+ f"experimental_present_mode='{experimental_present_mode}', "
+ f"experimental_fps_limit={experimental_fps_limit}")
return {
"success": True,
- "config": None,
+ "config": config,
"message": "lsfg configuration updated successfully",
"error": None
}
@@ -171,3 +131,54 @@ class ConfigurationService(BaseService):
"message": None,
"error": str(e)
}
+
+ def update_dll_path(self, dll_path: str) -> ConfigurationResponse:
+ """Update just the DLL path in the configuration
+
+ Args:
+ dll_path: Path to the Lossless.dll file
+
+ Returns:
+ ConfigurationResponse with success status
+ """
+ try:
+ # Get current config
+ current_response = self.get_config()
+ if not current_response["success"] or current_response["config"] is None:
+ # If we can't read current config, use defaults with DLL detection
+ from .dll_detection import DllDetectionService
+ dll_service = DllDetectionService(self.log)
+ config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
+ else:
+ config = current_response["config"]
+
+ # Update just the DLL path
+ config["dll"] = dll_path
+
+ # Generate TOML content and write it
+ toml_content = ConfigurationManager.generate_toml_content(config)
+
+ # Ensure config directory exists
+ self.config_dir.mkdir(parents=True, exist_ok=True)
+
+ # Write the updated config directly to preserve inode for file watchers
+ self._write_file(self.config_file_path, toml_content, 0o644)
+
+ self.log.info(f"Updated DLL path in lsfg configuration: '{dll_path}'")
+
+ return {
+ "success": True,
+ "config": config,
+ "message": f"DLL path updated to: {dll_path}",
+ "error": None
+ }
+
+ except Exception as e:
+ error_msg = f"Error updating DLL path: {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 5f1e5a2..252c7a5 100644
--- a/py_modules/lsfg_vk/constants.py
+++ b/py_modules/lsfg_vk/constants.py
@@ -8,9 +8,11 @@ from pathlib import Path
LOCAL_LIB = ".local/lib"
LOCAL_SHARE_BASE = ".local/share"
VULKAN_LAYER_DIR = ".local/share/vulkan/implicit_layer.d"
+CONFIG_DIR = ".config/lsfg-vk"
# File names
SCRIPT_NAME = "lsfg"
+CONFIG_FILENAME = "conf.toml"
LIB_FILENAME = "liblsfg-vk.so"
JSON_FILENAME = "VkLayer_LS_frame_generation.json"
ZIP_FILENAME = "lsfg-vk_archlinux.zip"
diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py
index 767a97a..d193219 100644
--- a/py_modules/lsfg_vk/installation.py
+++ b/py_modules/lsfg_vk/installation.py
@@ -51,8 +51,11 @@ class InstallationService(BaseService):
# Extract and install files
self._extract_and_install_files(zip_path)
- # Create the lsfg script
- self._create_lsfg_script()
+ # Create the config file
+ self._create_config_file()
+
+ # Create the lsfg launch script
+ self._create_lsfg_launch_script()
self.log.info("lsfg-vk installed successfully")
return {"success": True, "message": "lsfg-vk installed successfully", "error": None}
@@ -102,17 +105,38 @@ class InstallationService(BaseService):
shutil.copy2(src_file, dst_file)
self.log.info(f"Copied {file} to {dst_file}")
- def _create_lsfg_script(self) -> None:
- """Create the lsfg script in home directory with default configuration"""
- # Get default configuration
- defaults = ConfigurationManager.get_defaults()
+ def _create_config_file(self) -> None:
+ """Create the TOML config file in ~/.config/lsfg-vk with default configuration and detected DLL path"""
+ # Import here to avoid circular imports
+ from .dll_detection import DllDetectionService
+
+ # Try to detect DLL path
+ dll_service = DllDetectionService(self.log)
+ config = ConfigurationManager.get_defaults_with_dll_detection(dll_service)
+
+ # Generate TOML content using centralized manager
+ toml_content = ConfigurationManager.generate_toml_content(config)
- # Generate script content using centralized manager
- script_content = ConfigurationManager.generate_script_content(defaults)
+ # Write initial config file
+ self._write_file(self.config_file_path, toml_content, 0o644)
+ self.log.info(f"Created config file at {self.config_file_path}")
- # Use atomic write to prevent corruption
- self._atomic_write(self.lsfg_script_path, script_content, 0o755)
- self.log.info(f"Created executable lsfg script at {self.lsfg_script_path}")
+ # Log detected DLL path if found
+ if config["dll"]:
+ self.log.info(f"Configured DLL path: {config['dll']}")
+
+ def _create_lsfg_launch_script(self) -> None:
+ """Create the ~/lsfg launch script for easier game setup"""
+ script_content = """#!/bin/bash
+# lsfg-vk launch script generated by decky-lossless-scaling-vk plugin
+# This script sets up the environment for lsfg-vk to work with the plugin configuration
+export LSFG_PROCESS=decky-lsfg-vk
+exec "$@"
+"""
+
+ # Write the script file
+ self._write_file(self.lsfg_launch_script_path, script_content, 0o755)
+ self.log.info(f"Created lsfg launch script at {self.lsfg_launch_script_path}")
def check_installation(self) -> InstallationCheckResponse:
"""Check if lsfg-vk is already installed
@@ -123,18 +147,18 @@ class InstallationService(BaseService):
try:
lib_exists = self.lib_file.exists()
json_exists = self.json_file.exists()
- script_exists = self.lsfg_script_path.exists()
+ config_exists = self.config_file_path.exists()
- self.log.info(f"Installation check: lib={lib_exists}, json={json_exists}, script={script_exists}")
+ self.log.info(f"Installation check: lib={lib_exists}, json={json_exists}, config={config_exists}")
return {
"installed": lib_exists and json_exists,
"lib_exists": lib_exists,
"json_exists": json_exists,
- "script_exists": script_exists,
+ "script_exists": config_exists, # Keep script_exists for backward compatibility
"lib_path": str(self.lib_file),
"json_path": str(self.json_file),
- "script_path": str(self.lsfg_script_path),
+ "script_path": str(self.config_file_path), # Keep script_path for backward compatibility
"error": None
}
@@ -148,7 +172,7 @@ class InstallationService(BaseService):
"script_exists": False,
"lib_path": str(self.lib_file),
"json_path": str(self.json_file),
- "script_path": str(self.lsfg_script_path),
+ "script_path": str(self.config_file_path),
"error": str(e)
}
@@ -160,12 +184,24 @@ class InstallationService(BaseService):
"""
try:
removed_files = []
- files_to_remove = [self.lib_file, self.json_file, self.lsfg_script_path]
+ files_to_remove = [self.lib_file, self.json_file, self.config_file_path, self.lsfg_launch_script_path]
for file_path in files_to_remove:
if self._remove_if_exists(file_path):
removed_files.append(str(file_path))
+ # Also try to remove the old script file if it exists (for backward compatibility)
+ if self._remove_if_exists(self.lsfg_script_path):
+ removed_files.append(str(self.lsfg_script_path))
+
+ # Remove config directory if it's empty
+ try:
+ if self.config_dir.exists() and not any(self.config_dir.iterdir()):
+ self.config_dir.rmdir()
+ removed_files.append(str(self.config_dir))
+ except OSError:
+ pass # Directory not empty or other error, ignore
+
if not removed_files:
return {
"success": True,
@@ -198,10 +234,12 @@ class InstallationService(BaseService):
self.log.info("Checking for lsfg-vk files to clean up:")
self.log.info(f" Library file: {self.lib_file}")
self.log.info(f" JSON file: {self.json_file}")
- self.log.info(f" lsfg script: {self.lsfg_script_path}")
+ self.log.info(f" Config file: {self.config_file_path}")
+ self.log.info(f" Launch script: {self.lsfg_launch_script_path}")
+ self.log.info(f" Old script file: {self.lsfg_script_path}")
removed_files = []
- files_to_remove = [self.lib_file, self.json_file, self.lsfg_script_path]
+ files_to_remove = [self.lib_file, self.json_file, self.config_file_path, self.lsfg_launch_script_path, self.lsfg_script_path]
for file_path in files_to_remove:
try:
@@ -210,6 +248,14 @@ class InstallationService(BaseService):
except OSError as e:
self.log.error(f"Failed to remove {file_path}: {e}")
+ # Try to remove config directory if empty
+ try:
+ if self.config_dir.exists() and not any(self.config_dir.iterdir()):
+ self.config_dir.rmdir()
+ removed_files.append(str(self.config_dir))
+ except OSError:
+ pass # Directory not empty or other error, ignore
+
if removed_files:
self.log.info(f"Cleaned up {len(removed_files)} lsfg-vk files during plugin uninstall: {removed_files}")
else:
diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py
index 3e37047..101542c 100644
--- a/py_modules/lsfg_vk/plugin.py
+++ b/py_modules/lsfg_vk/plugin.py
@@ -69,6 +69,38 @@ class Plugin:
"""
return self.dll_detection_service.check_lossless_scaling_dll()
+ async def check_lossless_scaling_dll_and_update_config(self) -> Dict[str, Any]:
+ """Check for DLL and automatically update configuration if found
+
+ This method should only be used during installation or when explicitly
+ requested by the user, not for routine DLL detection checks.
+
+ Returns:
+ DllDetectionResponse dict with detection status and path info
+ """
+ result = self.dll_detection_service.check_lossless_scaling_dll()
+
+ # Convert to dict to allow modification
+ result_dict = dict(result)
+
+ # If DLL was detected, automatically update the configuration
+ if result.get("detected") and result.get("path"):
+ try:
+ dll_path = result["path"]
+ if dll_path: # Type guard
+ update_result = self.configuration_service.update_dll_path(dll_path)
+ if update_result.get("success"):
+ result_dict["config_updated"] = True
+ result_dict["message"] = f"DLL detected and configuration updated: {dll_path}"
+ else:
+ result_dict["config_updated"] = False
+ result_dict["message"] = f"DLL detected but config update failed: {update_result.get('error', 'Unknown error')}"
+ except Exception as e:
+ result_dict["config_updated"] = False
+ result_dict["message"] = f"DLL detected but config update failed: {str(e)}"
+
+ return result_dict
+
# Configuration methods
async def get_lsfg_config(self) -> Dict[str, Any]:
"""Read current lsfg script configuration
@@ -90,27 +122,41 @@ class Plugin:
"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
+ async def update_lsfg_config(self, enable: bool, dll: str, multiplier: int, flow_scale: float,
+ performance_mode: bool, hdr_mode: bool,
+ experimental_present_mode: str = "",
+ experimental_fps_limit: int = 0) -> Dict[str, Any]:
+ """Update lsfg TOML configuration
Args:
- enable_lsfg: Whether to enable LSFG
+ enable: Whether to enable LSFG
+ dll: Path to Lossless.dll
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)
- disable_vkbasalt: Whether to disable vkbasalt layer
- frame_cap: Frame rate cap value (0-60, 0 = disabled)
+ performance_mode: Whether to enable performance mode
+ hdr_mode: Whether to enable HDR mode
+ experimental_present_mode: Experimental Vulkan present mode override
+ experimental_fps_limit: Experimental FPS limit for DXVK games
Returns:
ConfigurationResponse dict with success status
"""
return self.configuration_service.update_config(
- enable_lsfg, multiplier, flow_scale, hdr, perf_mode, immediate_mode, disable_vkbasalt, frame_cap
+ enable, dll, multiplier, flow_scale, performance_mode, hdr_mode,
+ experimental_present_mode, experimental_fps_limit
)
+ async def update_dll_path(self, dll_path: str) -> Dict[str, Any]:
+ """Update the DLL path in the configuration when detected
+
+ Args:
+ dll_path: Path to the detected Lossless.dll file
+
+ Returns:
+ ConfigurationResponse dict with success status
+ """
+ return self.configuration_service.update_dll_path(dll_path)
+
# Self-updater methods
async def check_for_plugin_update(self) -> Dict[str, Any]:
"""Check for plugin updates by comparing current version with latest GitHub release
@@ -306,29 +352,43 @@ class Plugin:
return False
# Plugin lifecycle methods
+ # Launch option methods
+ async def get_launch_option(self) -> Dict[str, Any]:
+ """Get the launch option that users need to set for their games
+
+ Returns:
+ Dict containing the launch option string and instructions
+ """
+ return {
+ "launch_option": "~/lsfg %command%",
+ "instructions": "Add this to your game's launch options in Steam Properties",
+ "explanation": "The lsfg script is created during installation and sets up the environment for the plugin"
+ }
+
+ # Lifecycle methods
async def _main(self):
"""
- Asyncio-compatible long-running code, executed in a task when the plugin is loaded.
+ Main entry point for the plugin.
- This method is called by Decky Loader when the plugin starts up.
- Currently just logs that the plugin has loaded successfully.
+ This method is called by Decky Loader when the plugin is loaded.
+ Any initialization code should go here.
"""
import decky
- decky.logger.info("Lossless Scaling VK plugin loaded!")
+ decky.logger.info("Lossless Scaling VK plugin loaded")
async def _unload(self):
"""
- Function called first during the unload process.
+ Cleanup tasks when the plugin is unloaded.
This method is called by Decky Loader when the plugin is being unloaded.
- Use this for cleanup that should happen when the plugin stops.
+ Any cleanup code should go here.
"""
import decky
- decky.logger.info("Lossless Scaling VK plugin unloading")
+ decky.logger.info("Lossless Scaling VK plugin unloaded")
async def _uninstall(self):
"""
- Function called after `_unload` during uninstall.
+ Cleanup tasks when the plugin is uninstalled.
This method is called by Decky Loader when the plugin is being uninstalled.
It automatically cleans up any lsfg-vk files that were installed.
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts
index f7363c1..5d866ef 100644
--- a/src/api/lsfgApi.ts
+++ b/src/api/lsfgApi.ts
@@ -66,6 +66,12 @@ export interface UpdateDownloadResult {
error?: string;
}
+export interface LaunchOptionResult {
+ launch_option: string;
+ instructions: string;
+ explanation: string;
+}
+
// API functions
export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk");
export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk");
@@ -73,17 +79,18 @@ export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg
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");
+export const getLaunchOption = callable<[], LaunchOptionResult>("get_launch_option");
// Updated config function using centralized configuration
export const updateLsfgConfig = callable<
- [boolean, number, number, boolean, boolean, boolean, boolean, number],
+ [boolean, string, number, number, boolean, boolean, string, 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]);
+ return updateLsfgConfig(...args as [boolean, string, number, number, boolean, boolean, string, number]);
};
// Self-updater API functions
diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx
index 2545217..dc8da89 100644
--- a/src/components/ConfigurationSection.tsx
+++ b/src/components/ConfigurationSection.tsx
@@ -1,9 +1,9 @@
-import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui";
+import { PanelSectionRow, ToggleField, SliderField, DropdownItem } from "@decky/ui";
import { ConfigurationData } from "../config/configSchema";
interface ConfigurationSectionProps {
config: ConfigurationData;
- onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number) => Promise<void>;
+ onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string) => Promise<void>;
}
export function ConfigurationSection({
@@ -27,28 +27,38 @@ export function ConfigurationSection({
</div>
</PanelSectionRow>
- <PanelSectionRow>
+ {/* <PanelSectionRow>
<ToggleField
label="Enable LSFG"
- description="Enables the frame generation layer"
- checked={config.enable_lsfg}
- onChange={(value) => onConfigChange('enable_lsfg', value)}
+ description="Enables lsfg globally (apply before launching games)"
+ checked={config.enable}
+ onChange={(value) => onConfigChange('enable', value)}
/>
- </PanelSectionRow>
+ </PanelSectionRow> */}
+
+ {/* <PanelSectionRow>
+ <TextField
+ label="Lossless.dll Path"
+ description="specify where Lossless.dll is stored"
+ value={config.dll}
+ onChange={(e) => onConfigChange('dll', e.target.value)}
+ />
+ </PanelSectionRow> */}
<PanelSectionRow>
<SliderField
label="FPS Multiplier"
description="Traditional FPS multiplier value"
value={config.multiplier}
- min={2}
+ min={1}
max={4}
step={1}
- notchCount={3}
+ notchCount={4}
notchLabels={[
- { notchIndex: 0, label: "2X" },
- { notchIndex: 1, label: "3X" },
- { notchIndex: 2, label: "4X" }
+ { notchIndex: 0, label: "OFF" },
+ { notchIndex: 1, label: "2X" },
+ { notchIndex: 2, label: "3X" },
+ { notchIndex: 3, label: "4X" }
]}
onChange={(value) => onConfigChange('multiplier', value)}
/>
@@ -57,7 +67,7 @@ export function ConfigurationSection({
<PanelSectionRow>
<SliderField
label={`Flow Scale ${Math.round(config.flow_scale * 100)}%`}
- description="Lowers the internal motion estimation resolution"
+ description="Lowers internal motion estimation resolution, improving performance slightly"
value={config.flow_scale}
min={0.25}
max={1.0}
@@ -68,51 +78,66 @@ export function ConfigurationSection({
<PanelSectionRow>
<ToggleField
- label="HDR Mode"
- description="Enable HDR mode (only if Game supports HDR)"
- checked={config.hdr}
- onChange={(value) => onConfigChange('hdr', value)}
+ label="Performance Mode"
+ description="Uses a lighter model for FG (Recommended for most games)"
+ checked={config.performance_mode}
+ onChange={(value) => onConfigChange('performance_mode', value)}
/>
</PanelSectionRow>
<PanelSectionRow>
<ToggleField
- label="Performance Mode"
- description="Use lighter model for FG"
- checked={config.perf_mode}
- onChange={(value) => onConfigChange('perf_mode', value)}
+ label="HDR Mode"
+ description="Enables HDR mode (only for games that support HDR)"
+ checked={config.hdr_mode}
+ onChange={(value) => onConfigChange('hdr_mode', value)}
/>
</PanelSectionRow>
<PanelSectionRow>
- <ToggleField
- label="Immediate Mode"
- description="Reduce input lag (Experimental, will cause issues in many games)"
- checked={config.immediate_mode}
- onChange={(value) => onConfigChange('immediate_mode', value)}
+ <div
+ style={{
+ fontSize: "14px",
+ fontWeight: "bold",
+ marginTop: "16px",
+ marginBottom: "8px",
+ borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
+ paddingBottom: "4px",
+ color: "white"
+ }}
+ >
+ Experimental Features
+ </div>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <DropdownItem
+ label="Override Vulkan present mode"
+ description="Select a specific Vulkan presentation mode for better performance or compatibility (may cause crashes)"
+ menuLabel="Select presentation mode"
+ selectedOption={config.experimental_present_mode}
+ onChange={(value) => onConfigChange('experimental_present_mode', value.data)}
+ rgOptions={[
+ { data: "", label: "Default" },
+ { data: "fifo", label: "FIFO" },
+ { data: "vsync", label: "VSync" },
+ { data: "mailbox", label: "Mailbox" },
+ { data: "immediate", label: "Immediate" }
+ ]}
/>
</PanelSectionRow>
<PanelSectionRow>
<SliderField
- label={`Game Frame Cap ${config.frame_cap === 0 ? "(Disabled)" : `(${config.frame_cap} FPS)`}`}
- description="Limit base game FPS (0 = disabled)"
- value={config.frame_cap}
+ label={`FPS Limit${config.experimental_fps_limit > 0 ? ` (${config.experimental_fps_limit} FPS)` : ' (Off)'}`}
+ description="Base framerate cap for DirectX games, before frame multiplier (requires game re-launch)"
+ value={config.experimental_fps_limit}
min={0}
max={60}
step={1}
- onChange={(value) => onConfigChange('frame_cap', value)}
+ onChange={(value) => onConfigChange('experimental_fps_limit', value)}
/>
</PanelSectionRow>
-
- {/* <PanelSectionRow>
- <ToggleField
- label="Disable vkbasalt"
- description="Some plugins add vkbasalt layer, which can break lsfg. Toggling on fixes this"
- checked={config.disable_vkbasalt}
- onChange={(value) => onConfigChange('disable_vkbasalt', value)}
- />
- </PanelSectionRow> */}
</>
);
}
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index ea3f3c1..613e722 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -37,7 +37,7 @@ export function Content() {
}, [isInstalled, loadLsfgConfig]);
// Generic configuration change handler
- const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number) => {
+ const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number | string) => {
await updateField(fieldName, value);
};
diff --git a/src/components/LaunchOptionInfo.tsx b/src/components/LaunchOptionInfo.tsx
new file mode 100644
index 0000000..298c45a
--- /dev/null
+++ b/src/components/LaunchOptionInfo.tsx
@@ -0,0 +1,25 @@
+import { PanelSectionRow, Field } from "@decky/ui";
+
+export function LaunchOptionInfo() {
+ return (
+ <PanelSectionRow>
+ <Field
+ bottomSeparator="none"
+ label="Setup Instructions"
+ description={
+ <>
+ <div>For each game where you want to use lsfg-vk:</div>
+ <div style={{ marginTop: "8px" }}>
+ 1. Right-click the game in Steam → Properties<br/>
+ 2. Add this to Launch Options: <code>LSFG_PROCESS=decky-lsfg-vk %command%</code><br/>
+ 3. Or use the "Copy Launch Option" button above
+ </div>
+ <div style={{ marginTop: "8px", fontStyle: "italic" }}>
+ This temporary solution allows hot-reloading while keeping you on the latest lsfg-vk version.
+ </div>
+ </>
+ }
+ />
+ </PanelSectionRow>
+ );
+}
diff --git a/src/components/PluginUpdateChecker.tsx b/src/components/PluginUpdateChecker.tsx
index 0028a79..d427c18 100644
--- a/src/components/PluginUpdateChecker.tsx
+++ b/src/components/PluginUpdateChecker.tsx
@@ -1,7 +1,10 @@
import React, { useState, useEffect } from 'react';
import {
ButtonItem,
- PanelSection
+ PanelSection,
+ PanelSectionRow,
+ Field,
+ Focusable
} from '@decky/ui';
import { checkForPluginUpdate, downloadPluginUpdate, UpdateCheckResult, UpdateDownloadResult } from '../api/lsfgApi';
@@ -99,88 +102,74 @@ export const PluginUpdateChecker: React.FC<PluginUpdateCheckerProps> = () => {
if (updateInfo.updateAvailable) {
if (downloadResult?.success) {
- return (
- <div style={{ color: 'lightgreen', marginTop: '5px' }}>
- ✓ v{updateInfo.latestVersion} downloaded - ready to install
- </div>
- );
+ return "✓ v" + updateInfo.latestVersion + " downloaded - ready to install";
} else {
- return (
- <div style={{ color: 'orange', marginTop: '5px' }}>
- Update available: v{updateInfo.latestVersion}
- </div>
- );
+ return "Update available: v" + updateInfo.latestVersion;
}
} else {
- return (
- <div style={{ color: 'lightgreen', marginTop: '5px' }}>
- Up to date (v{updateInfo.currentVersion})
- </div>
- );
+ return "Up to date (v" + updateInfo.currentVersion + ")";
}
};
return (
- <PanelSection title="Plugin Updates">
- <ButtonItem
- layout="below"
- onClick={handleCheckForUpdate}
- disabled={checkingUpdate}
- description={getStatusMessage()}
- >
- {checkingUpdate ? 'Checking for updates...' : 'Check for Updates'}
- </ButtonItem>
-
- {updateInfo && updateInfo.updateAvailable && !downloadResult?.success && (
+ <PanelSection title="PLUGIN UPDATES">
+ <PanelSectionRow>
<ButtonItem
layout="below"
- onClick={handleDownloadUpdate}
- disabled={downloadingUpdate}
- description={`Download version ${updateInfo.latestVersion}`}
+ onClick={handleCheckForUpdate}
+ disabled={checkingUpdate}
+ description={getStatusMessage()}
>
- {downloadingUpdate ? 'Downloading...' : 'Download Update'}
+ {checkingUpdate ? 'Checking for updates...' : 'Check for Updates'}
</ButtonItem>
+ </PanelSectionRow>
+
+ {updateInfo && updateInfo.updateAvailable && !downloadResult?.success && (
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={handleDownloadUpdate}
+ disabled={downloadingUpdate}
+ description={`Download version ${updateInfo.latestVersion}`}
+ >
+ {downloadingUpdate ? 'Downloading...' : 'Download Update'}
+ </ButtonItem>
+ </PanelSectionRow>
)}
{downloadResult?.success && (
- <div style={{
- marginTop: '10px',
- padding: '10px',
- backgroundColor: 'rgba(0, 255, 0, 0.1)',
- borderRadius: '4px',
- border: '1px solid rgba(0, 255, 0, 0.3)'
- }}>
- <div style={{ color: 'lightgreen', fontWeight: 'bold', marginBottom: '5px' }}>
- ✓ Download Complete!
- </div>
- <div style={{ fontSize: '12px', marginBottom: '10px' }}>
- File saved to: {downloadResult.download_path}
- </div>
- <div style={{ fontSize: '12px' }}>
- <strong>Installation Instructions:</strong>
- <ol style={{ paddingLeft: '20px', marginTop: '5px' }}>
- <li>Go to Decky Loader settings</li>
- <li>Click "Developer" tab</li>
- <li>Click "Uninstall" next to "Lossless Scaling"</li>
- <li>Click "Install from ZIP"</li>
- <li>Select the downloaded file</li>
- <li>Restart Steam or reload plugins</li>
- </ol>
- </div>
- </div>
+ <>
+ <PanelSectionRow>
+ <Field label="Download Complete!">
+ <Focusable>
+ File saved to: {downloadResult.download_path}
+ </Focusable>
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <Field label="Installation Instructions:">
+ <Focusable>
+ 1. Go to Decky Loader settings
+ <br />2. Click "Developer" tab
+ <br />3. Click "Uninstall" next to "Lossless Scaling"
+ <br />4. Click "Install from ZIP"
+ <br />5. Select the downloaded file
+ <br />6. Restart Steam or reload plugins
+ </Focusable>
+ </Field>
+ </PanelSectionRow>
+ </>
)}
{updateError && (
- <div style={{
- color: 'red',
- marginTop: '10px',
- padding: '8px',
- backgroundColor: 'rgba(255, 0, 0, 0.1)',
- borderRadius: '4px',
- fontSize: '12px'
- }}>
- {updateError}
- </div>
+ <PanelSectionRow>
+ <Field label="Error:">
+ <Focusable>
+ {updateError}
+ </Focusable>
+ </Field>
+ </PanelSectionRow>
)}
</PanelSection>
);
diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx
index 727a0ab..8ac94d8 100644
--- a/src/components/UsageInstructions.tsx
+++ b/src/components/UsageInstructions.tsx
@@ -6,88 +6,89 @@ interface UsageInstructionsProps {
}
export function UsageInstructions({ config }: UsageInstructionsProps) {
- // Build manual environment variables string based on current config
- const buildManualEnvVars = (): string => {
- const envVars: string[] = [];
-
- 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.enable_lsfg) {
- envVars.push(`LSFG_MULTIPLIER=${config.multiplier}`);
- envVars.push(`LSFG_FLOW_SCALE=${config.flow_scale}`);
- }
-
- if (config.hdr) {
- envVars.push("LSFG_HDR=1");
- }
-
- if (config.perf_mode) {
- envVars.push("LSFG_PERF_MODE=1");
- }
-
- if (config.immediate_mode) {
- envVars.push("MESA_VK_WSI_PRESENT_MODE=immediate");
- }
-
- if (config.disable_vkbasalt) {
- envVars.push("DISABLE_VKBASALT=1");
- }
-
- if (config.frame_cap > 0) {
- envVars.push(`DXVK_FRAME_RATE=${config.frame_cap}`);
- }
-
- return envVars.length > 0 ? `${envVars.join(" ")} %command%` : "%command%";
- };
-
return (
<>
<PanelSectionRow>
<div
style={{
- fontSize: "13px",
- marginTop: "12px",
- padding: "8px",
- backgroundColor: "rgba(255, 255, 255, 0.05)",
- borderRadius: "4px"
+ fontSize: "14px",
+ fontWeight: "bold",
+ marginTop: "16px",
+ marginBottom: "8px",
+ borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
+ paddingBottom: "4px"
+ }}
+ >
+ Usage Instructions
+ </div>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <div
+ style={{
+ fontSize: "12px",
+ lineHeight: "1.4",
+ opacity: "0.8",
+ whiteSpace: "pre-wrap"
+ }}
+ >
+ {config.enable
+ ? "Add the launch option below (or use \"Launch Option Clipboard\") to Steam games to activate frame generation."
+ : "LSFG is disabled. Enable it above and add the launch option to activate frame generation."
+ }
+ </div>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <div
+ style={{
+ fontSize: "12px",
+ lineHeight: "1.4",
+ opacity: "0.8",
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ padding: "8px",
+ borderRadius: "4px",
+ fontFamily: "monospace",
+ marginTop: "8px",
+ marginBottom: "8px",
+ textAlign: "center"
+ }}
+ >
+ <strong>~/lsfg %command%</strong>
+ </div>
+ </PanelSectionRow>
+
+ {/* <PanelSectionRow>
+ <div
+ style={{
+ fontSize: "12px",
+ lineHeight: "1.4",
+ opacity: "0.8",
+ whiteSpace: "pre-wrap"
+ }}
+ >
+ {`Current Configuration:
+• Enable: ${config.enable ? "Yes" : "No"}
+• DLL Path: ${config.dll}
+• Multiplier: ${config.multiplier}x
+• Flow Scale: ${Math.round(config.flow_scale * 100)}%
+• Performance Mode: ${config.performance_mode ? "Yes" : "No"}
+• HDR Mode: ${config.hdr_mode ? "Yes" : "No"}
+• Present Mode: ${config.experimental_present_mode || "Default (FIFO)"}
+• FPS Limit: ${config.experimental_fps_limit > 0 ? `${config.experimental_fps_limit} FPS` : "Off"}`}
+ </div>
+ </PanelSectionRow> */}
+
+ <PanelSectionRow>
+ <div
+ style={{
+ fontSize: "11px",
+ lineHeight: "1.3",
+ opacity: "0.6",
+ marginTop: "8px"
}}
>
- <div style={{ fontWeight: "bold", marginBottom: "6px" }}>
- Usage Instructions:
- </div>
- <div style={{ marginBottom: "4px" }}>
- Option 1: Use the lsfg script (recommended):
- </div>
- <div
- style={{
- fontFamily: "monospace",
- backgroundColor: "rgba(0, 0, 0, 0.3)",
- padding: "4px",
- borderRadius: "2px",
- fontSize: "12px",
- marginBottom: "6px"
- }}
- >
- ~/lsfg %command%
- </div>
- <div style={{ marginBottom: "4px" }}>
- Option 2: Manual environment variables:
- </div>
- <div
- style={{
- fontFamily: "monospace",
- backgroundColor: "rgba(0, 0, 0, 0.3)",
- padding: "4px",
- borderRadius: "2px",
- fontSize: "12px",
- marginBottom: "6px"
- }}
- >
- {buildManualEnvVars()}
- </div>
+The configuration is stored in ~/.config/lsfg-vk/conf.toml and hot-reloads while games are running.
</div>
</PanelSectionRow>
</>
diff --git a/src/components/index.ts b/src/components/index.ts
index d26159d..ed0b803 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -2,7 +2,8 @@ export { Content } from "./Content";
export { StatusDisplay } from "./StatusDisplay";
export { InstallationButton } from "./InstallationButton";
export { ConfigurationSection } from "./ConfigurationSection";
-export { UsageInstructions } from "./UsageInstructions";
+// export { UsageInstructions } from "./UsageInstructions";
export { WikiButton } from "./WikiButton";
export { ClipboardButton } from "./ClipboardButton";
+export { LaunchOptionInfo } from "./LaunchOptionInfo";
export { PluginUpdateChecker } from "./PluginUpdateChecker";
diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts
index 6956030..9b6fc41 100644
--- a/src/config/configSchema.ts
+++ b/src/config/configSchema.ts
@@ -9,102 +9,87 @@
export enum ConfigFieldType {
BOOLEAN = "boolean",
INTEGER = "integer",
- FLOAT = "float"
+ FLOAT = "float",
+ STRING = "string"
}
// Configuration field definition
export interface ConfigField {
name: string;
fieldType: ConfigFieldType;
- default: boolean | number;
+ default: boolean | number | string;
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",
+ enable: {
+ name: "enable",
fieldType: ConfigFieldType.BOOLEAN,
default: true,
- description: "Enables the frame generation layer",
- scriptTemplate: "export ENABLE_LSFG={value}",
- scriptComment: "# export ENABLE_LSFG=1"
+ description: "enable/disable lsfg on every game"
+ },
+
+ dll: {
+ name: "dll",
+ fieldType: ConfigFieldType.STRING,
+ default: "/games/Lossless Scaling/Lossless.dll",
+ description: "specify where Lossless.dll is stored"
},
multiplier: {
name: "multiplier",
fieldType: ConfigFieldType.INTEGER,
default: 2,
- description: "Traditional FPS multiplier value",
- scriptTemplate: "export LSFG_MULTIPLIER={value}"
+ description: "change the fps multiplier"
},
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"
+ description: "change the flow scale"
},
- perf_mode: {
- name: "perf_mode",
+ performance_mode: {
+ name: "performance_mode",
fieldType: ConfigFieldType.BOOLEAN,
default: true,
- description: "Use lighter model for FG",
- scriptTemplate: "export LSFG_PERF_MODE={value}",
- scriptComment: "# export LSFG_PERF_MODE=1"
+ description: "toggle performance mode"
},
- immediate_mode: {
- name: "immediate_mode",
+ hdr_mode: {
+ name: "hdr_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"
+ description: "enable hdr in games that support it"
},
- 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"
+ experimental_present_mode: {
+ name: "experimental_present_mode",
+ fieldType: ConfigFieldType.STRING,
+ default: "",
+ description: "experimental: override vulkan present mode (empty/fifo/vsync/mailbox/immediate)"
},
- frame_cap: {
- name: "frame_cap",
+ experimental_fps_limit: {
+ name: "experimental_fps_limit",
fieldType: ConfigFieldType.INTEGER,
default: 0,
- description: "Limit base game FPS (0 = disabled)",
- scriptTemplate: "export DXVK_FRAME_RATE={value}",
- scriptComment: "# export DXVK_FRAME_RATE=60"
+ description: "experimental: base framerate cap for dxvk games, before frame multiplier (0 = disabled)"
}
};
// Type-safe configuration data structure
export interface ConfigurationData {
- enable_lsfg: boolean;
+ enable: boolean;
+ dll: string;
multiplier: number;
flow_scale: number;
- hdr: boolean;
- perf_mode: boolean;
- immediate_mode: boolean;
- disable_vkbasalt: boolean;
- frame_cap: number;
+ performance_mode: boolean;
+ hdr_mode: boolean;
+ experimental_present_mode: string;
+ experimental_fps_limit: number;
}
// Centralized configuration manager
@@ -140,7 +125,7 @@ export class ConfigurationManager {
/**
* Create ordered arguments array from configuration object
*/
- static createArgsFromConfig(config: ConfigurationData): (boolean | number)[] {
+ static createArgsFromConfig(config: ConfigurationData): (boolean | number | string)[] {
return this.getFieldNames().map(fieldName =>
config[fieldName as keyof ConfigurationData]
);
@@ -163,6 +148,8 @@ export class ConfigurationManager {
(validated as any)[fieldName] = parseInt(String(value), 10);
} else if (fieldDef.fieldType === ConfigFieldType.FLOAT) {
(validated as any)[fieldName] = parseFloat(String(value));
+ } else if (fieldDef.fieldType === ConfigFieldType.STRING) {
+ (validated as any)[fieldName] = String(value);
}
}
});
diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts
index 8ff9061..e514d72 100644
--- a/src/hooks/useLsfgHooks.ts
+++ b/src/hooks/useLsfgHooks.ts
@@ -110,7 +110,7 @@ export function useLsfgConfig() {
}
}, []);
- const updateField = useCallback(async (fieldName: keyof ConfigurationData, value: boolean | number): Promise<ConfigUpdateResult> => {
+ const updateField = useCallback(async (fieldName: keyof ConfigurationData, value: boolean | number | string): Promise<ConfigUpdateResult> => {
const newConfig = { ...config, [fieldName]: value };
return updateConfig(newConfig);
}, [config, updateConfig]);
diff --git a/src/index.tsx b/src/index.tsx
index e8ab56f..785ecf5 100755
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -11,6 +11,8 @@ export default definePlugin(() => {
name: "Lossless Scaling",
// The element displayed at the top of your plugin's menu
titleView: <div className={staticClasses.Title}>Lossless Scaling</div>,
+ // Always render to retain state when panel is toggled
+ alwaysRender: true,
// The content of your plugin's menu
content: <Content />,
// The icon displayed in the plugin list