Files
The412Banner ef7f031388 Merge GE-Proton11-12 (f85547f0) into rebase/11-12
GE-Proton11-12 now carries the three GE master commits 11-11.1 took early,
so our copies go:
- patches/protonprep-valve-staging.sh: GE's version (our two added
  apply lines for 98efe1cd and e15b1e15 are GE's own now).
- ntdll-take-in-flight-alert-when-wait-on-address-times-out.patch: GE's
  newer version (960acd0a, with its test).
- winewayland-advertise-openvr-openxr-device-extensions.patch and
  lsteamclient 0017: identical to GE's, merged without a change.

README.md: ours (updated for 11-12 in a later commit).
wine: still banner-proton/11.11 here; moved to banner-proton/11.12 in a
later commit.
2026-11-03 15:25:52 +00:00

3086 lines
139 KiB
Python
Executable File

#!/usr/bin/env python3
#script to launch Wine with the correct environment
import fcntl
import array
import fnmatch
import filecmp
import os
import re
import shutil
import errno
import platform
import resource
import stat
import subprocess
import sys
import shlex
import uuid
import json
import time
from ctypes import CDLL
from ctypes import CFUNCTYPE
from ctypes import POINTER
from ctypes import Structure
from ctypes import addressof
from ctypes import cast
from ctypes import get_errno
from ctypes import sizeof
from ctypes import c_int
from ctypes import c_int64
from ctypes import c_uint
from ctypes import c_long
from ctypes import c_char_p
from ctypes import c_void_p
from ctypes import c_size_t
from ctypes import c_ssize_t
from filelock import FileLock
from random import randrange
from pathlib import Path
import protonfixes
#To enable debug logging, copy "user_settings.sample.py" to "user_settings.py"
#and edit it if needed.
import utilities
# Keep Valve's prefix version so upstream Proton and its prefix migrations
# understand prefixes we write. The GE build is logged separately (see
# proton_build_name()); older GE prefixes used "GE-ProtonX-Y" and are still
# parsed by upgrade_pfx().
CURRENT_PREFIX_VERSION="11.0-100"
PFX="Proton: "
ld_path_var = "LD_LIBRARY_PATH"
# https://bugs.kde.org/show_bug.cgi?id=524702
# Ark can omit empty directory entries when extracting Proton tarballs. Keep
# fresh prefixes valid even when their extracted default_pfx lost Wine's shell
# directory skeleton.
STANDARD_PREFIX_DIRECTORIES = (
"drive_c/ProgramData/Microsoft/Windows/Start Menu/Programs/Administrative Tools",
"drive_c/ProgramData/Microsoft/Windows/Start Menu/Programs/StartUp",
"drive_c/ProgramData/Microsoft/Windows/Templates",
"drive_c/users/Public/Desktop",
"drive_c/users/Public/Documents",
"drive_c/users/Public/Music",
"drive_c/users/Public/Pictures",
"drive_c/users/Public/Videos",
"drive_c/users/steamuser/AppData/Local/Microsoft/Windows/History",
"drive_c/users/steamuser/AppData/Local/Microsoft/Windows/INetCache",
"drive_c/users/steamuser/AppData/Local/Microsoft/Windows/INetCookies",
"drive_c/users/steamuser/AppData/Local/Temp",
"drive_c/users/steamuser/AppData/LocalLow",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/AccountPictures",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Network Shortcuts",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Printer Shortcuts",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Recent",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/SendTo",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Administrative Tools",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/StartUp",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Templates",
"drive_c/users/steamuser/AppData/Roaming/Microsoft/Windows/Themes",
"drive_c/users/steamuser/Contacts",
"drive_c/users/steamuser/Desktop",
"drive_c/users/steamuser/Documents/Downloads",
"drive_c/users/steamuser/Documents/Music",
"drive_c/users/steamuser/Documents/Pictures",
"drive_c/users/steamuser/Documents/Templates",
"drive_c/users/steamuser/Documents/Videos",
"drive_c/users/steamuser/Downloads",
"drive_c/users/steamuser/Favorites",
"drive_c/users/steamuser/Links",
"drive_c/users/steamuser/Music",
"drive_c/users/steamuser/Pictures/Screenshots",
"drive_c/users/steamuser/Saved Games",
"drive_c/users/steamuser/Searches",
"drive_c/users/steamuser/Videos",
)
proton_config = set()
if "PROTON_ADD_CONFIG" in os.environ:
try:
config = os.environ["PROTON_ADD_CONFIG"].split(',')
for c in config:
proton_config.add(c)
except:
pass
def empty_directory(path):
# Walk through the directory structure top-down (False)
for root, dirs, files in os.walk(path, topdown=False):
# Remove files and symlinks to files
for name in files:
item_path = os.path.join(root, name)
if os.path.islink(item_path):
os.remove(item_path) # Remove symlink
else:
os.remove(item_path) # Remove file
# Remove directories and symlinks to directories
for name in dirs:
item_path = os.path.join(root, name)
if os.path.islink(item_path):
os.remove(item_path) # Remove symlink
else:
os.rmdir(item_path) # Remove directory
# If -steamdeck mode is enabled, try to copy existing game prefixes to steamdeck's prefix location -- this helps to preserve mods and saves across systems
# Note: this completely wipes the existing destination prefix and copies the source prefix over instead.
# source = game partition, destination = steamdeck prefix location
# From game partition to steamdeck (steamdeck enabled)
if os.getenv("SteamDeck") == "1" and os.getenv("COPYPREFIX") == "1":
# Get the value of the STEAM_COMPAT_INSTALL_PATH environment variable
steam_compat_install_path = os.getenv('STEAM_COMPAT_INSTALL_PATH')
# Navigate up two directories
parent_directory = Path(steam_compat_install_path).parents[1]
# Check if the "compatdata/<steam game>" folder exists and is not empty
compatdata_path = parent_directory / "compatdata" / str(os.environ.get("SteamAppId", 0))
if compatdata_path.exists() and os.listdir(compatdata_path):
# Define the destination directory
steamdeck_compatdata = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "compatdata" / str(os.environ.get("SteamAppId", 0))
# Remove destination if it already exists:
if os.path.isdir(steamdeck_compatdata) and os.listdir(steamdeck_compatdata):
empty_directory(steamdeck_compatdata)
# Copy the "compatdata/<steam game>" folder to the destination directory
# Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists
shutil.copytree(compatdata_path, steamdeck_compatdata, dirs_exist_ok=True, symlinks=True)
# Check if the "shadercache/<steam game>" folder exists and is not empty
shadercache_path = parent_directory / "shadercache" / str(os.environ.get("SteamAppId", 0))
if shadercache_path.exists() and os.listdir(shadercache_path):
# Define the destination directory
steamdeck_shadercache = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "shadercache" / str(os.environ.get("SteamAppId", 0))
# Remove destination if it already exists:
if os.path.isdir(steamdeck_shadercache) and os.listdir(steamdeck_shadercache):
empty_directory(steamdeck_shadercache)
# Copy the "shadercache/<steam game>" folder to the destination directory
# Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists
shutil.copytree(shadercache_path, steamdeck_shadercache, dirs_exist_ok=True, symlinks=True)
# source = steamdeck prefix location, destination = game partition
# From steamdeck to game partition (steamdeck disabled)
if os.getenv("SteamDeck") == "0" or "SteamDeck" not in os.environ:
if os.getenv("COPYPREFIX") == "1":
# Get the value of the STEAM_COMPAT_INSTALL_PATH environment variable
steam_compat_install_path = os.getenv('STEAM_COMPAT_INSTALL_PATH')
# Navigate up two directories
parent_directory = Path(steam_compat_install_path).parents[1]
# Check if the "compatdata/<steam game>" folder exists and is not empty
steamdeck_compatdata = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "compatdata" / str(os.environ.get("SteamAppId", 0))
if os.path.isdir(steamdeck_compatdata) and os.listdir(steamdeck_compatdata):
# Define the destination directory
compatdata_path = parent_directory / "compatdata" / str(os.environ.get("SteamAppId", 0))
# Remove destination if it already exists:
if os.path.isdir(compatdata_path) and os.listdir(compatdata_path):
empty_directory(compatdata_path)
# Copy the steamdeck_compatdata to the compatdata_path
# Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists
shutil.copytree(steamdeck_compatdata, compatdata_path, dirs_exist_ok=True, symlinks=True)
# Similar logic for shadercache
steamdeck_shadercache = Path(os.environ["STEAM_BASE_FOLDER"]) / "steamapps" / "shadercache" / str(os.environ.get("SteamAppId", 0))
if os.path.isdir(steamdeck_shadercache) and os.listdir(steamdeck_shadercache):
# Define the destination directory
shadercache_path = parent_directory / "shadercache" / str(os.environ.get("SteamAppId", 0))
# Remove destination if it already exists:
if os.path.isdir(shadercache_path) and os.listdir(shadercache_path):
empty_directory(shadercache_path)
# Copy the steamdeck_shadercache to the shadercache_path
# Use dirs_exist_ok=True to avoid FileExistsError if the destination directory already exists
shutil.copytree(steamdeck_shadercache, shadercache_path, dirs_exist_ok=True, symlinks=True)
def file_exists(s, *, follow_symlinks):
if follow_symlinks:
#'exists' returns False on broken symlinks
return os.path.exists(s)
#'lexists' returns True on broken symlinks
return os.path.lexists(s)
def nonzero(s):
return len(s) > 0 and s != "0"
def proton_build_name():
"""The build name from this Proton's version file, e.g. "GE-Proton11-12"."""
try:
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "version"), "r") as f:
parts = f.readline().split()
return parts[1] if len(parts) > 1 else "unknown"
except OSError:
return "unknown"
def host_gpu_vendors():
"""PCI vendor IDs of the host's DRM devices, e.g. {"0x1002"} for AMD."""
vendors = set()
try:
cards = os.listdir("/sys/class/drm")
except OSError:
return vendors
for card in cards:
if not re.fullmatch(r"card[0-9]+", card):
continue
try:
with open("/sys/class/drm/" + card + "/device/vendor", "r") as f:
vendors.add(f.read().strip().lower())
except OSError:
pass
return vendors
def prepend_to_env_str(env, variable, prepend_str, separator):
if variable not in env:
env[variable] = prepend_str
else:
env[variable] = prepend_str + separator + env[variable]
def append_to_env_str(env, variable, append_str, separator):
if variable not in env:
env[variable] = append_str
else:
env[variable] = env[variable] + separator + append_str
def log(msg):
try:
sys.stderr.write(PFX + msg + os.linesep)
sys.stderr.flush()
except OSError:
# e.g. see https://github.com/ValveSoftware/Proton/issues/6277
# There's not much we can usefully do about this: printing a
# warning to stderr isn't going to work any better the second time
pass
def file_is_wine_builtin_dll(path):
if os.path.islink(path):
contents = os.readlink(path)
if os.path.dirname(contents).endswith((
'/lib/wine/i386-unix',
'/lib/wine/i386-windows',
'/lib/wine/x86_64-unix',
'/lib/wine/x86_64-windows',
'/lib/wine/aarch64-unix',
'/lib/wine/aarch64-windows',
# old paths
'/lib/wine',
'/lib/wine/fakedlls',
'/lib64/wine',
'/lib64/wine/fakedlls',
'/lib64/wine/x86_64-unix',
'/lib64/wine/x86_64-windows'
)):
# This may be a broken link to a dll in a removed Proton install
return True
if not file_exists(path, follow_symlinks=True):
return False
try:
sfile = open(path, "rb")
sfile.seek(0x40)
tag = sfile.read(20)
return tag.startswith((b"Wine placeholder DLL", b"Wine builtin DLL"))
except IOError:
return False
def makedirs(path):
try:
#replace broken symlinks with a new directory
if os.path.islink(path) and not file_exists(path, follow_symlinks=True):
os.remove(path)
os.makedirs(path)
except OSError:
#already exists
pass
def merge_user_dir(src, dst):
extant_dirs = []
for src_dir, dirs, files in os.walk(src):
dst_dir = src_dir.replace(src, dst, 1)
#as described below, avoid merging game save subdirs, too
child_of_extant_dir = False
for dir_ in extant_dirs:
if dir_ in dst_dir:
child_of_extant_dir = True
break
if child_of_extant_dir:
continue
#we only want to copy into directories which don't already exist. games
#may not react well to two save directory instances being merged.
if not file_exists(dst_dir, follow_symlinks=True) or os.path.samefile(dst_dir, dst):
makedirs(dst_dir)
for dir_ in dirs:
src_file = os.path.join(src_dir, dir_)
dst_file = os.path.join(dst_dir, dir_)
if os.path.islink(src_file) and not file_exists(dst_file, follow_symlinks=True):
try_copy(src_file, dst_file, copy_metadata=True, follow_symlinks=False)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if not file_exists(dst_file, follow_symlinks=True):
try_copy(src_file, dst_file, copy_metadata=True, follow_symlinks=False)
else:
extant_dirs += dst_dir
def try_copy(src, dst, prefix=None, add_write_perm=True, copy_metadata=False, optional=False,
follow_symlinks=True, track_file=None, link_debug=False):
try:
if prefix is not None:
dst = os.path.join(prefix, dst)
if os.path.isdir(dst):
dst = os.path.join(dst, os.path.basename(src))
if file_exists(dst, follow_symlinks=False):
os.remove(dst)
elif track_file and prefix is not None:
track_file.write(os.path.relpath(dst, prefix) + '\n')
if os.path.islink(src) and not follow_symlinks:
shutil.copyfile(src, dst, follow_symlinks=False)
else:
copyfile(src, dst)
if copy_metadata:
shutil.copystat(src, dst, follow_symlinks=follow_symlinks)
else:
shutil.copymode(src, dst, follow_symlinks=follow_symlinks)
if add_write_perm:
new_mode = os.lstat(dst).st_mode | stat.S_IWUSR | stat.S_IWGRP
os.chmod(dst, new_mode)
if not file_exists(src + '.debug', follow_symlinks=True):
link_debug = False
if file_exists(dst + '.debug', follow_symlinks=False):
os.remove(dst + '.debug')
elif link_debug and track_file:
track_file.write(os.path.relpath(dst + '.debug', prefix) + '\n')
if link_debug:
os.symlink(src + '.debug', dst + '.debug')
except FileNotFoundError as e:
if optional:
log('Error while copying to \"' + dst + '\": ' + e.strerror)
else:
raise
except PermissionError as e:
if e.errno == errno.EPERM:
#be forgiving about permissions errors; if it's a real problem, things will explode later anyway
log('Error while copying to \"' + dst + '\": ' + e.strerror)
else:
raise
#dxvk-igdext is only built for x86_64-windows (there is no i386 or aarch64 build). On ARM64 the
#host PE arch is aarch64-windows, so look there first and fall back to the x86_64 build, which is
#what x86_64 games running under ARM64EC load. Returns None when no build is shipped, so the copy
#is skipped instead of failing with "No such file or directory" on every launch.
def find_igdext_dll():
for arch in dict.fromkeys((g_proton.host_pe_arch, "x86_64-windows")):
dll = g_proton.lib_dir + "wine/igdext/" + arch + "/igdext64.dll"
if file_exists(dll, follow_symlinks=True):
return dll
return None
#rsync-like directory sync, deletes files at dst not present in src
def dir_sync(dst, src):
if not os.path.exists(dst):
os.makedirs(dst)
comp = filecmp.dircmp(src, dst)
for f in comp.left_only + comp.diff_files:
s = os.path.join(src, f)
d = os.path.join(dst, f)
if os.path.isdir(s):
shutil.copytree(s, d)
else:
shutil.copy2(s, d, follow_symlinks=False)
for f in comp.right_only:
d = os.path.join(dst, f)
if os.path.isdir(d):
shutil.rmtree(d)
else:
os.remove(d)
for dir in comp.common_dirs:
dir_sync(os.path.join(dst, dir), os.path.join(src, dir))
# copy_file_range implementation for old Python versions
__syscall__copy_file_range = None
def copy_file_range_ctypes(fd_in, fd_out, count):
"Copy data using the copy_file_range syscall through ctypes, assuming x86_64 Linux"
global __syscall__copy_file_range
__NR_copy_file_range = 326
if __syscall__copy_file_range is None:
c_int64_p = POINTER(c_int64)
prototype = CFUNCTYPE(c_ssize_t, c_long, c_int, c_int64_p,
c_int, c_int64_p, c_size_t, c_uint, use_errno=True)
__syscall__copy_file_range = prototype(('syscall', CDLL(None, use_errno=True)))
while True:
ret = __syscall__copy_file_range(__NR_copy_file_range, fd_in, None, fd_out, None, count, 0)
if ret >= 0 or get_errno() != errno.EINTR:
break
if ret < 0:
raise OSError(get_errno(), errno.errorcode.get(get_errno(), 'unknown'))
return ret
def copyfile_reflink(srcname, dstname):
"Copy srcname to dstname, making reflink if possible"
global copyfile
with open(srcname, 'rb', buffering=0) as src:
bytes_to_copy = os.fstat(src.fileno()).st_size
try:
with open(dstname, 'wb', buffering=0) as dst:
while bytes_to_copy > 0:
bytes_to_copy -= copy_file_range(src.fileno(), dst.fileno(), bytes_to_copy)
except OSError as e:
if e.errno not in (errno.EXDEV, errno.ENOSYS, errno.EINVAL, errno.EOPNOTSUPP):
raise e
if e.errno == errno.ENOSYS or e.errno == errno.EOPNOTSUPP:
copyfile = shutil.copyfile
shutil.copyfile(srcname, dstname)
if hasattr(os, 'copy_file_range'):
copyfile = copyfile_reflink
copy_file_range = os.copy_file_range
elif sys.platform == 'linux' and platform.machine() == 'x86_64' and sizeof(c_void_p) == 8:
copyfile = copyfile_reflink
copy_file_range = copy_file_range_ctypes
else:
copyfile = shutil.copyfile
def try_copyfile(src, dst):
try:
if os.path.isdir(dst):
dst = dst + "/" + os.path.basename(src)
if file_exists(dst, follow_symlinks=False):
os.remove(dst)
copyfile(src, dst)
except PermissionError as e:
if e.errno == errno.EPERM:
#be forgiving about permissions errors; if it's a real problem, things will explode later anyway
log('Error while copying to \"' + dst + '\": ' + e.strerror)
else:
raise
def getmtimestr(*path_fragments):
path = os.path.join(*path_fragments)
try:
return str(os.path.getmtime(path))
except IOError:
return "0"
def get_validated_steamapps_parent(orig_path):
try:
path = os.path.realpath(orig_path)
if os.path.isdir(path) and os.path.basename(path) == "steamapps":
parent = os.path.dirname(path)
if os.access(parent, os.W_OK) and os.stat(path).st_dev == os.stat(parent).st_dev:
return parent
except: pass
log("Error: unable to use parent for game drive, path " + orig_path)
return orig_path
def try_get_game_library_dir():
if "STEAM_COMPAT_INSTALL_PATH" not in g_session.env or \
"STEAM_COMPAT_LIBRARY_PATHS" not in g_session.env:
return None
#find library path which is a subset of the game path
library_paths = g_session.env["STEAM_COMPAT_LIBRARY_PATHS"].split(":")
for path in library_paths:
if path in g_session.env["STEAM_COMPAT_INSTALL_PATH"]:
return get_validated_steamapps_parent(path)
return None
def try_get_steam_dir():
if "STEAM_COMPAT_CLIENT_INSTALL_PATH" not in g_session.env:
return None
return g_session.env["STEAM_COMPAT_CLIENT_INSTALL_PATH"]
def setup_dir_drive(compat_option, drive_name, dest_dir):
drive_path = g_compatdata.prefix_dir + "dosdevices/" + drive_name
if compat_option in g_session.compat_config:
if not dest_dir:
if file_exists(drive_path, follow_symlinks=False):
os.remove(drive_path)
else:
if file_exists(drive_path, follow_symlinks=False):
cur_tgt = os.readlink(drive_path)
if cur_tgt != dest_dir:
os.remove(drive_path)
os.symlink(dest_dir, drive_path)
else:
os.symlink(dest_dir, drive_path)
elif file_exists(drive_path, follow_symlinks=False):
os.remove(drive_path)
def setup_game_dir_drive():
setup_dir_drive("gamedrive", "s:", try_get_game_library_dir())
def setup_steam_dir_drive():
setup_dir_drive("steamdrive", "t:", try_get_steam_dir())
def unix_to_nt_file_name(path):
return '\\??\\unix' + path
def setup_openvr_paths():
if 'VR_PATHREG_OVERRIDE' in g_session.env:
openvr_paths = g_session.env['VR_PATHREG_OVERRIDE']
del g_session.env['VR_PATHREG_OVERRIDE']
elif 'XDG_CONFIG_HOME' in g_session.env:
openvr_paths = os.path.join(g_session.env['XDG_CONFIG_HOME'], 'openvr/openvrpaths.vrpath')
elif 'HOME' in g_session.env:
openvr_paths = os.path.join(g_session.env['HOME'], '.config/openvr/openvrpaths.vrpath')
else:
openvr_paths = None
if not openvr_paths or not file_exists(openvr_paths, follow_symlinks=True):
return
try:
with open(openvr_paths, 'r') as file:
contents = json.load(file)
except json.decoder.JSONDecodeError:
return
if 'runtime' not in contents or type(contents['runtime']) != list:
contents['runtime'] = []
if 'config' in contents and type(contents['config']) != list:
del contents['config']
if 'log' in contents and type(contents['log']) != list:
del contents['log']
if 'VR_OVERRIDE' in g_session.env:
g_session.env['PROTON_VR_RUNTIME'] = g_session.env['VR_OVERRIDE']
del g_session.env['VR_OVERRIDE']
elif len(contents['runtime']) > 0:
g_session.env['PROTON_VR_RUNTIME'] = contents['runtime'][0]
contents['runtime'] = ["C:\\vrclient\\", "C:\\vrclient"]
for i, path in enumerate(contents.get('config', [])):
contents['config'][i] = unix_to_nt_file_name(path)
for i, path in enumerate(contents.get('log', [])):
contents['log'][i] = unix_to_nt_file_name(path)
openvr_paths = os.path.join(g_compatdata.prefix_dir, "drive_c/users/steamuser/AppData/Local/openvr")
makedirs(openvr_paths)
openvr_paths = os.path.join(openvr_paths, "openvrpaths.vrpath")
with open(openvr_paths, 'w') as file:
json.dump(contents, file, indent=3)
# Function to find the installed location of DLL files for use by Wine/Proton
# from the NVIDIA Linux driver
#
# See https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/issues/71 for
# background on the chosen method of DLL discovery.
#
# On success, returns a str() of the absolute-path to the directory at which DLL
# files are stored
#
# On failure, returns None
def find_nvidia_wine_dll_dir():
try:
libdl = CDLL("libdl.so.2")
except (OSError):
return None
try:
libglx_nvidia = CDLL("libGLX_nvidia.so.0")
except OSError:
return None
# from dlinfo(3)
#
# struct link_map {
# ElfW(Addr) l_addr; /* Difference between the
# address in the ELF file and
# the address in memory */
# char *l_name; /* Absolute pathname where
# object was found */
# ElfW(Dyn) *l_ld; /* Dynamic section of the
# shared object */
# struct link_map *l_next, *l_prev;
# /* Chain of loaded objects */
#
# /* Plus additional fields private to the
# implementation */
# };
RTLD_DI_LINKMAP = 2
class link_map(Structure):
_fields_ = [("l_addr", c_void_p), ("l_name", c_char_p), ("l_ld", c_void_p)]
# from dlinfo(3)
#
# int dlinfo (void *restrict handle, int request, void *restrict info)
dlinfo_func = libdl.dlinfo
dlinfo_func.argtypes = c_void_p, c_int, c_void_p
dlinfo_func.restype = c_int
# Allocate a link_map object
glx_nvidia_info_ptr = POINTER(link_map)()
# Run dlinfo(3) on the handle to libGLX_nvidia.so.0, storing results at the
# address represented by glx_nvidia_info_ptr
if dlinfo_func(libglx_nvidia._handle,
RTLD_DI_LINKMAP,
addressof(glx_nvidia_info_ptr)) != 0:
return None
# Grab the contents our of our pointer
glx_nvidia_info = cast(glx_nvidia_info_ptr, POINTER(link_map)).contents
# Decode the path to our library to a str()
if glx_nvidia_info.l_name is None:
return None
try:
libglx_nvidia_path = os.fsdecode(glx_nvidia_info.l_name)
except UnicodeDecodeError:
return None
# Follow any symlinks to the actual file
libglx_nvidia_realpath = os.path.realpath(libglx_nvidia_path)
# Go to the relative path ./nvidia/wine from our library
nvidia_wine_dir = os.path.join(os.path.dirname(libglx_nvidia_realpath), "nvidia", "wine")
# Check that nvngx.dll exists here, or fail
if file_exists(os.path.join(nvidia_wine_dir, "nvngx.dll"), follow_symlinks=True):
return nvidia_wine_dir
return None
EXT2_IOC_GETFLAGS = 0x80086601
EXT2_IOC_SETFLAGS = 0x40086602
EXT4_CASEFOLD_FL = 0x40000000
def set_dir_casefold_bit(dir_path):
dr = os.open(dir_path, 0o644)
if dr < 0:
return
try:
dat = array.array('I', [0])
if fcntl.ioctl(dr, EXT2_IOC_GETFLAGS, dat, True) >= 0:
dat[0] = dat[0] | EXT4_CASEFOLD_FL
fcntl.ioctl(dr, EXT2_IOC_SETFLAGS, dat, False)
except (OSError, IOError):
#no problem
pass
os.close(dr)
def get_replace_reg_value(file, key, name, new_value=None):
if not file_exists(file, follow_symlinks=True):
return None
replaced = False
out = None
if new_value is not None:
out = open(file + ".new", "w")
found_key = False
old_value = None
namestr="\"" + name + "\"="
with open(file, "r") as reg_in:
for line in reg_in:
if not replaced:
if line[0] == '[':
if found_key:
if out is not None:
out.close()
return None
if line[1:len(key) + 1] == key:
found_key = True
elif found_key:
idx = line.find(namestr)
if idx != -1:
old_value = line[idx + len(namestr):]
if out is not None:
out.write(namestr + new_value)
replaced = True
continue
else:
return old_value
if out is not None:
out.write(line)
if out is not None:
out.close()
if replaced:
try:
os.rename(file, file + ".old")
except OSError:
os.remove(file)
pass
try:
os.rename(file + ".new", file)
except OSError:
log("Unable to write new registry file to " + file)
pass
return old_value
class Proton:
def __init__(self, base_dir):
self.base_dir = base_dir + "/"
self.contrib_dir = self.path("contrib/")
self.dist_dir = self.path("files/")
self.bin_dir = self.path("files/bin/")
self.lib_dir = self.path("files/lib/")
self.fonts_dir = self.path("files/share/fonts/")
self.media_dir = self.path("files/share/media/")
self.wine_fonts_dir = self.path("files/share/wine/fonts/")
self.wine_inf = self.path("files/share/wine/wine.inf")
self.version_file = self.path("version")
self.default_pfx_dir = self.path("files/share/default_pfx/")
self.user_settings_file = self.path("user_settings.py")
self.wine_bin = self.bin_dir + "wine"
self.wineserver_bin = self.bin_dir + "wineserver"
self.xrandr_bin = self.bin_dir + "xrandr"
self.dist_lock = FileLock(self.path("dist.lock"), timeout=-1)
self.host_pe_arch = "x86_64-windows"
self.wow64_pe_arch = "i386-windows"
if os.environ.get("PROTON_USE_ARM64", "1") == "1" and \
platform.machine() == 'aarch64' and \
file_exists(self.path("files/bin-arm64/"), follow_symlinks=True):
self.host_pe_arch = "aarch64-windows"
self.default_pfx_dir = self.path("files/share/default_pfx_arm64/")
self.bin_dir = self.path("files/bin-arm64/")
self.wine_bin = self.bin_dir + "wine"
self.xrandr_bin = self.bin_dir + "xrandr"
self.wineserver_bin = self.bin_dir + "wineserver"
def path(self, d):
return self.base_dir + d
def arch_pe_dir(self, d, wow64):
return self.lib_dir + d + "/" + (self.wow64_pe_arch if wow64 else self.host_pe_arch) + "/"
def cleanup_legacy_dist(self):
old_dist_dir = self.path("dist/")
if file_exists(old_dist_dir, follow_symlinks=True):
with self.dist_lock:
if file_exists(old_dist_dir, follow_symlinks=True):
shutil.rmtree(old_dist_dir)
def do_steampipe_fixups(self):
fixups_json = self.path("steampipe_fixups.json")
fixups_mtime = self.path("files/steampipe_fixups_mtime")
if file_exists(fixups_json, follow_symlinks=True):
with self.dist_lock:
import steampipe_fixups
current_fixup_mtime = None
if file_exists(fixups_mtime, follow_symlinks=True):
with open(fixups_mtime, "r") as f:
current_fixup_mtime = f.readline().strip()
new_fixup_mtime = getmtimestr(fixups_json)
if current_fixup_mtime != new_fixup_mtime:
result_code = steampipe_fixups.do_restore(self.base_dir, fixups_json)
if result_code == 0:
with open(fixups_mtime, "w") as f:
f.write(new_fixup_mtime + "\n")
def missing_default_prefix(self):
'''Check if the default prefix dir is missing. Returns true if missing, false if present'''
return not os.path.isdir(self.default_pfx_dir)
def make_default_prefix(self):
with self.dist_lock:
local_env = dict(g_session.env)
if self.missing_default_prefix():
#make default prefix
local_env["WINEPREFIX"] = self.default_pfx_dir
local_env["WINEDEBUG"] = "-all"
g_session.run_proc([self.wine_bin, "wineboot"], local_env)
g_session.run_proc([self.wineserver_bin, "-w"], local_env)
class CompatData:
def __init__(self, compatdata):
self.base_dir = compatdata + "/"
self.prefix_dir = self.path("pfx/")
self.creation_sync_guard = self.path("pfx/creation_sync_guard")
self.version_file = self.path("version")
self.config_info_file = self.path("config_info")
self.fex_config_file = self.path("proton-fex-config.json")
self.tracked_files_file = self.path("tracked_files")
self.prefix_lock = FileLock(self.path("pfx.lock"), timeout=-1)
self.old_machine_guid = None
self.steam_in_pfx_dir = "drive_c/Program Files (x86)/Steam/"
def path(self, d):
return self.base_dir + d
def remove_tracked_files(self):
if not file_exists(self.tracked_files_file, follow_symlinks=True):
log("Prefix has no tracked_files??")
return
with open(self.tracked_files_file, "r") as tracked_files:
dirs = []
for f in tracked_files:
path = self.prefix_dir + f.strip()
if file_exists(path, follow_symlinks=False):
if os.path.isfile(path) or os.path.islink(path):
os.remove(path)
else:
dirs.append(path)
for d in dirs:
try:
os.rmdir(d)
except OSError:
#not empty
pass
os.remove(self.tracked_files_file)
os.remove(self.version_file)
def upgrade_pfx(self, old_ver):
def parse_prefix_version(ver):
# Valve and most forks use "11.0-3" (or "CachyOS-11.0-100"), GE uses
# "GE-Proton11-11". Treat a GE release as major.0 with its release
# number as the prefix revision, so GE11-x vs 11.0 isn't a downgrade.
m = re.fullmatch(r'(?:.*-)?(\d+)\.(\d+)-(?:GE-)?(\d+)', ver) or \
re.fullmatch(r'GE-Proton(\d+)()-(\d+)', ver)
if not m:
raise ValueError("unrecognized prefix version " + ver)
return m.group(1), m.group(2) or "0", m.group(3)
if old_ver == CURRENT_PREFIX_VERSION:
return
log("Upgrading prefix from " + str(old_ver) + " to " + CURRENT_PREFIX_VERSION +
" with " + proton_build_name() + " (" + self.base_dir + ")")
if old_ver is None:
return
if '-' not in old_ver:
#How can this happen??
log("Prefix has an invalid version?! You may want to back up user files and delete this prefix.")
#If it does, just let the Wine upgrade happen and hope it works...
return
try:
old_proton_maj, old_proton_min, old_prefix_ver = parse_prefix_version(old_ver)
old_proton_ver = old_proton_maj + "." + old_proton_min
new_proton_maj, new_proton_min, _ = parse_prefix_version(CURRENT_PREFIX_VERSION)
if int(new_proton_maj) < int(old_proton_maj) or \
(int(new_proton_maj) == int(old_proton_maj) and \
int(new_proton_min) < int(old_proton_min)):
log("Removing newer prefix")
self.old_machine_guid = get_replace_reg_value(self.prefix_dir + "system.reg", "Software\\\\Microsoft\\\\Cryptography", "MachineGuid")
if old_proton_ver == "3.7" and not file_exists(self.tracked_files_file, follow_symlinks=True):
#proton 3.7 did not generate tracked_files, so copy it into place first
try_copy(g_proton.path("proton_3.7_tracked_files"), self.tracked_files_file)
self.remove_tracked_files()
path = self.prefix_dir + "/drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/version.txt"
if file_exists(path, follow_symlinks=False) and os.path.isfile(path):
os.remove(path)
if file_exists(self.creation_sync_guard, follow_symlinks=False):
os.remove(self.creation_sync_guard)
return
if old_proton_ver == "3.7" and old_prefix_ver == "1":
if not file_exists(self.prefix_dir + "/drive_c/windows/syswow64/kernel32.dll", follow_symlinks=True):
#shipped a busted 64-bit-only installation on 20180822. detect and wipe clean
log("Detected broken 64-bit-only installation, re-creating prefix.")
shutil.rmtree(self.prefix_dir)
return
#replace broken .NET installations with wine-mono support
if file_exists(self.prefix_dir + "/drive_c/windows/Microsoft.NET/NETFXRepair.exe", follow_symlinks=True) and \
file_is_wine_builtin_dll(self.prefix_dir + "/drive_c/windows/system32/mscoree.dll"):
log("Broken .NET installation detected, switching to wine-mono.")
#deleting this directory allows wine-mono to work
shutil.rmtree(self.prefix_dir + "/drive_c/windows/Microsoft.NET")
#prior to prefix version 4.11-2, all controllers were xbox controllers. wipe out the old registry entries.
if (int(old_proton_maj) < 4 or (int(old_proton_maj) == 4 and int(old_proton_min) == 11)) and \
int(old_prefix_ver) < 2:
log("Removing old xinput registry entries.")
with open(self.prefix_dir + "system.reg", "r") as reg_in:
with open(self.prefix_dir + "system.reg.new", "w") as reg_out:
for line in reg_in:
if line[0] == '[' and "CurrentControlSet" in line and "IG_" in line:
if "DeviceClasses" in line:
reg_out.write(line.replace("DeviceClasses", "DeviceClasses_old"))
elif "Enum" in line:
reg_out.write(line.replace("Enum", "Enum_old"))
else:
reg_out.write(line)
try:
os.rename(self.prefix_dir + "system.reg", self.prefix_dir + "system.reg.old")
except OSError:
os.remove(self.prefix_dir + "system.reg")
pass
try:
os.rename(self.prefix_dir + "system.reg.new", self.prefix_dir + "system.reg")
except OSError:
log("Unable to write new registry file to " + self.prefix_dir + "system.reg")
pass
delete_dde_keys = {}
# Prior to prefix version 6.3-3, ShellExecute* APIs used DDE.
# Wipe out old registry entries.
if int(old_proton_maj) < 6 or (int(old_proton_maj) == 6 and int(old_proton_min) < 3) or \
(int(old_proton_maj) == 6 and int(old_proton_min) == 3 and int(old_prefix_ver) < 3):
delete_dde_keys = {
"[Software\\\\Classes\\\\htmlfile\\\\shell\\\\open\\\\ddeexec",
"[Software\\\\Classes\\\\pdffile\\\\shell\\\\open\\\\ddeexec",
"[Software\\\\Classes\\\\xmlfile\\\\shell\\\\open\\\\ddeexec",
"[Software\\\\Classes\\\\ftp\\\\shell\\\\open\\\\ddeexec",
"[Software\\\\Classes\\\\http\\\\shell\\\\open\\\\ddeexec",
"[Software\\\\Classes\\\\https\\\\shell\\\\open\\\\ddeexec",
}
dde_wb = '@="\\"C:\\\\windows\\\\system32\\\\winebrowser.exe\\" -nohome"'
log("Removing ShellExecute DDE registry entries.")
delete_keys = {}
# Remove shadergpures.sys service which was set up in prefix versions before 11
if int(old_proton_maj) < 11:
delete_keys = {
"[System\\\\ControlSet001\\\\Services\\\\SharedGpuResources]",
"[System\\\\CurrentControlSet\\\\Services\\\\SharedGpuResources]",
}
log("Removing sharedgpures.sys service.")
if delete_dde_keys or delete_keys:
sysreg_fp = self.prefix_dir + "system.reg"
new_sysreg_fp = self.prefix_dir + "system.reg.new"
with open(sysreg_fp, "r") as reg_in:
with open(new_sysreg_fp, "w") as reg_out:
deleting_key = False
for line in reg_in:
if deleting_key:
if line[0] != '[':
continue
deleting_key = False
if line.split(' ')[0] in delete_keys:
deleting_key = True
continue
if not delete_dde_keys:
reg_out.write(line)
continue
if line[:line.find("ddeexec")+len("ddeexec")] in delete_dde_keys:
reg_out.write(line.replace("ddeexec", "ddeexec_old", 1))
elif line.rstrip() == dde_wb:
reg_out.write(line.replace("-nohome", "%1"))
else:
reg_out.write(line)
# Slightly randomize backup file name to avoid colliding with
# other backups.
backup_sysreg_fp = "{}system.reg.{:x}.old".format(self.prefix_dir, randrange(16 ** 8))
try:
os.rename(sysreg_fp, backup_sysreg_fp)
except OSError:
log("Failed to back up old system.reg. Simply deleting it.")
os.remove(sysreg_fp)
pass
try:
os.rename(new_sysreg_fp, sysreg_fp)
except OSError:
log("Unable to write new registry file to " + sysreg_fp)
pass
if int(old_proton_maj) < 10 or (int(old_proton_maj) == 10 and
int(old_proton_min) == 0 and
int(old_prefix_ver) < 105):
with open(self.creation_sync_guard, "w"):
pass
os.sync()
stale_builtins = [self.prefix_dir + "/drive_c/windows/system32/amd_ags_x64.dll",
self.prefix_dir + "/drive_c/windows/syswow64/amd_ags_x64.dll",
self.prefix_dir + "/drive_c/windows/system32/ir50_32.dll",
self.prefix_dir + "/drive_c/windows/syswow64/ir50_32.dll" ]
for builtin in stale_builtins:
if file_exists(builtin, follow_symlinks=False) and file_is_wine_builtin_dll(builtin):
log("Removing stale builtin " + builtin)
os.remove(builtin)
stale_vkd3d = [self.prefix_dir + "/drive_c/windows/system32/libvkd3d-1.dll",
self.prefix_dir + "/drive_c/windows/syswow64/libvkd3d-1.dll",
self.prefix_dir + "/drive_c/windows/system32/libvkd3d-shader-1.dll",
self.prefix_dir + "/drive_c/windows/syswow64/libvkd3d-shader-1.dll" ]
for dll in stale_vkd3d:
if file_exists(dll, follow_symlinks=False):
log("Removing stale vkd3d dll " + dll)
os.remove(dll)
except ValueError:
log("Prefix has an invalid version?! You may want to back up user files and delete this prefix.")
#Just let the Wine upgrade happen and hope it works...
with open(self.creation_sync_guard, "w"):
pass
os.sync()
return
def pfx_copy(self, src, dst, dll_copy=False):
if os.path.islink(src):
contents = os.readlink(src)
if os.path.dirname(contents).endswith(('/lib/wine/i386-unix', '/lib/wine/i386-windows',
'/lib/wine/x86_64-unix', '/lib/wine/x86_64-windows',
'/lib/wine/aarch64-unix', '/lib/wine/aarch64-windows',
# old paths:
'/lib64/wine/x86_64-unix', '/lib64/wine/x86_64-windows')):
# wine builtin dll
# make the destination an absolute symlink
contents = os.path.normpath(os.path.join(os.path.dirname(src), contents))
if dll_copy:
try_copyfile(src, dst)
else:
os.symlink(contents, dst)
else:
try_copyfile(src, dst)
def copy_pfx(self):
with open(self.tracked_files_file, "w") as tracked_files:
for src_dir, dirs, files in os.walk(g_proton.default_pfx_dir):
rel_dir = src_dir.replace(g_proton.default_pfx_dir, "", 1).lstrip('/')
if len(rel_dir) > 0:
rel_dir = rel_dir + "/"
dst_dir = src_dir.replace(g_proton.default_pfx_dir, self.prefix_dir, 1)
if not file_exists(dst_dir, follow_symlinks=True):
makedirs(dst_dir)
tracked_files.write(rel_dir + "\n")
for dir_ in dirs:
src_file = os.path.join(src_dir, dir_)
dst_file = os.path.join(dst_dir, dir_)
if os.path.islink(src_file) and not file_exists(dst_file, follow_symlinks=True):
self.pfx_copy(src_file, dst_file)
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if not file_exists(dst_file, follow_symlinks=True):
self.pfx_copy(src_file, dst_file)
tracked_files.write(rel_dir + file_ + "\n")
created_dirs = []
for rel_dir in STANDARD_PREFIX_DIRECTORIES:
dst_dir = self.prefix_dir.rstrip('/')
for component in rel_dir.split('/'):
dst_dir = os.path.join(dst_dir, component)
if not file_exists(dst_dir, follow_symlinks=True):
makedirs(dst_dir)
created_dirs.append(os.path.relpath(dst_dir, self.prefix_dir))
# Track children before parents so prefix upgrades can remove them.
for rel_dir in reversed(created_dirs):
tracked_files.write(rel_dir + "/\n")
# Set .update-timestamp so Wine doesn't try to update the prefix.
# This is needed in case the mtime of wine.inf has changed in distribution.
with open(os.path.join(self.prefix_dir, '.update-timestamp'), 'w') as update_timestamp:
mtime = int(os.stat(g_proton.wine_inf).st_mtime)
update_timestamp.write(str(mtime))
def update_builtin_libs(self, dll_copy_patterns):
dll_copy_patterns = dll_copy_patterns.split(',')
prev_tracked_files = set()
with open(self.tracked_files_file, "r") as tracked_files:
for line in tracked_files:
prev_tracked_files.add(line.strip())
with open(self.tracked_files_file, "a") as tracked_files:
for src_dir, _, files in os.walk(g_proton.default_pfx_dir):
rel_dir = src_dir.replace(g_proton.default_pfx_dir, "", 1).lstrip('/')
if len(rel_dir) > 0:
rel_dir = rel_dir + "/"
dst_dir = src_dir.replace(g_proton.default_pfx_dir, self.prefix_dir, 1)
if not file_exists(dst_dir, follow_symlinks=True):
makedirs(dst_dir)
tracked_files.write(rel_dir + "\n")
for file_ in files:
src_file = os.path.join(src_dir, file_)
dst_file = os.path.join(dst_dir, file_)
if not file_is_wine_builtin_dll(src_file):
# Not a builtin library
continue
if file_is_wine_builtin_dll(dst_file):
os.unlink(dst_file)
elif file_exists(dst_file, follow_symlinks=False):
# builtin library was replaced
continue
else:
os.makedirs(dst_dir, exist_ok=True)
dll_copy = any(fnmatch.fnmatch(file_, pattern) for pattern in dll_copy_patterns)
self.pfx_copy(src_file, dst_file, dll_copy)
tracked_name = rel_dir + file_
if tracked_name not in prev_tracked_files:
tracked_files.write(tracked_name + "\n")
self.replace_foreign_builtin_links()
def replace_foreign_builtin_links(self):
# A prefix created by another Proton install keeps symlinks to that install's builtin
# libraries wherever our default prefix has no file of the same name (e.g. Valve's
# wine-mono Microsoft.NET tree), so the walk above never refreshes them. The libraries
# are read-only, so installers that overwrite those files fail (dotnet40/48 aborts with
# 1603 after winetricks has already removed mscoree.dll). A prefix created by us has
# plain files there from wineboot, so replace the links with writable copies of our
# library of the same name, or of the old target if we don't ship one.
builtin_arch_dirs = ('i386-unix', 'i386-windows', 'x86_64-unix', 'x86_64-windows',
'aarch64-unix', 'aarch64-windows')
own_lib_dir = os.path.realpath(g_proton.lib_dir) + "/"
replaced = 0
for dir_, _, files in os.walk(self.prefix_dir + "/drive_c/windows"):
for file_ in files:
path = os.path.join(dir_, file_)
if not os.path.islink(path) or not file_is_wine_builtin_dll(path):
continue
target = os.path.normpath(os.path.join(dir_, os.readlink(path)))
if os.path.realpath(target).startswith(own_lib_dir):
continue
arch_dir = os.path.basename(os.path.dirname(target))
ours = os.path.normpath(os.path.join(g_proton.lib_dir, "wine", arch_dir, os.path.basename(target)))
os.unlink(path)
if arch_dir in builtin_arch_dirs and file_exists(ours, follow_symlinks=True):
try_copyfile(ours, path)
elif file_exists(target, follow_symlinks=True):
try_copyfile(target, path)
replaced += 1
if replaced:
log("Replaced " + str(replaced) + " builtin library links into another Proton install")
def create_symlink(self, lname, fname):
if file_exists(lname, follow_symlinks=False):
if os.path.islink(lname):
os.remove(lname)
os.symlink(fname, lname)
else:
os.symlink(fname, lname)
def create_fonts_symlinks(self):
ALTERNATIVES = {
('1313860', 'arial.ttf'), # FIFA 21
('1506830', 'arial.ttf'), # FIFA 22
}
windowsfonts = self.prefix_dir + "/drive_c/windows/Fonts"
makedirs(windowsfonts)
sgi = os.environ.get('SteamGameId', '')
for fonts_dir in [g_proton.fonts_dir, g_proton.wine_fonts_dir]:
for font in os.listdir(fonts_dir):
if not font.endswith('.ttf') and not font.endswith('.ttc'):
continue
lname = os.path.join(windowsfonts, font)
fname = os.path.join(fonts_dir, font)
if (sgi, font) in ALTERNATIVES:
fname = os.path.join(fonts_dir, 'alt', font)
self.create_symlink(lname, fname)
def remove_prefix_file(self, path):
path = os.path.join(self.prefix_dir, path)
if file_exists(path, follow_symlinks=False):
os.remove(path)
if file_exists(path + ".debug", follow_symlinks=False):
os.remove(path + ".debug")
def setup_ddraw(self, use_d7vk):
wine_ddraw64 = g_proton.arch_pe_dir("wine", False) + "ddraw.dll"
wine_ddraw32 = g_proton.arch_pe_dir("wine", True) + "ddraw.dll"
d7vk_ddraw = g_proton.arch_pe_dir("wine/d7vk", True) + "ddraw.dll"
system32_ddraw = "drive_c/windows/system32/ddraw.dll"
syswow64_ddraw = "drive_c/windows/syswow64/ddraw.dll"
syswow64_wine_ddraw = "drive_c/windows/syswow64/ddraw_.dll"
if not use_d7vk:
if file_exists(os.path.join(self.prefix_dir, syswow64_wine_ddraw), follow_symlinks=False):
self.remove_prefix_file(syswow64_ddraw)
self.remove_prefix_file(syswow64_wine_ddraw)
try_copy(wine_ddraw32, syswow64_ddraw, prefix=self.prefix_dir, link_debug=True)
return
self.remove_prefix_file(syswow64_ddraw)
self.remove_prefix_file(syswow64_wine_ddraw)
if not file_exists(os.path.join(self.prefix_dir, system32_ddraw), follow_symlinks=True):
try_copy(wine_ddraw64, system32_ddraw, prefix=self.prefix_dir, link_debug=True)
if not file_exists(d7vk_ddraw, follow_symlinks=True):
log("D7VK requested but ddraw.dll was not found at %s" % d7vk_ddraw)
try_copy(wine_ddraw32, syswow64_ddraw,
prefix=self.prefix_dir, link_debug=True)
return
try_copy(wine_ddraw32, syswow64_wine_ddraw, prefix=self.prefix_dir, link_debug=True)
try_copy(d7vk_ddraw, syswow64_ddraw, prefix=self.prefix_dir, link_debug=True)
append_to_env_str(g_session.env, "WINEDLLOVERRIDES", "ddraw=n,b", ";")
def migrate_user_paths(self):
#move winxp-style paths to vista+ paths. we can't do this in
#upgrade_pfx because Steam may drop cloud files here at any time.
for (old, new, link) in \
[
("drive_c/users/steamuser/Local Settings/Application Data",
self.prefix_dir + "drive_c/users/steamuser/AppData/Local",
"../AppData/Local"),
("drive_c/users/steamuser/Application Data",
self.prefix_dir + "drive_c/users/steamuser/AppData/Roaming",
"./AppData/Roaming"),
("drive_c/users/steamuser/My Documents",
self.prefix_dir + "drive_c/users/steamuser/Documents",
"./Documents"),
]:
#running unofficial Proton/Wine builds against a Proton prefix could
#create an infinite symlink loop. detect this and clean it up.
if file_exists(new, follow_symlinks=False) and os.path.islink(new) and os.readlink(new).endswith(old):
os.remove(new)
old = self.prefix_dir + old
if file_exists(old, follow_symlinks=False) and not os.path.islink(old):
merge_user_dir(src=old, dst=new)
os.rename(old, old + " BACKUP")
if not file_exists(old, follow_symlinks=False):
makedirs(os.path.dirname(old))
os.symlink(src=link, dst=old)
elif os.path.islink(old) and not (os.readlink(old) == link):
os.remove(old)
os.symlink(src=link, dst=old)
def setup_prefix(self):
with self.prefix_lock:
if file_exists(self.version_file, follow_symlinks=True):
with open(self.version_file, "r") as f:
old_ver = f.readline().strip()
else:
old_ver = None
self.upgrade_pfx(old_ver)
if not file_exists(self.creation_sync_guard, follow_symlinks=False):
makedirs(self.prefix_dir + "/drive_c")
set_dir_casefold_bit(self.prefix_dir + "/drive_c")
self.copy_pfx()
machine_guid = self.old_machine_guid
if machine_guid is None:
machine_guid = "\"" + str(uuid.uuid4()) + "\""
get_replace_reg_value(self.prefix_dir + "system.reg", "Software\\\\Microsoft\\\\Cryptography", "MachineGuid", machine_guid)
os.sync()
with open(self.creation_sync_guard, "w"):
pass
os.sync()
self.migrate_user_paths()
if not file_exists(self.prefix_dir + "/dosdevices/c:", follow_symlinks=False):
os.makedirs(f"{self.prefix_dir}/dosdevices", exist_ok=True)
os.symlink("../drive_c", self.prefix_dir + "/dosdevices/c:")
if not file_exists(self.prefix_dir + "/dosdevices/z:", follow_symlinks=False):
os.makedirs(f"{self.prefix_dir}/dosdevices", exist_ok=True)
os.symlink("/", self.prefix_dir + "/dosdevices/z:")
# collect configuration info
steam_linux_path = os.environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"]
use_wined3d = "wined3d" in g_session.compat_config
use_dxvk_dxgi = not use_wined3d and \
not ("WINEDLLOVERRIDES" in g_session.env and "dxgi=b" in g_session.env["WINEDLLOVERRIDES"])
use_nvapi = 'disablenvapi' not in g_session.compat_config or 'forcenvapi' in g_session.compat_config
use_dxvk_d3d8 = "dxvkd3d8" in g_session.compat_config
builtin_dll_copy = os.environ.get("PROTON_DLL_COPY",
#dxsetup redist
"d3dcompiler_*.dll," +
"d3dcsx*.dll," +
"d3dx*.dll," +
"dx8vb.dll," +
"x3daudio*.dll," +
"xactengine*.dll," +
"xapofx*.dll," +
"xaudio*.dll," +
"xinput*.dll," +
#vcruntime redist
#VC2005/2008 installers also replace the WinSxS copies.
#https://github.com/GloriousEggroll/proton-ge-custom/issues/705
"atl[89]0.dll," +
"msvcm[89]0.dll," +
"msvcp[89]0.dll," +
"msvcr[89]0.dll," +
"vcomp[89]0.dll," +
"atl1*.dll," +
"atl.dll," +
"concrt*.dll," +
"msvcp1*.dll," +
"msvcrt*.dll," +
"msvcp7*.dll," +
"msvcp6*.dll," +
"msvcp_win.dll," +
"msvcr1*.dll," +
"msvcrt*.dll," +
"msvcr7*.dll," +
"vcamp1*.dll," +
"vcomp1*.dll," +
"vccorlib1*.dll," +
"vcruntime1*.dll," +
"ucrtbase.dll," +
#there are different instances (comctl32.dll in system32 and a copy of comctl32_v6.dll
#in winsxs, handle that by leaving a copy)
"comctl32.dll," +
#some games balk at ntdll symlink(?)
"ntdll.dll," +
#some games require official vulkan loader
"vulkan-1.dll," +
#let the games install native
"ir50_32.dll"
)
# If any of this info changes, we must rerun the tasks below
prefix_info = '\n'.join((
CURRENT_PREFIX_VERSION,
g_proton.fonts_dir,
g_proton.lib_dir,
steam_linux_path,
getmtimestr(steam_linux_path, 'legacycompat', 'steamclient.dll'),
getmtimestr(steam_linux_path, 'legacycompat', 'steamclient64.dll'),
getmtimestr(steam_linux_path, 'legacycompat', 'Steam.dll'),
g_proton.default_pfx_dir,
getmtimestr(g_proton.default_pfx_dir, 'system.reg'),
str(use_wined3d),
str(use_dxvk_dxgi),
builtin_dll_copy,
str(use_nvapi),
str(use_dxvk_d3d8),
))
# check whether any prefix config has changed
try:
with open(self.config_info_file, "r") as f:
old_prefix_info = f.read()
except IOError:
old_prefix_info = ""
if old_ver != CURRENT_PREFIX_VERSION or old_prefix_info != prefix_info:
# update builtin dll symlinks or copies
self.update_builtin_libs(builtin_dll_copy)
with open(self.config_info_file, "w") as f:
f.write(prefix_info)
with open(self.version_file, "w") as f:
f.write(CURRENT_PREFIX_VERSION + "\n")
#create font files symlinks
self.create_fonts_symlinks()
with open(self.tracked_files_file, "a") as tracked_files:
#copy steam files into place
makedirs(self.prefix_dir + self.steam_in_pfx_dir)
filestocopy = [("steamclient.dll", "steamclient.dll"),
("steamclient64.dll", "steamclient64.dll"),
("GameOverlayRenderer64.dll", "GameOverlayRenderer64.dll"),
("SteamService.exe", "steam.exe"),
("Steam.dll", "Steam.dll")]
for (src,tgt) in filestocopy:
srcfile = steam_linux_path + '/legacycompat/' + src
if os.path.isfile(srcfile):
try_copy(srcfile, self.steam_in_pfx_dir + tgt, prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
filestocopy = [("steamclient64.dll", "steamclient64.dll"),
("GameOverlayRenderer.dll", "GameOverlayRenderer.dll"),
("GameOverlayRenderer64.dll", "GameOverlayRenderer64.dll")]
for (src,tgt) in filestocopy:
srcfile = g_proton.path(src)
if os.path.isfile(srcfile):
try_copy(srcfile, self.steam_in_pfx_dir + tgt, prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
# CW Bug 19152. IL-2 Sturmovik: Cliffs of Dover Blitz Edition needs user's localconfig.vdf
if os.environ.get("SteamGameId", 0) == "754530":
srcdir = os.path.join(steam_linux_path, 'userdata')
dstdir = os.path.join(self.prefix_dir, self.steam_in_pfx_dir, 'userdata')
# figuring out the current user is hard, so copy the config for all users
for userid in os.listdir(srcdir):
srcvdf = os.path.join(srcdir, userid, 'config', 'localconfig.vdf')
dstvdf = os.path.join(dstdir, userid, 'config', 'localconfig.vdf')
if not os.path.exists(srcvdf):
continue
os.makedirs(os.path.dirname(dstvdf), exist_ok=True)
shutil.copyfile(srcvdf, dstvdf)
#copy openvr files into place
makedirs(self.prefix_dir + "/drive_c/vrclient/bin")
try_copy(g_proton.arch_pe_dir("wine", True) + "vrclient.dll", "drive_c/vrclient/bin",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine", False) + "vrclient_x64.dll", "drive_c/vrclient/bin",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine/dxvk", True) + "openvr_api_dxvk.dll", "drive_c/windows/syswow64",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine/dxvk", False) + "openvr_api_dxvk.dll", "drive_c/windows/system32",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
#copy x86_64 wineopenxr files into place
makedirs(self.prefix_dir + "/drive_c/openxr")
try_copy(g_proton.dist_dir + "share/openxr/wineopenxr64.json", "drive_c/openxr",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
if use_wined3d:
dxvkfiles = []
vkd3d_protonfiles = []
wined3dfiles = ["d3d12", "d3d11", "d3d10", "d3d10core", "d3d10_1", "d3d9"]
else:
dxvkfiles = ["d3d11", "d3d10core", "d3d9"]
vkd3d_protonfiles = ["d3d12", "d3d12core"]
wined3dfiles = []
if use_dxvk_dxgi:
dxvkfiles.append("dxgi")
else:
wined3dfiles.append("dxgi")
if use_dxvk_d3d8:
dxvkfiles.append("d3d8")
else:
wined3dfiles.append("d3d8")
icufiles = ["icuin68", "icuuc68", "icudt68"]
for f in wined3dfiles:
try_copy(g_proton.default_pfx_dir + "drive_c/windows/system32/" + f + ".dll", "drive_c/windows/system32",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.default_pfx_dir + "drive_c/windows/syswow64/" + f + ".dll", "drive_c/windows/syswow64",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
for f in dxvkfiles:
try_copy(g_proton.arch_pe_dir("wine/dxvk", False) + f + ".dll", "drive_c/windows/system32",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine/dxvk", True) + f + ".dll", "drive_c/windows/syswow64",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
g_session.dlloverrides[f] = "n"
for f in vkd3d_protonfiles:
optional = False
if f == "d3d12core":
optional = True
try_copy(g_proton.arch_pe_dir("wine/vkd3d-proton", False) + f + ".dll", "drive_c/windows/system32",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True, optional=optional)
try_copy(g_proton.arch_pe_dir("wine/vkd3d-proton", True) + f + ".dll", "drive_c/windows/syswow64",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True, optional=optional)
g_session.dlloverrides[f] = "n"
for f in icufiles:
dst = "drive_c/windows/system32/" + f + ".dll"
if not file_exists(self.prefix_dir + dst, follow_symlinks=False):
tracked_files.write(dst + '\n')
self.create_symlink(self.prefix_dir + dst, g_proton.arch_pe_dir("wine/icu", False) + f + ".dll")
dst = "drive_c/windows/syswow64/" + f + ".dll"
if not file_exists(self.prefix_dir + dst, follow_symlinks=False):
tracked_files.write(dst + '\n')
self.create_symlink(self.prefix_dir + dst, g_proton.arch_pe_dir("wine/icu", True) + f + ".dll")
allow_nvidia_libs = os.path.isdir(g_proton.lib_dir + "wine/nvidia-libs")
disable_nvlibs32 = "nvidialibsno32" in g_session.compat_config or "wow64" in g_session.compat_config
use_nvoptix = "nvoptix" in g_session.compat_config and allow_nvidia_libs
use_nvml = ("nvml" in g_session.compat_config and allow_nvidia_libs) or use_nvoptix
use_nvenc = "nvenc" in g_session.compat_config and allow_nvidia_libs
use_nvcuda = ("nvcuda" in g_session.compat_config and allow_nvidia_libs) or use_nvenc or use_nvoptix
nvapi_path = "nvapi/"
# If the user requested the NVAPI be available, copy it into place.
# If they didn't, clean up any stray nvapi DLLs.
if use_nvapi:
try_copy(g_proton.arch_pe_dir("wine/" + nvapi_path, False) + "nvapi64.dll", "drive_c/windows/system32",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine/" + nvapi_path, False) + "nvofapi64.dll", "drive_c/windows/system32",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine/" + nvapi_path, True) + "nvapi.dll", "drive_c/windows/syswow64",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
g_session.dlloverrides["nvapi64"] = "n"
g_session.dlloverrides["nvofapi64"] = "n"
g_session.dlloverrides["nvapi"] = "n"
g_session.dlloverrides["nvcuda"] = "b"
else:
nvapi64_dll = self.prefix_dir + "drive_c/windows/system32/nvapi64.dll"
nvapi32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvapi.dll"
if file_exists(nvapi64_dll, follow_symlinks=False):
os.unlink(nvapi64_dll)
if file_exists(nvapi64_dll + '.debug', follow_symlinks=False):
os.unlink(nvapi64_dll + '.debug')
if file_exists(nvapi32_dll, follow_symlinks=False):
os.unlink(nvapi32_dll)
if file_exists(nvapi32_dll + '.debug', follow_symlinks=False):
os.unlink(nvapi32_dll + '.debug')
if use_nvcuda and use_nvapi:
try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvcuda", False) + "nvcuda.dll.so", "drive_c/windows/system32/nvcuda.dll",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
g_session.dlloverrides["nvcuda"] = "n,b"
else:
nvcuda64_dll = self.prefix_dir + "drive_c/windows/system32/nvcuda.dll"
if file_exists(nvcuda64_dll, follow_symlinks=False):
os.unlink(nvcuda64_dll)
try_copy(g_proton.default_pfx_dir + "drive_c/windows/system32/nvcuda.dll", "drive_c/windows/system32",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
if use_nvcuda and use_nvapi and not disable_nvlibs32:
try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvcuda", True) + "nvcuda.dll.so", "drive_c/windows/syswow64/nvcuda.dll",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
g_session.dlloverrides["nvcuda"] = "n,b"
else:
nvcuda32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvcuda.dll"
if file_exists(nvcuda32_dll, follow_symlinks=False):
os.unlink(nvcuda32_dll)
try_copy(g_proton.default_pfx_dir + "drive_c/windows/syswow64/nvcuda.dll", "drive_c/windows/syswow64",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
if use_nvenc and use_nvapi:
try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", False) + "nvcuvid.dll.so", "drive_c/windows/system32/nvcuvid.dll",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", False) + "nvencodeapi64.dll.so", "drive_c/windows/system32/nvencodeapi64.dll",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
g_session.dlloverrides["nvcuvid"] = "n,b"
g_session.dlloverrides["nvencodeapi64"] = "n,b"
else:
nvcuvid64_dll = self.prefix_dir + "drive_c/windows/system32/nvcuvid.dll"
nvencodeapi64_dll = self.prefix_dir + "drive_c/windows/system32/nvencodeapi64.dll"
if file_exists(nvcuvid64_dll, follow_symlinks=False):
os.unlink(nvcuvid64_dll)
if file_exists(nvencodeapi64_dll, follow_symlinks=False):
os.unlink(nvencodeapi64_dll)
if use_nvenc and use_nvapi and not disable_nvlibs32:
try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", True) + "nvcuvid.dll.so", "drive_c/windows/syswow64/nvcuvid.dll",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvenc", True) + "nvencodeapi.dll.so", "drive_c/windows/syswow64/nvencodeapi.dll",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
g_session.dlloverrides["nvcuvid"] = "n,b"
g_session.dlloverrides["nvencodeapi"] = "n,b"
else:
nvcuvid32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvcuvid.dll"
nvencodeapi32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvencodeapi.dll"
if file_exists(nvcuvid32_dll, follow_symlinks=False):
os.unlink(nvcuvid32_dll)
if file_exists(nvencodeapi32_dll, follow_symlinks=False):
os.unlink(nvencodeapi32_dll)
if use_nvml and use_nvapi:
prepend_to_env_str(g_session.env, "WINEDLLPATH", g_proton.lib_dir + "wine/nvidia-libs/nvml/wine", ":")
else:
nvml64_dll = self.prefix_dir + "drive_c/windows/system32/nvml.dll"
nvml32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvml.dll"
if file_exists(nvml64_dll, follow_symlinks=False):
os.unlink(nvml64_dll)
if file_exists(nvml32_dll, follow_symlinks=False):
os.unlink(nvml32_dll)
if use_nvoptix and use_nvapi:
try_copy(g_proton.arch_pe_dir("wine/nvidia-libs/nvoptix", False) + "nvoptix.dll.so", "drive_c/windows/system32/nvoptix.dll",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
g_session.dlloverrides["nvoptix"] = "n,b"
else:
nvoptix64_dll = self.prefix_dir + "drive_c/windows/system32/nvoptix.dll"
nvoptix32_dll = self.prefix_dir + "drive_c/windows/syswow64/nvoptix.dll"
if file_exists(nvoptix64_dll, follow_symlinks=False):
os.unlink(nvoptix64_dll)
if file_exists(nvoptix32_dll, follow_symlinks=False):
os.unlink(nvoptix32_dll)
if os.path.exists(g_proton.lib_dir + "wine/discord-rpc-bridge/bridge.exe"):
makedirs(self.prefix_dir + "drive_c/windows/system32/discord/")
try_copy(g_proton.lib_dir + "wine/discord-rpc-bridge/bridge.exe", "drive_c/windows/system32/discord/bridge.exe",
prefix=self.prefix_dir, track_file=tracked_files, link_debug=True)
# Try to detect known DLLs that ship with the NVIDIA Linux Driver
# and add them into the prefix
if g_session.nvidia_wine_dll_dir:
for dll in ["_nvngx.dll", "nvngx.dll"]:
try_copy(g_session.nvidia_wine_dll_dir + "/" + dll,
"drive_c/windows/system32",
optional=True,
prefix=self.prefix_dir,
track_file=tracked_files,
link_debug=True)
intc_store = "drive_c/windows/system32/DriverStore/FileRepository/igd_faux.inf_1"
igdext_dll = find_igdext_dll()
if igdext_dll:
makedirs(os.path.join(self.prefix_dir, intc_store))
try_copy(igdext_dll,
intc_store,
optional=True,
prefix=self.prefix_dir,
track_file=tracked_files,
link_debug=True)
setup_game_dir_drive()
setup_steam_dir_drive()
setup_openvr_paths()
# valve ffmpeg configuration below is disabled for proton-ge, we ship our own ffmpeg build.
# add Steam ffmpeg libraries to path
#prepend_to_env_str(g_session.env, ld_path_var, steam_linux_path + "/ubuntu12_64/video/:" + steam_linux_path + "/ubuntu12_32/video/:" + steam_linux_path + "steamrtarm64/video/", ":")
def sync_appcache(self):
sgi = os.environ.get('SteamGameId', '')
if sgi == '':
return
steam_linux_path = os.environ["STEAM_COMPAT_CLIENT_INSTALL_PATH"]
dst = self.prefix_dir + self.steam_in_pfx_dir + 'appcache'
try:
os.unlink(dst)
except: pass
try:
dir_sync(dst + "/librarycache/" + sgi, steam_linux_path + '/appcache/librarycache/' + sgi)
except: pass
def comma_escaped(s):
escaped = False
idx = -1
while s[idx] == '\\':
escaped = not escaped
idx = idx - 1
return escaped
additional_config = utilities.proton_add_config()
#hopefully short-lived, app-specific workarounds for Proton bugs
def default_compat_config():
ret = set()
ret.update(additional_config)
if "SteamAppId" in os.environ:
appid = os.environ["SteamAppId"]
if appid in [
#affected by CW bug 20240
"1331440", #FUSER
#affected by Unity race
"2620730", #DEVIATOR - CW bug 24913
"2882920", #SSR Wives: The Murder Of My Winter Crush Demo - CW bug 25730
"2712910", #Spark & Kling - CW bug 25778
]:
ret.add("nomfdxgiman")
if appid in [
# OPWR may be causing text input delays in login windows in these games on XWayland due to
# blit happening before presentation
# Keep Forza Horizon 5 out: its bundled WebView2 needs cross-process Vulkan surfaces.
"1172620", #Sea of Thieves
"962130", #Grounded
"495420", #State of Decay 2: Juggernaut Edition
"976730", #Halo: The Master Chief Collection
"1017900", #Age of Empires: Definitive Edition
"1056090", #Ori and the Will of the Wisps
"1293830", #Forza Horizon 4
"813780", #Age of Empires II: Definitive Edition
"933110", #Age of Empires III: Definitive Edition
"1466860", #Age of Empires IV
"1097840", #Gears 5
"1244950", #Battletoads
"1189800", #Bleeding Edge
"1184050", #Gears Tactics
"1240440", #Halo Infinite
"1250410", #Microsoft Flight Simulator
"1672970", #Minecraft Dungeons
"1180660", #Tell Me Why
"1238430", #Tell Me Why Chapter 2
"1266670", #Tell Me Why Chapter 3
# Other issues arising from OWPR code path in apps, e. g., hitting unimplemented bits in
# d3dcompiler.
"230410", #Warframe
"3513350", #Wuthering Waves
"3728370",
]:
ret.add("noopwr")
if appid in [
"2710", #Act of War: Direct Action
"1621680", #Sword and Fairy 4
"888040", #Metal Fatigue
]:
ret.add("noforcelgadd")
if appid in [
"257420", #Serious Sam 4
"2021880", #Ara: History Untold
]:
ret.add("hidevggpu")
if appid in [
"1977170", #Jusant
]:
ret.add("hideintelgpu")
if appid in [
"202990", #Call of Duty: Black Ops II - Multiplayer
"212910", #Call of Duty: Black Ops II - Zombies
"499100", #Dark Parables: The Exiled Prince Collector's Edition (499100)
"1404090", #Trivia Tricks
"2052410", #WITCH ON THE HOLY NIGHT
"789910", #Planet of the Apes: Last Frontier
"1183470", #Imperiums: Greek Wars
"876340", #VR Slots 3D
]:
ret.add("heapdelayfree")
if appid in [
"21980", #Call of Juarez: Bound in Blood
"553850", #Helldivers 2
"2055290", #Sonic Colors: Ultimate
]:
ret.add("heapzeromemory")
if appid in [
"71230", #Crazy Taxi
"3328910", #MySims Kingdom
]:
ret.add("heaptopdown")
if appid in [
"2630", #Call of Duty 2
"1060210", #Disaster Report 4: Summer Memories
"414740", #RAID: World War II
"201510", #Flatout 3
"1233880", #Disgaea 4 Complete+
]:
ret.add("nofsync")
ret.add("noesync")
if appid in [
"1237970", #Titanfall 2
]:
for idx, arg in enumerate(sys.argv):
if '-northstar' in arg:
ret.add("northstar")
if appid in [
# disable dxvknvapi for titles which dislike it
"1088850", #Marvel's Guardians of the Galaxy
"1418100", #Swords of Legends Online
"2080180", #Go Home Annie Demo
"1939100", #Go Home Annie
"435150", #Divinity: Original Sin 2 - Definitive Edition
"2176900", #Fablecraft
"2853730", #Skull and Bones
"1761380", #Otherworld Legends (just to disable nvcuda.dll)
]:
ret.add("disablenvapi")
if appid in [
"2698940", #The Crew Motorfest
"2079120", #Warudo
]:
ret.add("hidenvgpu")
if appid in [
"2395210", #Tony Hawk's Pro Skater 1 + 2
"1577120", #The Quarry
]:
ret.add("forcenvapi")
if appid in [
"1252330" #Deathloop
]:
ret.add("hideapu")
if appid in [
"249610", # Galactic Arms Race
"287240", # Eterium Demo
"280200", # Eterium
"312530", # Duck Game
"448230", # AsteroidsHD
"1072860", # Real Scary
"512490", # Zombie Estate 2
]:
ret.add("fnad3d11")
#options to also be enabled for prerequisite setup steps
ret.add("gamedrive")
if "STEAM_COMPAT_APP_ID" in os.environ:
appid = os.environ["STEAM_COMPAT_APP_ID"]
if appid in [
"247660", #Deadly Premonition: The Director's Cut
"1026680", #FINAL FANTASY VIII - REMASTERED
"3513350", #Wuthering Waves
"3837340", #FINAL FANTASY VII
]:
ret.add("noxalia")
if appid in [
"275850", #No Man's Sky
"2012840", #Portal with RTX
]:
ret.add("nohardwarescheduling")
if "STORE" in os.environ:
store = os.environ["STORE"]
if store in [
"battlenet",
"ea",
"ubisoft",
]:
ret.add("writecopy")
# hack to force gamedrive compat_config always for umu
if os.environ.get('UMU_ID', ''):
ret.add("gamedrive")
return ret
default_cpu_limit = {
"19900" : "16", # Far Cry 2
"298110" : "16", # Far Cry 4
"20920" : "16", # The Witcher 2: Assassins of Kings Enchanced Edition
"35130" : "16", # Lara Croft and the Guardian of Light
"55150" : "16", # Warhammer 40,000: Space Marine
"204450" : "16", # Call of Juarez: Gunslinger
"15620" : "8", # Warhammer 40,000: Dawn of War II
"20570" : "8", # Warhammer 40,000: Dawn of War II - Chaos Rising
"56400" : "8", # Warhammer 40,000: Dawn of War II - Retribution
"618970" : "4", # Outcast - Second Contact
"10150" : "8", # Prototype
"2229830" : "1", # Command & Conquer and The Covert Operations
"259170" : "8", # Alone in the Dark (2008)
"11440" : "4", # DiRT
"316260" : "16", # Disney Universe
"286810" : "30", # Hard Truck Apocalypse: Rise of Clans / Ex Machina: Meridian 113
"70000" : "28", # Dino D-Day
"115320" : "8", # Prototype 2
"65540" : "16", # Gothic 1 Classic
"1038250" : "P", # DIRT 5
}
fex_application_profiles = {
"292030" : { # The Witcher 3: Wild Hunt
"setup*": {
"X87ReducedPrecision": "0"
}
}
}
def get_current_cpu_affinity():
try:
affinity = os.sched_getaffinity(0)
if len(affinity) >= os.cpu_count():
return ""
topo_str = ",".join(map(str, list(affinity)))
return f"{len(affinity)}:{topo_str}"
except: return ""
class Session:
def __init__(self):
self.log_file = None
self.env = dict(os.environ)
self.xrandr_bin = g_proton.xrandr_bin
self.dlloverrides = {
"steam.exe": "b", #always use our special built-in steam.exe
"dotnetfx35.exe": "b", #replace the broken installer, as does Windows
"dotnetfx35setup.exe": "b",
"beclient.dll": "b,n",
"beclient_x64.dll": "b,n",
"winebth.sys": "d", #disable winebth.sys as it crashes winedevice.exe
}
# CW Bug 21737. Locoland executable happens to be steam.exe.
if os.environ.get("SteamGameId", 0) == "352130":
del self.dlloverrides["steam.exe"]
if os.environ.get("SteamGameId", 0) not in ["2767030", "2274200"]:
self.dlloverrides["opencl"] = "n,d"
self.compat_config = default_compat_config()
self.cmdlineappend = []
if "STEAM_COMPAT_CONFIG" in os.environ:
config = os.environ["STEAM_COMPAT_CONFIG"]
while config:
(cur, _, config) = config.partition(',')
if cur.startswith("cmdlineappend:"):
while comma_escaped(cur):
(a, _, c) = config.partition(',')
cur = cur[:-1] + ',' + a
config = c
self.cmdlineappend.append(cur[14:].replace('\\\\','\\'))
else:
self.compat_config.add(cur)
#turn forcelgadd on by default unless it is disabled in compat config
if "noforcelgadd" not in self.compat_config:
self.compat_config.add("forcelgadd")
appid = os.environ.get("SteamGameId", 0)
if not appid:
umu_id = os.environ.get("UMU_ID", "")
if umu_id.startswith("umu-") and umu_id[4:].isdigit():
appid = umu_id[4:]
# Castlevania caches its playable devices at startup, even though it keeps
# polling XInput. Keep player 1 available across controller replacements.
if appid == "1018010": # Castlevania Anniversary Collection
self.env["PROTON_XINPUT_PERSISTENT_PLAYER1"] = "1"
# Rune Factory 5 shows keyboard prompts for a Steam Deck or Steam Controller.
if appid == "1702330":
self.env.setdefault("PROTON_STEAM_INPUT_XBOX_PROMPTS", "1")
# Darktide only offers its own FSR 4.1.1 on RDNA3 with its native AGS and
# without our amdxc64 FSR4 provider. Only on AMD-only systems: native AGS
# uses Wine's builtin ADL, which other vendors don't have on Windows.
# PROTON_NATIVE_AGS=0 or an amdxc64 entry in WINEDLLOVERRIDES opts out, and
# PROTON_FSR4_UPGRADE keeps amdxc64. #869
if appid == "1361210" and host_gpu_vendors() == {"0x1002"}: # Warhammer 40,000: Darktide
self.env.setdefault("PROTON_NATIVE_AGS", "1")
if (nonzero(self.env["PROTON_NATIVE_AGS"]) and
"amdxc64" not in self.env.get("WINEDLLOVERRIDES", "") and
"PROTON_FSR4_UPGRADE" not in self.env and
"fsr4" not in self.compat_config):
self.dlloverrides["amdxc64"] = "d"
# Diablo IV supports native DualSense, but does not recognize the Edge PID.
# Winebus leaves Steam Input and the physical audio/haptics identity intact.
if appid == "2344520": # Diablo IV
self.env.setdefault("PROTON_SONY_DUALSENSE_EDGE_AS_DUALSENSE", "1")
# These games support native Sony controllers without Steam Input, but
# also assign the automatic XInput fallback to a player slot. Sackboy
# puts the DualSense on player two; Rune Factory 4 Special scrambles DS4 input.
# Where Winds Meet loses the DualSense touchpad when the controller is
# plugged in after the game starts.
if appid in [
"1599660", # Sackboy: A Big Adventure (#843)
"1580240", # Rune Factory 4 Special (#843)
"3564740", # Where Winds Meet (#829)
]:
self.env.setdefault("PROTON_SONY_AUTO_XINPUT", "0")
# These games have perfect DS4 mappings, but do not support DS5/DS5 Edge.
# In this case we fake a DS4 controller for perfect button mappings and icons
if appid in [
"911400", # Assassin's Creed III / Liberation Remastered
"812140", # Assassin's Creed Odyssey
"582160", # Assassin's Creed Origins
"1222140", # Detroit: Become Human
"1174180", # Red Dead Redemption 2
"1190460", # Death Stranding
"1593500", # God of War (2018)
"1172710", # Dune Awakening
"731490", # Crash Bandicoot N'Sane Trilogy
"253230", # A Hat in Time
"638970", # Yakuza 0
"612880", # Wolfenstein II: The New Colossus
# Batman Arkham Knight turns out to ship with a flawed libScePad_x64.dll
# that prevents both the DS4 v1 and v2 from working. Replacing it with
# one from another game fixed the issue and let both DS4 models work
# properly. Because this means the game needs to be modded to support
# the DS4 properly, it's debatable if this entry should make it into the
# final commit.
"208650", # Batman: Arkham Knight
# The enhanced edition of Little nightmares has full support for the
# DualSense. This entry is here in case anyone prefers the original and
# still wants to use the DualSense.
"424840", # Little Nightmares (Original)
"996580", # Spyro Reignited Trilogy
# Recommended by rjbs91. I don't have Greedfall, so I would recommend
# testing it's support; whether it supports the DS4 v2 or only the v1.
# rjbs91 claims it has proper button mappings and glyphs.
"606880", # Greedfall
"570940", # Dark Souls Remastered
"278080", # Dynasty Wariors 8: Xtreme Legends Complete Edition
]:
self.env["PROTON_SONY_DUALSENSE_AS_DUALSHOCK4"] = "1"
# Some games only support DS4 v1
if appid in [
"731490", # Crash Bandicoot N'Sane Trilogy
"638970", # Yakuza 0
"612880", # Wolfenstein II: The New Colossus
]:
self.env["PROTON_SONY_DUALSHOCK4_V2_AS_V1"] = "1"
# Work around quirk caused by our Dynamic exe relocation patch
if appid in [
"201870", # Assassin's Creed: Revelations
]:
self.env["WINE_DISABLE_EXE_ASLR"] = "1"
# Work around MH Wilds audio device quirk
if appid in [
"2246340", # Monster Hunter Wilds
]:
self.env["PROTON_ENABLE_MHWILDS_USB_AUDIO"] = "1"
# Initialize the DualSense USB audio system, preserve its internal-speaker
# route, and select the controller through Death Stranding's native BB
# output setting while keeping the four-channel haptic stream active.
if appid in [
"1850570", # Death Stranding Director's Cut
"3280350", # Death Stranding 2
]:
self.env["PROTON_DEATH_STRANDING_CONTROLLER_EFFECTS"] = "1"
# Director's Cut does not expose the PS5 environmental-speaker option.
# Death Stranding 2 has a native in-game setting and must retain it.
if appid == "1850570":
self.env["PROTON_DEATH_STRANDING_FORCE_ENVIRONMENT_EFFECTS"] = "1"
# These games read the DualSense audio endpoint name and only start USB
# haptics on the Windows-style four-channel endpoint. They never probe its
# formats, so the automatic Windows Sony audio mode does not trigger. (#860)
if appid in [
"2909400", # Final Fantasy VII Rebirth
"2420110", # Horizon Forbidden West Complete Edition
"2561580", # Horizon Zero Dawn Remastered
]:
self.env.setdefault("PROTON_SONY_WINDOWS_DEVICE_NAMES", "1")
if "PROTON_PIPEWIRE_ALSA_PLUGIN" not in self.env:
if g_proton.host_pe_arch == "aarch64-windows":
alsa_arch = "aarch64-linux-gnu"
else:
alsa_arch = "x86_64-linux-gnu"
self.env["PROTON_PIPEWIRE_ALSA_PLUGIN"] = os.path.join(
g_proton.lib_dir, alsa_arch, "alsa-lib",
"libasound_module_pcm_pipewire.so")
if "PROTON_CPU_TOPOLOGY" in self.env:
self.env["WINE_CPU_TOPOLOGY"] = self.env["PROTON_CPU_TOPOLOGY"]
elif appid in default_cpu_limit:
self.env["WINE_CPU_TOPOLOGY"] = default_cpu_limit[appid]
elif "WINE_CPU_TOPOLOGY" not in self.env and platform.machine() == 'aarch64':
topo_str = get_current_cpu_affinity()
if topo_str != "":
self.env["WINE_CPU_TOPOLOGY"] = topo_str
if "WINE_HIDE_AMD_GPU" not in self.env and appid in [
"1282690",
]:
self.env["WINE_HIDE_AMD_GPU"] = "1"
if "WINE_HIDE_AMD_CPU" not in self.env and appid in [
"17300",
"17340",
]:
self.env["WINE_HIDE_AMD_CPU"] = "1"
if "WINE_DISABLE_GAMESCOPE_MAX_SIZE_HACK" not in self.env and appid in [
"3754990",
"3495730"
]:
self.env["WINE_DISABLE_GAMESCOPE_MAX_SIZE_HACK"] = "1"
def init_wine(self):
if "HOST_LC_ALL" in self.env and len(self.env["HOST_LC_ALL"]) > 0:
#steam sets LC_ALL=C to help some games, but Wine requires the real value
#in order to do path conversion between win32 and host. steam sets
#HOST_LC_ALL to allow us to use the real value.
self.env["LC_ALL"] = self.env["HOST_LC_ALL"]
else:
self.env.pop("LC_ALL", "")
# FIXME: temporary workaround to libxkbcommon's current integration
if "XLOCALEDIR" not in self.env:
self.env["XLOCALEDIR"] = g_proton.dist_dir + "share/X11/locale"
# CW-Bug-Id: #23185 Enable the new SDL 2.30 Steam Input integration.
if "SteamVirtualGamepadInfo_Proton" in self.env and "SteamVirtualGamepadInfo" not in self.env:
self.env["SteamVirtualGamepadInfo"] = self.env["SteamVirtualGamepadInfo_Proton"]
if "PROTON_USE_WOW64" not in self.env and "SteamGameId" in self.env:
if self.env["SteamGameId"] in [
"747920", # Hero Plus
"4005300", # Dating 4 Girls
]:
self.env["PROTON_USE_WOW64"] = "1"
self.env.pop("WINEARCH", "")
if "PROTON_USE_WOW64" in self.env and self.env["PROTON_USE_WOW64"] == "1" and platform.machine() == "x86_64":
self.env["WINEARCH"] = "wow64"
if 'ORIG_'+ld_path_var not in os.environ:
# Allow wine to restore this when calling an external app.
self.env['ORIG_'+ld_path_var] = os.environ.get(ld_path_var, '')
if g_proton.host_pe_arch == "aarch64-windows":
ld_library_path = [
g_proton.lib_dir + "aarch64-linux-gnu",
g_proton.lib_dir + "x86_64-linux-gnu",
g_proton.lib_dir + "i386-linux-gnu",
]
else:
ld_library_path = [
g_proton.lib_dir + "x86_64-linux-gnu",
g_proton.lib_dir + "i386-linux-gnu",
]
prepend_to_env_str(self.env, ld_path_var, ':'.join(ld_library_path), ":")
dllpaths = [g_proton.lib_dir + "vkd3d", g_proton.lib_dir + "wine"]
if "WINEDLLPATH" in os.environ:
dllpaths.append(os.environ["WINEDLLPATH"])
self.env["WINEDLLPATH"] = ':'.join(dllpaths)
if "STEAM_COMPAT_MEDIA_PATH" in os.environ:
old_audiofoz_path = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/audio.foz"
if file_exists(old_audiofoz_path, follow_symlinks=False):
os.remove(old_audiofoz_path)
self.env["MEDIACONV_AUDIO_DUMP_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/audiov2.foz"
self.env["MEDIACONV_VIDEO_DUMP_FILE"] = os.environ["STEAM_COMPAT_MEDIA_PATH"] + "/video.foz"
if "STEAM_COMPAT_TRANSCODED_MEDIA_PATH" in os.environ:
self.env["MEDIACONV_AUDIO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] + "/transcoded_audio.foz"
self.env["MEDIACONV_VIDEO_TRANSCODED_FILE"] = os.environ["STEAM_COMPAT_TRANSCODED_MEDIA_PATH"] + "/transcoded_video.foz"
if os.environ.get("PROTON_MEDIACONV_NO_VIDEO",
"1" if os.environ.get("SteamGameId", 0) in (
"283640", # Salt and Sanctuary
"455490", # Don't Die Dateless, Dummy!
"787810", # Rogue Heroes: Ruins of Tasos
"1199570", # Rogue Heroes: Ruins of Tasos Demo
"1491460", # Tor Eternum
"1588990", # Tor Eternum Demo
"1895130", # Darza's Dominion
) else "0") != "0":
self.env["MEDIACONV_BLANK_VIDEO_FILE"] = g_proton.media_dir + "blank.mka"
else:
self.env["MEDIACONV_BLANK_VIDEO_FILE"] = g_proton.media_dir + "blank.mkv"
self.env["MEDIACONV_BLANK_AUDIO_FILE"] = g_proton.media_dir + "blank.ptna"
self.env["ESPEAK_DATA_PATH"] = g_proton.dist_dir + "share"
prepend_to_env_str(self.env, "PATH", g_proton.bin_dir, ":")
def check_environment(self, env_name, config_name):
if env_name not in self.env:
return False
if nonzero(self.env[env_name]):
self.compat_config.add(config_name)
else:
self.compat_config.discard(config_name)
return True
def try_log_slr_versions(self):
try:
if "PRESSURE_VESSEL_RUNTIME_BASE" in self.env:
with open(self.env["PRESSURE_VESSEL_RUNTIME_BASE"] + "/VERSIONS.txt", "r") as f:
for line in f:
line = line.strip()
if len(line) > 0 and not line.startswith("#"):
cleaned = line.split("#")[0].strip().replace("\t", " ")
split = cleaned.split(" ", maxsplit=1)
self.log_file.write(split[0] + ": " + split[1] + "\n")
except (OSError, IOError, TypeError, KeyError):
pass
def setup_logging(self, *, append_forever):
basedir = self.env.get("PROTON_LOG_DIR", os.environ["HOME"])
if append_forever:
#SteamGameId is not always available
lfile_path = basedir + "/steam-proton.log"
else:
if "SteamGameId" not in os.environ:
return False
lfile_path = basedir + "/steam-" + os.environ["SteamGameId"] + ".log"
if file_exists(lfile_path, follow_symlinks=False):
os.remove(lfile_path)
makedirs(basedir)
self.log_file = open(lfile_path, "a")
return True
def log_enabled_for(self, target: str, default_value: bool = False) -> bool:
log_list = self.env["PROTON_LOG"]
if nonzero(log_list):
for entry in log_list.split(','):
if entry[1:] == target:
if entry.startswith('+'):
return True
elif entry.startswith('-'):
return False
break
return default_value # logging enabled, return specified default when not found
return False # logging disabled, all false
def generate_fex_app_config(self):
"""
Translate some environment variables into FEX config which is in files.
"""
app_config = {
"Config": {},
"ThunksDB": {}
}
# appinfo based environment variable as default
try:
app_config["Config"]["TSOEnabled"] = self.env['STEAM_FEX_TSOENABLED']
except (KeyError, ValueError):
pass
try:
app_config["Config"]["Multiblock"] = self.env['STEAM_FEX_MULTIBLOCK']
except (KeyError, ValueError):
pass
# if user has specified an override, take those values
try:
config_env = self.env['STEAM_COMPAT_FEX_CONFIG']
if config_env:
app_config["Config"]["TSOEnabled"] = "1" if ("TSOEnabled:1" in config_env) else "0"
app_config["Config"]["Multiblock"] = "1" if ("Multiblock:1" in config_env) else "0"
except KeyError:
pass
if "PROTON_LOG" in self.env:
app_config["Config"]["SilentLog"] = "0" if self.log_enabled_for("fex", True) else "1"
if "SteamGameId" in os.environ and os.environ["SteamGameId"] in fex_application_profiles:
try:
app_config["AppOverrides"] = fex_application_profiles[os.environ["SteamGameId"]]
except (KeyError, ValueError):
pass
return app_config
def get_primary_monitor(self):
try:
result = subprocess.run(
[self.xrandr_bin],
check=True,
capture_output=True,
text=True,
)
except FileNotFoundError:
print("Error: xrandr is not installed or not in PATH.", file=sys.stderr)
return None
except subprocess.CalledProcessError as e:
print(f"Error: failed to run xrandr: {e}", file=sys.stderr)
return None
for line in result.stdout.splitlines():
# Example:
# HDMI-A-1 connected primary 3840x2160+3840+0 ...
match = re.match(r"^(\S+)\s+connected\s+primary\b", line)
if match:
return match.group(1)
return None
def init_session(self, update_prefix_files):
self.env["WINEPREFIX"] = g_compatdata.prefix_dir
#load environment overrides
used_user_settings = {}
if file_exists(g_proton.user_settings_file, follow_symlinks=True):
try:
import user_settings # pyright: ignore [reportMissingImports]
for key, value in user_settings.user_settings.items():
if key not in self.env:
self.env[key] = value
used_user_settings[key] = value
except Exception:
log("************************************************")
log("THERE IS AN ERROR IN YOUR user_settings.py FILE:")
log("%s" % sys.exc_info()[1])
log("************************************************")
if "PROTON_LOG" in self.env and nonzero(self.env["PROTON_LOG"]):
self.env.setdefault("WINEDEBUG", "+timestamp,+pid,+tid,+seh,+unwind,+threadname,+debugstr,+loaddll,+mscoree")
self.env.setdefault("DXVK_LOG_LEVEL", "info")
self.env.setdefault("DXVK_NVAPI_LOG_LEVEL", "info")
self.env.setdefault("VKD3D_DEBUG", "warn")
self.env.setdefault("VKD3D_SHADER_DEBUG", "fixme")
self.env.setdefault("WINE_MONO_TRACE", "E:System.NotImplementedException")
if self.env["PROTON_LOG"] != "1":
append_to_env_str(self.env, "WINEDEBUG", self.env["PROTON_LOG"], ",")
#for performance, logging is disabled by default; override with user_settings.py
self.env.setdefault("WINEDEBUG", "-all")
self.env.setdefault("DXVK_LOG_LEVEL", "none")
self.env.setdefault("VKD3D_DEBUG", "none")
self.env.setdefault("VKD3D_SHADER_DEBUG", "none")
# Enable wine-nvml by default on Nvidia (experimental)
if utilities.is_driver_loaded({'nvidia'}):
self.compat_config.add("nvml")
if "wined3d11" in self.compat_config:
self.compat_config.add("wined3d")
self.check_environment("PROTON_USE_WOW64", "wow64")
if not self.check_environment("PROTON_USE_WINED3D", "wined3d"):
self.check_environment("PROTON_USE_WINED3D11", "wined3d")
self.check_environment("PROTON_NO_WM_DECORATION", "nowmdecoration")
self.check_environment("PROTON_DXVK_D3D8", "dxvkd3d8")
self.check_environment("PROTON_NO_D3D11", "nod3d11")
self.check_environment("PROTON_NO_D3D10", "nod3d10")
self.check_environment("PROTON_NO_ESYNC", "noesync")
self.check_environment("PROTON_NO_FSYNC", "nofsync")
self.check_environment("PROTON_FORCE_LARGE_ADDRESS_AWARE", "forcelgadd")
self.check_environment("PROTON_OLD_GL_STRING", "oldglstr")
self.check_environment("PROTON_HIDE_NVIDIA_GPU", "hidenvgpu")
self.check_environment("PROTON_HIDE_VANGOGH_GPU", "hidevggpu")
self.check_environment("PROTON_HIDE_INTEL_GPU", "hideintelgpu")
self.check_environment("PROTON_SET_GAME_DRIVE", "gamedrive")
self.check_environment("PROTON_SET_STEAM_DRIVE", "steamdrive")
self.check_environment("PROTON_NO_XIM", "noxim")
self.check_environment("PROTON_HEAP_DELAY_FREE", "heapdelayfree")
self.check_environment("PROTON_HEAP_ZERO_MEMORY", "heapzeromemory")
self.check_environment("PROTON_DISABLE_NVAPI", "disablenvapi")
self.check_environment("PROTON_FORCE_NVAPI", "forcenvapi")
self.check_environment("PROTON_NVIDIA_LIBS", "nvidialibs")
self.check_environment("PROTON_NVIDIA_LIBS_NO_32BIT", "nvidialibsno32")
self.check_environment("PROTON_NVIDIA_NVCUDA", "nvcuda")
self.check_environment("PROTON_NVIDIA_NVENC", "nvenc")
self.check_environment("PROTON_NVIDIA_NVML", "nvml")
self.check_environment("PROTON_NVIDIA_NVOPTIX", "nvoptix")
self.check_environment("PROTON_HIDE_APU", "hideapu")
self.check_environment("PROTON_ENABLE_WAYLAND", "wayland")
self.check_environment("PROTON_USE_WAYLAND", "wayland")
self.check_environment("PROTON_PREFER_SDL", "sdlinput")
self.check_environment("PROTON_ENABLE_WINEBTH", "winebth")
self.check_environment("PROTON_USE_SDL", "sdlinput")
# Sony controllers need hidraw to feed the XInput-backed Steam Input
# fallback. Native Xbox controllers continue to use their XInput slot.
if (self.env.get("PROTON_STEAMINPUT_FALLBACK") == "1" or
self.env.get("PROTON_STEAMINPUT_XINPUT_FALLBACK") == "1"):
self.compat_config.discard("sdlinput")
self.env.pop("PROTON_DISABLE_HIDRAW", None)
self.check_environment("PROTON_NATIVE_AGS", "nativeags")
self.check_environment("PROTON_FSR3_UPGRADE", "fsr3")
self.check_environment("PROTON_FSR4_UPGRADE", "fsr4")
self.check_environment("PROTON_FSR4_INDICATOR", "fsr4hud")
self.check_environment("PROTON_DLSS_UPGRADE", "dlss")
self.check_environment("PROTON_DLSS_INDICATOR", "dlsshud")
self.check_environment("PROTON_XESS_UPGRADE", "xess")
self.check_environment("PROTON_STREAMLINE_UPGRADE", "streamline")
self.check_environment("PROTON_USE_OPTISCALER", "optiscaler")
self.check_environment("PROTON_NATIVE_AGS", "nativeags")
# Legacy overrides only; HDR detection no longer requires either flag.
# Preserve PROTON_USE_HDR precedence when both aliases are set.
self.check_environment("PROTON_ENABLE_HDR", "hdr")
self.check_environment("PROTON_USE_HDR", "hdr")
self.check_environment("PROTON_FORCE_HDR", "forcehdr")
self.check_environment("PROTON_NO_NTSYNC", "nontsync")
self.check_environment("PROTON_USE_WRITECOPY", "writecopy")
if "nontsync" in self.compat_config:
self.env.pop("WINENTSYNC", "")
else:
self.env["WINENTSYNC"] = "1"
if "noesync" in self.compat_config:
self.env.pop("WINEESYNC", "")
self.env["WINEESYNC"] = "0"
else:
self.env["WINEESYNC"] = "1"
if "nowmdecoration" in self.compat_config:
self.env["WAYLANDDRV_SSD"] = "0"
self.env["WINE_NO_WM_DECORATION"] = "1"
if "nvidialibs" in self.compat_config:
self.compat_config.add("nvcuda")
self.compat_config.add("nvenc")
self.compat_config.add("nvml")
self.compat_config.add("nvoptix")
self.check_environment("DXVK_NVAPI_VKREFLEX", "vkreflex")
if "vkreflex" in self.compat_config and "disablenvapi" not in self.compat_config and utilities.is_driver_loaded({'nvidia'}):
append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/dxvk-nvapi-vkreflex-layer/implicit_layer.d", ":")
self.check_environment("LOW_LATENCY_LAYER", "lowlatencylayer")
if "lowlatencylayer" in self.compat_config:
append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/low_latency_layer/implicit_layer.d", ":")
self.check_environment("ENABLE_VKBASALT", "vkbasalt")
if "vkbasalt" in self.compat_config:
append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/vkbasalt/implicit_layer.d", ":")
self.check_environment("PYROVEIL", "pyroveil")
if "pyroveil" in self.compat_config:
append_to_env_str(self.env, "VK_IMPLICIT_LAYER_PATH", g_proton.dist_dir + "share/pyroveil/implicit_layer.d", ":")
pyroveil_config = self.env.get("PYROVEIL_CONFIG", "")
if pyroveil_config:
self.env["PYROVEIL_CONFIG"] = pyroveil_config if os.path.isabs(pyroveil_config) else g_proton.dist_dir + "share/pyroveil/hacks/" + pyroveil_config
if "nofsync" in self.compat_config:
self.env.pop("WINEFSYNC", "")
self.env["WINEFSYNC"] = "0"
else:
self.env["WINEFSYNC"] = "1"
if "oldglstr" in self.compat_config:
#mesa override
self.env["MESA_EXTENSION_MAX_YEAR"] = "2003"
#nvidia override
self.env["__GL_ExtensionStringVersion"] = "17700"
if os.environ.get("SteamGameId", 0) in [
"661920", #Claybook
"558260", #Gravel
"386360", #SMITE
"487720", #Agony
"879420", #Agony UNRATED
"649950", #Ashen
"1150080", #Azur Lane: Crosswave
"463150", #BARRIER X
"420290", #Blackwake
"1092660", #Blair Witch
"1052070", #Burning Daylight
"399810", #Call of Cthulhu
"967390", #Chronos: Before the Ashes
"968870", #Close to the Sun
"544920", #Darwin Project
"1055610", #Deep Space Battle Simulator
"682990", #Drug Dealer Simulator
"551770", #ECHO
"392110", #ENDLESS Space 2
"447020", #Farming Simulator 17
"834280", #Fishing Sim World: Pro Tour
"890880", #FROSTBITE: Deadly Climate
"433530", #Heliborne - Enhanced Edition
"521890", #Hello Neighbor
"783170", #INSOMNIA: The Ark
"1195460", #Last Year
"1029890", #Layers of Fear 2 (2019)
"535850", #Micro Machines World Series
"711750", #Monster Energy Supercross - The Official Videogame
"882020", #Monster Energy Supercross - The Official Videogame 2
"824280", #Monster Jam Steel Titans
"775900", #MotoGP18
"415200", #Motorsport Manager
"748360", #MY HERO ONE'S JUSTICE
"1058450", #MY HERO ONE'S JUSTICE 2
"556180", #Mysterium: A Psychic Clue Game
"561600", #MXGP3 - The Official Motocross Videogame
"798290", #MXGP PRO
"366870", #Narcosis
"485030", #PLANET ALPHA
"469610", #Rick and Morty: Virtual Rick-ality
"759740", #RIDE 3
"1342260", #SAMURAI SHODOWN
"425670", #Seraph
"1096570", #SONG OF HORROR COMPLETE EDITION
"492230", #Space Hulk: Tactics
"996580", #Spyro Reignited Trilogy
"437630", #State of Mind
"442780", #STRAFE: Gold Edition
"433100", #The Town of Light
"406970", #The Uncertain: Last Quiet Day
"451520", #theBlu: Season 1
"1237970", #Titanfall® 2
"1051200", #Trover Saves the Universe
"285190", #Warhammer 40,000: Dawn of War III
"1133320", #Westworld Awakening
"522020", #Blue Effect VR
]:
self.env["OPENSSL_ia32cap"] = "~0x20000000"
if os.environ.get("SteamGameId", 0) in [
"500810", #Arcanum
"4249100", #Resident Evil (1996)
"4249110", #Resident Evil 2 (1998)
"4249130", #Dino Crisis
"4249150", #Breath of Fire IV
]:
self.dlloverrides["ddraw"] = "n,b"
# winebth.sys is disabled by default (see above); BLE apps such as Zwift opt in.
if "winebth" in self.compat_config:
self.dlloverrides.pop("winebth.sys", None)
# CW Bug 26050
if os.environ.get("SteamGameId", 0) == "3780660":
self.dlloverrides["dinput"] = "n,b"
# CW Bug 26133
if os.environ.get("SteamGameId", 0) == "2471120":
self.dlloverrides["winmm"] = "n,b"
# CW Bug 26376
if os.environ.get("SteamGameId", 0) == "1928420":
self.dlloverrides["gameinput"] = "d"
if "PROTON_LIMIT_RESOLUTIONS" not in self.env:
if os.environ.get("SteamGameId", 0) in [
"39540", #SpellForce: Platinum Edition
]:
self.env["PROTON_LIMIT_RESOLUTIONS"] = "16"
elif os.environ.get("SteamGameId", 0) in [
"524220", #NieR: Automata
"814380", #Sekiro: Shadows Die Twice
"374320", #DARK SOULS III
"357190", #Ultimate Marvel vs Capcom 3
"730830", #Escape from Monkey Island
"229480", #Dungeons & Dragons: Chronicles of Mystara
]:
self.env["PROTON_LIMIT_RESOLUTIONS"] = "32"
if "PROTON_SPOOF_STEAMINPUT_VIDPID" not in self.env:
if os.environ.get("SteamGameId", 0) in [
"939960", #Far Cry New Dawn
"250900", #The Binding of Isaac: Rebirth
"297860", #Split/Second
]:
self.env["PROTON_SPOOF_STEAMINPUT_VIDPID"] = "1"
if "forcelgadd" in self.compat_config:
self.env["WINE_LARGE_ADDRESS_AWARE"] = "1"
else:
if "noforcelgadd" in self.compat_config:
self.env["WINE_LARGE_ADDRESS_AWARE"] = "0"
if "heapdelayfree" in self.compat_config:
self.env["WINE_HEAP_DELAY_FREE"] = "1"
if "heapzeromemory" in self.compat_config:
self.env["WINE_HEAP_ZERO_MEMORY"] = "1"
if "heaptopdown" in self.compat_config:
self.env["WINE_HEAP_TOP_DOWN"] = "1"
if "vkd3dbindlesstb" in self.compat_config:
append_to_env_str(self.env, "VKD3D_CONFIG", "force_bindless_texel_buffer", ",")
if "northstar" in self.compat_config:
append_to_env_str(self.env, "WINEDLLOVERRIDES", "wsock32=n,b", ";")
if "vkd3dfl12" in self.compat_config:
if "VKD3D_FEATURE_LEVEL" not in self.env:
self.env["VKD3D_FEATURE_LEVEL"] = "12_0"
if "hidevggpu" in self.compat_config:
self.env["WINE_HIDE_VANGOGH_GPU"] = "1"
if "hidenvgpu" in self.compat_config and "forcenvapi" not in self.compat_config:
self.env["WINE_HIDE_NVIDIA_GPU"] = "1"
if "hideintelgpu" in self.compat_config:
self.env["WINE_HIDE_INTEL_GPU"] = "1"
if "hideapu" in self.compat_config:
self.env["WINE_HIDE_APU"] = "1"
if "usenativexinput13" in self.compat_config:
self.dlloverrides["xinput1_3"] = "n"
if "disablelibglesv2" in self.compat_config:
self.dlloverrides["libglesv2"] = "d"
if "nativeags" in self.compat_config:
self.dlloverrides["atiadlxx"] = "b"
self.dlloverrides["atidxx64"] = "b"
self.dlloverrides["amd_ags_x64"] = "n"
if "nomfdxgiman" in self.compat_config:
self.env["WINE_DO_NOT_CREATE_DXGI_DEVICE_MANAGER"] = "1"
if "noopwr" in self.compat_config:
self.env["WINE_DISABLE_VULKAN_OPWR"] = "1"
if "noopwrx11" in self.compat_config and "wayland" not in self.compat_config:
self.env["WINE_DISABLE_VULKAN_OPWR"] = "1"
if "PROTON_USE_XALIA" not in self.env:
if "noxalia" in self.compat_config:
self.env["PROTON_USE_XALIA"] = "0"
else:
self.env["PROTON_USE_XALIA"] = "1"
if "xalia" not in self.compat_config:
self.env["XALIA_SUPPORTED_ONLY"] = "1"
if "nohardwarescheduling" in self.compat_config and "WINE_DISABLE_HARDWARE_SCHEDULING" not in self.env:
self.env["WINE_DISABLE_HARDWARE_SCHEDULING"] = "1"
if "writecopy" in self.compat_config:
self.env["WINE_SIMULATE_WRITECOPY"] = "1"
if "PROTON_CRASH_REPORT_DIR" in self.env:
self.env["WINE_CRASH_REPORT_DIR"] = self.env["PROTON_CRASH_REPORT_DIR"]
if "sdlinput" in self.compat_config:
self.env["PROTON_DISABLE_HIDRAW"] = "1"
self.env["PROTON_NO_STEAMINPUT"] = "1"
if "SDL_GAMECONTROLLER_IGNORE_DEVICES" in self.env:
del self.env["SDL_GAMECONTROLLER_IGNORE_DEVICES"]
# Leave DXVK_HDR unset by default: shared DXGI detects HDR for DX11/DX12
# from Wine Wayland output state or Gamescope XWayland feedback.
# Retain legacy opt-ins/opt-outs without changing the display driver.
# forcehdr reports HDR on Wine Wayland outputs the compositor reports
# as SDR (KWin at SDR brightness == peak, Hyprland cm_auto_hdr).
if "forcehdr" in self.compat_config:
self.env["DXVK_HDR"] = "1"
self.env["WAYLANDDRV_FORCE_HDR"] = "1"
elif "hdr" in self.compat_config:
self.env["DXVK_HDR"] = "1"
elif "PROTON_ENABLE_HDR" in self.env or "PROTON_USE_HDR" in self.env:
self.env["DXVK_HDR"] = "0"
self.env["DXVK_NO_HDR"] = "1"
if "fnad3d11" in self.compat_config and "FNA3D_FORCE_DRIVER" not in self.env:
self.env["FNA3D_FORCE_DRIVER"] = "D3D11"
# igdext is loaded either using the LoadLibraryExW hack on 1.3 or lower
# or using the INTC_ALT_DRIVER_EXTENSIONS_PATH on 2.0 or newer
# TODO: Load it through driver store data like it is on windows
# TODO: D3D11 has a different loading mechanism because why not
self.env["INTC_ALT_DRIVER_EXTENSIONS_PATH"] = "C:\\Windows\\System32"
# PROTON_DISABLE_HIDRAW workarounds
if (os.environ.get("SteamGameId", 0) == "2322010" and # God of War: Ragnarok
os.environ.get("SteamDeck", 0) == "1"):
# Disable hidraw for Sony DualShock and DualSense controllers.
self.env["PROTON_DISABLE_HIDRAW"] = "0x054C/0x05C4,0x054C/0x09CC,0x054C/0x0BA0,0x054C/0x0CE6,0x054C/0x0DF2"
if "__GLVND_DISALLOW_PATCHING" not in self.env:
self.env["__GLVND_DISALLOW_PATCHING"] = "1"
# Keep native Sony HID available; XInput withdraws only its automatic
# fallback when this process consumes native input or Steam Input owns it.
self.env.setdefault("PROTON_SONY_AUTO_XINPUT", "1")
if (os.environ.get("SteamGameId", 0) == "1183470"): # Imperiums: Greek Wars
self.env.setdefault("WINE_MONO_HIDETYPES", "1")
self.env.setdefault("WINE_MONO_HIDETYPES", "0")
# Proton-EM custom options:
# Don't use wayland backend when WAYLAND_DISPLAY is not set
if "wayland" in self.compat_config and "WAYLAND_DISPLAY" in self.env:
# Make the temporary Steam overlay bridge discoverable only when
# Steam actually injected its overlay and the X11 path remains
# available. win32u bootstraps the selected layer for launchers.
overlay_preload = ":".join((
self.env.get("LD_PRELOAD", ""),
self.env.get("WINE_LD_PRELOAD", ""),
))
winex11_order = self.dlloverrides.get("winex11.drv")
winex11_available = (
winex11_order is None or
"b" in winex11_order.lower() or
"n" in winex11_order.lower()
)
if ("gameoverlayrenderer.so" in overlay_preload and
winex11_available and
not nonzero(self.env.get("DISABLE_WINE_WAYLAND_STEAM_OVERLAY_LAYER", "0"))):
self.env["WINE_WAYLAND_STEAM_OVERLAY_LAYER"] = "1"
append_to_env_str(
self.env,
"VK_IMPLICIT_LAYER_PATH",
g_proton.dist_dir + "share/steam-overlay-wayland/implicit_layer.d",
":")
# Set primary monitor if given
if "PROTON_WAYLAND_MONITOR" in self.env:
self.env["WAYLANDDRV_PRIMARY_MONITOR"] = self.env["PROTON_WAYLAND_MONITOR"]
# Try to determine the primary monitor if not given.
if "WAYLANDDRV_PRIMARY_MONITOR" not in self.env:
primary_monitor = self.get_primary_monitor()
if primary_monitor:
self.env["WAYLANDDRV_PRIMARY_MONITOR"] = primary_monitor
# Select Wayland explicitly while keeping X11 available for
# per-process compatibility fallbacks used by launcher UIs.
self.env["WINE_GRAPHICS_DRIVER"] = "wayland"
self.dlloverrides["winewayland.drv"] = "b"
# This is required for OpenGL to work on winewayland
self.env["WINE_USE_EGL"] = "1"
# FSHack is not supported on winewayland (yet)
self.env["WINE_DISABLE_FULLSCREEN_HACK"] = "1"
# Enable the move hack
self.env.setdefault("WINE_MOVE_HACK", "1")
# It is not possible for winewayland to support xalia since
# there is no way to position toplevel surfaces dynamically in wayland
self.env["PROTON_USE_XALIA"] = "0"
# Steam advertises its per-game virtual controller through these
# variables. Keep that device available when the game's Steam Input
# toggle is enabled: the overlay controller UI consumes its translated
# XInput stream. Otherwise suppress the desktop virtual controller and
# leave the game's native controller path active.
steam_input_enabled = (
("SteamVirtualGamepadInfo" in self.env and
nonzero(self.env["SteamVirtualGamepadInfo"])) or
("SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD" in self.env and
nonzero(self.env["SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD"]))
)
if "PROTON_NO_STEAMINPUT" not in self.env:
self.env["PROTON_NO_STEAMINPUT"] = "0" if steam_input_enabled else "1"
# Disable other steam input related stuff
if nonzero(self.env["PROTON_NO_STEAMINPUT"]):
# When steam input is disabled we need the ignored controllers to work again
if "SDL_GAMECONTROLLER_IGNORE_DEVICES" in self.env:
del self.env["SDL_GAMECONTROLLER_IGNORE_DEVICES"]
# Some other steam input related options we need to remove
if "SteamVirtualGamepadInfo" in self.env:
del self.env["SteamVirtualGamepadInfo"]
if "SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD" in self.env:
del self.env["SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD"]
# FIXME: temporary workaround for libxkbcommon's current integration
self.env.setdefault("XLOCALEDIR", g_proton.dist_dir + "share/X11/locale")
# Keep DISPLAY available for processes that explicitly fall back to
# XWayland. WINE_GRAPHICS_DRIVER still selects native Wayland for
# the desktop and every process without such a fallback.
# Use SDL backend for inputs instead of hidraw/Steam Input.
if "sdlinput" in self.compat_config:
self.env["PROTON_DISABLE_HIDRAW"] = "1"
self.env["PROTON_NO_STEAMINPUT"] = "1"
if "SDL_GAMECONTROLLER_IGNORE_DEVICES" in self.env:
del self.env["SDL_GAMECONTROLLER_IGNORE_DEVICES"]
if not ("PROTON_EMULATE_STEAMINPUT" in self.env and nonzero(self.env["PROTON_EMULATE_STEAMINPUT"])):
if "SteamVirtualGamepadInfo" in self.env:
del self.env["SteamVirtualGamepadInfo"]
if "SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD" in self.env:
del self.env["SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD"]
if (os.environ.get("SteamGameId", 0) == "1183470"): # Imperiums: Greek Wars
self.env.setdefault("WINE_MONO_HIDETYPES", "1")
# NVIDIA software may check for the "DriverStore" by querying the
# NGXCore\NGXPath registry key via `D3DDDI_QUERYREGISTRY_SERVICEKEY` for
# a given adapter. In the case where this path cannot be found, the
# `NVIDIA_WINE_DLL_DIR` environment variable is read as a fallback.
#
# TODO: Add support for populating NGXCore\NGXPath so we can remove the
# NGX copies done in setup_prefix(), and this environment variable.
self.nvidia_wine_dll_dir = find_nvidia_wine_dll_dir()
if self.nvidia_wine_dll_dir:
self.env["NVIDIA_WINE_DLL_DIR"] = self.nvidia_wine_dll_dir
if "PROTON_LOG" in self.env and nonzero(self.env["PROTON_LOG"]):
if self.setup_logging(append_forever=False):
self.log_file.write("======================\n")
with open(g_proton.version_file, "r") as f:
self.log_file.write("Proton: " + f.readline().strip() + "\n")
if "SteamGameId" in self.env:
self.log_file.write("SteamGameId: " + self.env["SteamGameId"] + "\n")
self.log_file.write("Command: " + str(sys.argv[2:] + self.cmdlineappend) + "\n")
self.log_file.write("Options: " + str(self.compat_config) + "\n")
self.try_log_slr_versions()
try:
uname = os.uname()
kernel_version = f"{uname.sysname} {uname.release} {uname.version} {uname.machine}"
except OSError:
kernel_version = "unknown"
self.log_file.write(f"Kernel: {kernel_version}\n")
self.log_file.write("Language: LC_ALL " + str(self.env.get("HOST_LC_ALL", None)) +
", LC_MESSAGES " + str(self.env.get("LC_MESSAGES", None)) +
", LC_CTYPE " + str(self.env.get("LC_CTYPE", None)) + "\n")
self.log_file.write("PATH: " + str(self.env.get("PATH", None)) + "\n")
#dump some important variables into the log header
for var in ["WINEDLLOVERRIDES", "WINEDEBUG"]:
if var in os.environ:
self.log_file.write("System " + var + ": " + os.environ[var] + "\n")
if var in used_user_settings:
self.log_file.write("User settings " + var + ": " + used_user_settings[var] + "\n")
if var in self.env:
self.log_file.write("Effective " + var + ": " + self.env[var] + "\n")
# check for low fd limit
_soft_limit, hard_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
if hard_limit < 524288:
self.log_file.write(f"WARNING: Low file descriptor limit: {hard_limit} (see https://github.com/ValveSoftware/Proton/wiki/File-Descriptors)\n")
if os.path.exists("/proc/sys/vm/max_map_count"):
with open("/proc/sys/vm/max_map_count", "r") as f:
max_map_count = int(f.read())
if max_map_count < 1048576:
self.log_file.write(f"WARNING: Low /proc/sys/vm/max_map_count: {max_map_count} will prevent some games from working\n")
utilities.log_environment(self.env, self.log_file)
self.log_file.write("======================\n")
self.log_file.flush()
else:
self.env["WINEDEBUG"] = "-all"
if "PROTON_REMOTE_DEBUG_CMD" in self.env:
self.remote_debug_cmd = shlex.split(self.env["PROTON_REMOTE_DEBUG_CMD"])
else:
self.remote_debug_cmd = None
if ("disablenvapi" not in self.compat_config) and ("forcenvapi" not in self.compat_config):
try:
with open('/proc/modules') as f:
drivers = set([line.partition(' ')[0] for line in f.read().splitlines()])
if not drivers.intersection({'nvidia', 'nouveau', 'nova'}):
if self.log_file is not None:
self.log_file.write("No NV driver found, disabling nvapi\n")
self.compat_config.add("disablenvapi")
except:
if self.log_file is not None:
self.log_file.write("Error: unable to check /proc/modules for NV driver presence\n")
if self.log_file is not None:
self.log_file.flush()
if update_prefix_files:
g_compatdata.setup_prefix()
g_compatdata.sync_appcache()
if "fsr4hud" in self.compat_config:
self.env["FSR_WATERMARK"] = "1"
self.env["FSR_FG_WATERMARK"] = "1"
if "dlsshud" in self.compat_config:
self.env["DXVK_NVAPI_SET_NGX_DEBUG_OPTIONS"] = "DLSSIndicator=1024,DLSSGIndicator=2,"
else:
self.env.setdefault("DXVK_NVAPI_SET_NGX_DEBUG_OPTIONS", "DLSSIndicator=0,DLSSGIndicator=0,")
if "nod3d11" in self.compat_config:
self.dlloverrides["d3d11"] = ""
if "dxgi" in self.dlloverrides:
del self.dlloverrides["dxgi"]
if "nod3d10" in self.compat_config:
self.dlloverrides["d3d10_1"] = ""
self.dlloverrides["d3d10"] = ""
self.dlloverrides["dxgi"] = ""
if "nativevulkanloader" in self.compat_config:
self.dlloverrides["vulkan-1"] = "n"
if "disablenvapi" not in self.compat_config or "forcenvapi" in self.compat_config:
self.env["DXVK_ENABLE_NVAPI"] = "1"
if "forcenvapi" in self.compat_config:
self.env["DXVK_NVAPI_ALLOW_OTHER_DRIVERS"] = "1"
self.env["DXVK_NVAPI_DRIVER_VERSION"] = "99999"
self.env["WINE_HIDE_AMD_GPU"] = "1"
if not ("WINEDLLOVERRIDES" in g_session.env and "atiadlxx" in g_session.env["WINEDLLOVERRIDES"]) and "SteamAppId" in os.environ:
if os.environ["SteamAppId"] in [
"2767030",
]:
g_session.dlloverrides["atiadlxx"] = "b"
if not ("PROTON_LIMIT_ADDRESS_SPACE" in g_session.env) and "SteamAppId" in os.environ:
if os.environ["SteamAppId"] in [
"1282270",
"2963870",
"4212480",
]:
self.env["PROTON_LIMIT_ADDRESS_SPACE"] = "1"
s = ""
for dll in self.dlloverrides:
setting = self.dlloverrides[dll]
if len(s) > 0:
s = s + ";" + dll + "=" + setting
else:
s = dll + "=" + setting
append_to_env_str(self.env, "WINEDLLOVERRIDES", s, ";")
if platform.machine() == 'aarch64':
self.env.setdefault("FEX_DISKCACHE", "1")
if os.environ.get("SteamGameId", 0) == "1167630": # Teardown
# https://github.com/microsoft/mimalloc/issues/958
self.env["MIMALLOC_DISABLE_REDIRECT"] = "1"
if not 'FEX_APP_CONFIG' in self.env:
# custom per-app config driven by Steam env vars
app_config = self.generate_fex_app_config()
with open(g_compatdata.fex_config_file, 'w') as f:
f.write(json.dumps(app_config, indent=2))
self.env['FEX_APP_CONFIG'] = g_compatdata.fex_config_file
self.env["FEX_APP_CONFIG_LOCATION"] = os.path.join(g_proton.dist_dir, "share/fex-emu/")
def run_proc(self, args, local_env=None):
if local_env is None:
local_env = self.env
return subprocess.call(args, env=local_env, stderr=self.log_file, stdout=self.log_file)
def run(self):
umu_without_steam = "UMU_ID" in os.environ and os.environ.get("UMU_USE_STEAM") != "1"
if umu_without_steam:
append_to_env_str(self.env, "WINEDLLOVERRIDES", "lsteamclient=d", ";")
if shutil.which('steam-runtime-launcher-interface-0') is not None:
adverb = ['steam-runtime-launcher-interface-0', 'proton']
else:
adverb = []
if self.remote_debug_cmd:
remote_debug_cmd = self.remote_debug_cmd
if not os.path.isabs(remote_debug_cmd[0]):
remote_debug_cmd[0] = g_proton.path(remote_debug_cmd[0])
remote_debug_proc = subprocess.Popen([g_proton.wine_bin] + self.remote_debug_cmd,
env=self.env, stderr=self.log_file, stdout=self.log_file)
else:
remote_debug_proc = None
# CoD: Black Ops 3 workaround
if os.environ.get("SteamGameId", 0) in [
"311210", # CoD: Black Ops 3
"1985810", # CoD: Black Ops Cold War
"1549250", # Undecember
]:
argv = [g_proton.wine_bin, "c:\\Program Files (x86)\\Steam\\steam.exe"]
# Don't use steam if it's not a steam game
# Prevent this warning for non-steam games:
# [S_API FAIL] SteamAPI_Init() failed; no appID found.
# Either launch the game from Steam, or put the file steam_appid.txt containing the correct appID in your game folder.
elif umu_without_steam:
if len(sys.argv) >= 3 and sys.argv[2].startswith('/'):
log(sys.argv[2])
log("Executable is a unix path, launching with 'umu.exe'.")
if g_proton.host_pe_arch == "x86_64-windows":
#run with winepreloader directly to avoid restart through start.exe
self.env["WINELOADERNOEXEC"] = "1"
argv = [g_proton.lib_dir + "/wine/x86_64-unix/wine-preloader", g_proton.lib_dir + "/wine/x86_64-unix/wine", "c:\\windows\\system32\\umu.exe"]
else:
argv = [g_proton.wine_bin, "c:\\windows\\system32\\umu.exe"]
else:
log("Executable is inside wine prefix, launching normally.")
argv = [g_proton.wine_bin]
else:
if g_proton.host_pe_arch == "x86_64-windows":
#run with winepreloader directly to avoid restart through start.exe
self.env["WINELOADERNOEXEC"] = "1"
argv = [g_proton.lib_dir + "/wine/x86_64-unix/wine-preloader", g_proton.lib_dir + "/wine/x86_64-unix/wine", "c:\\windows\\system32\\steam.exe"]
else:
argv = [g_proton.wine_bin, "c:\\windows\\system32\\steam.exe"]
rc = self.run_proc(adverb + argv + sys.argv[2:] + self.cmdlineappend)
if remote_debug_proc:
remote_debug_proc.kill()
try:
remote_debug_proc.communicate(timeout=2)
except subprocess.TimeoutExpired:
log("terminate remote debugger")
remote_debug_proc.terminate()
remote_debug_proc.communicate()
return rc
if __name__ == "__main__":
if "STEAM_COMPAT_DATA_PATH" not in os.environ:
log("No compat data path?")
sys.exit(1)
g_proton = Proton(os.path.dirname(sys.argv[0]))
g_proton.cleanup_legacy_dist()
g_proton.do_steampipe_fixups()
g_compatdata = CompatData(os.environ["STEAM_COMPAT_DATA_PATH"])
g_session = Session()
g_session.init_wine()
if g_proton.missing_default_prefix():
g_proton.make_default_prefix()
g_session.init_session(sys.argv[1] != "runinprefix")
# protonfixes execution
protonfixes.setup(g_session.env, "PATH", ld_path_var, prepend_to_env_str)
protonfixes.setup_mount_drives(setup_dir_drive)
protonfixes.winetricks(g_session.env, g_proton.wine_bin, g_proton.wineserver_bin)
# HACK: handle DXVK_FRAME_RATE and VKD3D_FRAME_RATE through DXVK_CONFIG
protonfixes.setup_frame_rate(g_session.env, append_to_env_str)
g_session.check_environment("PROTON_LOCAL_SHADER_CACHE", "localshadercache")
protonfixes.setup_local_shader_cache(g_session.compat_config, g_session.env)
g_session.check_environment("PROTON_FSR3_UPGRADE", "fsr3")
g_session.check_environment("PROTON_FSR4_UPGRADE", "fsr4")
g_session.compat_config.add("mlfg")
g_session.check_environment("PROTON_MLFG_UPGRADE", "mlfg")
g_session.check_environment("PROTON_DLSS_UPGRADE", "dlss")
g_session.check_environment("PROTON_XESS_UPGRADE", "xess")
g_session.check_environment("PROTON_STREAMLINE_UPGRADE", "streamline")
g_session.check_environment("PROTON_USE_OPTISCALER", "optiscaler")
protonfixes.setup_upscalers(g_session.compat_config, g_session.env, g_compatdata.base_dir, g_compatdata.prefix_dir)
protonfixes.execute()
g_compatdata.setup_ddraw("PROTON_USE_D7VK" in g_session.env
and nonzero(g_session.env["PROTON_USE_D7VK"]))
#determine mode
rc = 0
if sys.argv[1] == "run":
#start target app
setup_game_dir_drive()
setup_steam_dir_drive()
rc = g_session.run()
elif sys.argv[1] == "waitforexitandrun":
#wait for wineserver to shut down
g_session.run_proc([g_proton.wineserver_bin, "-w"])
#then run
rc = g_session.run()
elif sys.argv[1] == "runinprefix":
rc = g_session.run_proc([g_proton.wine_bin] + sys.argv[2:])
elif sys.argv[1] == "destroyprefix":
g_compatdata.remove_tracked_files()
elif sys.argv[1] == "getcompatpath":
#linux -> windows path
path = subprocess.check_output([g_proton.wine_bin, "winepath", "-w", sys.argv[2]], env=g_session.env, stderr=g_session.log_file)
sys.stdout.buffer.write(path)
elif sys.argv[1] == "getnativepath":
#windows -> linux path
path = subprocess.check_output([g_proton.wine_bin, "winepath", sys.argv[2]], env=g_session.env, stderr=g_session.log_file)
sys.stdout.buffer.write(path)
else:
log("Need a verb.")
sys.exit(1)
sys.exit(rc)
#pylint --disable=C0301,C0326,C0330,C0111,C0103,R0902,C1801,R0914,R0912,R0915
# vim: set syntax=python: