diff options
| -rw-r--r-- | assets/image.png | bin | 1232759 -> 0 bytes | |||
| -rw-r--r-- | main.py | 461 | ||||
| -rw-r--r-- | src/api/lsfgApi.ts | 60 | ||||
| -rw-r--r-- | src/components/ConfigurationSection.tsx | 115 | ||||
| -rw-r--r-- | src/components/Content.tsx | 108 | ||||
| -rw-r--r-- | src/components/InstallationButton.tsx | 64 | ||||
| -rw-r--r-- | src/components/StatusDisplay.tsx | 38 | ||||
| -rw-r--r-- | src/components/UsageInstructions.tsx | 68 | ||||
| -rw-r--r-- | src/components/index.ts | 5 | ||||
| -rw-r--r-- | src/hooks/useInstallationActions.ts | 89 | ||||
| -rw-r--r-- | src/hooks/useLsfgHooks.ts | 156 | ||||
| -rwxr-xr-x | src/index.tsx | 450 | ||||
| -rw-r--r-- | src/plugin_lifecycle.py | 47 | ||||
| -rw-r--r-- | src/services/__init__.py | 5 | ||||
| -rw-r--r-- | src/services/configuration_service.py | 164 | ||||
| -rw-r--r-- | src/services/dll_detection_service.py | 60 | ||||
| -rw-r--r-- | src/services/installation_service.py | 205 |
17 files changed, 1224 insertions, 871 deletions
diff --git a/assets/image.png b/assets/image.png Binary files differdeleted file mode 100644 index 51680a3..0000000 --- a/assets/image.png +++ /dev/null @@ -10,455 +10,62 @@ import tempfile import decky import asyncio +# Import our modular services +from src.services import InstallationService, DllDetectionService, ConfigurationService +from src.plugin_lifecycle import PluginLifecycleManager + + class Plugin: + def __init__(self): + # Initialize services + self.installation_service = InstallationService() + self.dll_detection_service = DllDetectionService() + self.configuration_service = ConfigurationService() + self.lifecycle_manager = PluginLifecycleManager() + + # Installation methods async def install_lsfg_vk(self) -> dict: """Install lsfg-vk by extracting the zip file to ~/.local""" - try: - # Get the path to the lsfg-vk_archlinux.zip file in the bin directory - plugin_dir = os.path.dirname(os.path.realpath(__file__)) - zip_path = os.path.join(plugin_dir, "bin", "lsfg-vk_archlinux.zip") - - # Check if the zip file exists - if not os.path.exists(zip_path): - decky.logger.error(f"lsfg-vk_archlinux.zip not found at {zip_path}") - return {"success": False, "error": "lsfg-vk_archlinux.zip file not found"} - - # Get the user's home directory - user_home = os.path.expanduser("~") - local_lib_dir = os.path.join(user_home, ".local", "lib") - local_share_dir = os.path.join(user_home, ".local", "share", "vulkan", "implicit_layer.d") - - # Create directories if they don't exist - os.makedirs(local_lib_dir, exist_ok=True) - os.makedirs(local_share_dir, exist_ok=True) - - # Extract the zip file - with zipfile.ZipFile(zip_path, 'r') as zip_ref: - # Use /tmp for temporary extraction since we may not have write permissions in plugin dir - with tempfile.TemporaryDirectory() as temp_dir: - zip_ref.extractall(temp_dir) - - # Look for the extracted files and copy them to the correct locations - for root, dirs, files in os.walk(temp_dir): - for file in files: - src_file = os.path.join(root, file) - if file.endswith('.so'): - # Copy library files to ~/.local/lib - dst_file = os.path.join(local_lib_dir, file) - shutil.copy2(src_file, dst_file) - decky.logger.info(f"Copied {file} to {dst_file}") - elif file.endswith('.json'): - # Copy JSON files to ~/.local/share/vulkan/implicit_layer.d - dst_file = os.path.join(local_share_dir, file) - shutil.copy2(src_file, dst_file) - decky.logger.info(f"Copied {file} to {dst_file}") - - # 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 -# export LSFG_PERF_MODE=1 -# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync - -# Execute the passed command with the environment variables set -exec "$@" -""" - - 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"} - - except Exception as e: - decky.logger.error(f"Error installing lsfg-vk: {str(e)}") - return {"success": False, "error": str(e)} + return await self.installation_service.install() async def check_lsfg_vk_installed(self) -> dict: """Check if lsfg-vk is already installed""" - try: - 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") - - lib_exists = os.path.exists(lib_file) - json_exists = os.path.exists(json_file) - script_exists = os.path.exists(lsfg_script) - - decky.logger.info(f"Installation check: lib={lib_exists}, json={json_exists}, script={script_exists}") - - return { - "installed": lib_exists and json_exists, - "lib_exists": lib_exists, - "json_exists": json_exists, - "script_exists": script_exists, - "lib_path": lib_file, - "json_path": json_file, - "script_path": lsfg_script - } - - except Exception as e: - decky.logger.error(f"Error checking lsfg-vk installation: {str(e)}") - return {"installed": False, "error": str(e)} + return await self.installation_service.check_installation() async def uninstall_lsfg_vk(self) -> dict: """Uninstall lsfg-vk by removing the installed files""" - try: - 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 = [] - - # Remove library file if it exists - if os.path.exists(lib_file): - os.remove(lib_file) - removed_files.append(lib_file) - decky.logger.info(f"Removed {lib_file}") - - # Remove JSON file if it exists - if os.path.exists(json_file): - os.remove(json_file) - 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"} - - decky.logger.info("lsfg-vk uninstalled successfully") - return { - "success": True, - "message": f"lsfg-vk uninstalled successfully. Removed {len(removed_files)} files.", - "removed_files": removed_files - } - - except Exception as e: - decky.logger.error(f"Error uninstalling lsfg-vk: {str(e)}") - return {"success": False, "error": str(e)} + return await self.installation_service.uninstall() + # DLL detection methods async def check_lossless_scaling_dll(self) -> dict: """Check if Lossless Scaling DLL is available at the expected paths""" - try: - # Check environment variable first - dll_path = os.getenv("LSFG_DLL_PATH") - if dll_path and dll_path.strip(): - dll_path_str = dll_path.strip() - if os.path.exists(dll_path_str): - return { - "detected": True, - "path": dll_path_str, - "source": "LSFG_DLL_PATH environment variable" - } - - # Check XDG_DATA_HOME path - data_dir = os.getenv("XDG_DATA_HOME") - if data_dir and data_dir.strip(): - dll_path_str = os.path.join(data_dir.strip(), "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll") - if os.path.exists(dll_path_str): - return { - "detected": True, - "path": dll_path_str, - "source": "XDG_DATA_HOME Steam directory" - } - - # Check HOME/.local/share path - home_dir = os.getenv("HOME") - if home_dir and home_dir.strip(): - dll_path_str = os.path.join(home_dir.strip(), ".local", "share", "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll") - if os.path.exists(dll_path_str): - return { - "detected": True, - "path": dll_path_str, - "source": "HOME/.local/share Steam directory" - } - - # DLL not found in any expected location - return { - "detected": False, - "path": None, - "source": None, - "message": "Lossless Scaling DLL not found in expected locations" - } - - except Exception as e: - decky.logger.error(f"Error checking Lossless Scaling DLL: {str(e)}") - return { - "detected": False, - "path": None, - "source": None, - "error": str(e) - } + return await self.dll_detection_service.check_lossless_scaling_dll() + # Configuration methods 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, - "perf_mode": False, - "immediate_mode": False - } - - lines = content.split('\n') - for line in lines: - line = line.strip() - - # Handle ENABLE_LSFG - check if it's commented out or not - if line.startswith('export ENABLE_LSFG='): - try: - value = line.split('=')[1].strip() - config["enable_lsfg"] = value == '1' - except: - pass - elif line.startswith('# export ENABLE_LSFG='): - config["enable_lsfg"] = False - - # Handle LSFG_MULTIPLIER - elif line.startswith('export LSFG_MULTIPLIER='): - try: - value = line.split('=')[1].strip() - config["multiplier"] = int(value) - except: - pass - - # Handle LSFG_FLOW_SCALE - elif line.startswith('export LSFG_FLOW_SCALE='): - try: - value = line.split('=')[1].strip() - config["flow_scale"] = float(value) - except: - pass - - # Handle LSFG_HDR - check if it's commented out or not - elif line.startswith('export LSFG_HDR='): - try: - value = line.split('=')[1].strip() - config["hdr"] = value == '1' - except: - pass - elif line.startswith('# export LSFG_HDR='): - config["hdr"] = False - - # Handle LSFG_PERF_MODE - check if it's commented out or not - elif line.startswith('export LSFG_PERF_MODE='): - try: - value = line.split('=')[1].strip() - config["perf_mode"] = value == '1' - except: - pass - elif line.startswith('# export LSFG_PERF_MODE='): - config["perf_mode"] = False - - # Handle MESA_VK_WSI_PRESENT_MODE - check if it's commented out or not - elif line.startswith('export MESA_VK_WSI_PRESENT_MODE='): - try: - value = line.split('=')[1].strip() - # Remove any comments after the value - value = value.split('#')[0].strip() - config["immediate_mode"] = value == 'immediate' - except: - pass - elif line.startswith('# export MESA_VK_WSI_PRESENT_MODE='): - config["immediate_mode"] = False - - decky.logger.info(f"Parsed lsfg config: {config}") - - 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) - } + return await self.configuration_service.get_config() - async def update_lsfg_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float, hdr: bool, perf_mode: bool, immediate_mode: bool) -> dict: + async def update_lsfg_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float, + hdr: bool, perf_mode: bool, immediate_mode: 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" - - if perf_mode: - script_content += "export LSFG_PERF_MODE=1\n" - else: - script_content += "# export LSFG_PERF_MODE=1\n" - - if immediate_mode: - script_content += "export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n" - else: - script_content += "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n" - - # Add the exec line to allow the script to execute passed commands - script_content += "\n# Execute the passed command with the environment variables set\n" - script_content += "exec \"$@\"\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}, perf_mode={perf_mode}, immediate_mode={immediate_mode}") - - 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) - } + return await self.configuration_service.update_config( + enable_lsfg, multiplier, flow_scale, hdr, perf_mode, immediate_mode + ) - # Asyncio-compatible long-running code, executed in a task when the plugin is loaded + # Plugin lifecycle methods async def _main(self): - decky.logger.info("Lossless Scaling loaded!") + """Asyncio-compatible long-running code, executed in a task when the plugin is loaded""" + await self.lifecycle_manager.on_load() - # Function called first during the unload process, utilize this to handle your plugin being stopped, but not - # completely removed async def _unload(self): - decky.logger.info("Lossless Scaling unloading") - pass + """Function called first during the unload process""" + await self.lifecycle_manager.on_unload() - # Function called after `_unload` during uninstall, utilize this to clean up processes and other remnants of your - # plugin that may remain on the system async def _uninstall(self): - decky.logger.info("Lossless Scaling uninstalled - starting cleanup") - - # Clean up lsfg-vk files when the plugin is uninstalled - try: - 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 = [] - - # Remove library file if it exists - if os.path.exists(lib_file): - decky.logger.info(f"Found library file, attempting to remove: {lib_file}") - try: - os.remove(lib_file) - removed_files.append(lib_file) - decky.logger.info(f"Successfully removed {lib_file}") - except Exception as e: - decky.logger.error(f"Failed to remove {lib_file}: {str(e)}") - else: - decky.logger.info(f"Library file not found: {lib_file}") - - # Remove JSON file if it exists - if os.path.exists(json_file): - decky.logger.info(f"Found JSON file, attempting to remove: {json_file}") - try: - os.remove(json_file) - removed_files.append(json_file) - decky.logger.info(f"Successfully removed {json_file}") - except Exception as e: - decky.logger.error(f"Failed to remove {json_file}: {str(e)}") - 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: - decky.logger.info("No lsfg-vk files found to clean up during plugin uninstall") - - except Exception as e: - decky.logger.error(f"Error cleaning up lsfg-vk files during uninstall: {str(e)}") - import traceback - decky.logger.error(f"Traceback: {traceback.format_exc()}") - - decky.logger.info("Lossless Scaling uninstall cleanup completed") - pass + """Function called after `_unload` during uninstall""" + await self.lifecycle_manager.on_uninstall() - # Migrations that should be performed before entering `_main()`. async def _migration(self): - decky.logger.info("Migrating") - # Here's a migration example for logs: - # - `~/.config/decky-template/template.log` will be migrated to `decky.decky_LOG_DIR/template.log` - decky.migrate_logs(os.path.join(decky.DECKY_USER_HOME, - ".config", "decky-template", "template.log")) - # Here's a migration example for settings: - # - `~/homebrew/settings/template.json` is migrated to `decky.decky_SETTINGS_DIR/template.json` - # - `~/.config/decky-template/` all files and directories under this root are migrated to `decky.decky_SETTINGS_DIR/` - decky.migrate_settings( - os.path.join(decky.DECKY_HOME, "settings", "template.json"), - os.path.join(decky.DECKY_USER_HOME, ".config", "decky-template")) - # Here's a migration example for runtime data: - # - `~/homebrew/template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/` - # - `~/.local/share/decky-template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/` - decky.migrate_runtime( - os.path.join(decky.DECKY_HOME, "template"), - os.path.join(decky.DECKY_USER_HOME, ".local", "share", "decky-template")) + """Migrations that should be performed before entering `_main()`""" + await self.lifecycle_manager.on_migration() diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts new file mode 100644 index 0000000..a35f274 --- /dev/null +++ b/src/api/lsfgApi.ts @@ -0,0 +1,60 @@ +import { callable } from "@decky/api"; + +// Type definitions for API responses +export interface InstallationResult { + success: boolean; + error?: string; + message?: string; + removed_files?: string[]; +} + +export interface InstallationStatus { + installed: boolean; + lib_exists: boolean; + json_exists: boolean; + script_exists: boolean; + lib_path: string; + json_path: string; + script_path: string; + error?: string; +} + +export interface DllDetectionResult { + detected: boolean; + path?: string; + source?: string; + message?: string; + error?: string; +} + +export interface LsfgConfig { + enable_lsfg: boolean; + multiplier: number; + flow_scale: number; + hdr: boolean; + perf_mode: boolean; + immediate_mode: boolean; +} + +export interface ConfigResult { + success: boolean; + config?: LsfgConfig; + error?: string; +} + +export interface ConfigUpdateResult { + success: boolean; + message?: string; + error?: string; +} + +// 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 updateLsfgConfig = callable< + [boolean, number, number, boolean, boolean, boolean], + ConfigUpdateResult +>("update_lsfg_config"); diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx new file mode 100644 index 0000000..707ad2a --- /dev/null +++ b/src/components/ConfigurationSection.tsx @@ -0,0 +1,115 @@ +import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui"; + +interface LsfgConfig { + enableLsfg: boolean; + multiplier: number; + flowScale: number; + hdr: boolean; + perfMode: boolean; + immediateMode: boolean; +} + +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>; +} + +export function ConfigurationSection({ + config, + onEnableLsfgChange, + onMultiplierChange, + onFlowScaleChange, + onHdrChange, + onPerfModeChange, + onImmediateModeChange +}: ConfigurationSectionProps) { + return ( + <> + <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={config.enableLsfg} + onChange={onEnableLsfgChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <SliderField + label="FPS Multiplier" + description="Traditional FPS multiplier value" + value={config.multiplier} + min={2} + max={4} + step={1} + notchCount={3} + notchLabels={[ + { notchIndex: 0, label: "2X" }, + { notchIndex: 1, label: "3X" }, + { notchIndex: 2, label: "4X" } + ]} + onChange={onMultiplierChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <SliderField + label={`Flow Scale ${Math.round(config.flowScale * 100)}%`} + description="Lowers the generated frame's resolution" + value={config.flowScale} + min={0.25} + max={1.0} + step={0.01} + onChange={onFlowScaleChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <ToggleField + label="HDR Mode" + description="Enable HDR mode (only if Game supports HDR)" + checked={config.hdr} + onChange={onHdrChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <ToggleField + label="Performance Mode" + description="Use lighter model for FG (experimental)" + checked={config.perfMode} + onChange={onPerfModeChange} + /> + </PanelSectionRow> + + <PanelSectionRow> + <ToggleField + label="Immediate Mode" + description="Disable vsync for reduced input lag" + checked={config.immediateMode} + onChange={onImmediateModeChange} + /> + </PanelSectionRow> + </> + ); +} diff --git a/src/components/Content.tsx b/src/components/Content.tsx new file mode 100644 index 0000000..cecb142 --- /dev/null +++ b/src/components/Content.tsx @@ -0,0 +1,108 @@ +import { useEffect } from "react"; +import { PanelSection } from "@decky/ui"; +import { useInstallationStatus, useDllDetection, useLsfgConfig } from "../hooks/useLsfgHooks"; +import { useInstallationActions } from "../hooks/useInstallationActions"; +import { StatusDisplay } from "./StatusDisplay"; +import { InstallationButton } from "./InstallationButton"; +import { ConfigurationSection } from "./ConfigurationSection"; +import { UsageInstructions } from "./UsageInstructions"; + +export function Content() { + const { + isInstalled, + installationStatus, + setIsInstalled, + setInstallationStatus + } = useInstallationStatus(); + + const { dllDetected, dllDetectionStatus } = useDllDetection(); + + const { + config, + setters, + loadLsfgConfig, + updateConfig + } = useLsfgConfig(); + + const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); + + // Reload config when installation status changes + useEffect(() => { + if (isInstalled) { + loadLsfgConfig(); + } + }, [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); + }; + + const handleMultiplierChange = async (value: number) => { + setters.setMultiplier(value); + await updateConfig(config.enableLsfg, value, config.flowScale, config.hdr, config.perfMode, config.immediateMode); + }; + + const handleFlowScaleChange = async (value: number) => { + setters.setFlowScale(value); + await updateConfig(config.enableLsfg, config.multiplier, value, config.hdr, config.perfMode, config.immediateMode); + }; + + const handleHdrChange = async (value: boolean) => { + setters.setHdr(value); + await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, value, config.perfMode, config.immediateMode); + }; + + const handlePerfModeChange = async (value: boolean) => { + setters.setPerfMode(value); + await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, value, config.immediateMode); + }; + + const handleImmediateModeChange = async (value: boolean) => { + setters.setImmediateMode(value); + await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, value); + }; + + const onInstall = () => { + handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig); + }; + + const onUninstall = () => { + handleUninstall(setIsInstalled, setInstallationStatus); + }; + + return ( + <PanelSection> + <StatusDisplay + dllDetected={dllDetected} + dllDetectionStatus={dllDetectionStatus} + isInstalled={isInstalled} + installationStatus={installationStatus} + /> + + <InstallationButton + isInstalled={isInstalled} + isInstalling={isInstalling} + isUninstalling={isUninstalling} + onInstall={onInstall} + onUninstall={onUninstall} + /> + + {/* Configuration Section - only show if installed */} + {isInstalled && ( + <ConfigurationSection + config={config} + onEnableLsfgChange={handleEnableLsfgChange} + onMultiplierChange={handleMultiplierChange} + onFlowScaleChange={handleFlowScaleChange} + onHdrChange={handleHdrChange} + onPerfModeChange={handlePerfModeChange} + onImmediateModeChange={handleImmediateModeChange} + /> + )} + + <UsageInstructions multiplier={config.multiplier} /> + </PanelSection> + ); +} diff --git a/src/components/InstallationButton.tsx b/src/components/InstallationButton.tsx new file mode 100644 index 0000000..7892678 --- /dev/null +++ b/src/components/InstallationButton.tsx @@ -0,0 +1,64 @@ +import { ButtonItem, PanelSectionRow } from "@decky/ui"; +import { FaDownload, FaTrash } from "react-icons/fa"; + +interface InstallationButtonProps { + isInstalled: boolean; + isInstalling: boolean; + isUninstalling: boolean; + onInstall: () => void; + onUninstall: () => void; +} + +export function InstallationButton({ + isInstalled, + isInstalling, + isUninstalling, + onInstall, + onUninstall +}: InstallationButtonProps) { + const renderButtonContent = () => { + if (isInstalling) { + return ( + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <div>Installing...</div> + </div> + ); + } + + if (isUninstalling) { + return ( + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <div>Uninstalling...</div> + </div> + ); + } + + if (isInstalled) { + return ( + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <FaTrash /> + <div>Uninstall lsfg-vk</div> + </div> + ); + } + + return ( + <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> + <FaDownload /> + <div>Install lsfg-vk</div> + </div> + ); + }; + + return ( + <PanelSectionRow> + <ButtonItem + layout="below" + onClick={isInstalled ? onUninstall : onInstall} + disabled={isInstalling || isUninstalling} + > + {renderButtonContent()} + </ButtonItem> + </PanelSectionRow> + ); +} diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx new file mode 100644 index 0000000..8eecd42 --- /dev/null +++ b/src/components/StatusDisplay.tsx @@ -0,0 +1,38 @@ +import { PanelSectionRow } from "@decky/ui"; + +interface StatusDisplayProps { + dllDetected: boolean; + dllDetectionStatus: string; + isInstalled: boolean; + installationStatus: string; +} + +export function StatusDisplay({ + dllDetected, + dllDetectionStatus, + isInstalled, + installationStatus +}: StatusDisplayProps) { + return ( + <PanelSectionRow> + <div style={{ marginBottom: "8px", fontSize: "14px" }}> + <div + style={{ + color: dllDetected ? "#4CAF50" : "#F44336", + fontWeight: "bold", + marginBottom: "4px" + }} + > + {dllDetectionStatus} + </div> + <div + style={{ + color: isInstalled ? "#4CAF50" : "#FF9800" + }} + > + Status: {installationStatus} + </div> + </div> + </PanelSectionRow> + ); +} diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx new file mode 100644 index 0000000..712d4c1 --- /dev/null +++ b/src/components/UsageInstructions.tsx @@ -0,0 +1,68 @@ +import { PanelSectionRow } from "@decky/ui"; + +interface UsageInstructionsProps { + multiplier: number; +} + +export function UsageInstructions({ multiplier }: UsageInstructionsProps) { + return ( + <PanelSectionRow> + <div + style={{ + fontSize: "13px", + marginTop: "12px", + padding: "8px", + backgroundColor: "rgba(255, 255, 255, 0.05)", + borderRadius: "4px" + }} + > + <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" + }} + > + 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.25-1.0 - Flow scale (for performance) + <br /> + • LSFG_HDR=1 - HDR mode (only if using HDR) + <br /> + • MESA_VK_WSI_PRESENT_MODE=immediate - Disable vsync + </div> + </div> + </PanelSectionRow> + ); +} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..77f11d4 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,5 @@ +export { Content } from "./Content"; +export { StatusDisplay } from "./StatusDisplay"; +export { InstallationButton } from "./InstallationButton"; +export { ConfigurationSection } from "./ConfigurationSection"; +export { UsageInstructions } from "./UsageInstructions"; diff --git a/src/hooks/useInstallationActions.ts b/src/hooks/useInstallationActions.ts new file mode 100644 index 0000000..8dcf831 --- /dev/null +++ b/src/hooks/useInstallationActions.ts @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { toaster } from "@decky/api"; +import { installLsfgVk, uninstallLsfgVk } from "../api/lsfgApi"; + +export function useInstallationActions() { + const [isInstalling, setIsInstalling] = useState<boolean>(false); + const [isUninstalling, setIsUninstalling] = useState<boolean>(false); + + const handleInstall = async ( + setIsInstalled: (value: boolean) => void, + setInstallationStatus: (value: string) => void, + reloadConfig?: () => Promise<void> + ) => { + setIsInstalling(true); + setInstallationStatus("Installing lsfg-vk..."); + + try { + const result = await installLsfgVk(); + if (result.success) { + setIsInstalled(true); + setInstallationStatus("lsfg-vk installed successfully!"); + toaster.toast({ + title: "Installation Complete", + body: "lsfg-vk has been installed successfully" + }); + + // Reload lsfg config after installation + if (reloadConfig) { + await reloadConfig(); + } + } else { + setInstallationStatus(`Installation failed: ${result.error}`); + toaster.toast({ + title: "Installation Failed", + body: result.error || "Unknown error occurred" + }); + } + } catch (error) { + setInstallationStatus(`Installation failed: ${error}`); + toaster.toast({ + title: "Installation Failed", + body: `Error: ${error}` + }); + } finally { + setIsInstalling(false); + } + }; + + const handleUninstall = async ( + setIsInstalled: (value: boolean) => void, + setInstallationStatus: (value: string) => void + ) => { + setIsUninstalling(true); + setInstallationStatus("Uninstalling lsfg-vk..."); + + try { + const result = await uninstallLsfgVk(); + if (result.success) { + setIsInstalled(false); + setInstallationStatus("lsfg-vk uninstalled successfully!"); + toaster.toast({ + title: "Uninstallation Complete", + body: result.message || "lsfg-vk has been uninstalled successfully" + }); + } else { + setInstallationStatus(`Uninstallation failed: ${result.error}`); + toaster.toast({ + title: "Uninstallation Failed", + body: result.error || "Unknown error occurred" + }); + } + } catch (error) { + setInstallationStatus(`Uninstallation failed: ${error}`); + toaster.toast({ + title: "Uninstallation Failed", + body: `Error: ${error}` + }); + } finally { + setIsUninstalling(false); + } + }; + + return { + isInstalling, + isUninstalling, + handleInstall, + handleUninstall + }; +} diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts new file mode 100644 index 0000000..76acd33 --- /dev/null +++ b/src/hooks/useLsfgHooks.ts @@ -0,0 +1,156 @@ +import { useState, useEffect } from "react"; +import { toaster } from "@decky/api"; +import { + checkLsfgVkInstalled, + checkLosslessScalingDll, + getLsfgConfig, + updateLsfgConfig, + type ConfigUpdateResult +} from "../api/lsfgApi"; + +export function useInstallationStatus() { + const [isInstalled, setIsInstalled] = useState<boolean>(false); + const [installationStatus, setInstallationStatus] = useState<string>(""); + + const checkInstallation = async () => { + try { + const status = await checkLsfgVkInstalled(); + setIsInstalled(status.installed); + if (status.installed) { + setInstallationStatus("lsfg-vk is installed"); + } else { + setInstallationStatus("lsfg-vk is not installed"); + } + return status.installed; + } catch (error) { + setInstallationStatus("Error checking installation status"); + return false; + } + }; + + useEffect(() => { + checkInstallation(); + }, []); + + return { + isInstalled, + installationStatus, + setIsInstalled, + setInstallationStatus, + checkInstallation + }; +} + +export function useDllDetection() { + const [dllDetected, setDllDetected] = useState<boolean>(false); + const [dllDetectionStatus, setDllDetectionStatus] = useState<string>(""); + + const checkDllDetection = async () => { + try { + const result = await checkLosslessScalingDll(); + setDllDetected(result.detected); + if (result.detected) { + setDllDetectionStatus(`Lossless Scaling App detected (${result.source})`); + } else { + setDllDetectionStatus(result.message || "Lossless Scaling App not detected"); + } + } catch (error) { + setDllDetectionStatus("Error checking Lossless Scaling App"); + } + }; + + useEffect(() => { + checkDllDetection(); + }, []); + + return { + dllDetected, + dllDetectionStatus + }; +} + +export function useLsfgConfig() { + 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); + const [perfMode, setPerfMode] = useState<boolean>(false); + const [immediateMode, setImmediateMode] = useState<boolean>(false); + + 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); + setPerfMode(result.config.perf_mode); + setImmediateMode(result.config.immediate_mode); + console.log("Loaded lsfg config:", result.config); + } else { + console.log("lsfg config not available, using defaults:", result.error); + } + } catch (error) { + console.error("Error loading lsfg config:", error); + } + }; + + const updateConfig = async ( + newEnableLsfg: boolean, + newMultiplier: number, + newFlowScale: number, + newHdr: boolean, + newPerfMode: boolean, + newImmediateMode: boolean + ): Promise<ConfigUpdateResult> => { + try { + const result = await updateLsfgConfig( + newEnableLsfg, + newMultiplier, + newFlowScale, + newHdr, + newPerfMode, + newImmediateMode + ); + if (!result.success) { + toaster.toast({ + title: "Update Failed", + body: result.error || "Failed to update configuration" + }); + } + return result; + } catch (error) { + toaster.toast({ + title: "Update Failed", + body: `Error: ${error}` + }); + return { success: false, error: String(error) }; + } + }; + + useEffect(() => { + loadLsfgConfig(); + }, []); + + return { + config: { + enableLsfg, + multiplier, + flowScale, + hdr, + perfMode, + immediateMode + }, + setters: { + setEnableLsfg, + setMultiplier, + setFlowScale, + setHdr, + setPerfMode, + setImmediateMode + }, + loadLsfgConfig, + updateConfig + }; +} diff --git a/src/index.tsx b/src/index.tsx index 2b895b3..e8ab56f 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,448 +1,10 @@ -import { - ButtonItem, - PanelSection, - PanelSectionRow, - staticClasses, - ToggleField, - SliderField -} from "@decky/ui"; -import { - callable, - definePlugin, - toaster, -} from "@decky/api" -import { useState, useEffect } from "react"; -import { FaDownload, FaTrash } from "react-icons/fa"; +import { staticClasses } from "@decky/ui"; +import { definePlugin } from "@decky/api"; import { GiPlasticDuck } from "react-icons/gi"; - -// Function to install lsfg-vk -const installLsfgVk = callable<[], { success: boolean; error?: string; message?: string }>("install_lsfg_vk"); - -// Function to uninstall lsfg-vk -const uninstallLsfgVk = callable<[], { success: boolean; error?: string; message?: string; removed_files?: string[] }>("uninstall_lsfg_vk"); - -// Function to check if lsfg-vk is installed -const checkLsfgVkInstalled = callable<[], { installed: boolean; lib_exists: boolean; json_exists: boolean; script_exists: boolean; lib_path: string; json_path: string; script_path: string; error?: string }>("check_lsfg_vk_installed"); - -// 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; perf_mode: boolean; immediate_mode: boolean }; error?: string }>("get_lsfg_config"); - -// Function to update lsfg configuration -const updateLsfgConfig = callable<[boolean, number, number, boolean, boolean, boolean], { success: boolean; message?: string; error?: string }>("update_lsfg_config"); - -function Content() { - const [isInstalled, setIsInstalled] = useState<boolean>(false); - const [isInstalling, setIsInstalling] = useState<boolean>(false); - const [isUninstalling, setIsUninstalling] = useState<boolean>(false); - const [installationStatus, setInstallationStatus] = useState<string>(""); - 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); - const [perfMode, setPerfMode] = useState<boolean>(false); - const [immediateMode, setImmediateMode] = useState<boolean>(false); - - // Check installation status on component mount - useEffect(() => { - const checkInstallation = async () => { - try { - const status = await checkLsfgVkInstalled(); - setIsInstalled(status.installed); - if (status.installed) { - setInstallationStatus("lsfg-vk is installed"); - } else { - setInstallationStatus("lsfg-vk is not installed"); - } - } catch (error) { - setInstallationStatus("Error checking installation status"); - } - }; - - const checkDllDetection = async () => { - try { - const result = await checkLosslessScalingDll(); - setDllDetected(result.detected); - if (result.detected) { - setDllDetectionStatus(`Lossless Scaling App detected (${result.source})`); - } else { - setDllDetectionStatus(result.message || "Lossless Scaling App not detected"); - } - } catch (error) { - setDllDetectionStatus("Error checking Lossless Scaling App"); - } - }; - - 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); - setPerfMode(result.config.perf_mode); - setImmediateMode(result.config.immediate_mode); - console.log("Loaded lsfg config:", result.config); - } else { - // If script doesn't exist or can't be read, keep default values - console.log("lsfg config not available, using defaults:", result.error); - } - } catch (error) { - console.error("Error loading lsfg config:", error); - } - }; - - checkInstallation(); - checkDllDetection(); - - // Always try to load config, regardless of installation status - // This handles cases where the script exists but plugin shows as not installed - loadLsfgConfig(); - }, []); - - // Add a second useEffect to reload config when isInstalled changes - // This ensures UI reflects script state when plugin detects installation - useEffect(() => { - if (isInstalled) { - const reloadConfig = 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); - console.log("Reloaded lsfg config after installation detected:", result.config); - } - } catch (error) { - console.error("Error reloading lsfg config:", error); - } - }; - reloadConfig(); - } - }, [isInstalled]); const handleInstall = async () => { - setIsInstalling(true); - setInstallationStatus("Installing lsfg-vk..."); - - try { - const result = await installLsfgVk(); - if (result.success) { - setIsInstalled(true); - setInstallationStatus("lsfg-vk installed successfully!"); - toaster.toast({ - 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); - setPerfMode(configResult.config.perf_mode); - setImmediateMode(configResult.config.immediate_mode); - } - } catch (error) { - console.error("Error reloading config after install:", error); - } - } else { - setInstallationStatus(`Installation failed: ${result.error}`); - toaster.toast({ - title: "Installation Failed", - body: result.error || "Unknown error occurred" - }); - } - } catch (error) { - setInstallationStatus(`Installation failed: ${error}`); - toaster.toast({ - title: "Installation Failed", - body: `Error: ${error}` - }); - } finally { - setIsInstalling(false); - } - }; - - const handleUninstall = async () => { - setIsUninstalling(true); - setInstallationStatus("Uninstalling lsfg-vk..."); - - try { - const result = await uninstallLsfgVk(); - if (result.success) { - setIsInstalled(false); - setInstallationStatus("lsfg-vk uninstalled successfully!"); - toaster.toast({ - title: "Uninstallation Complete", - body: result.message || "lsfg-vk has been uninstalled successfully" - }); - } else { - setInstallationStatus(`Uninstallation failed: ${result.error}`); - toaster.toast({ - title: "Uninstallation Failed", - body: result.error || "Unknown error occurred" - }); - } - } catch (error) { - setInstallationStatus(`Uninstallation failed: ${error}`); - toaster.toast({ - title: "Uninstallation Failed", - body: `Error: ${error}` - }); - } finally { - setIsUninstalling(false); - } - }; - - const updateConfig = async (newEnableLsfg: boolean, newMultiplier: number, newFlowScale: number, newHdr: boolean, newPerfMode: boolean, newImmediateMode: boolean) => { - try { - const result = await updateLsfgConfig(newEnableLsfg, newMultiplier, newFlowScale, newHdr, newPerfMode, newImmediateMode); - if (!result.success) { - toaster.toast({ - title: "Update Failed", - body: result.error || "Failed to update configuration" - }); - } - // Only show error notifications, not success notifications to avoid spam - } catch (error) { - toaster.toast({ - title: "Update Failed", - body: `Error: ${error}` - }); - } - }; - - const handleEnableLsfgChange = async (value: boolean) => { - setEnableLsfg(value); - await updateConfig(value, multiplier, flowScale, hdr, perfMode, immediateMode); - }; - - const handleMultiplierChange = async (value: number) => { - setMultiplier(value); - await updateConfig(enableLsfg, value, flowScale, hdr, perfMode, immediateMode); - }; - - const handleFlowScaleChange = async (value: number) => { - setFlowScale(value); - await updateConfig(enableLsfg, multiplier, value, hdr, perfMode, immediateMode); - }; - - const handleHdrChange = async (value: boolean) => { - setHdr(value); - await updateConfig(enableLsfg, multiplier, flowScale, value, perfMode, immediateMode); - }; - - const handlePerfModeChange = async (value: boolean) => { - setPerfMode(value); - await updateConfig(enableLsfg, multiplier, flowScale, hdr, value, immediateMode); - }; - - const handleImmediateModeChange = async (value: boolean) => { - setImmediateMode(value); - await updateConfig(enableLsfg, multiplier, flowScale, hdr, perfMode, value); - }; - - return ( - <PanelSection> - <PanelSectionRow> - <div style={{ marginBottom: "8px", fontSize: "14px" }}> - <div style={{ - color: dllDetected ? "#4CAF50" : "#F44336", - fontWeight: "bold", - marginBottom: "4px" - }}> - {dllDetectionStatus} - </div> - <div style={{ - color: isInstalled ? "#4CAF50" : "#FF9800" - }}> - Status: {installationStatus} - </div> - </div> - </PanelSectionRow> - - <PanelSectionRow> - <ButtonItem - layout="below" - onClick={isInstalled ? handleUninstall : handleInstall} - disabled={isInstalling || isUninstalling} - > - {isInstalling ? ( - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> - <div>Installing...</div> - </div> - ) : isUninstalling ? ( - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> - <div>Uninstalling...</div> - </div> - ) : isInstalled ? ( - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> - <FaTrash /> - <div>Uninstall lsfg-vk</div> - </div> - ) : ( - <div style={{ display: "flex", alignItems: "center", gap: "8px" }}> - <FaDownload /> - <div>Install lsfg-vk</div> - </div> - )} - </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 ${Math.round(flowScale * 100)}%`} - 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> - <ToggleField - label="Performance Mode" - description="Enable performance mode (can quintuple performance)" - checked={perfMode} - onChange={handlePerfModeChange} - /> - </PanelSectionRow> - - <PanelSectionRow> - <ToggleField - label="Immediate Mode" - description="Disable vsync for reduced input lag" - checked={immediateMode} - onChange={handleImmediateModeChange} - /> - </PanelSectionRow> - </> - )} - - <PanelSectionRow> - <div style={{ - fontSize: "13px", - marginTop: "12px", - padding: "8px", - backgroundColor: "rgba(255, 255, 255, 0.05)", - borderRadius: "4px" - }}> - <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" - }}> - 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.25-1.0 - Flow scale (for performance) - <br /> - • LSFG_HDR=1 - HDR mode (only if using HDR) - <br /> - • MESA_VK_WSI_PRESENT_MODE=immediate - Disable vsync - </div> - </div> - </PanelSectionRow> - </PanelSection> - ); -}; +import { Content } from "./components"; export default definePlugin(() => { - console.log("Lossless Scaling plugin initializing") + console.log("Lossless Scaling plugin initializing"); return { // The name shown in various decky menus @@ -455,7 +17,7 @@ export default definePlugin(() => { icon: <GiPlasticDuck />, // The function triggered when your plugin unloads onDismount() { - console.log("Lossless Scaling unloading") - }, + console.log("Lossless Scaling unloading"); + } }; }); diff --git a/src/plugin_lifecycle.py b/src/plugin_lifecycle.py new file mode 100644 index 0000000..4fb5c69 --- /dev/null +++ b/src/plugin_lifecycle.py @@ -0,0 +1,47 @@ +import os +import decky +from .services import InstallationService + + +class PluginLifecycleManager: + """Manages plugin lifecycle events""" + + def __init__(self): + self.installation_service = InstallationService() + + async def on_load(self): + """Called when plugin is loaded""" + decky.logger.info("Lossless Scaling loaded!") + + async def on_unload(self): + """Called when plugin is unloaded""" + decky.logger.info("Lossless Scaling unloading") + + async def on_uninstall(self): + """Called when plugin is uninstalled""" + decky.logger.info("Lossless Scaling uninstalled - starting cleanup") + + # Clean up lsfg-vk files when the plugin is uninstalled + self.installation_service.cleanup_on_uninstall() + + decky.logger.info("Lossless Scaling uninstall cleanup completed") + + async def on_migration(self): + """Called during plugin migration""" + decky.logger.info("Migrating") + # Here's a migration example for logs: + # - `~/.config/decky-template/template.log` will be migrated to `decky.decky_LOG_DIR/template.log` + decky.migrate_logs(os.path.join(decky.DECKY_USER_HOME, + ".config", "decky-template", "template.log")) + # Here's a migration example for settings: + # - `~/homebrew/settings/template.json` is migrated to `decky.decky_SETTINGS_DIR/template.json` + # - `~/.config/decky-template/` all files and directories under this root are migrated to `decky.decky_SETTINGS_DIR/` + decky.migrate_settings( + os.path.join(decky.DECKY_HOME, "settings", "template.json"), + os.path.join(decky.DECKY_USER_HOME, ".config", "decky-template")) + # Here's a migration example for runtime data: + # - `~/homebrew/template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/` + # - `~/.local/share/decky-template/` all files and directories under this root are migrated to `decky.decky_RUNTIME_DIR/` + decky.migrate_runtime( + os.path.join(decky.DECKY_HOME, "template"), + os.path.join(decky.DECKY_USER_HOME, ".local", "share", "decky-template")) diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..a00af5c --- /dev/null +++ b/src/services/__init__.py @@ -0,0 +1,5 @@ +from .installation_service import InstallationService +from .dll_detection_service import DllDetectionService +from .configuration_service import ConfigurationService + +__all__ = ["InstallationService", "DllDetectionService", "ConfigurationService"] diff --git a/src/services/configuration_service.py b/src/services/configuration_service.py new file mode 100644 index 0000000..a4975c5 --- /dev/null +++ b/src/services/configuration_service.py @@ -0,0 +1,164 @@ +import os +from typing import Dict, Any +import decky + + +class ConfigurationService: + """Service for managing lsfg script configuration""" + + def __init__(self): + self.user_home = os.path.expanduser("~") + self.lsfg_script_path = os.path.join(self.user_home, "lsfg") + + async def get_config(self) -> Dict[str, Any]: + """Read current lsfg script configuration""" + try: + if not os.path.exists(self.lsfg_script_path): + return { + "success": False, + "error": "lsfg script not found" + } + + with open(self.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, + "perf_mode": False, + "immediate_mode": False + } + + lines = content.split('\n') + for line in lines: + line = line.strip() + + # Handle ENABLE_LSFG - check if it's commented out or not + if line.startswith('export ENABLE_LSFG='): + try: + value = line.split('=')[1].strip() + config["enable_lsfg"] = value == '1' + except: + pass + elif line.startswith('# export ENABLE_LSFG='): + config["enable_lsfg"] = False + + # Handle LSFG_MULTIPLIER + elif line.startswith('export LSFG_MULTIPLIER='): + try: + value = line.split('=')[1].strip() + config["multiplier"] = int(value) + except: + pass + + # Handle LSFG_FLOW_SCALE + elif line.startswith('export LSFG_FLOW_SCALE='): + try: + value = line.split('=')[1].strip() + config["flow_scale"] = float(value) + except: + pass + + # Handle LSFG_HDR - check if it's commented out or not + elif line.startswith('export LSFG_HDR='): + try: + value = line.split('=')[1].strip() + config["hdr"] = value == '1' + except: + pass + elif line.startswith('# export LSFG_HDR='): + config["hdr"] = False + + # Handle LSFG_PERF_MODE - check if it's commented out or not + elif line.startswith('export LSFG_PERF_MODE='): + try: + value = line.split('=')[1].strip() + config["perf_mode"] = value == '1' + except: + pass + elif line.startswith('# export LSFG_PERF_MODE='): + config["perf_mode"] = False + + # Handle MESA_VK_WSI_PRESENT_MODE - check if it's commented out or not + elif line.startswith('export MESA_VK_WSI_PRESENT_MODE='): + try: + value = line.split('=')[1].strip() + # Remove any comments after the value + value = value.split('#')[0].strip() + config["immediate_mode"] = value == 'immediate' + except: + pass + elif line.startswith('# export MESA_VK_WSI_PRESENT_MODE='): + config["immediate_mode"] = False + + decky.logger.info(f"Parsed lsfg config: {config}") + + 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_config(self, enable_lsfg: bool, multiplier: int, flow_scale: float, + hdr: bool, perf_mode: bool, immediate_mode: bool) -> Dict[str, Any]: + """Update lsfg script configuration""" + try: + # 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" + + if perf_mode: + script_content += "export LSFG_PERF_MODE=1\n" + else: + script_content += "# export LSFG_PERF_MODE=1\n" + + if immediate_mode: + script_content += "export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n" + else: + script_content += "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync\n" + + # Add the exec line to allow the script to execute passed commands + script_content += "\n# Execute the passed command with the environment variables set\n" + script_content += "exec \"$@\"\n" + + # Write the updated script + with open(self.lsfg_script_path, 'w') as f: + f.write(script_content) + + # Make sure it's executable + os.chmod(self.lsfg_script_path, 0o755) + + decky.logger.info(f"Updated lsfg script configuration: enable={enable_lsfg}, multiplier={multiplier}, flow_scale={flow_scale}, hdr={hdr}, perf_mode={perf_mode}, immediate_mode={immediate_mode}") + + 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) + } diff --git a/src/services/dll_detection_service.py b/src/services/dll_detection_service.py new file mode 100644 index 0000000..095b3b5 --- /dev/null +++ b/src/services/dll_detection_service.py @@ -0,0 +1,60 @@ +import os +from typing import Dict, Any +import decky + + +class DllDetectionService: + """Service for detecting Lossless Scaling DLL""" + + async def check_lossless_scaling_dll(self) -> Dict[str, Any]: + """Check if Lossless Scaling DLL is available at the expected paths""" + try: + # Check environment variable first + dll_path = os.getenv("LSFG_DLL_PATH") + if dll_path and dll_path.strip(): + dll_path_str = dll_path.strip() + if os.path.exists(dll_path_str): + return { + "detected": True, + "path": dll_path_str, + "source": "LSFG_DLL_PATH environment variable" + } + + # Check XDG_DATA_HOME path + data_dir = os.getenv("XDG_DATA_HOME") + if data_dir and data_dir.strip(): + dll_path_str = os.path.join(data_dir.strip(), "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll") + if os.path.exists(dll_path_str): + return { + "detected": True, + "path": dll_path_str, + "source": "XDG_DATA_HOME Steam directory" + } + + # Check HOME/.local/share path + home_dir = os.getenv("HOME") + if home_dir and home_dir.strip(): + dll_path_str = os.path.join(home_dir.strip(), ".local", "share", "Steam", "steamapps", "common", "Lossless Scaling", "Lossless.dll") + if os.path.exists(dll_path_str): + return { + "detected": True, + "path": dll_path_str, + "source": "HOME/.local/share Steam directory" + } + + # DLL not found in any expected location + return { + "detected": False, + "path": None, + "source": None, + "message": "Lossless Scaling DLL not found in expected locations" + } + + except Exception as e: + decky.logger.error(f"Error checking Lossless Scaling DLL: {str(e)}") + return { + "detected": False, + "path": None, + "source": None, + "error": str(e) + } diff --git a/src/services/installation_service.py b/src/services/installation_service.py new file mode 100644 index 0000000..92d5132 --- /dev/null +++ b/src/services/installation_service.py @@ -0,0 +1,205 @@ +import os +import zipfile +import shutil +import tempfile +from typing import Dict, Any +import decky + + +class InstallationService: + """Service for handling lsfg-vk installation and uninstallation""" + + def __init__(self): + self.user_home = os.path.expanduser("~") + self.local_lib_dir = os.path.join(self.user_home, ".local", "lib") + self.local_share_dir = os.path.join(self.user_home, ".local", "share", "vulkan", "implicit_layer.d") + self.lsfg_script_path = os.path.join(self.user_home, "lsfg") + + # File paths + self.lib_file = os.path.join(self.local_lib_dir, "liblsfg-vk.so") + self.json_file = os.path.join(self.local_share_dir, "VkLayer_LS_frame_generation.json") + + async def install(self) -> Dict[str, Any]: + """Install lsfg-vk by extracting the zip file to ~/.local""" + try: + # Get the path to the lsfg-vk_archlinux.zip file in the bin directory + plugin_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + zip_path = os.path.join(plugin_dir, "bin", "lsfg-vk_archlinux.zip") + + # Check if the zip file exists + if not os.path.exists(zip_path): + decky.logger.error(f"lsfg-vk_archlinux.zip not found at {zip_path}") + return {"success": False, "error": "lsfg-vk_archlinux.zip file not found"} + + # Create directories if they don't exist + os.makedirs(self.local_lib_dir, exist_ok=True) + os.makedirs(self.local_share_dir, exist_ok=True) + + # Extract the zip file + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + # Use /tmp for temporary extraction since we may not have write permissions in plugin dir + with tempfile.TemporaryDirectory() as temp_dir: + zip_ref.extractall(temp_dir) + + # Look for the extracted files and copy them to the correct locations + for root, dirs, files in os.walk(temp_dir): + for file in files: + src_file = os.path.join(root, file) + if file.endswith('.so'): + # Copy library files to ~/.local/lib + dst_file = os.path.join(self.local_lib_dir, file) + shutil.copy2(src_file, dst_file) + decky.logger.info(f"Copied {file} to {dst_file}") + elif file.endswith('.json'): + # Copy JSON files to ~/.local/share/vulkan/implicit_layer.d + dst_file = os.path.join(self.local_share_dir, file) + shutil.copy2(src_file, dst_file) + decky.logger.info(f"Copied {file} to {dst_file}") + + # Create the lsfg script + self._create_lsfg_script() + + decky.logger.info("lsfg-vk installed successfully") + return {"success": True, "message": "lsfg-vk installed successfully"} + + except Exception as e: + decky.logger.error(f"Error installing lsfg-vk: {str(e)}") + return {"success": False, "error": str(e)} + + def _create_lsfg_script(self): + """Create the lsfg script in home directory""" + script_content = """#!/bin/bash + +export ENABLE_LSFG=1 +export LSFG_MULTIPLIER=2 +export LSFG_FLOW_SCALE=1.0 +# export LSFG_HDR=1 +# export LSFG_PERF_MODE=1 +# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync + +# Execute the passed command with the environment variables set +exec "$@" +""" + + with open(self.lsfg_script_path, 'w') as script_file: + script_file.write(script_content) + + # Make the script executable + os.chmod(self.lsfg_script_path, 0o755) + decky.logger.info(f"Created executable lsfg script at {self.lsfg_script_path}") + + async def check_installation(self) -> Dict[str, Any]: + """Check if lsfg-vk is already installed""" + try: + lib_exists = os.path.exists(self.lib_file) + json_exists = os.path.exists(self.json_file) + script_exists = os.path.exists(self.lsfg_script_path) + + decky.logger.info(f"Installation check: lib={lib_exists}, json={json_exists}, script={script_exists}") + + return { + "installed": lib_exists and json_exists, + "lib_exists": lib_exists, + "json_exists": json_exists, + "script_exists": script_exists, + "lib_path": self.lib_file, + "json_path": self.json_file, + "script_path": self.lsfg_script_path + } + + except Exception as e: + decky.logger.error(f"Error checking lsfg-vk installation: {str(e)}") + return {"installed": False, "error": str(e)} + + async def uninstall(self) -> Dict[str, Any]: + """Uninstall lsfg-vk by removing the installed files""" + try: + removed_files = [] + + # Remove library file if it exists + if os.path.exists(self.lib_file): + os.remove(self.lib_file) + removed_files.append(self.lib_file) + decky.logger.info(f"Removed {self.lib_file}") + + # Remove JSON file if it exists + if os.path.exists(self.json_file): + os.remove(self.json_file) + removed_files.append(self.json_file) + decky.logger.info(f"Removed {self.json_file}") + + # Remove lsfg script if it exists + if os.path.exists(self.lsfg_script_path): + os.remove(self.lsfg_script_path) + removed_files.append(self.lsfg_script_path) + decky.logger.info(f"Removed {self.lsfg_script_path}") + + if not removed_files: + return {"success": True, "message": "No lsfg-vk files found to remove"} + + decky.logger.info("lsfg-vk uninstalled successfully") + return { + "success": True, + "message": f"lsfg-vk uninstalled successfully. Removed {len(removed_files)} files.", + "removed_files": removed_files + } + + except Exception as e: + decky.logger.error(f"Error uninstalling lsfg-vk: {str(e)}") + return {"success": False, "error": str(e)} + + def cleanup_on_uninstall(self) -> None: + """Clean up lsfg-vk files when the plugin is uninstalled""" + try: + decky.logger.info(f"Checking for lsfg-vk files to clean up:") + decky.logger.info(f" Library file: {self.lib_file}") + decky.logger.info(f" JSON file: {self.json_file}") + decky.logger.info(f" lsfg script: {self.lsfg_script_path}") + + removed_files = [] + + # Remove library file if it exists + if os.path.exists(self.lib_file): + decky.logger.info(f"Found library file, attempting to remove: {self.lib_file}") + try: + os.remove(self.lib_file) + removed_files.append(self.lib_file) + decky.logger.info(f"Successfully removed {self.lib_file}") + except Exception as e: + decky.logger.error(f"Failed to remove {self.lib_file}: {str(e)}") + else: + decky.logger.info(f"Library file not found: {self.lib_file}") + + # Remove JSON file if it exists + if os.path.exists(self.json_file): + decky.logger.info(f"Found JSON file, attempting to remove: {self.json_file}") + try: + os.remove(self.json_file) + removed_files.append(self.json_file) + decky.logger.info(f"Successfully removed {self.json_file}") + except Exception as e: + decky.logger.error(f"Failed to remove {self.json_file}: {str(e)}") + else: + decky.logger.info(f"JSON file not found: {self.json_file}") + + # Remove lsfg script if it exists + if os.path.exists(self.lsfg_script_path): + decky.logger.info(f"Found lsfg script, attempting to remove: {self.lsfg_script_path}") + try: + os.remove(self.lsfg_script_path) + removed_files.append(self.lsfg_script_path) + decky.logger.info(f"Successfully removed {self.lsfg_script_path}") + except Exception as e: + decky.logger.error(f"Failed to remove {self.lsfg_script_path}: {str(e)}") + else: + decky.logger.info(f"lsfg script not found: {self.lsfg_script_path}") + + if removed_files: + decky.logger.info(f"Cleaned up {len(removed_files)} lsfg-vk files during plugin uninstall: {removed_files}") + else: + decky.logger.info("No lsfg-vk files found to clean up during plugin uninstall") + + except Exception as e: + decky.logger.error(f"Error cleaning up lsfg-vk files during uninstall: {str(e)}") + import traceback + decky.logger.error(f"Traceback: {traceback.format_exc()}") |
