From 7868396718b13443209e7c5d83a2c96cd7eee31e Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 16 Jul 2025 13:54:09 -0400 Subject: centralized configuration system for lsfg-vk params --- src/api/lsfgApi.ts | 28 +++-- src/components/ConfigurationSection.tsx | 64 ++++------- src/components/Content.tsx | 55 ++-------- src/components/UsageInstructions.tsx | 30 ++---- src/config/configSchema.ts | 186 ++++++++++++++++++++++++++++++++ src/hooks/useLsfgHooks.ts | 91 +++++----------- 6 files changed, 266 insertions(+), 188 deletions(-) create mode 100644 src/config/configSchema.ts (limited to 'src') diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 31ecbb5..2e7964c 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -1,4 +1,5 @@ import { callable } from "@decky/api"; +import { ConfigurationData, ConfigurationManager } from "../config/configSchema"; // Type definitions for API responses export interface InstallationResult { @@ -27,16 +28,8 @@ export interface DllDetectionResult { error?: string; } -export interface LsfgConfig { - enable_lsfg: boolean; - multiplier: number; - flow_scale: number; - hdr: boolean; - perf_mode: boolean; - immediate_mode: boolean; - disable_vkbasalt: boolean; - frame_cap: number; -} +// Use centralized configuration data type +export type LsfgConfig = ConfigurationData; export interface ConfigResult { success: boolean; @@ -50,13 +43,28 @@ export interface ConfigUpdateResult { error?: string; } +export interface ConfigSchemaResult { + field_names: string[]; + field_types: Record; + defaults: ConfigurationData; +} + // API functions export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk"); export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk"); export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg_vk_installed"); export const checkLosslessScalingDll = callable<[], DllDetectionResult>("check_lossless_scaling_dll"); export const getLsfgConfig = callable<[], ConfigResult>("get_lsfg_config"); +export const getConfigSchema = callable<[], ConfigSchemaResult>("get_config_schema"); + +// Updated config function using centralized configuration export const updateLsfgConfig = callable< [boolean, number, number, boolean, boolean, boolean, boolean, number], ConfigUpdateResult >("update_lsfg_config"); + +// Helper function to create config update from configuration object +export const updateLsfgConfigFromObject = async (config: ConfigurationData): Promise => { + const args = ConfigurationManager.createArgsFromConfig(config); + return updateLsfgConfig(...args as [boolean, number, number, boolean, boolean, boolean, boolean, number]); +}; diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index 11bb6b9..2545217 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -1,38 +1,14 @@ import { PanelSectionRow, ToggleField, SliderField } from "@decky/ui"; - -interface LsfgConfig { - enableLsfg: boolean; - multiplier: number; - flowScale: number; - hdr: boolean; - perfMode: boolean; - immediateMode: boolean; - disableVkbasalt: boolean; - frameCap: number; -} +import { ConfigurationData } from "../config/configSchema"; interface ConfigurationSectionProps { - config: LsfgConfig; - onEnableLsfgChange: (value: boolean) => Promise; - onMultiplierChange: (value: number) => Promise; - onFlowScaleChange: (value: number) => Promise; - onHdrChange: (value: boolean) => Promise; - onPerfModeChange: (value: boolean) => Promise; - onImmediateModeChange: (value: boolean) => Promise; - onDisableVkbasaltChange: (value: boolean) => Promise; - onFrameCapChange: (value: number) => Promise; + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number) => Promise; } export function ConfigurationSection({ config, - onEnableLsfgChange, - onMultiplierChange, - onFlowScaleChange, - onHdrChange, - onPerfModeChange, - onImmediateModeChange, - onDisableVkbasaltChange, - onFrameCapChange + onConfigChange }: ConfigurationSectionProps) { return ( <> @@ -55,8 +31,8 @@ export function ConfigurationSection({ onConfigChange('enable_lsfg', value)} /> @@ -74,19 +50,19 @@ export function ConfigurationSection({ { notchIndex: 1, label: "3X" }, { notchIndex: 2, label: "4X" } ]} - onChange={onMultiplierChange} + onChange={(value) => onConfigChange('multiplier', value)} /> onConfigChange('flow_scale', value)} /> @@ -95,7 +71,7 @@ export function ConfigurationSection({ label="HDR Mode" description="Enable HDR mode (only if Game supports HDR)" checked={config.hdr} - onChange={onHdrChange} + onChange={(value) => onConfigChange('hdr', value)} /> @@ -103,8 +79,8 @@ export function ConfigurationSection({ onConfigChange('perf_mode', value)} /> @@ -112,20 +88,20 @@ export function ConfigurationSection({ onConfigChange('immediate_mode', value)} /> onConfigChange('frame_cap', value)} /> @@ -133,8 +109,8 @@ export function ConfigurationSection({ onConfigChange('disable_vkbasalt', value)} /> */} diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 895b2fc..ba651d4 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -8,6 +8,7 @@ import { ConfigurationSection } from "./ConfigurationSection"; import { UsageInstructions } from "./UsageInstructions"; import { WikiButton } from "./WikiButton"; import { ClipboardButton } from "./ClipboardButton"; +import { ConfigurationData } from "../config/configSchema"; export function Content() { const { @@ -21,9 +22,8 @@ export function Content() { const { config, - setters, loadLsfgConfig, - updateConfig + updateField } = useLsfgConfig(); const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions(); @@ -35,45 +35,9 @@ export function Content() { } }, [isInstalled, loadLsfgConfig]); - // Configuration change handlers - const handleEnableLsfgChange = async (value: boolean) => { - setters.setEnableLsfg(value); - await updateConfig(value, config.multiplier, config.flowScale, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleMultiplierChange = async (value: number) => { - setters.setMultiplier(value); - await updateConfig(config.enableLsfg, value, config.flowScale, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleFlowScaleChange = async (value: number) => { - setters.setFlowScale(value); - await updateConfig(config.enableLsfg, config.multiplier, value, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleHdrChange = async (value: boolean) => { - setters.setHdr(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, value, config.perfMode, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handlePerfModeChange = async (value: boolean) => { - setters.setPerfMode(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, value, config.immediateMode, config.disableVkbasalt, config.frameCap); - }; - - const handleImmediateModeChange = async (value: boolean) => { - setters.setImmediateMode(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, value, config.disableVkbasalt, config.frameCap); - }; - - const handleDisableVkbasaltChange = async (value: boolean) => { - setters.setDisableVkbasalt(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, config.immediateMode, value, config.frameCap); - }; - - const handleFrameCapChange = async (value: number) => { - setters.setFrameCap(value); - await updateConfig(config.enableLsfg, config.multiplier, config.flowScale, config.hdr, config.perfMode, config.immediateMode, config.disableVkbasalt, value); + // Generic configuration change handler + const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number) => { + await updateField(fieldName, value); }; const onInstall = () => { @@ -105,14 +69,7 @@ export function Content() { {isInstalled && ( )} diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx index 3589c04..727a0ab 100644 --- a/src/components/UsageInstructions.tsx +++ b/src/components/UsageInstructions.tsx @@ -1,18 +1,8 @@ import { PanelSectionRow } from "@decky/ui"; - -interface ConfigType { - enableLsfg: boolean; - multiplier: number; - flowScale: number; - hdr: boolean; - perfMode: boolean; - immediateMode: boolean; - disableVkbasalt: boolean; - frameCap: number; -} +import { ConfigurationData } from "../config/configSchema"; interface UsageInstructionsProps { - config: ConfigType; + config: ConfigurationData; } export function UsageInstructions({ config }: UsageInstructionsProps) { @@ -20,34 +10,34 @@ export function UsageInstructions({ config }: UsageInstructionsProps) { const buildManualEnvVars = (): string => { const envVars: string[] = []; - if (config.enableLsfg) { + if (config.enable_lsfg) { envVars.push("ENABLE_LSFG=1"); } // Always include multiplier and flow_scale if LSFG is enabled, as they have defaults - if (config.enableLsfg) { + if (config.enable_lsfg) { envVars.push(`LSFG_MULTIPLIER=${config.multiplier}`); - envVars.push(`LSFG_FLOW_SCALE=${config.flowScale}`); + envVars.push(`LSFG_FLOW_SCALE=${config.flow_scale}`); } if (config.hdr) { envVars.push("LSFG_HDR=1"); } - if (config.perfMode) { + if (config.perf_mode) { envVars.push("LSFG_PERF_MODE=1"); } - if (config.immediateMode) { + if (config.immediate_mode) { envVars.push("MESA_VK_WSI_PRESENT_MODE=immediate"); } - if (config.disableVkbasalt) { + if (config.disable_vkbasalt) { envVars.push("DISABLE_VKBASALT=1"); } - if (config.frameCap > 0) { - envVars.push(`DXVK_FRAME_RATE=${config.frameCap}`); + if (config.frame_cap > 0) { + envVars.push(`DXVK_FRAME_RATE=${config.frame_cap}`); } return envVars.length > 0 ? `${envVars.join(" ")} %command%` : "%command%"; diff --git a/src/config/configSchema.ts b/src/config/configSchema.ts new file mode 100644 index 0000000..6956030 --- /dev/null +++ b/src/config/configSchema.ts @@ -0,0 +1,186 @@ +/** + * Centralized configuration schema for lsfg-vk frontend. + * + * This mirrors the Python configuration schema to ensure consistency + * between frontend and backend configuration handling. + */ + +// Configuration field type enum +export enum ConfigFieldType { + BOOLEAN = "boolean", + INTEGER = "integer", + FLOAT = "float" +} + +// Configuration field definition +export interface ConfigField { + name: string; + fieldType: ConfigFieldType; + default: boolean | number; + description: string; + scriptTemplate: string; + scriptComment?: string; +} + +// Configuration schema - must match Python CONFIG_SCHEMA +export const CONFIG_SCHEMA: Record = { + enable_lsfg: { + name: "enable_lsfg", + fieldType: ConfigFieldType.BOOLEAN, + default: true, + description: "Enables the frame generation layer", + scriptTemplate: "export ENABLE_LSFG={value}", + scriptComment: "# export ENABLE_LSFG=1" + }, + + multiplier: { + name: "multiplier", + fieldType: ConfigFieldType.INTEGER, + default: 2, + description: "Traditional FPS multiplier value", + scriptTemplate: "export LSFG_MULTIPLIER={value}" + }, + + flow_scale: { + name: "flow_scale", + fieldType: ConfigFieldType.FLOAT, + default: 0.8, + description: "Lowers the internal motion estimation resolution", + scriptTemplate: "export LSFG_FLOW_SCALE={value}" + }, + + hdr: { + name: "hdr", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Enable HDR mode (only if Game supports HDR)", + scriptTemplate: "export LSFG_HDR={value}", + scriptComment: "# export LSFG_HDR=1" + }, + + perf_mode: { + name: "perf_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: true, + description: "Use lighter model for FG", + scriptTemplate: "export LSFG_PERF_MODE={value}", + scriptComment: "# export LSFG_PERF_MODE=1" + }, + + immediate_mode: { + name: "immediate_mode", + fieldType: ConfigFieldType.BOOLEAN, + default: false, + description: "Reduce input lag (Experimental, will cause issues in many games)", + scriptTemplate: "export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync", + scriptComment: "# export MESA_VK_WSI_PRESENT_MODE=immediate # - disable vsync" + }, + + disable_vkbasalt: { + name: "disable_vkbasalt", + fieldType: ConfigFieldType.BOOLEAN, + default: true, + description: "Some plugins add vkbasalt layer, which can break lsfg. Toggling on fixes this", + scriptTemplate: "export DISABLE_VKBASALT={value}", + scriptComment: "# export DISABLE_VKBASALT=1" + }, + + frame_cap: { + name: "frame_cap", + fieldType: ConfigFieldType.INTEGER, + default: 0, + description: "Limit base game FPS (0 = disabled)", + scriptTemplate: "export DXVK_FRAME_RATE={value}", + scriptComment: "# export DXVK_FRAME_RATE=60" + } +}; + +// Type-safe configuration data structure +export interface ConfigurationData { + enable_lsfg: boolean; + multiplier: number; + flow_scale: number; + hdr: boolean; + perf_mode: boolean; + immediate_mode: boolean; + disable_vkbasalt: boolean; + frame_cap: number; +} + +// Centralized configuration manager +export class ConfigurationManager { + /** + * Get default configuration values + */ + static getDefaults(): ConfigurationData { + const defaults = {} as ConfigurationData; + Object.values(CONFIG_SCHEMA).forEach(field => { + (defaults as any)[field.name] = field.default; + }); + return defaults; + } + + /** + * Get ordered list of configuration field names + */ + static getFieldNames(): string[] { + return Object.keys(CONFIG_SCHEMA); + } + + /** + * Get field type mapping + */ + static getFieldTypes(): Record { + return Object.values(CONFIG_SCHEMA).reduce((acc, field) => { + acc[field.name] = field.fieldType; + return acc; + }, {} as Record); + } + + /** + * Create ordered arguments array from configuration object + */ + static createArgsFromConfig(config: ConfigurationData): (boolean | number)[] { + return this.getFieldNames().map(fieldName => + config[fieldName as keyof ConfigurationData] + ); + } + + /** + * Validate configuration object against schema + */ + static validateConfig(config: Partial): ConfigurationData { + const defaults = this.getDefaults(); + const validated = { ...defaults }; + + Object.entries(CONFIG_SCHEMA).forEach(([fieldName, fieldDef]) => { + const value = config[fieldName as keyof ConfigurationData]; + if (value !== undefined) { + // Type validation + if (fieldDef.fieldType === ConfigFieldType.BOOLEAN) { + (validated as any)[fieldName] = Boolean(value); + } else if (fieldDef.fieldType === ConfigFieldType.INTEGER) { + (validated as any)[fieldName] = parseInt(String(value), 10); + } else if (fieldDef.fieldType === ConfigFieldType.FLOAT) { + (validated as any)[fieldName] = parseFloat(String(value)); + } + } + }); + + return validated; + } + + /** + * Get configuration field definition + */ + static getFieldDef(fieldName: string): ConfigField | undefined { + return CONFIG_SCHEMA[fieldName]; + } + + /** + * Get all field definitions + */ + static getAllFieldDefs(): ConfigField[] { + return Object.values(CONFIG_SCHEMA); + } +} diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts index f765ce6..8ff9061 100644 --- a/src/hooks/useLsfgHooks.ts +++ b/src/hooks/useLsfgHooks.ts @@ -1,12 +1,13 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { toaster } from "@decky/api"; import { checkLsfgVkInstalled, checkLosslessScalingDll, getLsfgConfig, - updateLsfgConfig, + updateLsfgConfigFromObject, type ConfigUpdateResult } from "../api/lsfgApi"; +import { ConfigurationData, ConfigurationManager } from "../config/configSchema"; export function useInstallationStatus() { const [isInstalled, setIsInstalled] = useState(false); @@ -70,58 +71,30 @@ export function useDllDetection() { } export function useLsfgConfig() { - const [enableLsfg, setEnableLsfg] = useState(true); - const [multiplier, setMultiplier] = useState(2); - const [flowScale, setFlowScale] = useState(0.8); - const [hdr, setHdr] = useState(false); - const [perfMode, setPerfMode] = useState(true); - const [immediateMode, setImmediateMode] = useState(false); - const [disableVkbasalt, setDisableVkbasalt] = useState(true); - const [frameCap, setFrameCap] = useState(0); + // Use centralized configuration for initial state + const [config, setConfig] = useState(() => ConfigurationManager.getDefaults()); - const loadLsfgConfig = async () => { + const loadLsfgConfig = useCallback(async () => { try { const result = await getLsfgConfig(); if (result.success && result.config) { - setEnableLsfg(result.config.enable_lsfg); - setMultiplier(result.config.multiplier); - setFlowScale(result.config.flow_scale); - setHdr(result.config.hdr); - setPerfMode(result.config.perf_mode); - setImmediateMode(result.config.immediate_mode); - setDisableVkbasalt(result.config.disable_vkbasalt); - setFrameCap(result.config.frame_cap); - console.log("Loaded lsfg config:", result.config); + setConfig(result.config); } else { console.log("lsfg config not available, using defaults:", result.error); + setConfig(ConfigurationManager.getDefaults()); } } catch (error) { console.error("Error loading lsfg config:", error); + setConfig(ConfigurationManager.getDefaults()); } - }; + }, []); - const updateConfig = async ( - newEnableLsfg: boolean, - newMultiplier: number, - newFlowScale: number, - newHdr: boolean, - newPerfMode: boolean, - newImmediateMode: boolean, - newDisableVkbasalt: boolean, - newFrameCap: number - ): Promise => { + const updateConfig = useCallback(async (newConfig: ConfigurationData): Promise => { try { - const result = await updateLsfgConfig( - newEnableLsfg, - newMultiplier, - newFlowScale, - newHdr, - newPerfMode, - newImmediateMode, - newDisableVkbasalt, - newFrameCap - ); - if (!result.success) { + const result = await updateLsfgConfigFromObject(newConfig); + if (result.success) { + setConfig(newConfig); + } else { toaster.toast({ title: "Update Failed", body: result.error || "Failed to update configuration" @@ -135,34 +108,22 @@ export function useLsfgConfig() { }); return { success: false, error: String(error) }; } - }; + }, []); + + const updateField = useCallback(async (fieldName: keyof ConfigurationData, value: boolean | number): Promise => { + const newConfig = { ...config, [fieldName]: value }; + return updateConfig(newConfig); + }, [config, updateConfig]); useEffect(() => { loadLsfgConfig(); - }, []); + }, []); // Empty dependency array to prevent infinite loop return { - config: { - enableLsfg, - multiplier, - flowScale, - hdr, - perfMode, - immediateMode, - disableVkbasalt, - frameCap - }, - setters: { - setEnableLsfg, - setMultiplier, - setFlowScale, - setHdr, - setPerfMode, - setImmediateMode, - setDisableVkbasalt, - setFrameCap - }, + config, + setConfig, loadLsfgConfig, - updateConfig + updateConfig, + updateField }; } -- cgit v1.2.3