summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com>2026-05-19 14:34:55 -0400
committerGitHub <noreply@github.com>2026-05-19 14:34:55 -0400
commit92737d9dbae61e25fb0b2f816475d10285f0ff67 (patch)
treec7f2228aa45fd4325a68de6fe6f32864f383791c
parent9e12c11b6189972ea04de454dad5c8554efe657a (diff)
parent19e5aefa2f41ebd4389bf27136adacf3e75d3502 (diff)
downloadDecky-Framegen-0.15.5.tar.gz
Decky-Framegen-0.15.5.zip
Merge pull request #195 from xXJSONDeruloXx/feat/asset-manifest-fsr4-variantsHEADv0.15.5main
feat: update OptiScaler bundle and add FSR4 runtime manifests
-rw-r--r--README.md12
-rwxr-xr-xdefaults/assets/fgmod.sh78
-rw-r--r--main.py731
-rw-r--r--package.json11
-rw-r--r--pnpm-lock.yaml17
-rw-r--r--src/api/index.ts53
-rw-r--r--src/components/ClipboardCommands.tsx33
-rw-r--r--src/components/CustomPathOverride.tsx7
-rw-r--r--src/components/InstalledGamesSection.tsx4
-rw-r--r--src/components/OptiScalerControls.tsx106
-rw-r--r--src/components/SteamGamePatcher.tsx21
-rw-r--r--src/index.tsx15
-rw-r--r--src/utils/constants.ts18
13 files changed, 837 insertions, 269 deletions
diff --git a/README.md b/README.md
index 1b08742..54d59ed 100644
--- a/README.md
+++ b/README.md
@@ -25,15 +25,17 @@ This plugin uses OptiScaler to replace DLSS calls with FSR3/FSR3.1, giving you:
1. **Install the Plugin**: Download and install through Decky Loader "install from zip" option in developer settings
2. **Setup OptiScaler**: Open the plugin and click "Setup OptiScaler Mod"
3. **Configure Games**: For each game you want to enhance:
- - Click "Copy Patch Command" in the plugin
+ - Click "Copy launch options" in the plugin for the standard direct launch-options method
- Go to your game's Properties → Launch Options in Steam
- - Paste the command: `~/fgmod/fgmod %command%`
+ - Paste the copied command
+ - If you want the wrapper commands instead, enable Manual Mode and use "Copy Patch Command" / "Copy Unpatch Command"
4. **Enable Features**: Launch your game and enable DLSS in the graphics settings
5. **Advanced Options**: Press the Insert key in-game for additional OptiScaler settings
### Removing the Mod from Games
-- Click "Copy Unpatch Command" and replace the launch options with: `~/fgmod/fgmod-uninstaller.sh %command%`
-- Run the game at least once to make the uninstaller script run. After you can leave the launch option or remove it
+- If you used the wrapper method, enable Manual Mode and click "Copy Unpatch Command", then replace the launch options with: `~/fgmod/fgmod-uninstaller.sh %command%`
+- If you used the standard direct patch flow, use the in-plugin unpatch button instead
+- Run the game at least once to make the uninstaller script run. After that you can leave the launch option or remove it
### Configuring OptiScaler via Environment Variables
As of v0.15.1, you can update OptiScaler settings before a game launches by adding environment variables.
@@ -78,7 +80,7 @@ Dx12Upscaler=fsr31 ~/fgmod/fgmod %command%
## Technical Details
### What's Included
-- **[OptiScaler 0.9.0-pre11](https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/tag/opti-9-pre-11)**: Bleeding-edge OptiScaler bundle used by this plugin, paired with the RDNA2-optimized `amd_fidelityfx_upscaler_dx12.dll` override for Steam Deck compatibility
+- **[OptiScaler 0.9.2a](https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/tag/opti-9-2-a)**: Bleeding-edge OptiScaler bundle used by this plugin, with bundled FSR4 runtime variants for either the archive-native RDNA4 path or the Steam Deck / RDNA2-3 optimized INT8 override
- **Nukem9's DLSSG to FSR3 mod**: Allows use of DLSS inputs for FSR frame gen outputs, and xess or FSR upscaling outputs
- **FakeNVAPI**: NVIDIA API emulation for AMD/Intel GPUs, to make DLSS options selectable in game
- **Supporting Libraries**: All required DX12/Vulkan libraries (libxess.dll, amd_fidelityfx, etc.)
diff --git a/defaults/assets/fgmod.sh b/defaults/assets/fgmod.sh
index ed8aaf5..c7bc4f7 100755
--- a/defaults/assets/fgmod.sh
+++ b/defaults/assets/fgmod.sh
@@ -18,6 +18,8 @@ error_exit() {
fgmod_path="$HOME/fgmod"
dll_name="${DLL:-dxgi.dll}"
preserve_ini="${PRESERVE_INI:-true}"
+fsr4_variant="${FGMOD_FSR4_VARIANT:-}"
+python_bin="$(command -v python3 || command -v python || true)"
# === Resolve Game Path ===
if [[ "$#" -lt 1 ]]; then
@@ -135,6 +137,67 @@ has_patch_fingerprint() {
return 1
}
+resolve_fsr4_variant() {
+ if [[ -n "$fsr4_variant" ]]; then
+ echo "$fsr4_variant"
+ return
+ fi
+
+ local manifest_path="$fgmod_path/install-manifest.json"
+ if [[ -f "$manifest_path" && -n "$python_bin" ]]; then
+ local manifest_variant
+ manifest_variant=$("$python_bin" - <<PY 2>/dev/null
+import json
+from pathlib import Path
+path = Path(r'''$manifest_path''')
+try:
+ data = json.loads(path.read_text(encoding='utf-8'))
+ value = str(data.get('selected_default_variant') or '').strip()
+ print(value)
+except Exception:
+ pass
+PY
+)
+ if [[ -n "$manifest_variant" ]]; then
+ echo "$manifest_variant"
+ return
+ fi
+ fi
+
+ echo "rdna23-int8"
+}
+
+selected_fsr4_variant="$(resolve_fsr4_variant)"
+case "$selected_fsr4_variant" in
+ rdna4-native)
+ fsr4_upscaler_src="$fgmod_path/fsr4-rdna4/amd_fidelityfx_upscaler_dx12.dll"
+ ;;
+ *)
+ selected_fsr4_variant="rdna23-int8"
+ fsr4_upscaler_src="$fgmod_path/fsr4-rdna2-3/amd_fidelityfx_upscaler_dx12.dll"
+ ;;
+esac
+[[ -f "$fsr4_upscaler_src" ]] || fsr4_upscaler_src="$fgmod_path/amd_fidelityfx_upscaler_dx12.dll"
+logger -t fgmod "Using FSR4 variant: $selected_fsr4_variant (source: $fsr4_upscaler_src)"
+
+is_managed_support_file() {
+ local existing_file="$1"
+ local filename
+ filename="$(basename "$existing_file")"
+ local candidate
+ if [[ "$filename" == "amd_fidelityfx_upscaler_dx12.dll" ]]; then
+ for candidate in \
+ "$fgmod_path/amd_fidelityfx_upscaler_dx12.dll" \
+ "$fgmod_path/fsr4-rdna2-3/amd_fidelityfx_upscaler_dx12.dll" \
+ "$fgmod_path/fsr4-rdna4/amd_fidelityfx_upscaler_dx12.dll"; do
+ [[ -f "$candidate" && -f "$existing_file" ]] && cmp -s "$existing_file" "$candidate" && return 0
+ done
+ return 1
+ fi
+ candidate="$fgmod_path/$filename"
+ [[ -f "$candidate" && -f "$existing_file" ]] && cmp -s "$existing_file" "$candidate"
+}
+
# === Backup Pre-existing Proxy DLLs Before Cleanup ===
for dll in "${proxy_backup_files[@]}"; do
existing_path="$exe_folder_path/$dll"
@@ -160,8 +223,19 @@ unset cleanup_file
# === Optional: Backup Original DLLs ===
original_dlls=("d3dcompiler_47.dll" "amd_fidelityfx_dx12.dll" "amd_fidelityfx_framegeneration_dx12.dll" "amd_fidelityfx_upscaler_dx12.dll" "amd_fidelityfx_vk.dll")
for dll in "${original_dlls[@]}"; do
- [[ -f "$exe_folder_path/$dll" && ! -f "$exe_folder_path/$dll.b" ]] && mv -f "$exe_folder_path/$dll" "$exe_folder_path/$dll.b"
+ existing_path="$exe_folder_path/$dll"
+ backup_path="$exe_folder_path/$dll.b"
+ if [[ -f "$existing_path" && ! -f "$backup_path" ]]; then
+ if has_patch_fingerprint && is_managed_support_file "$existing_path"; then
+ rm -f "$existing_path"
+ logger -t fgmod "Removed managed support file before repatch: $dll"
+ else
+ mv -f "$existing_path" "$backup_path"
+ logger -t fgmod "Backed up original game DLL: $dll"
+ fi
+ fi
done
+unset existing_path backup_path
# === Remove nvapi64.dll and its backup (conflicts from previous fakenvapi versions) ===
rm -f "$exe_folder_path/nvapi64.dll" "$exe_folder_path/nvapi64.dll.b"
@@ -239,7 +313,7 @@ cp -f "$fgmod_path/libxess_fg.dll" "$exe_folder_path/" || true
cp -f "$fgmod_path/libxell.dll" "$exe_folder_path/" || true
cp -f "$fgmod_path/amd_fidelityfx_dx12.dll" "$exe_folder_path/" || true
cp -f "$fgmod_path/amd_fidelityfx_framegeneration_dx12.dll" "$exe_folder_path/" || true
-cp -f "$fgmod_path/amd_fidelityfx_upscaler_dx12.dll" "$exe_folder_path/" || true
+cp -f "$fsr4_upscaler_src" "$exe_folder_path/amd_fidelityfx_upscaler_dx12.dll" || true
cp -f "$fgmod_path/amd_fidelityfx_vk.dll" "$exe_folder_path/" || true
# === Nukem FG Mod Files (now in fgmod directory) ===
diff --git a/main.py b/main.py
index 3faa1e9..95825c2 100644
--- a/main.py
+++ b/main.py
@@ -5,12 +5,56 @@ import json
import shutil
import re
import filecmp
+import hashlib
from datetime import datetime, timezone
from pathlib import Path
-# Toggle to enable overwriting the upscaler DLL from the static remote binary.
-# Set to False or comment out this constant to skip the overwrite by default.
-UPSCALER_OVERWRITE_ENABLED = True
+OPTISCALER_ARCHIVE_ASSET = {
+ "name": "Optiscaler_0.9.2a-final.20260517._Reup.7z",
+ "sha256": "6426a16085f6128c810e0de58947029664439afd0567b6a286c0e3ef784a92a1",
+ "version": "0.9.2a-final.20260517._Reup",
+}
+
+FSR4_INT8_ASSET = {
+ "name": "amd_fidelityfx_upscaler_dx12.dll",
+ "sha256": "c7720bc16bede334f59a1a32cd22edbcbbb159685ed5240e61350a5fb0bc8a94",
+ "version": "4.0.2c",
+}
+
+OPTIPATCHER_ASSET = {
+ "name": "OptiPatcher_rolling.asi",
+ "sha256": "88b9e1be3559737cd205fdf5f2c8550cf1923fb1def4c603e5bf03c3e84131b1",
+ "version": "rolling",
+}
+
+FSR4_UPSCALER_FILENAME = "amd_fidelityfx_upscaler_dx12.dll"
+INSTALL_MANIFEST_FILENAME = "install-manifest.json"
+VERSION_FILENAME = "version.txt"
+DEFAULT_FSR4_VARIANT = "rdna23-int8"
+
+FSR4_VARIANTS = {
+ "rdna23-int8": {
+ "label": "Steam Deck / RDNA2-3 optimized",
+ "dir_name": "fsr4-rdna2-3",
+ "sha256": "c7720bc16bede334f59a1a32cd22edbcbbb159685ed5240e61350a5fb0bc8a94",
+ "source_asset_name": FSR4_INT8_ASSET["name"],
+ "source_version": FSR4_INT8_ASSET["version"],
+ "uses_archive_native": False,
+ },
+ "rdna4-native": {
+ "label": "Native bundle / RDNA4",
+ "dir_name": "fsr4-rdna4",
+ "sha256": "ec7ed3ca674e288240e6f04b986342aece47454c41d9b0959449e82e22bd7f6d",
+ "source_asset_name": OPTISCALER_ARCHIVE_ASSET["name"],
+ "source_version": OPTISCALER_ARCHIVE_ASSET["version"],
+ "uses_archive_native": True,
+ },
+}
+FSR4_VARIANT_BY_SHA256 = {
+ variant["sha256"].lower(): variant_id
+ for variant_id, variant in FSR4_VARIANTS.items()
+ if variant.get("sha256")
+}
PROXY_DLL_BACKUPS = [
"dxgi.dll",
@@ -58,7 +102,7 @@ ORIGINAL_DLL_BACKUPS = [
"d3dcompiler_47.dll",
"amd_fidelityfx_dx12.dll",
"amd_fidelityfx_framegeneration_dx12.dll",
- "amd_fidelityfx_upscaler_dx12.dll",
+ FSR4_UPSCALER_FILENAME,
"amd_fidelityfx_vk.dll",
]
@@ -74,7 +118,6 @@ SUPPORT_FILES = [
"libxell.dll",
"amd_fidelityfx_dx12.dll",
"amd_fidelityfx_framegeneration_dx12.dll",
- "amd_fidelityfx_upscaler_dx12.dll",
"amd_fidelityfx_vk.dll",
"dlssg_to_fsr3_amd_is_better.dll",
"fakenvapi.dll",
@@ -213,6 +256,137 @@ class Plugin:
backed_up.append(filename)
return backed_up
+ def _file_sha256(self, path: Path) -> str:
+ digest = hashlib.sha256()
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
+ digest.update(chunk)
+ return digest.hexdigest()
+
+ def _read_json_file(self, path: Path) -> dict:
+ try:
+ with open(path, "r", encoding="utf-8") as f:
+ data = json.load(f)
+ return data if isinstance(data, dict) else {}
+ except Exception:
+ return {}
+
+ def _write_json_file(self, path: Path, payload: dict) -> None:
+ with open(path, "w", encoding="utf-8") as f:
+ json.dump(payload, f, indent=2)
+
+ def _extract_archive(self, archive_path: Path, output_dir: Path, members: list[str] | None = None) -> None:
+ output_dir.mkdir(parents=True, exist_ok=True)
+ extract_cmd = [
+ "7z",
+ "x",
+ "-y",
+ "-o" + str(output_dir),
+ str(archive_path),
+ ]
+ if members:
+ extract_cmd.extend(members)
+
+ clean_env = os.environ.copy()
+ clean_env["LD_LIBRARY_PATH"] = ""
+ result = subprocess.run(
+ extract_cmd,
+ capture_output=True,
+ text=True,
+ check=False,
+ env=clean_env,
+ )
+ if result.returncode != 0:
+ raise RuntimeError(result.stderr or result.stdout or f"Failed to extract {archive_path.name}")
+
+ def _verify_bundled_asset(self, path: Path, expected_sha256: str, description: str) -> str:
+ actual_sha256 = self._file_sha256(path)
+ if actual_sha256.lower() != expected_sha256.lower():
+ raise RuntimeError(
+ f"{description} hash mismatch: expected {expected_sha256}, got {actual_sha256}"
+ )
+ return actual_sha256
+
+ def _install_manifest_path(self, fgmod_path: Path) -> Path:
+ return fgmod_path / INSTALL_MANIFEST_FILENAME
+
+ def _load_install_manifest(self, fgmod_path: Path) -> dict:
+ return self._read_json_file(self._install_manifest_path(fgmod_path))
+
+ def _normalize_fsr4_variant(self, fsr4_variant: str | None) -> str:
+ variant = str(fsr4_variant or "").strip()
+ if variant in FSR4_VARIANTS:
+ return variant
+ return DEFAULT_FSR4_VARIANT
+
+ def _selected_fsr4_variant(self, fgmod_path: Path, requested_variant: str | None = None) -> str:
+ normalized_requested = str(requested_variant or "").strip()
+ if normalized_requested in FSR4_VARIANTS:
+ return normalized_requested
+ manifest = self._load_install_manifest(fgmod_path)
+ manifest_variant = str(manifest.get("selected_default_variant") or "").strip()
+ if manifest_variant in FSR4_VARIANTS:
+ return manifest_variant
+ return DEFAULT_FSR4_VARIANT
+
+ def _fsr4_variant_info(self, fsr4_variant: str | None) -> dict:
+ return FSR4_VARIANTS[self._normalize_fsr4_variant(fsr4_variant)]
+
+ def _fsr4_variant_path(self, fgmod_path: Path, fsr4_variant: str | None) -> Path:
+ variant_id = self._normalize_fsr4_variant(fsr4_variant)
+ return fgmod_path / FSR4_VARIANTS[variant_id]["dir_name"] / FSR4_UPSCALER_FILENAME
+
+ def _activate_default_fsr4_variant(self, fgmod_path: Path, fsr4_variant: str | None) -> str:
+ variant_id = self._normalize_fsr4_variant(fsr4_variant)
+ variant_path = self._fsr4_variant_path(fgmod_path, variant_id)
+ if not variant_path.exists():
+ raise FileNotFoundError(f"Prepared FSR4 variant missing: {variant_path}")
+ shutil.copy2(variant_path, fgmod_path / FSR4_UPSCALER_FILENAME)
+ return variant_id
+
+ def _detect_fsr4_variant(self, upscaler_sha256: str | None) -> str | None:
+ if not upscaler_sha256:
+ return None
+ return FSR4_VARIANT_BY_SHA256.get(str(upscaler_sha256).lower())
+
+ def _fgmod_version(self, fgmod_path: Path) -> str | None:
+ manifest = self._load_install_manifest(fgmod_path)
+ optiscaler = manifest.get("optiscaler") if isinstance(manifest, dict) else None
+ if isinstance(optiscaler, dict) and optiscaler.get("version"):
+ return str(optiscaler.get("version"))
+ version_file = fgmod_path / VERSION_FILENAME
+ try:
+ if version_file.exists():
+ return version_file.read_text(encoding="utf-8").strip() or None
+ except Exception:
+ return None
+ return None
+
+ def _managed_support_candidate_paths(self, fgmod_path: Path, filename: str) -> list[Path]:
+ candidates: list[Path] = []
+ if filename == FSR4_UPSCALER_FILENAME:
+ candidates.append(fgmod_path / FSR4_UPSCALER_FILENAME)
+ for variant_id in FSR4_VARIANTS:
+ candidates.append(self._fsr4_variant_path(fgmod_path, variant_id))
+ else:
+ candidates.append(fgmod_path / filename)
+ unique: list[Path] = []
+ seen: set[str] = set()
+ for candidate in candidates:
+ key = str(candidate)
+ if key not in seen:
+ unique.append(candidate)
+ seen.add(key)
+ return unique
+
+ def _is_managed_support_file(self, path: Path, fgmod_path: Path) -> bool:
+ if not path.exists():
+ return False
+ for candidate in self._managed_support_candidate_paths(fgmod_path, path.name):
+ if self._files_match(path, candidate):
+ return True
+ return False
+
def _migrate_optiscaler_ini(self, ini_file):
"""Migrate pre-v0.9-final OptiScaler.ini: replace FGType with FGInput + FGOutput.
@@ -315,199 +489,143 @@ class Plugin:
decky.logger.error(f"Failed to modify OptiScaler.ini: {e}")
return False
- async def extract_static_optiscaler(self) -> dict:
- """Extract OptiScaler from the plugin's bin directory and copy additional files."""
+ async def extract_static_optiscaler(self, selected_default_variant: str = DEFAULT_FSR4_VARIANT) -> dict:
+ """Prepare the shared ~/fgmod bundle with both FSR4 runtime variants."""
try:
decky.logger.info("Starting extract_static_optiscaler method")
-
- # Set up paths
+
bin_path = Path(decky.DECKY_PLUGIN_DIR) / "bin"
extract_path = Path(decky.HOME) / "fgmod"
-
- decky.logger.info(f"Bin path: {bin_path}")
- decky.logger.info(f"Extract path: {extract_path}")
-
- # Check if bin directory exists
+ assets_dir = Path(decky.DECKY_PLUGIN_DIR) / "assets"
+ selected_default_variant = self._normalize_fsr4_variant(selected_default_variant)
+
if not bin_path.exists():
- decky.logger.error(f"Bin directory does not exist: {bin_path}")
return {"status": "error", "message": f"Bin directory not found: {bin_path}"}
-
- # List files in bin directory for debugging
- bin_files = list(bin_path.glob("*"))
- decky.logger.info(f"Files in bin directory: {[f.name for f in bin_files]}")
-
- # Find the OptiScaler archive in the bin directory
- optiscaler_archive = None
- for file in bin_path.glob("*.7z"):
- decky.logger.info(f"Checking 7z file: {file.name}")
- # Check for both "OptiScaler" and "Optiscaler" (case variations) and exclude BUNDLE files
- if ("OptiScaler" in file.name or "Optiscaler" in file.name) and "BUNDLE" not in file.name:
- optiscaler_archive = file
- decky.logger.info(f"Found OptiScaler archive: {file.name}")
- break
-
- if not optiscaler_archive:
- decky.logger.error("OptiScaler archive not found in plugin bin directory")
- return {"status": "error", "message": "OptiScaler archive not found in plugin bin directory"}
-
- decky.logger.info(f"Using archive: {optiscaler_archive}")
-
- # Clean up existing directory
- if extract_path.exists():
- decky.logger.info(f"Removing existing directory: {extract_path}")
- shutil.rmtree(extract_path)
-
- extract_path.mkdir(exist_ok=True)
- decky.logger.info(f"Created extract directory: {extract_path}")
-
- decky.logger.info(f"Extracting {optiscaler_archive.name} to {extract_path}")
-
- # Extract the 7z file
- extract_cmd = [
- "7z",
- "x",
- "-y",
- "-o" + str(extract_path),
- str(optiscaler_archive)
- ]
-
- decky.logger.info(f"Running extraction command: {' '.join(extract_cmd)}")
-
- # Create a clean environment to avoid PyInstaller issues
- clean_env = os.environ.copy()
- clean_env["LD_LIBRARY_PATH"] = ""
-
- decky.logger.info("Starting subprocess.run for extraction")
- extract_result = subprocess.run(
- extract_cmd,
- capture_output=True,
- text=True,
- check=False,
- env=clean_env
- )
-
- decky.logger.info(f"Extraction completed with return code: {extract_result.returncode}")
- decky.logger.info(f"Extraction stdout: {extract_result.stdout}")
- if extract_result.stderr:
- decky.logger.info(f"Extraction stderr: {extract_result.stderr}")
-
- if extract_result.returncode != 0:
- decky.logger.error(f"Extraction failed: {extract_result.stderr}")
- return {
- "status": "error",
- "message": f"Failed to extract OptiScaler archive: {extract_result.stderr}"
- }
-
- # Copy additional individual files from bin directory
- # Note: v0.9.0-final includes dlssg_to_fsr3_amd_is_better.dll, fakenvapi.dll, and fakenvapi.ini in the 7z
- # Only copy files that aren't already in the archive (separate remote binaries)
- # nvngx.dll is intentionally excluded: it was a stale DLSS 3.10.3 stub from a
- # pre-0.9 nightly that is missing DLSS 3.1+ exports (AllocateParameters,
- # GetCapabilityParameters, Init_with_ProjectID, etc.) present in OptiScaler
- # 0.9.0-final's own NGX proxy layer. OptiScaler handles all NGX interception
- # internally; the bare nvidia DLL caused export-not-found failures on Proton.
- additional_files = [
- "OptiPatcher_rolling.asi" # ASI plugin for OptiScaler spoofing
- ]
-
- decky.logger.info("Starting additional files copy")
- for file_name in additional_files:
- src_file = bin_path / file_name
- dest_file = extract_path / file_name
-
- decky.logger.info(f"Checking for additional file: {file_name} at {src_file}")
- if src_file.exists():
- shutil.copy2(src_file, dest_file)
- decky.logger.info(f"Copied additional file: {file_name}")
- else:
- decky.logger.warning(f"Additional file not found: {file_name}")
+
+ optiscaler_archive = bin_path / OPTISCALER_ARCHIVE_ASSET["name"]
+ fsr4_int8_src = bin_path / FSR4_INT8_ASSET["name"]
+ optipatcher_src = bin_path / OPTIPATCHER_ASSET["name"]
+ for required_path, asset in [
+ (optiscaler_archive, OPTISCALER_ARCHIVE_ASSET),
+ (fsr4_int8_src, FSR4_INT8_ASSET),
+ (optipatcher_src, OPTIPATCHER_ASSET),
+ ]:
+ if not required_path.exists():
return {
"status": "error",
- "message": f"Required file {file_name} not found in plugin bin directory"
+ "message": f"Required bundled asset missing: {asset['name']}",
}
-
- decky.logger.info("Creating renamed copies of OptiScaler.dll")
- # Create renamed copies of OptiScaler.dll
+ self._verify_bundled_asset(required_path, asset["sha256"], asset["name"])
+
+ if extract_path.exists():
+ shutil.rmtree(extract_path)
+ extract_path.mkdir(parents=True, exist_ok=True)
+
+ self._extract_archive(optiscaler_archive, extract_path)
+
source_file = extract_path / "OptiScaler.dll"
renames_dir = extract_path / "renames"
- self._create_renamed_copies(source_file, renames_dir)
-
- decky.logger.info("Copying launcher scripts")
- # Copy launcher scripts from assets
- assets_dir = Path(decky.DECKY_PLUGIN_DIR) / "assets"
- self._copy_launcher_scripts(assets_dir, extract_path)
+ if not self._create_renamed_copies(source_file, renames_dir):
+ return {"status": "error", "message": "Failed to prepare renamed OptiScaler proxies."}
+
+ if not self._copy_launcher_scripts(assets_dir, extract_path):
+ return {"status": "error", "message": "Failed to copy launcher scripts."}
+
+ plugins_dir = extract_path / "plugins"
+ plugins_dir.mkdir(parents=True, exist_ok=True)
+ optipatcher_dst = plugins_dir / "OptiPatcher.asi"
+ shutil.copy2(optipatcher_src, optipatcher_dst)
+ optipatcher_sha256 = self._verify_bundled_asset(
+ optipatcher_dst,
+ OPTIPATCHER_ASSET["sha256"],
+ "Prepared OptiPatcher plugin",
+ )
- decky.logger.info("Setting up ASI plugins directory")
- # Create plugins directory and copy OptiPatcher ASI file
- try:
- plugins_dir = extract_path / "plugins"
- plugins_dir.mkdir(exist_ok=True)
- decky.logger.info(f"Created plugins directory: {plugins_dir}")
-
- # Copy OptiPatcher ASI file to plugins directory
- asi_src = bin_path / "OptiPatcher_rolling.asi"
- asi_dst = plugins_dir / "OptiPatcher.asi" # Rename to generic name
-
- if asi_src.exists():
- shutil.copy2(asi_src, asi_dst)
- decky.logger.info(f"Copied OptiPatcher ASI to plugins directory: {asi_dst}")
- else:
- decky.logger.warning("OptiPatcher ASI file not found in bin directory")
- except Exception as e:
- decky.logger.error(f"Failed to setup ASI plugins directory: {e}")
-
- decky.logger.info("Starting upscaler DLL overwrite check")
- # Optionally overwrite amd_fidelityfx_upscaler_dx12.dll with the separately bundled
- # RDNA2-optimized static binary used for Steam Deck compatibility.
- # Toggle via env DECKY_SKIP_UPSCALER_OVERWRITE=true to skip.
- try:
- skip_overwrite = os.environ.get("DECKY_SKIP_UPSCALER_OVERWRITE", "false").lower() in ("1", "true", "yes")
- if UPSCALER_OVERWRITE_ENABLED and not skip_overwrite:
- upscaler_src = bin_path / "amd_fidelityfx_upscaler_dx12.dll"
- upscaler_dst = extract_path / "amd_fidelityfx_upscaler_dx12.dll"
- if upscaler_src.exists():
- shutil.copy2(upscaler_src, upscaler_dst)
- decky.logger.info("Overwrote amd_fidelityfx_upscaler_dx12.dll with static remote binary")
- else:
- decky.logger.warning("amd_fidelityfx_upscaler_dx12.dll not found in bin; skipping overwrite")
- else:
- decky.logger.info("Skipping upscaler DLL overwrite due to DECKY_SKIP_UPSCALER_OVERWRITE")
- except Exception as e:
- decky.logger.error(f"Failed upscaler overwrite step: {e}")
-
- # Extract version from filename (e.g., OptiScaler_0.7.9.7z -> v0.7.9)
- version_match = optiscaler_archive.name.replace('.7z', '')
- if 'OptiScaler_' in version_match:
- version = 'v' + version_match.split('OptiScaler_')[1]
- elif 'Optiscaler_' in version_match:
- version = 'v' + version_match.split('Optiscaler_')[1]
- else:
- version = version_match
-
- # Create version file
- version_file = extract_path / "version.txt"
- try:
- with open(version_file, 'w') as f:
- f.write(version)
- decky.logger.info(f"Created version file: {version}")
- except Exception as e:
- decky.logger.error(f"Failed to create version file: {e}")
-
- # Modify OptiScaler.ini to set FGType=nukems and Fsr4Update=true
- decky.logger.info("Modifying OptiScaler.ini")
ini_file = extract_path / "OptiScaler.ini"
self._modify_optiscaler_ini(ini_file)
-
- decky.logger.info(f"Successfully completed extraction to ~/fgmod with version {version}")
+
+ native_upscaler_root = extract_path / FSR4_UPSCALER_FILENAME
+ native_upscaler_sha256 = self._verify_bundled_asset(
+ native_upscaler_root,
+ FSR4_VARIANTS["rdna4-native"]["sha256"],
+ "Archive-native FSR4 upscaler",
+ )
+
+ rdna4_dir = extract_path / FSR4_VARIANTS["rdna4-native"]["dir_name"]
+ rdna4_dir.mkdir(parents=True, exist_ok=True)
+ rdna4_upscaler = rdna4_dir / FSR4_UPSCALER_FILENAME
+ shutil.copy2(native_upscaler_root, rdna4_upscaler)
+ self._verify_bundled_asset(
+ rdna4_upscaler,
+ FSR4_VARIANTS["rdna4-native"]["sha256"],
+ "Prepared rdna4-native FSR4 upscaler",
+ )
+
+ rdna23_dir = extract_path / FSR4_VARIANTS["rdna23-int8"]["dir_name"]
+ rdna23_dir.mkdir(parents=True, exist_ok=True)
+ self._verify_bundled_asset(
+ fsr4_int8_src,
+ FSR4_VARIANTS["rdna23-int8"]["sha256"],
+ "Bundled rdna23-int8 FSR4 upscaler",
+ )
+ shutil.copy2(fsr4_int8_src, rdna23_dir / FSR4_UPSCALER_FILENAME)
+ self._verify_bundled_asset(
+ rdna23_dir / FSR4_UPSCALER_FILENAME,
+ FSR4_VARIANTS["rdna23-int8"]["sha256"],
+ "Prepared rdna23-int8 FSR4 upscaler",
+ )
+
+ selected_default_variant = self._activate_default_fsr4_variant(extract_path, selected_default_variant)
+ active_upscaler_sha256 = self._file_sha256(extract_path / FSR4_UPSCALER_FILENAME)
+
+ version_file = extract_path / VERSION_FILENAME
+ version_file.write_text(OPTISCALER_ARCHIVE_ASSET["version"], encoding="utf-8")
+
+ install_manifest = {
+ "schema_version": 1,
+ "installed_at": datetime.now(timezone.utc).isoformat(),
+ "optiscaler": {
+ "asset_name": OPTISCALER_ARCHIVE_ASSET["name"],
+ "version": OPTISCALER_ARCHIVE_ASSET["version"],
+ "sha256": OPTISCALER_ARCHIVE_ASSET["sha256"],
+ "native_upscaler_sha256": native_upscaler_sha256,
+ },
+ "optipatcher": {
+ "asset_name": OPTIPATCHER_ASSET["name"],
+ "version": OPTIPATCHER_ASSET["version"],
+ "sha256": optipatcher_sha256,
+ "target_path": str(optipatcher_dst.relative_to(extract_path)),
+ },
+ "fsr4_variants": {
+ variant_id: {
+ "label": variant["label"],
+ "dir_name": variant["dir_name"],
+ "path": str((Path(variant["dir_name"]) / FSR4_UPSCALER_FILENAME).as_posix()),
+ "sha256": variant["sha256"],
+ "source_asset_name": variant["source_asset_name"],
+ "source_version": variant["source_version"],
+ "uses_archive_native": bool(variant["uses_archive_native"]),
+ }
+ for variant_id, variant in FSR4_VARIANTS.items()
+ },
+ "selected_default_variant": selected_default_variant,
+ "active_root_upscaler": {
+ "path": FSR4_UPSCALER_FILENAME,
+ "sha256": active_upscaler_sha256,
+ "variant": selected_default_variant,
+ },
+ }
+ self._write_json_file(self._install_manifest_path(extract_path), install_manifest)
+
return {
"status": "success",
- "message": f"Successfully extracted OptiScaler {version} to ~/fgmod",
- "version": version
+ "message": f"Successfully extracted OptiScaler {OPTISCALER_ARCHIVE_ASSET['version']} to ~/fgmod",
+ "version": OPTISCALER_ARCHIVE_ASSET["version"],
+ "selected_default_variant": selected_default_variant,
+ "selected_default_variant_label": FSR4_VARIANTS[selected_default_variant]["label"],
}
-
except Exception as e:
decky.logger.error(f"Extract failed with exception: {str(e)}")
- decky.logger.error(f"Exception type: {type(e).__name__}")
import traceback
decky.logger.error(f"Traceback: {traceback.format_exc()}")
return {"status": "error", "message": f"Extract failed: {str(e)}"}
@@ -538,22 +656,63 @@ class Plugin:
"output": str(e)
}
- async def run_install_fgmod(self) -> dict:
+ async def set_default_fsr4_variant(self, selected_default_variant: str = DEFAULT_FSR4_VARIANT) -> dict:
+ try:
+ fgmod_path = Path(decky.HOME) / "fgmod"
+ if not fgmod_path.exists():
+ return {"status": "error", "message": "OptiScaler bundle not installed. Run Install first."}
+
+ selected_default_variant = self._normalize_fsr4_variant(selected_default_variant)
+ manifest = self._load_install_manifest(fgmod_path)
+ if not manifest:
+ return {"status": "error", "message": "Install manifest missing. Reinstall OptiScaler."}
+
+ selected_default_variant = self._activate_default_fsr4_variant(fgmod_path, selected_default_variant)
+ active_upscaler_sha256 = self._file_sha256(fgmod_path / FSR4_UPSCALER_FILENAME)
+ manifest["selected_default_variant"] = selected_default_variant
+ manifest["active_root_upscaler"] = {
+ "path": FSR4_UPSCALER_FILENAME,
+ "sha256": active_upscaler_sha256,
+ "variant": selected_default_variant,
+ }
+ manifest["updated_at"] = datetime.now(timezone.utc).isoformat()
+ self._write_json_file(self._install_manifest_path(fgmod_path), manifest)
+ return {
+ "status": "success",
+ "output": f"Default FSR4 runtime switched to {FSR4_VARIANTS[selected_default_variant]['label']}.",
+ "version": self._fgmod_version(fgmod_path),
+ "selected_default_variant": selected_default_variant,
+ "selected_default_variant_label": FSR4_VARIANTS[selected_default_variant]["label"],
+ }
+ except Exception as e:
+ decky.logger.error(f"Failed to switch default FSR4 runtime: {e}")
+ return {"status": "error", "message": f"Failed to switch default FSR4 runtime: {e}"}
+
+ async def run_install_fgmod(self, selected_default_variant: str = DEFAULT_FSR4_VARIANT) -> dict:
try:
decky.logger.info("Starting OptiScaler installation from static bundle")
-
- # Extract the static OptiScaler bundle
- extract_result = await self.extract_static_optiscaler()
-
+ selected_default_variant = self._normalize_fsr4_variant(selected_default_variant)
+
+ extract_result = await self.extract_static_optiscaler(selected_default_variant)
if extract_result["status"] != "success":
return {
"status": "error",
"message": f"OptiScaler extraction failed: {extract_result.get('message', 'Unknown error')}"
}
-
+
return {
"status": "success",
- "output": "Successfully installed OptiScaler with all necessary components! You can now replace DLSS with FSR Frame Gen!"
+ "output": (
+ "Successfully installed OptiScaler "
+ f"{extract_result.get('version', OPTISCALER_ARCHIVE_ASSET['version'])} "
+ f"with {extract_result.get('selected_default_variant_label', FSR4_VARIANTS[selected_default_variant]['label'])}."
+ ),
+ "version": extract_result.get("version", OPTISCALER_ARCHIVE_ASSET["version"]),
+ "selected_default_variant": extract_result.get("selected_default_variant", selected_default_variant),
+ "selected_default_variant_label": extract_result.get(
+ "selected_default_variant_label",
+ FSR4_VARIANTS[selected_default_variant]["label"],
+ ),
}
except Exception as e:
@@ -568,37 +727,49 @@ class Plugin:
required_files = [
"OptiScaler.dll",
"OptiScaler.ini",
- "dlssg_to_fsr3_amd_is_better.dll",
- "fakenvapi.dll", # v0.9.0-final includes fakenvapi.dll in archive
+ "dlssg_to_fsr3_amd_is_better.dll",
+ "fakenvapi.dll",
"fakenvapi.ini",
"amd_fidelityfx_dx12.dll",
"amd_fidelityfx_framegeneration_dx12.dll",
- "amd_fidelityfx_upscaler_dx12.dll",
- "amd_fidelityfx_vk.dll",
+ FSR4_UPSCALER_FILENAME,
+ "amd_fidelityfx_vk.dll",
"libxess.dll",
"libxess_dx11.dll",
- "libxess_fg.dll", # added in v0.9.0
- "libxell.dll", # added in v0.9.0
+ "libxess_fg.dll",
+ "libxell.dll",
"fgmod",
"fgmod-uninstaller.sh",
- "update-optiscaler-config.py"
+ "update-optiscaler-config.py",
+ INSTALL_MANIFEST_FILENAME,
]
- if path.exists():
- # Check required files
- for file_name in required_files:
- if not path.joinpath(file_name).exists():
- return {"exists": False}
+ if not path.exists():
+ return {"exists": False}
- # Check plugins directory and OptiPatcher ASI
- plugins_dir = path / "plugins"
- if not plugins_dir.exists() or not (plugins_dir / "OptiPatcher.asi").exists():
+ for file_name in required_files:
+ if not path.joinpath(file_name).exists():
return {"exists": False}
- return {"exists": True}
- else:
+ plugins_dir = path / "plugins"
+ if not plugins_dir.exists() or not (plugins_dir / "OptiPatcher.asi").exists():
return {"exists": False}
+ for variant in FSR4_VARIANTS.values():
+ variant_path = path / variant["dir_name"] / FSR4_UPSCALER_FILENAME
+ if not variant_path.exists():
+ return {"exists": False}
+
+ manifest = self._load_install_manifest(path)
+ selected_variant = self._selected_fsr4_variant(path)
+ return {
+ "exists": True,
+ "version": self._fgmod_version(path),
+ "selected_fsr4_variant": selected_variant,
+ "selected_fsr4_variant_label": FSR4_VARIANTS[selected_variant]["label"],
+ "install_manifest_present": bool(manifest),
+ }
+
def _resolve_target_directory(self, directory: str) -> Path:
decky.logger.info(f"Resolving target directory: {directory}")
target = Path(directory).expanduser()
@@ -611,7 +782,13 @@ class Plugin:
decky.logger.info(f"Resolved directory {directory} to absolute path {target}")
return target
- def _manual_patch_directory_impl(self, directory: Path, dll_name: str = "dxgi.dll") -> dict:
+ def _manual_patch_directory_impl(
+ self,
+ directory: Path,
+ dll_name: str = "dxgi.dll",
+ fsr4_variant: str | None = None,
+ allow_managed_support_cleanup: bool = False,
+ ) -> dict:
fgmod_path = Path(decky.HOME) / "fgmod"
if not fgmod_path.exists():
return {
@@ -627,9 +804,23 @@ class Plugin:
}
preserve_ini = True
+ selected_variant = self._selected_fsr4_variant(fgmod_path, fsr4_variant)
+ selected_variant_info = FSR4_VARIANTS[selected_variant]
+ selected_upscaler_src = self._fsr4_variant_path(fgmod_path, selected_variant)
+ if not selected_upscaler_src.exists():
+ selected_upscaler_src = fgmod_path / FSR4_UPSCALER_FILENAME
+ if not selected_upscaler_src.exists():
+ return {
+ "status": "error",
+ "message": f"FSR4 upscaler variant not found for {selected_variant}. Reinstall OptiScaler.",
+ }
+ optiscaler_version = self._fgmod_version(fgmod_path)
+ selected_upscaler_sha256 = self._file_sha256(selected_upscaler_src)
try:
- decky.logger.info(f"Manual patch started for {directory}")
+ decky.logger.info(
+ f"Manual patch started for {directory} with FSR4 variant {selected_variant} ({selected_variant_info['label']})"
+ )
backed_up_proxies = self._backup_preexisting_proxy_files(directory, fgmod_path)
decky.logger.info(
@@ -651,12 +842,20 @@ class Plugin:
)
backed_up_originals = []
+ removed_managed_support = []
for dll in ORIGINAL_DLL_BACKUPS:
source = directory / dll
backup = directory / f"{dll}.b"
- if source.exists() and not backup.exists():
- shutil.move(source, backup)
- backed_up_originals.append(dll)
+ if not source.exists() or backup.exists():
+ continue
+ if allow_managed_support_cleanup and self._is_managed_support_file(source, fgmod_path):
+ source.unlink()
+ removed_managed_support.append(dll)
+ continue
+ shutil.move(source, backup)
+ backed_up_originals.append(dll)
+ if removed_managed_support:
+ decky.logger.info(f"Removed managed support files before repatch: {removed_managed_support}")
decky.logger.info(
f"Backed up original game DLLs: {backed_up_originals}"
if backed_up_originals
@@ -709,6 +908,11 @@ class Plugin:
copied_support.append(filename)
else:
missing_support.append(filename)
+
+ upscaler_dest = directory / FSR4_UPSCALER_FILENAME
+ shutil.copy2(selected_upscaler_src, upscaler_dest)
+ copied_support.append(FSR4_UPSCALER_FILENAME)
+
if copied_support:
decky.logger.info(f"Copied support files: {copied_support}")
if missing_support:
@@ -717,7 +921,14 @@ class Plugin:
decky.logger.info(f"Manual patch complete for {directory}")
return {
"status": "success",
- "message": f"OptiScaler files copied to {directory}",
+ "message": (
+ f"OptiScaler files copied to {directory} using "
+ f"{selected_variant_info['label']}"
+ ),
+ "fsr4_variant": selected_variant,
+ "fsr4_variant_label": selected_variant_info["label"],
+ "fsr4_upscaler_sha256": selected_upscaler_sha256,
+ "optiscaler_version": optiscaler_version,
}
except PermissionError as exc:
@@ -738,7 +949,7 @@ class Plugin:
decky.logger.info(f"Manual unpatch started for {directory}")
removed_files = []
- for filename in set(INJECTOR_FILENAMES + SUPPORT_FILES):
+ for filename in set(INJECTOR_FILENAMES + SUPPORT_FILES + [FSR4_UPSCALER_FILENAME]):
path = directory / filename
if path.exists():
path.unlink()
@@ -1007,7 +1218,12 @@ class Plugin:
target_dir: Path,
original_launch_options: str,
backed_up_files: list[str],
+ optiscaler_version: str | None = None,
+ fsr4_variant: str | None = None,
+ fsr4_upscaler_sha256: str | None = None,
) -> None:
+ normalized_variant = self._normalize_fsr4_variant(fsr4_variant)
+ variant_info = FSR4_VARIANTS[normalized_variant]
payload = {
"appid": str(appid),
"game_name": game_name,
@@ -1015,10 +1231,21 @@ class Plugin:
"target_dir": str(target_dir),
"original_launch_options": original_launch_options,
"backed_up_files": backed_up_files,
+ "optiscaler_version": optiscaler_version,
+ "fsr4_variant": normalized_variant,
+ "fsr4_variant_label": variant_info["label"],
+ "fsr4_upscaler_sha256": fsr4_upscaler_sha256,
+ "managed_files": [
+ {
+ "path": str(target_dir / FSR4_UPSCALER_FILENAME),
+ "sha256": fsr4_upscaler_sha256,
+ "kind": "fsr4-upscaler",
+ "variant": normalized_variant,
+ }
+ ],
"patched_at": datetime.now(timezone.utc).isoformat(),
}
- with open(marker_path, "w", encoding="utf-8") as f:
- json.dump(payload, f, indent=2)
+ self._write_json_file(marker_path, payload)
# ── Launch options helpers ────────────────────────────────────────────────
@@ -1073,7 +1300,12 @@ class Plugin:
async def log_error(self, error: str) -> None:
decky.logger.error(f"FRONTEND: {error}")
- async def manual_patch_directory(self, directory: str, dll_name: str = "dxgi.dll") -> dict:
+ async def manual_patch_directory(
+ self,
+ directory: str,
+ dll_name: str = "dxgi.dll",
+ fsr4_variant: str = DEFAULT_FSR4_VARIANT,
+ ) -> dict:
if dll_name not in VALID_DLL_NAMES:
return {"status": "error", "message": f"Invalid proxy DLL name: {dll_name}"}
try:
@@ -1082,7 +1314,13 @@ class Plugin:
decky.logger.error(f"Manual patch validation failed: {exc}")
return {"status": "error", "message": str(exc)}
- return self._manual_patch_directory_impl(target_dir, dll_name)
+ allow_managed_support_cleanup = (target_dir / MARKER_FILENAME).exists()
+ return self._manual_patch_directory_impl(
+ target_dir,
+ dll_name,
+ fsr4_variant,
+ allow_managed_support_cleanup=allow_managed_support_cleanup,
+ )
async def manual_unpatch_directory(self, directory: str) -> dict:
try:
@@ -1106,6 +1344,8 @@ class Plugin:
"patched": False,
"dll_name": None,
"target_dir": None,
+ "fsr4_variant": None,
+ "fsr4_variant_label": None,
"message": "Game not found in Steam library.",
}
install_root = Path(game_info["install_path"])
@@ -1118,6 +1358,8 @@ class Plugin:
"patched": False,
"dll_name": None,
"target_dir": None,
+ "fsr4_variant": None,
+ "fsr4_variant_label": None,
"message": "Game install directory not found.",
}
marker = self._find_marker(install_root)
@@ -1130,12 +1372,20 @@ class Plugin:
"patched": False,
"dll_name": None,
"target_dir": None,
+ "fsr4_variant": None,
+ "fsr4_variant_label": None,
"message": "Not patched.",
}
metadata = self._read_marker(marker)
dll_name = metadata.get("dll_name", "dxgi.dll")
target_dir = Path(metadata.get("target_dir", str(marker.parent)))
dll_present = (target_dir / dll_name).exists()
+ upscaler_path = target_dir / FSR4_UPSCALER_FILENAME
+ upscaler_sha256 = self._file_sha256(upscaler_path) if upscaler_path.exists() else None
+ detected_variant = self._detect_fsr4_variant(upscaler_sha256)
+ stored_variant = str(metadata.get("fsr4_variant") or "").strip() or None
+ effective_variant = detected_variant or (stored_variant if stored_variant in FSR4_VARIANTS else None)
+ effective_label = FSR4_VARIANTS[effective_variant]["label"] if effective_variant else None
return {
"status": "success",
"appid": str(appid),
@@ -1145,8 +1395,12 @@ class Plugin:
"dll_name": dll_name,
"target_dir": str(target_dir),
"patched_at": metadata.get("patched_at"),
+ "optiscaler_version": metadata.get("optiscaler_version"),
+ "fsr4_variant": effective_variant,
+ "fsr4_variant_label": effective_label,
+ "fsr4_upscaler_sha256": upscaler_sha256,
"message": (
- f"Patched using {dll_name}."
+ f"Patched using {dll_name}" + (f" with {effective_label}." if effective_label else ".")
if dll_present
else f"Marker found but {dll_name} is missing. Reinstall recommended."
),
@@ -1155,7 +1409,13 @@ class Plugin:
decky.logger.error(f"[Framegen] get_game_status failed for {appid}: {exc}")
return {"status": "error", "message": str(exc)}
- async def patch_game(self, appid: str, dll_name: str = "dxgi.dll", current_launch_options: str = "") -> dict:
+ async def patch_game(
+ self,
+ appid: str,
+ dll_name: str = "dxgi.dll",
+ current_launch_options: str = "",
+ fsr4_variant: str = DEFAULT_FSR4_VARIANT,
+ ) -> dict:
try:
if dll_name not in VALID_DLL_NAMES:
return {"status": "error", "message": f"Invalid proxy DLL name: {dll_name}"}
@@ -1174,15 +1434,14 @@ class Plugin:
# Preserve true original launch options across re-patches
original_launch_options = current_launch_options or ""
existing_marker = self._find_marker(install_root)
+ existing_marker_metadata = self._read_marker(existing_marker) if existing_marker else {}
+ existing_marker_target_dir = Path(
+ existing_marker_metadata.get("target_dir", str(existing_marker.parent))
+ ) if existing_marker else None
if existing_marker:
- metadata = self._read_marker(existing_marker)
- stored_opts = str(metadata.get("original_launch_options") or "")
+ stored_opts = str(existing_marker_metadata.get("original_launch_options") or "")
if stored_opts and not self._is_managed_launch_options(stored_opts):
original_launch_options = stored_opts
- try:
- existing_marker.unlink()
- except Exception:
- pass
if self._is_managed_launch_options(original_launch_options):
original_launch_options = ""
@@ -1190,7 +1449,15 @@ class Plugin:
target_dir, target_exe = self._guess_patch_target(game_info)
decky.logger.info(f"[Framegen] patch_game: appid={appid} dll={dll_name} target={target_dir} exe={target_exe}")
- result = self._manual_patch_directory_impl(target_dir, dll_name)
+ allow_managed_support_cleanup = bool(
+ existing_marker and existing_marker_target_dir == target_dir
+ ) or (target_dir / MARKER_FILENAME).exists()
+ result = self._manual_patch_directory_impl(
+ target_dir,
+ dll_name,
+ fsr4_variant,
+ allow_managed_support_cleanup=allow_managed_support_cleanup,
+ )
if result["status"] != "success":
return result
@@ -1204,8 +1471,17 @@ class Plugin:
target_dir=target_dir,
original_launch_options=original_launch_options,
backed_up_files=backed_up,
+ optiscaler_version=result.get("optiscaler_version"),
+ fsr4_variant=result.get("fsr4_variant"),
+ fsr4_upscaler_sha256=result.get("fsr4_upscaler_sha256"),
)
+ if existing_marker and existing_marker != marker_path:
+ try:
+ existing_marker.unlink()
+ except Exception:
+ pass
+
managed_launch_options = self._build_managed_launch_options(dll_name)
decky.logger.info(f"[Framegen] patch_game success: appid={appid} launch_options={managed_launch_options}")
return {
@@ -1216,7 +1492,14 @@ class Plugin:
"target_dir": str(target_dir),
"launch_options": managed_launch_options,
"original_launch_options": original_launch_options,
- "message": f"Patched {game_info['name']} using {dll_name}.",
+ "optiscaler_version": result.get("optiscaler_version"),
+ "fsr4_variant": result.get("fsr4_variant"),
+ "fsr4_variant_label": result.get("fsr4_variant_label"),
+ "fsr4_upscaler_sha256": result.get("fsr4_upscaler_sha256"),
+ "message": (
+ f"Patched {game_info['name']} using {dll_name} "
+ f"with {result.get('fsr4_variant_label', FSR4_VARIANTS[self._normalize_fsr4_variant(fsr4_variant)]['label'])}."
+ ),
}
except Exception as exc:
decky.logger.error(f"[Framegen] patch_game failed for {appid}: {exc}")
diff --git a/package.json b/package.json
index 97f17bc..e07bcb9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "decky-framegen",
- "version": "0.15.3",
+ "version": "0.15.5",
"description": "This plugin installs and manages OptiScaler, a tool that enhances upscaling and enables frame generation in a range of DirectX 12 games.",
"type": "module",
"scripts": {
@@ -30,6 +30,7 @@
"devDependencies": {
"@decky/rollup": "^1.0.1",
"@decky/ui": "^4.7.2",
+ "@rollup/rollup-linux-x64-musl": "^4.22.5",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/webpack": "^5.28.5",
@@ -53,13 +54,13 @@
"remote_binary":
[
{
- "sha256hash": "a988ce2c0a86bba58a6313659d1ed2ab78f994dbdfab246394a2e4293ac68010",
- "url": "https://github.com/optiscaler/OptiScaler/releases/download/v0.9.0/Optiscaler_0.9.0-final.20260401._AF.7z",
- "name": "Optiscaler_0.9.0-final.20260401._AF.7z"
+ "sha256hash": "6426a16085f6128c810e0de58947029664439afd0567b6a286c0e3ef784a92a1",
+ "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/opti-9-2-a/Optiscaler_0.9.2a-final.20260517._Reup.7z",
+ "name": "Optiscaler_0.9.2a-final.20260517._Reup.7z"
},
{
"sha256hash": "c7720bc16bede334f59a1a32cd22edbcbbb159685ed5240e61350a5fb0bc8a94",
- "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/bins-for-4.0.2.c/amd_fidelityfx_upscaler_dx12.dll",
+ "url": "https://github.com/xXJSONDeruloXx/OptiScaler-Bleeding-Edge/releases/download/opti-9-2-a/amd_fidelityfx_upscaler_dx12.dll",
"name": "amd_fidelityfx_upscaler_dx12.dll"
},
{
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8f954a4..af6c535 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,6 +24,9 @@ importers:
'@decky/ui':
specifier: ^4.7.2
version: 4.7.2
+ '@rollup/rollup-linux-x64-musl':
+ specifier: ^4.22.5
+ version: 4.22.5
'@types/react':
specifier: 18.3.3
version: 18.3.3
@@ -174,41 +177,49 @@ packages:
resolution: {integrity: sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.22.5':
resolution: {integrity: sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.22.5':
resolution: {integrity: sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.22.5':
resolution: {integrity: sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.22.5':
resolution: {integrity: sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.22.5':
resolution: {integrity: sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.22.5':
resolution: {integrity: sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.22.5':
resolution: {integrity: sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.22.5':
resolution: {integrity: sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==}
@@ -513,11 +524,12 @@ packages:
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
- deprecated: Glob versions prior to v9 are no longer supported
+ deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
globby@10.0.2:
resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==}
@@ -1055,8 +1067,7 @@ snapshots:
'@rollup/rollup-linux-x64-gnu@4.22.5':
optional: true
- '@rollup/rollup-linux-x64-musl@4.22.5':
- optional: true
+ '@rollup/rollup-linux-x64-musl@4.22.5': {}
'@rollup/rollup-win32-arm64-msvc@4.22.5':
optional: true
diff --git a/src/api/index.ts b/src/api/index.ts
index c205a87..b1bf0b8 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,8 +1,15 @@
import { callable } from "@decky/api";
export const runInstallFGMod = callable<
- [],
- { status: string; message?: string; output?: string }
+ [selected_default_variant?: string],
+ {
+ status: string;
+ message?: string;
+ output?: string;
+ version?: string;
+ selected_default_variant?: string;
+ selected_default_variant_label?: string;
+ }
>("run_install_fgmod");
export const runUninstallFGMod = callable<
@@ -10,9 +17,27 @@ export const runUninstallFGMod = callable<
{ status: string; message?: string; output?: string }
>("run_uninstall_fgmod");
+export const setDefaultFsr4Variant = callable<
+ [selected_default_variant?: string],
+ {
+ status: string;
+ message?: string;
+ output?: string;
+ version?: string;
+ selected_default_variant?: string;
+ selected_default_variant_label?: string;
+ }
+>("set_default_fsr4_variant");
+
export const checkFGModPath = callable<
[],
- { exists: boolean }
+ {
+ exists: boolean;
+ version?: string | null;
+ selected_fsr4_variant?: string | null;
+ selected_fsr4_variant_label?: string | null;
+ install_manifest_present?: boolean;
+ }
>("check_fgmod_path");
export const listInstalledGames = callable<
@@ -28,8 +53,16 @@ export const getPathDefaults = callable<
>("get_path_defaults");
export const runManualPatch = callable<
- [string, string],
- { status: string; message?: string; output?: string }
+ [string, string, string],
+ {
+ status: string;
+ message?: string;
+ output?: string;
+ fsr4_variant?: string;
+ fsr4_variant_label?: string;
+ fsr4_upscaler_sha256?: string;
+ optiscaler_version?: string | null;
+ }
>("manual_patch_directory");
export const runManualUnpatch = callable<
@@ -49,11 +82,15 @@ export const getGameStatus = callable<
dll_name?: string | null;
target_dir?: string | null;
patched_at?: string | null;
+ optiscaler_version?: string | null;
+ fsr4_variant?: string | null;
+ fsr4_variant_label?: string | null;
+ fsr4_upscaler_sha256?: string | null;
}
>("get_game_status");
export const patchGame = callable<
- [appid: string, dll_name: string, current_launch_options: string],
+ [appid: string, dll_name: string, current_launch_options: string, fsr4_variant: string],
{
status: string;
message?: string;
@@ -63,6 +100,10 @@ export const patchGame = callable<
target_dir?: string;
launch_options?: string;
original_launch_options?: string;
+ optiscaler_version?: string | null;
+ fsr4_variant?: string;
+ fsr4_variant_label?: string;
+ fsr4_upscaler_sha256?: string;
}
>("patch_game");
diff --git a/src/components/ClipboardCommands.tsx b/src/components/ClipboardCommands.tsx
index e1f6ef9..124423e 100644
--- a/src/components/ClipboardCommands.tsx
+++ b/src/components/ClipboardCommands.tsx
@@ -3,9 +3,16 @@ import { SmartClipboardButton } from "./SmartClipboardButton";
interface ClipboardCommandsProps {
pathExists: boolean | null;
dllName: string;
+ manualModeEnabled?: boolean;
+ showLaunchOptions?: boolean;
}
-export function ClipboardCommands({ pathExists, dllName }: ClipboardCommandsProps) {
+export function ClipboardCommands({
+ pathExists,
+ dllName,
+ manualModeEnabled = false,
+ showLaunchOptions = true,
+}: ClipboardCommandsProps) {
if (pathExists !== true) return null;
const launchCmd =
@@ -14,9 +21,25 @@ export function ClipboardCommands({ pathExists, dllName }: ClipboardCommandsProp
: `WINEDLLOVERRIDES=${dllName.replace(".dll", "")}=n,b SteamDeck=0 %command%`;
return (
- <SmartClipboardButton
- command={launchCmd}
- buttonText="Copy launch options"
- />
+ <>
+ {showLaunchOptions ? (
+ <SmartClipboardButton
+ command={launchCmd}
+ buttonText="Copy launch options"
+ />
+ ) : null}
+ {manualModeEnabled ? (
+ <>
+ <SmartClipboardButton
+ command="~/fgmod/fgmod %command%"
+ buttonText="Copy Patch Command"
+ />
+ <SmartClipboardButton
+ command="~/fgmod/fgmod-uninstaller.sh %command%"
+ buttonText="Copy Unpatch Command"
+ />
+ </>
+ ) : null}
+ </>
);
}
diff --git a/src/components/CustomPathOverride.tsx b/src/components/CustomPathOverride.tsx
index 4effc6c..af47735 100644
--- a/src/components/CustomPathOverride.tsx
+++ b/src/components/CustomPathOverride.tsx
@@ -37,6 +37,7 @@ interface ManualPatchControlsProps {
isAvailable: boolean;
onManualModeChange?: (enabled: boolean) => void;
dllName: string;
+ fsr4Variant: string;
}
interface PickerState {
@@ -57,7 +58,7 @@ const formatResultMessage = (result: ApiResponse | null) => {
return result.message || result.output || "Operation failed.";
};
-export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName }: ManualPatchControlsProps) => {
+export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName, fsr4Variant }: ManualPatchControlsProps) => {
const [isEnabled, setEnabled] = useState(false);
const [defaults, setDefaults] = useState<PathDefaults>(INITIAL_DEFAULTS);
const [pickerState, setPickerState] = useState<PickerState>(INITIAL_PICKER_STATE);
@@ -166,7 +167,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName }
try {
const response =
action === "patch"
- ? await runManualPatch(selectedPath, dllName)
+ ? await runManualPatch(selectedPath, dllName, fsr4Variant)
: await runManualUnpatch(selectedPath);
setOperationResult(response ?? { status: "error", message: "No response from backend." });
} catch (err) {
@@ -178,7 +179,7 @@ export const ManualPatchControls = ({ isAvailable, onManualModeChange, dllName }
setBusy(false);
}
},
- [selectedPath, dllName]
+ [selectedPath, dllName, fsr4Variant]
);
const handleToggle = (value: boolean) => {
diff --git a/src/components/InstalledGamesSection.tsx b/src/components/InstalledGamesSection.tsx
index 04d653b..e0e2677 100644
--- a/src/components/InstalledGamesSection.tsx
+++ b/src/components/InstalledGamesSection.tsx
@@ -48,7 +48,7 @@ export function InstalledGamesSection() {
strCancelButtonText="Cancel"
onOK={async () => {
try {
- await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod %COMMAND%');
+ await SteamClient.Apps.SetAppLaunchOptions(Number(selectedGame.appid), '~/fgmod/fgmod %COMMAND%');
setResult(`Frame generation enabled for ${selectedGame.name}. Launch the game, enable DLSS in graphics settings, then press Insert to access OptiScaler options.`);
} catch (error) {
logError('handlePatchClick: ' + String(error));
@@ -63,7 +63,7 @@ export function InstalledGamesSection() {
if (!selectedGame) return;
try {
- await SteamClient.Apps.SetAppLaunchOptions(selectedGame.appid, '~/fgmod/fgmod-uninstaller.sh %COMMAND%');
+ await SteamClient.Apps.SetAppLaunchOptions(Number(selectedGame.appid), '~/fgmod/fgmod-uninstaller.sh %COMMAND%');
setResult(`Frame generation will be disabled on next launch of ${selectedGame.name}.`);
} catch (error) {
logError('handleUnpatchClick: ' + String(error));
diff --git a/src/components/OptiScalerControls.tsx b/src/components/OptiScalerControls.tsx
index f88e8f9..2167fcd 100644
--- a/src/components/OptiScalerControls.tsx
+++ b/src/components/OptiScalerControls.tsx
@@ -1,9 +1,9 @@
import { useState, useEffect } from "react";
-import { DropdownItem, PanelSection, PanelSectionRow } from "@decky/ui";
-import { runInstallFGMod, runUninstallFGMod } from "../api";
+import { DropdownItem, Field, PanelSection, PanelSectionRow, ToggleField } from "@decky/ui";
+import { runInstallFGMod, runUninstallFGMod, setDefaultFsr4Variant } from "../api";
import { OperationResult } from "./ResultDisplay";
import { createAutoCleanupTimer } from "../utils";
-import { TIMEOUTS, PROXY_DLL_OPTIONS, DEFAULT_PROXY_DLL } from "../utils/constants";
+import { TIMEOUTS, PROXY_DLL_OPTIONS, DEFAULT_PROXY_DLL, FSR4_VARIANT_OPTIONS, DEFAULT_FSR4_VARIANT } from "../utils/constants";
import { InstallationStatus } from "./InstallationStatus";
import { OptiScalerHeader } from "./OptiScalerHeader";
import { ClipboardCommands } from "./ClipboardCommands";
@@ -13,18 +13,31 @@ import { UninstallButton } from "./UninstallButton";
import { ManualPatchControls } from "./CustomPathOverride";
import { SteamGamePatcher } from "./SteamGamePatcher";
+interface FgmodInfo {
+ exists: boolean;
+ version?: string | null;
+ selected_fsr4_variant?: string | null;
+ selected_fsr4_variant_label?: string | null;
+ install_manifest_present?: boolean;
+}
+
interface OptiScalerControlsProps {
pathExists: boolean | null;
setPathExists: (exists: boolean | null) => void;
+ fgmodInfo?: FgmodInfo | null;
}
-export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerControlsProps) {
+export function OptiScalerControls({ pathExists, setPathExists, fgmodInfo }: OptiScalerControlsProps) {
const [installing, setInstalling] = useState(false);
const [uninstalling, setUninstalling] = useState(false);
const [installResult, setInstallResult] = useState<OperationResult | null>(null);
const [uninstallResult, setUninstallResult] = useState<OperationResult | null>(null);
- const [manualModeEnabled, setManualModeEnabled] = useState(false);
+ const [advancedModeEnabled, setAdvancedModeEnabled] = useState(false);
+ const [manualClipboardModeEnabled, setManualClipboardModeEnabled] = useState(false);
const [dllName, setDllName] = useState<string>(DEFAULT_PROXY_DLL);
+ const [fsr4Variant, setFsr4Variant] = useState<string>(DEFAULT_FSR4_VARIANT);
+ const [fsr4VariantTouched, setFsr4VariantTouched] = useState(false);
+ const [switchingVariant, setSwitchingVariant] = useState(false);
useEffect(() => {
if (installResult) {
return createAutoCleanupTimer(() => setInstallResult(null), TIMEOUTS.resultDisplay);
@@ -39,10 +52,17 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
return () => {}; // Ensure a cleanup function is always returned
}, [uninstallResult]);
+ useEffect(() => {
+ const installedVariant = fgmodInfo?.selected_fsr4_variant;
+ if (!fsr4VariantTouched && installedVariant && FSR4_VARIANT_OPTIONS.some((option) => option.value === installedVariant)) {
+ setFsr4Variant(installedVariant);
+ }
+ }, [fgmodInfo?.selected_fsr4_variant, fsr4VariantTouched]);
+
const handleInstallClick = async () => {
try {
setInstalling(true);
- const result = await runInstallFGMod();
+ const result = await runInstallFGMod(fsr4Variant);
setInstallResult(result);
if (result.status === "success") {
setPathExists(true);
@@ -69,6 +89,31 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
}
};
+ const handleFsr4VariantChange = async (nextVariant: string) => {
+ const previousVariant = fsr4Variant;
+ setFsr4Variant(nextVariant);
+ setFsr4VariantTouched(true);
+
+ if (pathExists !== true) return;
+
+ try {
+ setSwitchingVariant(true);
+ const result = await setDefaultFsr4Variant(nextVariant);
+ if (result.status !== "success") {
+ throw new Error(result.message || result.output || "Failed to switch default FSR4 runtime.");
+ }
+ setFsr4Variant(result.selected_default_variant || nextVariant);
+ setFsr4VariantTouched(false);
+ } catch (error) {
+ console.error(error);
+ setFsr4Variant(previousVariant);
+ } finally {
+ setSwitchingVariant(false);
+ }
+ };
+
+ const installedVariantLabel = fgmodInfo?.selected_fsr4_variant_label || FSR4_VARIANT_OPTIONS.find((option) => option.value === fsr4Variant)?.label;
+
return (
<PanelSection>
<InstallationStatus
@@ -79,6 +124,28 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
<OptiScalerHeader pathExists={pathExists} />
+ <PanelSectionRow>
+ <DropdownItem
+ label="Default FSR4 runtime"
+ description={FSR4_VARIANT_OPTIONS.find((option) => option.value === fsr4Variant)?.hint}
+ menuLabel="Default FSR4 runtime"
+ selectedOption={fsr4Variant}
+ rgOptions={FSR4_VARIANT_OPTIONS.map((option) => ({ data: option.value, label: option.label }))}
+ disabled={installing || uninstalling || switchingVariant}
+ onChange={(option) => {
+ void handleFsr4VariantChange(String(option.data));
+ }}
+ />
+ </PanelSectionRow>
+
+ {pathExists === true && fgmodInfo?.version && installedVariantLabel && (
+ <PanelSectionRow>
+ <Field label="Installed bundle" description={`OptiScaler ${fgmodInfo.version}`}>
+ {installedVariantLabel}
+ </Field>
+ </PanelSectionRow>
+ )}
+
{pathExists === true && (
<PanelSectionRow>
<DropdownItem
@@ -93,18 +160,39 @@ export function OptiScalerControls({ pathExists, setPathExists }: OptiScalerCont
)}
{pathExists === true && (
- <SteamGamePatcher dllName={dllName} />
+ <SteamGamePatcher dllName={dllName} fsr4Variant={fsr4Variant} />
)}
<ClipboardCommands pathExists={pathExists} dllName={dllName} />
+ {pathExists === true && (
+ <PanelSectionRow>
+ <ToggleField
+ label="Manual Mode"
+ description="Show wrapper command clipboard buttons for patching and unpatching through ~/fgmod scripts."
+ checked={manualClipboardModeEnabled}
+ onChange={setManualClipboardModeEnabled}
+ />
+ </PanelSectionRow>
+ )}
+
+ {pathExists === true && manualClipboardModeEnabled ? (
+ <ClipboardCommands
+ pathExists={pathExists}
+ dllName={dllName}
+ manualModeEnabled
+ showLaunchOptions={false}
+ />
+ ) : null}
+
<ManualPatchControls
isAvailable={pathExists === true}
- onManualModeChange={setManualModeEnabled}
+ onManualModeChange={setAdvancedModeEnabled}
dllName={dllName}
+ fsr4Variant={fsr4Variant}
/>
- {!manualModeEnabled && (
+ {!advancedModeEnabled && (
<InstructionCard pathExists={pathExists} />
)}
<OptiScalerWiki pathExists={pathExists} />
diff --git a/src/components/SteamGamePatcher.tsx b/src/components/SteamGamePatcher.tsx
index b17ed48..2d3b0fa 100644
--- a/src/components/SteamGamePatcher.tsx
+++ b/src/components/SteamGamePatcher.tsx
@@ -50,6 +50,10 @@ type GameStatus = {
dll_name?: string | null;
target_dir?: string | null;
patched_at?: string | null;
+ optiscaler_version?: string | null;
+ fsr4_variant?: string | null;
+ fsr4_variant_label?: string | null;
+ fsr4_upscaler_sha256?: string | null;
};
// ─── Module-level state persistence ──────────────────────────────────────────
@@ -60,9 +64,10 @@ let lastSelectedAppId = "";
interface SteamGamePatcherProps {
dllName: string;
+ fsr4Variant: string;
}
-export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) {
+export function SteamGamePatcher({ dllName, fsr4Variant }: SteamGamePatcherProps) {
const [games, setGames] = useState<GameEntry[]>([]);
const [gamesLoading, setGamesLoading] = useState(true);
const [selectedAppId, setSelectedAppId] = useState<string>(() => lastSelectedAppId);
@@ -165,7 +170,7 @@ export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) {
} catch {
// non-fatal: proceed without current launch options
}
- const result = await patchGame(selectedAppId, dllName, currentLaunchOptions);
+ const result = await patchGame(selectedAppId, dllName, currentLaunchOptions, fsr4Variant);
if (result.status !== "success") throw new Error(result.message || "Patch failed.");
setAppLaunchOptions(Number(selectedAppId), result.launch_options || "");
const msg = result.message || `Patched ${selectedGame.name}.`;
@@ -179,7 +184,7 @@ export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) {
} finally {
setBusyAction(null);
}
- }, [busyAction, dllName, loadStatus, selectedAppId, selectedGame]);
+ }, [busyAction, dllName, fsr4Variant, loadStatus, selectedAppId, selectedGame]);
const handleUnpatch = useCallback(async () => {
if (!selectedGame || !selectedAppId || busyAction) return;
@@ -258,6 +263,16 @@ export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) {
</PanelSectionRow>
<PanelSectionRow>
+ <Field {...focusableFieldProps} label="FSR4 runtime">
+ {gameStatus?.patched
+ ? (gameStatus?.fsr4_variant_label || "Unknown")
+ : (fsr4Variant === "rdna4-native"
+ ? "Will patch with Native bundle / RDNA4"
+ : "Will patch with Steam Deck / RDNA2-3 optimized")}
+ </Field>
+ </PanelSectionRow>
+
+ <PanelSectionRow>
<ButtonItem layout="below" disabled={!canPatch} onClick={handlePatch}>
{patchButtonLabel}
</ButtonItem>
diff --git a/src/index.tsx b/src/index.tsx
index fb9635d..4a9a9f6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -7,8 +7,17 @@ import { checkFGModPath } from "./api";
import { safeAsyncOperation } from "./utils";
import { TIMEOUTS } from "./utils/constants";
+type FgmodInfo = {
+ exists: boolean;
+ version?: string | null;
+ selected_fsr4_variant?: string | null;
+ selected_fsr4_variant_label?: string | null;
+ install_manifest_present?: boolean;
+};
+
function MainContent() {
const [pathExists, setPathExists] = useState<boolean | null>(null);
+ const [fgmodInfo, setFgmodInfo] = useState<FgmodInfo | null>(null);
useEffect(() => {
const checkPath = async () => {
@@ -16,7 +25,10 @@ function MainContent() {
async () => await checkFGModPath(),
'MainContent -> checkPath'
);
- if (result) setPathExists(result.exists);
+ if (result) {
+ setFgmodInfo(result);
+ setPathExists(result.exists);
+ }
};
checkPath(); // Initial check
@@ -29,6 +41,7 @@ function MainContent() {
<OptiScalerControls
pathExists={pathExists}
setPathExists={setPathExists}
+ fgmodInfo={fgmodInfo}
/>
{pathExists === true ? (
<>
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 7fa6970..8444240 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -59,6 +59,22 @@ export const PROXY_DLL_OPTIONS = [
export type ProxyDllValue = typeof PROXY_DLL_OPTIONS[number]["value"];
export const DEFAULT_PROXY_DLL: ProxyDllValue = "dxgi.dll";
+export const FSR4_VARIANT_OPTIONS = [
+ {
+ value: "rdna23-int8",
+ label: "Steam Deck / RDNA2-3 optimized",
+ hint: "Uses the bundled FSR4 INT8 4.0.2c override. Recommended for Steam Deck and other non-RDNA4 systems.",
+ },
+ {
+ value: "rdna4-native",
+ label: "Native bundle / RDNA4",
+ hint: "Uses the amd_fidelityfx_upscaler_dx12.dll that ships inside the OptiScaler 0.9.2a bundle.",
+ },
+] as const;
+
+export type Fsr4VariantValue = typeof FSR4_VARIANT_OPTIONS[number]["value"];
+export const DEFAULT_FSR4_VARIANT: Fsr4VariantValue = "rdna23-int8";
+
// Common timeout values
export const TIMEOUTS = {
resultDisplay: 5000, // 5 seconds
@@ -76,5 +92,5 @@ export const MESSAGES = {
installSuccess: "OptiScaler mod setup successfully!",
uninstallSuccess: "OptiScaler mod removed successfully.",
instructionTitle: "How to Use:",
- instructionText: "Click 'Copy Patch Command' or 'Copy Unpatch Command', then go to your game's properties, and paste the command into the Launch Options field.\n\nIn-game: Enable DLSS in graphics settings to unlock FSR 3.1/XeSS 2.0 in DirectX12 Games.\n\nFor extended OptiScaler options, assign a back button to a keyboard's 'Insert' key."
+ instructionText: "Use 'Copy launch options' for the standard direct launch-options method. If you want the wrapper commands instead, enable Manual Mode to reveal 'Copy Patch Command' and 'Copy Unpatch Command'.\n\nIn-game: Enable DLSS in graphics settings to unlock FSR 3.1/XeSS 2.0 in DirectX12 Games.\n\nFor extended OptiScaler options, assign a back button to a keyboard's 'Insert' key."
};