diff options
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | main.py | 134 | ||||
| -rwxr-xr-x | src/index.tsx | 181 |
3 files changed, 318 insertions, 9 deletions
@@ -13,13 +13,23 @@ This plugin automates the installation of lsfg-vk, a compatibility layer that al 2. **Purchase and install** [Lossless Scaling](https://store.steampowered.com/app/993090/Lossless_Scaling/) from Steam 3. **Open the plugin** from the Decky menu 4. **Click "Install lsfg-vk"** to automatically set up the compatibility layer -5. **Apply launch commands** to the game you want to use frame generation with. The plugin provides a short guide for these commands in its interface. +5. **Configure settings** using the plugin's UI controls: + - Enable/disable LSFG + - Set FPS multiplier (2-4) + - Adjust flow scale (0.25-1.0) + - Toggle HDR mode +6. **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%` ## 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) +- Automatically updates the `lsfg` script when settings are changed - Provides easy uninstallation by removing these files when no longer needed ## Credits @@ -55,6 +55,23 @@ class Plugin: # temp_dir will be automatically cleaned up + # Create the lsfg script in home directory + lsfg_script_path = os.path.join(user_home, "lsfg") + script_content = """#!/bin/bash + +export ENABLE_LSFG=1 +export LSFG_MULTIPLIER=2 +export LSFG_FLOW_SCALE=1.0 +# export LSFG_HDR=1 +""" + + with open(lsfg_script_path, 'w') as script_file: + script_file.write(script_content) + + # Make the script executable + os.chmod(lsfg_script_path, 0o755) + decky.logger.info(f"Created executable lsfg script at {lsfg_script_path}") + decky.logger.info("lsfg-vk installed successfully") return {"success": True, "message": "lsfg-vk installed successfully"} @@ -90,6 +107,7 @@ class Plugin: user_home = os.path.expanduser("~") lib_file = os.path.join(user_home, ".local", "lib", "liblsfg-vk.so") json_file = os.path.join(user_home, ".local", "share", "vulkan", "implicit_layer.d", "VkLayer_LS_frame_generation.json") + lsfg_script = os.path.join(user_home, "lsfg") removed_files = [] @@ -105,6 +123,12 @@ class Plugin: removed_files.append(json_file) decky.logger.info(f"Removed {json_file}") + # Remove lsfg script if it exists + if os.path.exists(lsfg_script): + os.remove(lsfg_script) + removed_files.append(lsfg_script) + decky.logger.info(f"Removed {lsfg_script}") + if not removed_files: return {"success": True, "message": "No lsfg-vk files found to remove"} @@ -172,6 +196,102 @@ class Plugin: "error": str(e) } + async def get_lsfg_config(self) -> dict: + """Read current lsfg script configuration""" + try: + user_home = os.path.expanduser("~") + lsfg_script_path = os.path.join(user_home, "lsfg") + + if not os.path.exists(lsfg_script_path): + return { + "success": False, + "error": "lsfg script not found" + } + + with open(lsfg_script_path, 'r') as f: + content = f.read() + + # Parse the script content to extract current values + config = { + "enable_lsfg": False, + "multiplier": 2, + "flow_scale": 1.0, + "hdr": False + } + + lines = content.split('\n') + for line in lines: + line = line.strip() + if line.startswith('export ENABLE_LSFG='): + config["enable_lsfg"] = line.split('=')[1] == '1' + elif line.startswith('export LSFG_MULTIPLIER='): + try: + config["multiplier"] = int(line.split('=')[1]) + except: + config["multiplier"] = 2 + elif line.startswith('export LSFG_FLOW_SCALE='): + try: + config["flow_scale"] = float(line.split('=')[1]) + except: + config["flow_scale"] = 1.0 + elif line.startswith('export LSFG_HDR='): + config["hdr"] = line.split('=')[1] == '1' + + return { + "success": True, + "config": config + } + + except Exception as e: + decky.logger.error(f"Error reading lsfg config: {str(e)}") + return { + "success": False, + "error": str(e) + } + + async def update_lsfg_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float, hdr: bool) -> dict: + """Update lsfg script configuration""" + try: + user_home = os.path.expanduser("~") + lsfg_script_path = os.path.join(user_home, "lsfg") + + # Create script content based on parameters + script_content = "#!/bin/bash\n\n" + + if enable_lsfg: + script_content += "export ENABLE_LSFG=1\n" + else: + script_content += "# export ENABLE_LSFG=1\n" + + script_content += f"export LSFG_MULTIPLIER={multiplier}\n" + script_content += f"export LSFG_FLOW_SCALE={flow_scale}\n" + + if hdr: + script_content += "export LSFG_HDR=1\n" + else: + script_content += "# export LSFG_HDR=1\n" + + # Write the updated script + with open(lsfg_script_path, 'w') as f: + f.write(script_content) + + # Make sure it's executable + os.chmod(lsfg_script_path, 0o755) + + decky.logger.info(f"Updated lsfg script configuration: enable={enable_lsfg}, multiplier={multiplier}, flow_scale={flow_scale}, hdr={hdr}") + + return { + "success": True, + "message": "lsfg configuration updated successfully" + } + + except Exception as e: + decky.logger.error(f"Error updating lsfg config: {str(e)}") + return { + "success": False, + "error": str(e) + } + # Asyncio-compatible long-running code, executed in a task when the plugin is loaded async def _main(self): decky.logger.info("Lossless Scaling loaded!") @@ -192,10 +312,12 @@ class Plugin: user_home = os.path.expanduser("~") lib_file = os.path.join(user_home, ".local", "lib", "liblsfg-vk.so") json_file = os.path.join(user_home, ".local", "share", "vulkan", "implicit_layer.d", "VkLayer_LS_frame_generation.json") + lsfg_script = os.path.join(user_home, "lsfg") decky.logger.info(f"Checking for lsfg-vk files to clean up:") decky.logger.info(f" Library file: {lib_file}") decky.logger.info(f" JSON file: {json_file}") + decky.logger.info(f" lsfg script: {lsfg_script}") removed_files = [] @@ -223,6 +345,18 @@ class Plugin: else: decky.logger.info(f"JSON file not found: {json_file}") + # Remove lsfg script if it exists + if os.path.exists(lsfg_script): + decky.logger.info(f"Found lsfg script, attempting to remove: {lsfg_script}") + try: + os.remove(lsfg_script) + removed_files.append(lsfg_script) + decky.logger.info(f"Successfully removed {lsfg_script}") + except Exception as e: + decky.logger.error(f"Failed to remove {lsfg_script}: {str(e)}") + else: + decky.logger.info(f"lsfg script not found: {lsfg_script}") + if removed_files: decky.logger.info(f"Cleaned up {len(removed_files)} lsfg-vk files during plugin uninstall: {removed_files}") else: diff --git a/src/index.tsx b/src/index.tsx index 8465426..bddedf9 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,7 +2,9 @@ import { ButtonItem, PanelSection, PanelSectionRow, - staticClasses + staticClasses, + ToggleField, + SliderField } from "@decky/ui"; import { callable, @@ -25,6 +27,12 @@ const checkLsfgVkInstalled = callable<[], { installed: boolean; lib_exists: bool // Function to check if Lossless Scaling DLL is available const checkLosslessScalingDll = callable<[], { detected: boolean; path?: string; source?: string; message?: string; error?: string }>("check_lossless_scaling_dll"); +// Function to get lsfg configuration +const getLsfgConfig = callable<[], { success: boolean; config?: { enable_lsfg: boolean; multiplier: number; flow_scale: number; hdr: boolean }; error?: string }>("get_lsfg_config"); + +// Function to update lsfg configuration +const updateLsfgConfig = callable<[boolean, number, number, boolean], { success: boolean; message?: string; error?: string }>("update_lsfg_config"); + function Content() { const [isInstalled, setIsInstalled] = useState<boolean>(false); const [isInstalling, setIsInstalling] = useState<boolean>(false); @@ -33,6 +41,12 @@ function Content() { const [dllDetected, setDllDetected] = useState<boolean>(false); const [dllDetectionStatus, setDllDetectionStatus] = useState<string>(""); + // LSFG configuration state + const [enableLsfg, setEnableLsfg] = useState<boolean>(true); + const [multiplier, setMultiplier] = useState<number>(2); + const [flowScale, setFlowScale] = useState<number>(1.0); + const [hdr, setHdr] = useState<boolean>(false); + // Check installation status on component mount useEffect(() => { const checkInstallation = async () => { @@ -63,11 +77,24 @@ function Content() { } }; + const loadLsfgConfig = 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); + } + } catch (error) { + console.error("Error loading lsfg config:", error); + } + }; + checkInstallation(); checkDllDetection(); - }, []); - - const handleInstall = async () => { + loadLsfgConfig(); + }, []); const handleInstall = async () => { setIsInstalling(true); setInstallationStatus("Installing lsfg-vk..."); @@ -80,6 +107,19 @@ function Content() { title: "Installation Complete", body: "lsfg-vk has been installed successfully" }); + + // Reload lsfg config after installation + try { + const configResult = await getLsfgConfig(); + if (configResult.success && configResult.config) { + setEnableLsfg(configResult.config.enable_lsfg); + setMultiplier(configResult.config.multiplier); + setFlowScale(configResult.config.flow_scale); + setHdr(configResult.config.hdr); + } + } catch (error) { + console.error("Error reloading config after install:", error); + } } else { setInstallationStatus(`Installation failed: ${result.error}`); toaster.toast({ @@ -101,7 +141,7 @@ function Content() { const handleUninstall = async () => { setIsUninstalling(true); setInstallationStatus("Uninstalling lsfg-vk..."); - + try { const result = await uninstallLsfgVk(); if (result.success) { @@ -129,6 +169,48 @@ function Content() { } }; + const updateConfig = async (newEnableLsfg: boolean, newMultiplier: number, newFlowScale: number, newHdr: boolean) => { + try { + const result = await updateLsfgConfig(newEnableLsfg, newMultiplier, newFlowScale, newHdr); + if (result.success) { + toaster.toast({ + title: "Configuration Updated", + body: "lsfg script configuration has been updated" + }); + } else { + toaster.toast({ + title: "Update Failed", + body: result.error || "Failed to update configuration" + }); + } + } catch (error) { + toaster.toast({ + title: "Update Failed", + body: `Error: ${error}` + }); + } + }; + + const handleEnableLsfgChange = async (value: boolean) => { + setEnableLsfg(value); + await updateConfig(value, multiplier, flowScale, hdr); + }; + + const handleMultiplierChange = async (value: number) => { + setMultiplier(value); + await updateConfig(enableLsfg, value, flowScale, hdr); + }; + + const handleFlowScaleChange = async (value: number) => { + setFlowScale(value); + await updateConfig(enableLsfg, multiplier, value, hdr); + }; + + const handleHdrChange = async (value: boolean) => { + setHdr(value); + await updateConfig(enableLsfg, multiplier, flowScale, value); + }; + return ( <PanelSection> <PanelSectionRow> @@ -175,6 +257,72 @@ function Content() { )} </ButtonItem> </PanelSectionRow> + + {/* Configuration Section - only show if installed */} + {isInstalled && ( + <> + <PanelSectionRow> + <div style={{ + fontSize: "14px", + fontWeight: "bold", + marginTop: "16px", + marginBottom: "8px", + borderBottom: "1px solid rgba(255, 255, 255, 0.2)", + paddingBottom: "4px" + }}> + LSFG Configuration + </div> + </PanelSectionRow> + + <PanelSectionRow> + <ToggleField + label="Enable LSFG" + description="Enables the frame generation layer" + checked={enableLsfg} + onChange={handleEnableLsfgChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <SliderField + label="FPS Multiplier" + description="Traditional FPS multiplier value (2-4)" + value={multiplier} + min={2} + max={4} + step={1} + notchCount={3} + notchLabels={[ + { notchIndex: 0, label: "2" }, + { notchIndex: 1, label: "3" }, + { notchIndex: 2, label: "4" } + ]} + onChange={handleMultiplierChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <SliderField + label="Flow Scale" + description="Lowers the flow scale for performance (0.25-1.0)" + value={flowScale} + min={0.25} + max={1.0} + step={0.01} + onChange={handleFlowScaleChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <ToggleField + label="HDR Mode" + description="Enable HDR mode (only if using HDR)" + checked={hdr} + onChange={handleHdrChange} + /> + </PanelSectionRow> + </> + )} <PanelSectionRow> <div style={{ @@ -188,7 +336,7 @@ function Content() { Usage Instructions: </div> <div style={{ marginBottom: "4px" }}> - Add to your game's launch options in Steam: + Option 1: Use the lsfg script (recommended): </div> <div style={{ fontFamily: "monospace", @@ -198,14 +346,31 @@ function Content() { fontSize: "12px", marginBottom: "6px" }}> - ENABLE_LSFG=1 LSFG_MULTIPLIER=2 %COMMAND% + ~/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" + }}> + ENABLE_LSFG=1 LSFG_MULTIPLIER={multiplier} %COMMAND% </div> <div style={{ fontSize: "11px", opacity: 0.8 }}> + The lsfg script uses your current configuration settings. + <br /> • ENABLE_LSFG=1 - Enables frame generation <br /> • LSFG_MULTIPLIER=2-4 - FPS multiplier (start with 2) <br /> - • LSFG_FLOW_SCALE=0.1-1.0 - Flow scale (optional, for performance) + • LSFG_FLOW_SCALE=0.25-1.0 - Flow scale (for performance) + <br /> + • LSFG_HDR=1 - HDR mode (only if using HDR) </div> </div> </PanelSectionRow> |
