summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorxXJsonDeruloXx <danielhimebauch@gmail.com>2026-08-01 14:47:18 -0400
committerxXJsonDeruloXx <danielhimebauch@gmail.com>2026-08-01 14:47:18 -0400
commit02521a797e195b331af1778cd7bc854d3a396ead (patch)
tree63c48b246d9db2f6ab0140259d7fb8b15f2e085b /src
parent96eb17b0a9f2cfd2b00ad082bef893f4efc229f7 (diff)
downloadDecky-Framegen-02521a797e195b331af1778cd7bc854d3a396ead.tar.gz
Decky-Framegen-02521a797e195b331af1778cd7bc854d3a396ead.zip
refactor patch management and add ownership safeguards
Diffstat (limited to 'src')
-rw-r--r--src/api/index.ts25
-rw-r--r--src/components/CustomPathOverride.tsx11
-rw-r--r--src/components/OptiScalerControls.tsx82
-rw-r--r--src/components/SteamGamePatcher.tsx78
-rw-r--r--src/index.tsx8
-rw-r--r--src/utils/constants.ts16
6 files changed, 169 insertions, 51 deletions
diff --git a/src/api/index.ts b/src/api/index.ts
index b1bf0b8..4dda43c 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,7 +1,7 @@
import { callable } from "@decky/api";
export const runInstallFGMod = callable<
- [selected_default_variant?: string],
+ [selected_default_variant?: string, framegen_backend?: string],
{
status: string;
message?: string;
@@ -9,6 +9,8 @@ export const runInstallFGMod = callable<
version?: string;
selected_default_variant?: string;
selected_default_variant_label?: string;
+ framegen_backend?: string;
+ framegen_backend_label?: string;
}
>("run_install_fgmod");
@@ -29,6 +31,17 @@ export const setDefaultFsr4Variant = callable<
}
>("set_default_fsr4_variant");
+export const setFramegenBackend = callable<
+ [framegen_backend?: string],
+ {
+ status: string;
+ message?: string;
+ output?: string;
+ framegen_backend?: string;
+ framegen_backend_label?: string;
+ }
+>("set_framegen_backend");
+
export const checkFGModPath = callable<
[],
{
@@ -36,6 +49,8 @@ export const checkFGModPath = callable<
version?: string | null;
selected_fsr4_variant?: string | null;
selected_fsr4_variant_label?: string | null;
+ framegen_backend?: string | null;
+ framegen_backend_label?: string | null;
install_manifest_present?: boolean;
}
>("check_fgmod_path");
@@ -53,7 +68,7 @@ export const getPathDefaults = callable<
>("get_path_defaults");
export const runManualPatch = callable<
- [string, string, string],
+ [string, string, string, string],
{
status: string;
message?: string;
@@ -62,6 +77,7 @@ export const runManualPatch = callable<
fsr4_variant_label?: string;
fsr4_upscaler_sha256?: string;
optiscaler_version?: string | null;
+ framegen_backend?: string;
}
>("manual_patch_directory");
@@ -86,11 +102,13 @@ export const getGameStatus = callable<
fsr4_variant?: string | null;
fsr4_variant_label?: string | null;
fsr4_upscaler_sha256?: string | null;
+ framegen_backend?: string | null;
+ framegen_backend_label?: string | null;
}
>("get_game_status");
export const patchGame = callable<
- [appid: string, dll_name: string, current_launch_options: string, fsr4_variant: string],
+ [appid: string, dll_name: string, current_launch_options: string, fsr4_variant: string, framegen_backend: string],
{
status: string;
message?: string;
@@ -104,6 +122,7 @@ export const patchGame = callable<
fsr4_variant?: string;
fsr4_variant_label?: string;
fsr4_upscaler_sha256?: string;
+ framegen_backend?: string;
}
>("patch_game");
diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index af47735..fb7cd05 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -38,6 +38,7 @@ interface ManualPatchControlsProps {
onManualModeChange?: (enabled: boolean) => void;
dllName: string;
fsr4Variant: string;
+ framegenBackend: string;
}
interface PickerState {
@@ -58,7 +59,7 @@ const formatResultMessage = (result: ApiResponse | null) => {
return result.message || result.output || "Operation failed.";
};
-export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, fsr4Variant }: ManualPatchControlsProps) => {
+export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, fsr4Variant, framegenBackend }: ManualPatchControlsProps) => {
const [isEnabled, setEnabled] = useState(false);
const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS);
const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE);
@@ -160,14 +161,14 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName,
if (!selectedPath) return;
const setBusy = action === "patch" ? setIsPatching : setIsUnpatching;
- setLastOperation(action);
+ setLastOperation(action);
setBusy(true);
setOperationResult(null);
try {
const response =
action === "patch"
- ? await runManualPatch(selectedPath, dllName, fsr4Variant)
+ ? await runManualPatch(selectedPath, dllName, fsr4Variant, framegenBackend)
: await runManualUnpatch(selectedPath);
setOperationResult(response ?? { status: "error", message: "No response from backend." });
} catch (err) {
@@ -179,7 +180,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName,
setBusy(false);
}
},
- [selectedPath, dllName, fsr4Variant]
+ [selectedPath, dllName, fsr4Variant, framegenBackend]
);
const handleToggle = (value: boolean) => {
@@ -304,4 +305,4 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName,
)}
</>
);
-}; \ No newline at end of file
+};
diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx
index a8b0863..d6c4c77 100644
--- a/src/components/OptiScalerControls.tsx
+++ b/src/components/OptiScalerControls.tsx
@@ -1,9 +1,17 @@
import { useState, useEffect } from "react";
import { DropdownItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui";
-import { runInstallFGMod, runUninstallFGMod, setDefaultFsr4Variant } from "../api";
-import { OperationResult } from "./ResultDisplay";
+import { runInstallFGMod, runUninstallFGMod, setDefaultFsr4Variant, setFramegenBackend } from "../api";
+import { ResultDisplay, type OperationResult } from "./ResultDisplay";
import { createAutoCleanupTimer } from "../utils";
-import { TIMEOUTS, PROXY_DLL_OPTIONS, DEFAULT_PROXY_DLL, FSR4_VARIANT_OPTIONS, DEFAULT_FSR4_VARIANT } from "../utils/constants";
+import {
+ TIMEOUTS,
+ PROXY_DLL_OPTIONS,
+ DEFAULT_PROXY_DLL,
+ FSR4_VARIANT_OPTIONS,
+ DEFAULT_FSR4_VARIANT,
+ FRAMEGEN_BACKEND_OPTIONS,
+ DEFAULT_FRAMEGEN_BACKEND,
+} from "../utils/constants";
import { InstallationStatus } from "./InstallationStatus";
import { OptiScalerHeader } from "./OptiScalerHeader";
import { ClipboardCommands } from "./ClipboardCommands";
@@ -18,6 +26,8 @@ interface FgmodInfo {
version?: string | null;
selected_fsr4_variant?: string | null;
selected_fsr4_variant_label?: string | null;
+ framegen_backend?: string | null;
+ framegen_backend_label?: string | null;
install_manifest_present?: boolean;
}
@@ -37,19 +47,22 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt
const [dllName, setDllName] = useState<string>(DEFAULT_PROXY_DLL);
const [fsr4Variant, setFsr4Variant] = useState<string>(DEFAULT_FSR4_VARIANT);
const [fsr4VariantTouched, setFsr4VariantTouched] = useState(false);
+ const [framegenBackend, setFramegenBackendValue] = useState<string>(DEFAULT_FRAMEGEN_BACKEND);
+ const [framegenBackendTouched, setFramegenBackendTouched] = useState(false);
const [switchingVariant, setSwitchingVariant] = useState(false);
+ const [switchingBackend, setSwitchingBackend] = useState(false);
useEffect(() => {
if (installResult) {
return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay);
}
- return () => {}; // Ensure a cleanup function is always returned
+ return undefined;
}, [installResult]);
useEffect(() => {
if (uninstallResult) {
return createAutoCleanupTimer(() => setUninstallResult(null), TIMEOUTS.resultDisplay);
}
- return () => {}; // Ensure a cleanup function is always returned
+ return undefined;
}, [uninstallResult]);
useEffect(() => {
@@ -59,16 +72,26 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt
}
}, [fgmodInfo?.selected_fsr4_variant, fsr4VariantTouched]);
+ useEffect(() => {
+ const installedBackend = fgmodInfo?.framegen_backend;
+ if (!framegenBackendTouched && installedBackend && FRAMEGEN_BACKEND_OPTIONS.some((option) => option.value === installedBackend)) {
+ setFramegenBackendValue(installedBackend);
+ }
+ }, [fgmodInfo?.framegen_backend, framegenBackendTouched]);
+
const handleInstallClick = async () => {
try {
setInstalling(true);
- const result = await runInstallFGMod(fsr4Variant);
+ const result = await runInstallFGMod(fsr4Variant, framegenBackend);
setInstallResult(result);
if (result.status === "success") {
setPathExists(true);
+ setFsr4VariantTouched(false);
+ setFramegenBackendTouched(false);
}
} catch (e) {
console.error(e);
+ setInstallResult({ status: "error", message: e instanceof Error ? e.message : "Installation failed." });
} finally {
setInstalling(false);
}
@@ -84,11 +107,35 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt
}
} catch (e) {
console.error(e);
+ setUninstallResult({ status: "error", message: e instanceof Error ? e.message : "Uninstall failed." });
} finally {
setUninstalling(false);
}
};
+ const handleFramegenBackendChange = async (nextBackend: string) => {
+ const previousBackend = framegenBackend;
+ setFramegenBackendValue(nextBackend);
+ setFramegenBackendTouched(true);
+ if (pathExists !== true) return;
+
+ try {
+ setSwitchingBackend(true);
+ const result = await setFramegenBackend(nextBackend);
+ if (result.status !== "success") {
+ throw new Error(result.message || result.output || "Failed to update the frame-generation backend.");
+ }
+ setFramegenBackendValue(result.framegen_backend || nextBackend);
+ setFramegenBackendTouched(false);
+ } catch (error) {
+ console.error(error);
+ setFramegenBackendValue(previousBackend);
+ setFramegenBackendTouched(false);
+ } finally {
+ setSwitchingBackend(false);
+ }
+ };
+
const handleFsr4VariantChange = async (nextVariant: string) => {
const previousVariant = fsr4Variant;
setFsr4Variant(nextVariant);
@@ -132,13 +179,28 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt
menuLabel="Default FSR4 runtime"
selectedOption={fsr4Variant}
rgOptions={FSR4_VARIANT_OPTIONS.map((option) => ({ data: option.value, label: option.label }))}
- disabled={installing || uninstalling || switchingVariant}
+ disabled={installing || uninstalling || switchingVariant || switchingBackend}
onChange={(option) => {
void handleFsr4VariantChange(String(option.data));
}}
/>
</PanelSectionRow>
+ <PanelSectionRow>
+ <DropdownItem
+ layout="below"
+ label="Frame generation backend"
+ description={FRAMEGEN_BACKEND_OPTIONS.find((option) => option.value === framegenBackend)?.hint}
+ menuLabel="Frame generation backend"
+ selectedOption={framegenBackend}
+ rgOptions={FRAMEGEN_BACKEND_OPTIONS.map((option) => ({ data: option.value, label: option.label }))}
+ disabled={installing || uninstalling || switchingBackend}
+ onChange={(option) => {
+ void handleFramegenBackendChange(String(option.data));
+ }}
+ />
+ </PanelSectionRow>
+
{pathExists === true && fgmodInfo?.version && installedVariantLabel && (
<PanelSectionRow>
<Field label="Installed bundle" description={`OptiScaler ${fgmodInfo.version}`}>
@@ -162,7 +224,7 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt
)}
{pathExists === true && (
- <SteamGamePatcher dllName={dllName} fsr4Variant={fsr4Variant} />
+ <SteamGamePatcher dllName={dllName} fsr4Variant={fsr4Variant} framegenBackend={framegenBackend} />
)}
<ClipboardCommands pathExists={pathExists} dllName={dllName} />
@@ -192,8 +254,12 @@ export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: Opt
onManualModeChange={setAdvancedModeEnabled}
dllName={dllName}
fsr4Variant={fsr4Variant}
+ framegenBackend={framegenBackend}
/>
+ <ResultDisplay result={installResult} />
+ <ResultDisplay result={uninstallResult} />
+
{!advancedModeEnabled && (
<InstructionCard pathExists={pathExists} />
)}
diff --git a/src/components/SteamGamePatcher.tsx b/src/components/SteamGamePatcher.tsx
index e8ac3d2..cd8fe3c 100644
--- a/src/components/SteamGamePatcher.tsx
+++ b/src/components/SteamGamePatcher.tsx
@@ -4,12 +4,24 @@ import { toaster } from "@decky/api";
import { listInstalledGames, getGameStatus, patchGame, unpatchGame } from "../api";
import { FSR4_VARIANT_OPTIONS } from "../utils/constants";
-// ─── SteamClient helpers ─────────────────────────────────────────────────────
+type SteamAppsApi = {
+ RegisterForAppDetails?: (
+ appId: number,
+ callback: (details: { strLaunchOptions?: string }) => void
+ ) => { unregister: () => void };
+ SetAppLaunchOptions?: (appId: number, options: string) => void;
+};
+
+const getSteamApps = (): SteamAppsApi | undefined => {
+ if (typeof SteamClient === "undefined") return undefined;
+ return SteamClient.Apps as unknown as SteamAppsApi;
+};
const getAppLaunchOptions = (appId: number): Promise<string> =>
new Promise((resolve, reject) => {
- if (typeof SteamClient === "undefined" || !SteamClient?.Apps?.RegisterForAppDetails) {
- resolve("");
+ const apps = getSteamApps();
+ if (!apps || typeof apps.RegisterForAppDetails !== "function" || typeof apps.SetAppLaunchOptions !== "function") {
+ reject(new Error("Steam launch-options API is unavailable; patching was not started."));
return;
}
let settled = false;
@@ -20,7 +32,7 @@ const getAppLaunchOptions = (appId: number): Promise<string> =>
unregister();
reject(new Error("Timed out reading launch options."));
}, 5000);
- const registration = SteamClient.Apps.RegisterForAppDetails(
+ const registration = apps.RegisterForAppDetails(
appId,
(details: { strLaunchOptions?: string }) => {
if (settled) return;
@@ -31,15 +43,25 @@ const getAppLaunchOptions = (appId: number): Promise<string> =>
}
);
unregister = registration.unregister;
+ if (settled) unregister();
});
const setAppLaunchOptions = (appId: number, options: string): void => {
- if (typeof SteamClient !== "undefined" && SteamClient?.Apps?.SetAppLaunchOptions) {
- SteamClient.Apps.SetAppLaunchOptions(appId, options);
+ const apps = getSteamApps();
+ if (!apps || typeof apps.SetAppLaunchOptions !== "function") {
+ throw new Error("Steam launch-options API is unavailable; launch options were not changed.");
}
+ apps.SetAppLaunchOptions(appId, options);
};
-// ─── Types ───────────────────────────────────────────────────────────────────
+const canManageLaunchOptions = (): boolean => {
+ const apps = getSteamApps();
+ return Boolean(
+ apps &&
+ typeof apps.RegisterForAppDetails === "function" &&
+ typeof apps.SetAppLaunchOptions === "function"
+ );
+};
type GameEntry = { appid: string; name: string; install_found?: boolean };
@@ -55,20 +77,19 @@ type GameStatus = {
fsr4_variant?: string | null;
fsr4_variant_label?: string | null;
fsr4_upscaler_sha256?: string | null;
+ framegen_backend?: string | null;
+ framegen_backend_label?: string | null;
};
-// ─── Module-level state persistence ──────────────────────────────────────────
-
let lastSelectedAppId = "";
-// ─── Component ───────────────────────────────────────────────────────────────
-
interface SteamGamePatcherProps {
dllName: string;
fsr4Variant: string;
+ framegenBackend: string;
}
-export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps) {
+export function SteamGamePatcher({ dllName, fsr4Variant, framegenBackend }: SteamGamePatcherProps) {
const [games, setGames] = useState<GameEntry[]>([]);
const [gamesLoading, setGamesLoading] = useState(true);
const [selectedAppId, setSelectedAppId] = useState<string>(() => lastSelectedAppId);
@@ -77,8 +98,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps
const [busyAction, setBusyAction] = useState<"patch" | "unpatch" | null>(null);
const [resultMessage, setResultMessage] = useState<string>("");
- // ── Data loaders ───────────────────────────────────────────────────────────
-
const loadGames = useCallback(async () => {
setGamesLoading(true);
try {
@@ -136,8 +155,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps
void loadStatus(selectedAppId);
}, [selectedAppId, loadStatus]);
- // ── Derived state ──────────────────────────────────────────────────────────
-
const selectedGame = useMemo(
() => games.find((g) => g.appid === selectedAppId) ?? null,
[games, selectedAppId]
@@ -163,20 +180,16 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps
return `Patch with ${dllName}`;
}, [busyAction, dllName, gameStatus, isPatchedWithDifferentDll, selectedGame]);
- // ── Actions ────────────────────────────────────────────────────────────────
-
const handlePatch = useCallback(async () => {
if (!selectedGame || !selectedAppId || busyAction) return;
setBusyAction("patch");
setResultMessage("");
try {
- let currentLaunchOptions = "";
- try {
- currentLaunchOptions = await getAppLaunchOptions(Number(selectedAppId));
- } catch {
- // non-fatal: proceed without current launch options
+ if (!canManageLaunchOptions()) {
+ throw new Error("Steam launch-options API is unavailable; patching was not started.");
}
- const result = await patchGame(selectedAppId, dllName, currentLaunchOptions, fsr4Variant);
+ const currentLaunchOptions = await getAppLaunchOptions(Number(selectedAppId));
+ const result = await patchGame(selectedAppId, dllName, currentLaunchOptions, fsr4Variant, framegenBackend);
if (result.status !== "success") throw new Error(result.message || "Patch failed.");
setAppLaunchOptions(Number(selectedAppId), result.launch_options || "");
const msg = result.message || `Patched ${selectedGame.name}.`;
@@ -190,13 +203,16 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps
} finally {
setBusyAction(null);
}
- }, [busyAction, dllName, fsr4Variant, loadStatus, selectedAppId, selectedGame]);
+ }, [busyAction, dllName, framegenBackend, fsr4Variant, loadStatus, selectedAppId, selectedGame]);
const handleUnpatch = useCallback(async () => {
if (!selectedGame || !selectedAppId || busyAction) return;
setBusyAction("unpatch");
setResultMessage("");
try {
+ if (!canManageLaunchOptions()) {
+ throw new Error("Steam launch-options API is unavailable; unpatching was not started.");
+ }
const result = await unpatchGame(selectedAppId);
if (result.status !== "success") throw new Error(result.message || "Unpatch failed.");
setAppLaunchOptions(Number(selectedAppId), result.launch_options || "");
@@ -213,8 +229,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps
}
}, [busyAction, loadStatus, selectedAppId, selectedGame]);
- // ── Status display ─────────────────────────────────────────────────────────
-
const statusDisplay = useMemo(() => {
if (!selectedGame) return { text: "—", color: undefined as string | undefined };
if (statusLoading) return { text: "Loading...", color: undefined };
@@ -230,8 +244,6 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps
const focusableFieldProps = { focusable: true, highlightOnFocus: true } as const;
- // ── Render ─────────────────────────────────────────────────────────────────
-
return (
<>
<PanelSectionRow>
@@ -278,6 +290,14 @@ export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps
</PanelSectionRow>
<PanelSectionRow>
+ <Field {...focusableFieldProps} label="Frame generation backend">
+ {gameStatus?.patched
+ ? (gameStatus?.framegen_backend_label || gameStatus?.framegen_backend || "Unknown")
+ : `Will patch with ${framegenBackend}`}
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
<ButtonItem layout="below" disabled={!canPatch} onClick={handlePatch}>
{patchButtonLabel}
</ButtonItem>
diff --git a/src/index.tsx b/src/index.tsx
index 4a9a9f6..6d598fc 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,7 +2,6 @@ import { definePlugin } from "@decky/api";
import { MdOutlineAutoAwesomeMotion } from "react-icons/md";
import { useState, useEffect } from "react";
import { OptiScalerControls } from "./components";
-// import { InstalledGamesSection } from "./components/InstalledGamesSection";
import { checkFGModPath } from "./api";
import { safeAsyncOperation } from "./utils";
import { TIMEOUTS } from "./utils/constants";
@@ -12,6 +11,8 @@ type FgmodInfo = {
version?: string | null;
selected_fsr4_variant?: string | null;
selected_fsr4_variant_label?: string | null;
+ framegen_backend?: string | null;
+ framegen_backend_label?: string | null;
install_manifest_present?: boolean;
};
@@ -43,11 +44,6 @@ function MainContent() {
setPathExists={setPathExists}
fgmodInfo={fgmodInfo}
/>
- {pathExists === true ? (
- <>
- {/* <InstalledGamesSection /> */}
- </>
- ) : null}
</>
);
}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 3af9874..2568dd4 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -85,6 +85,22 @@ export const FSR4_VARIANT_OPTIONS = [
export type Fsr4VariantValue = typeof FSR4_VARIANT_OPTIONS[number]["value"];
export const DEFAULT_FSR4_VARIANT: Fsr4VariantValue = "rdna23-int8";
+export const FRAMEGEN_BACKEND_OPTIONS = [
+ {
+ value: "auto",
+ label: "OptiScaler automatic selection",
+ hint: "Let OptiScaler select the compatible frame-generation path.",
+ },
+ {
+ value: "nukems",
+ label: "Nukem's DLSSG → FSR3",
+ hint: "Use Nukem's DLSSG-to-FSR3 path for games that need it.",
+ },
+] as const;
+
+export type FramegenBackendValue = typeof FRAMEGEN_BACKEND_OPTIONS[number]["value"];
+export const DEFAULT_FRAMEGEN_BACKEND: FramegenBackendValue = "auto";
+
// Common timeout values
export const TIMEOUTS = {
resultDisplay: 5000, // 5 seconds