Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .codacy/codacy.yaml
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ test:
scp "out/Decky Framegen.zip" [email protected]:~/Desktop

clean:
rm -rf node_modules dist
sudo rm -rf node_modules dist
sudo rm -rf /tmp/decky
264 changes: 262 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,56 @@
# Set to False or comment out this constant to skip the overwrite by default.
UPSCALER_OVERWRITE_ENABLED = True

INJECTOR_FILENAMES = [
"dxgi.dll",
"winmm.dll",
"nvngx.dll",
"_nvngx.dll",
"nvngx-wrapper.dll",
"dlss-enabler.dll",
"OptiScaler.dll",
]

ORIGINAL_DLL_BACKUPS = [
"d3dcompiler_47.dll",
"amd_fidelityfx_dx12.dll",
"amd_fidelityfx_framegeneration_dx12.dll",
"amd_fidelityfx_upscaler_dx12.dll",
"amd_fidelityfx_vk.dll",
]

SUPPORT_FILES = [
"libxess.dll",
"libxess_dx11.dll",
"libxess_fg.dll",
"libxell.dll",
"amd_fidelityfx_dx12.dll",
"amd_fidelityfx_framegeneration_dx12.dll",
"amd_fidelityfx_upscaler_dx12.dll",
"amd_fidelityfx_vk.dll",
"nvngx.dll",
"dlssg_to_fsr3_amd_is_better.dll",
"fakenvapi.dll",
"fakenvapi.ini",
]

LEGACY_FILES = [
"dlssg_to_fsr3.ini",
"dlssg_to_fsr3.log",
"nvapi64.dll",
"nvapi64.dll.b",
"fakenvapi.log",
"dlss-enabler.dll",
"dlss-enabler-upscaler.dll",
"dlss-enabler.log",
"nvngx-wrapper.dll",
"_nvngx.dll",
"dlssg_to_fsr3_amd_is_better-3.0.dll",
"OptiScaler.asi",
"OptiScaler.ini",
"OptiScaler.log",
]

class Plugin:
async def _main(self):
decky.logger.info("Framegen plugin loaded")
Expand Down Expand Up @@ -370,16 +420,195 @@ async def check_fgmod_path(self) -> dict:
for file_name in required_files:
if not path.joinpath(file_name).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():
return {"exists": False}

return {"exists": True}
else:
return {"exists": False}

def _resolve_target_directory(self, directory: str) -> Path:
decky.logger.info(f"Resolving target directory: {directory}")
target = Path(directory).expanduser()
if not target.exists():
raise FileNotFoundError(f"Target directory does not exist: {directory}")
if not target.is_dir():
raise NotADirectoryError(f"Target path is not a directory: {directory}")
if not os.access(target, os.W_OK | os.X_OK):
raise PermissionError(f"Insufficient permissions for {directory}")
decky.logger.info(f"Resolved directory {directory} to absolute path {target}")
return target

def _manual_patch_directory_impl(self, directory: Path) -> dict:
fgmod_path = Path(decky.HOME) / "fgmod"
if not fgmod_path.exists():
return {
"status": "error",
"message": "OptiScaler bundle not installed. Run Install first.",
}

optiscaler_dll = fgmod_path / "OptiScaler.dll"
if not optiscaler_dll.exists():
return {
"status": "error",
"message": "OptiScaler.dll not found in ~/fgmod. Reinstall OptiScaler.",
}

dll_name = "dxgi.dll"
preserve_ini = True

try:
decky.logger.info(f"Manual patch started for {directory}")

removed_injectors = []
for filename in INJECTOR_FILENAMES:
path = directory / filename
if path.exists():
path.unlink()
removed_injectors.append(filename)
decky.logger.info(f"Removed injector DLLs: {removed_injectors}" if removed_injectors else "No injector DLLs found to remove")

backed_up_originals = []
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)
decky.logger.info(f"Backed up original DLLs: {backed_up_originals}" if backed_up_originals else "No original DLLs required backup")

removed_legacy = []
for legacy in ["nvapi64.dll", "nvapi64.dll.b"]:
legacy_path = directory / legacy
if legacy_path.exists():
legacy_path.unlink()
removed_legacy.append(legacy)
decky.logger.info(f"Removed legacy files: {removed_legacy}" if removed_legacy else "No legacy files to remove")

renamed = fgmod_path / "renames" / dll_name
destination_dll = directory / dll_name
source_for_copy = renamed if renamed.exists() else optiscaler_dll
shutil.copy2(source_for_copy, destination_dll)
decky.logger.info(f"Copied injector DLL from {source_for_copy} to {destination_dll}")

target_ini = directory / "OptiScaler.ini"
source_ini = fgmod_path / "OptiScaler.ini"
if preserve_ini and target_ini.exists():
decky.logger.info(f"Preserving existing OptiScaler.ini at {target_ini}")
elif source_ini.exists():
shutil.copy2(source_ini, target_ini)
decky.logger.info(f"Copied OptiScaler.ini from {source_ini} to {target_ini}")
else:
decky.logger.warning("No OptiScaler.ini found to copy")

plugins_src = fgmod_path / "plugins"
plugins_dest = directory / "plugins"
if plugins_src.exists():
shutil.copytree(plugins_src, plugins_dest, dirs_exist_ok=True)
decky.logger.info(f"Synced plugins directory from {plugins_src} to {plugins_dest}")
else:
decky.logger.warning("Plugins directory missing in fgmod bundle")

copied_support = []
missing_support = []
for filename in SUPPORT_FILES:
source = fgmod_path / filename
dest = directory / filename
if source.exists():
shutil.copy2(source, dest)
copied_support.append(filename)
else:
missing_support.append(filename)
if copied_support:
decky.logger.info(f"Copied support files: {copied_support}")
if missing_support:
decky.logger.warning(f"Support files missing from fgmod bundle: {missing_support}")

decky.logger.info(f"Manual patch complete for {directory}")
return {
"status": "success",
"message": f"OptiScaler files copied to {directory}",
}

except PermissionError as exc:
decky.logger.error(f"Manual patch permission error: {exc}")
return {
"status": "error",
"message": f"Permission error while patching: {exc}",
}
except Exception as exc:
decky.logger.error(f"Manual patch failed: {exc}")
return {
"status": "error",
"message": f"Manual patch failed: {exc}",
}

def _manual_unpatch_directory_impl(self, directory: Path) -> dict:
try:
decky.logger.info(f"Manual unpatch started for {directory}")

removed_files = []
for filename in set(INJECTOR_FILENAMES + SUPPORT_FILES):
path = directory / filename
if path.exists():
path.unlink()
removed_files.append(filename)
decky.logger.info(f"Removed injector/support files: {removed_files}" if removed_files else "No injector/support files found to remove")

legacy_removed = []
for legacy in LEGACY_FILES:
path = directory / legacy
if path.exists():
try:
path.unlink()
except IsADirectoryError:
shutil.rmtree(path, ignore_errors=True)
legacy_removed.append(legacy)
decky.logger.info(f"Removed legacy artifacts: {legacy_removed}" if legacy_removed else "No legacy artifacts present")

plugins_dir = directory / "plugins"
if plugins_dir.exists():
shutil.rmtree(plugins_dir, ignore_errors=True)
decky.logger.info(f"Removed plugins directory at {plugins_dir}")

restored_backups = []
for dll in ORIGINAL_DLL_BACKUPS:
backup = directory / f"{dll}.b"
original = directory / dll
if backup.exists():
if original.exists():
original.unlink()
shutil.move(backup, original)
restored_backups.append(dll)
decky.logger.info(f"Restored backups: {restored_backups}" if restored_backups else "No backups found to restore")

uninstaller = directory / "fgmod-uninstaller.sh"
if uninstaller.exists():
uninstaller.unlink()
decky.logger.info(f"Removed fgmod uninstaller at {uninstaller}")

decky.logger.info(f"Manual unpatch complete for {directory}")
return {
"status": "success",
"message": f"OptiScaler files removed from {directory}",
}

except PermissionError as exc:
decky.logger.error(f"Manual unpatch permission error: {exc}")
return {
"status": "error",
"message": f"Permission error while unpatching: {exc}",
}
except Exception as exc:
decky.logger.error(f"Manual unpatch failed: {exc}")
return {
"status": "error",
"message": f"Manual unpatch failed: {exc}",
}

async def list_installed_games(self) -> dict:
try:
steam_root = Path(decky.HOME) / ".steam" / "steam"
Expand Down Expand Up @@ -429,5 +658,36 @@ async def list_installed_games(self) -> dict:
decky.logger.error(str(e))
return {"status": "error", "message": str(e)}

async def get_path_defaults(self) -> dict:
try:
home_path = Path(decky.HOME)
except TypeError:
home_path = Path(str(decky.HOME))

steam_common = home_path / ".local" / "share" / "Steam" / "steamapps" / "common"

return {
"home": str(home_path),
"steam_common": str(steam_common),
}

async def log_error(self, error: str) -> None:
decky.logger.error(f"FRONTEND: {error}")

async def manual_patch_directory(self, directory: str) -> dict:
try:
target_dir = self._resolve_target_directory(directory)
except (FileNotFoundError, NotADirectoryError, PermissionError) as exc:
decky.logger.error(f"Manual patch validation failed: {exc}")
return {"status": "error", "message": str(exc)}

return self._manual_patch_directory_impl(target_dir)

async def manual_unpatch_directory(self, directory: str) -> dict:
try:
target_dir = self._resolve_target_directory(directory)
except (FileNotFoundError, NotADirectoryError, PermissionError) as exc:
decky.logger.error(f"Manual unpatch validation failed: {exc}")
return {"status": "error", "message": str(exc)}

return self._manual_unpatch_directory_impl(target_dir)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "decky-framegen",
"version": "0.12.6",
"version": "0.13.0",
"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": {
Expand Down
15 changes: 15 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,18 @@ export const listInstalledGames = callable<
>("list_installed_games");

export const logError = callable<[string], void>("log_error");

export const getPathDefaults = callable<
[],
{ home: string; steam_common?: string }
>("get_path_defaults");

export const runManualPatch = callable<
[string],
{ status: string; message?: string; output?: string }
>("manual_patch_directory");

export const runManualUnpatch = callable<
[string],
{ status: string; message?: string; output?: string }
>("manual_unpatch_directory");
Loading