feat: add fsr4 runtime manifests

This commit is contained in:
xXJSONDeruloXx
2026-05-19 14:26:39 -04:00
parent 9e12c11b61
commit 19e5aefa2f
13 changed files with 836 additions and 268 deletions
+7 -5
View File
@@ -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.)
+76 -2
View File
@@ -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) ===
+506 -223
View File
@@ -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."}
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}")
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("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}")
+6 -5
View File
@@ -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"
},
{
+14 -3
View File
@@ -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
+47 -6
View File
@@ -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");
+28 -5
View File
@@ -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}
</>
);
}
+4 -3
View File
@@ -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) => {
+2 -2
View File
@@ -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));
+97 -9
View File
@@ -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} />
+18 -3
View File
@@ -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;
@@ -257,6 +262,16 @@ export function SteamGamePatcher({ dllName }: SteamGamePatcherProps) {
</Field>
</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}
+14 -1
View File
@@ -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 ? (
<>
+17 -1
View File
@@ -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."
};