Compare commits

..
1 Commits
Author SHA1 Message Date
Boria138 2b60a79ae3 feat: add ReShade integration
Code check / Check code (pull_request) Canceled after 7m9s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-10-15 20:29:13 +05:00
17 changed files with 929 additions and 247 deletions

No files matched your search

-5
View File
@@ -9,14 +9,9 @@
### Changed
- Загрузка игр GOG переведена на форк `linux-gaming-ru/gogdl` с выбором CDN по скорости, исправленной статистикой загрузки и ограничением повторных запросов; ранее установленный gogdl автоматически заменяется форком
### Fixed
- Отключена проверка обновлений AppImage при запуске с `--silent`, чтобы не показывать диалог обновления
- Исправлена рекурсивная ошибка фильтра событий клавиатуры при закрытии приложения
- Исправлена высокая нагрузка на CPU из-за непрерывной перерисовки таблиц в настройках exe
- В меню системного трея при запуске игры с `--silent` добавлены отсутствовавшие действия приостановки и возобновления игры
- Исправлена гонка при тихом запуске игр
### Contributors
+2 -11
View File
@@ -16,15 +16,6 @@ export DEPLOY_PYTHON=1
export MAIN_BIN=portprotonqt
export OPTIMIZE_LAUNCH=1
# sharun 2.2.7 needs the matching deployment and preload format from 1.4.1.
ANYLINUX_TOOLS="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/bf5e3c44dc2daf8b5de51cdf6b2d11589d047104/useful-tools"
export SHARUN_LINK="https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.7/sharun-$ARCH"
export ANYLINUX_LIB_SOURCE="$ANYLINUX_TOOLS/lib/anylinux.c"
export HOOKSRC="$ANYLINUX_TOOLS/hooks"
wget --retry-connrefused --tries=30 "$ANYLINUX_TOOLS/quick-sharun.sh" -O ./quick-sharun
echo '6cd9e45a099d7173aa906aaca51e48e51db78069ead14ef8e6683db4cbb4be44 ./quick-sharun' | sha256sum -c -
chmod +x ./quick-sharun
# Adjust comp settings to bypass oom-killer
export DWARFS_COMP="zstd:level=15 -S22 -B5"
@@ -47,7 +38,7 @@ GAMEPAD_LIBRARY=$(find /usr/lib -type f \
-path '*/site-packages/portprotonqt/libportprotonqt_gamepad.so' \
-print -quit)
test -n "$GAMEPAD_LIBRARY"
./quick-sharun \
quick-sharun \
/usr/bin/portprotonqt* \
/usr/bin/bash \
/usr/bin/update-desktop-database \
@@ -107,4 +98,4 @@ done
rm -rf "$PYSIDE_DIR"/QtAsyncio
# Turn AppDir into AppImage
./quick-sharun --make-appimage
quick-sharun --make-appimage
@@ -2428,8 +2428,9 @@ pw_specialk_set_ini_value () {
[[ -f "$ini_file" ]] && sed -i '/./,$!d' "$ini_file"
if grep -q "^\[$section\]" "$ini_file" ; then
if sed -n "/^\[$section\]/,/^\[/{p}" "$ini_file" | grep -q "^$key=" ; then
sed -i "/^\[$section\]/,/^\[/{s/^$key=.*/$key=$value/}" "$ini_file"
if sed -n "/^\[$section\]/,/^\[/{p}" "$ini_file" \
| grep -q "^[[:space:]]*$key[[:space:]]*=" ; then
sed -i "/^\[$section\]/,/^\[/{s/^[[:space:]]*$key[[:space:]]*=.*/$key=$value/}" "$ini_file"
else
sed -i "/^\[$section\]/a $key=$value" "$ini_file"
fi
@@ -2522,6 +2523,171 @@ pw_specialk_sync_files () {
}
pw_reshade_sync_files () {
local root="${PW_PLUGINS_PATH}/reshade_windows" runtime detect arch dll highest ogl vulkan source
local manifest_reg package installed installed_arch installed_version extract_dir reshade_exe reshade_font font_win screenshot_path backup
local PATH_TO_GAME="${PATH_TO_GAME:-}"
runtime="$root/reshade/${PW_RESHADE_VER}_Addon"
reshade_exe="${PW_RESHADE_EXE:-$PW_EXE_FILE}"
if [[ ! -f "$reshade_exe" ]] ; then
print_warning "ReShade executable not found: $reshade_exe. Using $PW_EXE_FILE."
reshade_exe="$PW_EXE_FILE"
fi
if [[ "$PW_USE_RESHADE" == 1 ]] ; then
detect=$(python_module graphics_detector "$reshade_exe")
file "$reshade_exe" | grep -qE 'x86-64|PE32\+' && arch=64 || arch=32
highest=$(echo "$detect" | grep '^HIGHEST_DX=' | cut -d= -f2)
ogl=$(echo "$detect" | grep '^USES_OGL=' | cut -d= -f2)
vulkan=$(echo "$detect" | grep '^USES_VULKAN=' | cut -d= -f2)
source=$(echo "$detect" | grep '^SOURCE=' | cut -d= -f2-)
[[ "${source##*/}" == UnityPlayer.dll && "$highest" != None ]] && vulkan=false
fi
if [[ -n "$PW_RESHADE_EXE" ]] ; then
PATH_TO_GAME="$(dirname "$(realpath "$reshade_exe")")"
fi
if [[ "$PW_USE_RESHADE" == 1 ]] \
&& [[ ! -f "$runtime/ReShade32.dll" || ! -f "$runtime/ReShade32.json" \
|| ! -f "$runtime/ReShade64.dll" || ! -f "$runtime/ReShade64.json" ]] ; then
try_download "https://reshade.me/downloads/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe" \
"${PORT_DATA_PATH}/data/tmp/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe"
create_new_dir "$runtime"
unpack "${PORT_DATA_PATH}/data/tmp/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe" \
"$runtime" silent || fatal "Failed to extract ReShade."
try_remove_file "${PORT_DATA_PATH}/data/tmp/ReShade_Setup_${PW_RESHADE_VER}_Addon.exe"
fi
if [[ "$PW_USE_RESHADE" == 1 && ! -f "$root/d3dcompiler_47.dll" ]] ; then
try_download "https://github.com/itsOwen/LetMeReShade/releases/download/LetMeReShade_Binaries/d3dcompiler_47.dll" "$root/d3dcompiler_47.dll"
fi
if [[ "$PW_USE_RESHADE" == 1 && ! -f "$root/d3d8.dll" ]] ; then
try_download "https://github.com/crosire/d3d8to9/releases/download/v${PW_D3D8TO9_VER}/d3d8.dll" "$root/d3d8.dll"
fi
if [[ "$PW_USE_RESHADE" == 1 && ! -d "$root/shaders/Shaders" ]] ; then
try_remove_dir "$root/shaders"
create_new_dir "$root/shaders/Shaders"
create_new_dir "$root/shaders/Textures"
for package in reshade_shaders sweetfx_shaders immerse_shaders astrayfx_shaders prod80_shaders retroarch_shaders ; do
extract_dir="${PORT_DATA_PATH}/data/tmp/$package"
if [[ "$package" == immerse_shaders ]] ; then
try_download "https://github.com/martymcmodding/iMMERSE/archive/refs/heads/main.tar.gz" \
"${PORT_DATA_PATH}/data/tmp/${package}.tar.gz"
else
try_download "https://github.com/itsOwen/LetMeReShade/releases/download/LetMeReShade_Binaries/${package}.tar.gz" \
"${PORT_DATA_PATH}/data/tmp/${package}.tar.gz"
fi
try_remove_dir "$extract_dir"
create_new_dir "$extract_dir"
unpack "${PORT_DATA_PATH}/data/tmp/${package}.tar.gz" "$extract_dir" \
silent || fatal "Failed to extract $package."
find "$extract_dir" -type d -iname Shaders -exec cp -rf '{}/.' "$root/shaders/Shaders/" \;
find "$extract_dir" -type d -iname Textures -exec cp -rf '{}/.' "$root/shaders/Textures/" \;
try_remove_dir "$extract_dir"
try_remove_file "${PORT_DATA_PATH}/data/tmp/${package}.tar.gz"
done
fi
if [[ "$PW_USE_RESHADE" == 1 && "$vulkan" == true ]] ; then
dll="Vulkan${arch}"
elif [[ "$PW_USE_RESHADE" == 1 ]] ; then
case "$highest" in
"DirectX 10"|"DirectX 11"|"DirectX 12") dll=dxgi.dll ;;
"DirectX 8"|"DirectX 9") dll=d3d9.dll ;;
"DirectDraw") return 0 ;;
*) [[ "$ogl" == true ]] && dll=opengl32.dll || dll=dxgi.dll ;;
esac
fi
case "$PW_RESHADE_API" in
vulkan) vulkan=true; dll="Vulkan${arch}" ;;
dxgi.dll|d3d9.dll|opengl32.dll) vulkan=false; dll="$PW_RESHADE_API" ;;
esac
if [[ -f "$PATH_TO_GAME/reshade_version" ]] \
&& { [[ "$PW_USE_RESHADE" != 1 ]] \
|| [[ "$(<"$PATH_TO_GAME/reshade_version")" != "${PW_RESHADE_VER}_Addon"$'\n'"$dll" ]]; } ; then
installed_version=$(sed -n '1p' "$PATH_TO_GAME/reshade_version")
installed=$(sed -n '2p' "$PATH_TO_GAME/reshade_version")
if [[ "$installed" == Vulkan32 || "$installed" == Vulkan64 ]] ; then
installed_arch=${installed#Vulkan}
manifest_reg="Z:${root//\//\\}\\reshade\\${installed_version}\\ReShade${installed_arch}.json"
manifest_reg=${manifest_reg//\\/\\\\\\\\}
get_and_set_reg_file --delete 'Software\Khronos\Vulkan\ImplicitLayers' \
"$manifest_reg" "" "" user
if [[ "$installed_arch" == 32 ]] ; then
get_and_set_reg_file --delete 'Software\Wow6432Node\Khronos\Vulkan\ImplicitLayers' \
"$manifest_reg" "" "" system
else
get_and_set_reg_file --delete 'Software\Khronos\Vulkan\ImplicitLayers' \
"$manifest_reg" "" "" system
fi
fi
try_remove_file "$PATH_TO_GAME/$installed"
if [[ -L "$PATH_TO_GAME/${installed}.b" \
&& $(readlink -f "$PATH_TO_GAME/${installed}.b") == "$root/reshade/"* ]] ; then
unlink "$PATH_TO_GAME/${installed}.b"
fi
[[ -e "$PATH_TO_GAME/${installed}.b" ]] \
&& mv -f "$PATH_TO_GAME/${installed}.b" "$PATH_TO_GAME/$installed"
[[ -L "$PATH_TO_GAME/d3dcompiler_47.dll" ]] && unlink "$PATH_TO_GAME/d3dcompiler_47.dll"
[[ -L "$PATH_TO_GAME/d3d8.dll" ]] && unlink "$PATH_TO_GAME/d3d8.dll"
[[ -e "$PATH_TO_GAME/d3d8.dll.b" ]] \
&& mv -f "$PATH_TO_GAME/d3d8.dll.b" "$PATH_TO_GAME/d3d8.dll"
[[ -L "$PATH_TO_GAME/ReShade_shaders" ]] && unlink "$PATH_TO_GAME/ReShade_shaders"
try_remove_file "$PATH_TO_GAME/ReShade.ini"
try_remove_file "$PATH_TO_GAME/ReShadePreset.ini"
try_remove_file "$PATH_TO_GAME/ReShade.log"
try_remove_file "$PATH_TO_GAME/reshade_version"
fi
[[ "$PW_USE_RESHADE" != 1 ]] && return 0
[[ -L "$PATH_TO_GAME/ReShade_shaders" ]] && unlink "$PATH_TO_GAME/ReShade_shaders"
try_force_link_dir "$root/shaders" "$PATH_TO_GAME/ReShade_shaders"
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "GENERAL" "EffectSearchPaths" '.\\ReShade_shaders\\Shaders'
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "GENERAL" "TextureSearchPaths" '.\\ReShade_shaders\\Textures'
if [[ -n "$PW_RESHADE_FONT" ]] ; then
reshade_font=$(fc-match --format='%{file}' "$PW_RESHADE_FONT")
else
reshade_font=/usr/share/fonts/ttf/dejavu/DejaVuSans.ttf
fi
if [[ -f "$reshade_font" ]] ; then
font_win="Z:${reshade_font//\//\\\\}"
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "STYLE" "Font" "$font_win"
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "STYLE" "EditorFont" "$font_win"
elif [[ -n "$PW_RESHADE_FONT" ]] ; then
print_warning "ReShade font not found: $PW_RESHADE_FONT"
fi
screenshot_path='.\\'
if [[ -n "$PW_RESHADE_SCREENSHOT_PATH" ]] ; then
create_new_dir "$PW_RESHADE_SCREENSHOT_PATH"
screenshot_path="Z:${PW_RESHADE_SCREENSHOT_PATH//\//\\\\}"
fi
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "INPUT" "KeyScreenshot" "$PW_RESHADE_SCREENSHOT_KEY"
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "INPUT" "KeyOverlay" "$PW_RESHADE_OVERLAY_KEY"
pw_specialk_set_ini_value "$PATH_TO_GAME/ReShade.ini" "SCREENSHOT" "SavePath" "$screenshot_path"
if [[ "$vulkan" == true ]] ; then
manifest_reg="Z:${runtime//\//\\}\\ReShade${arch}.json"
manifest_reg=${manifest_reg//\\/\\\\\\\\}
get_and_set_reg_file --add 'Software\Khronos\Vulkan\ImplicitLayers' "$manifest_reg" REG_DWORD 0 user
if [[ "$arch" == 32 ]] ; then
get_and_set_reg_file --add 'Software\Wow6432Node\Khronos\Vulkan\ImplicitLayers' "$manifest_reg" REG_DWORD 0 system
else
get_and_set_reg_file --add 'Software\Khronos\Vulkan\ImplicitLayers' "$manifest_reg" REG_DWORD 0 system
fi
var_winedlloverride_update "vulkan-1=n,b"
printf '%s\n%s\n' "${PW_RESHADE_VER}_Addon" "$dll" > "$PATH_TO_GAME/reshade_version"
return 0
fi
[[ ! -f "$PATH_TO_GAME/reshade_version" \
&& -e "$PATH_TO_GAME/$dll" && ! -e "$PATH_TO_GAME/${dll}.b" \
&& ( ! -L "$PATH_TO_GAME/$dll" \
|| $(readlink -f "$PATH_TO_GAME/$dll") != "$root/reshade/"* ) ]] \
&& mv -f "$PATH_TO_GAME/$dll" "$PATH_TO_GAME/${dll}.b"
try_force_link_file "$runtime/ReShade${arch}.dll" "$PATH_TO_GAME/$dll"
try_force_link_file "$root/d3dcompiler_47.dll" "$PATH_TO_GAME/d3dcompiler_47.dll"
var_winedlloverride_update "d3dcompiler_47=n;${dll%.*}=n,b"
if [[ "$highest" == "DirectX 8" && "$dll" == d3d9.dll ]] ; then
[[ -e "$PATH_TO_GAME/d3d8.dll" && ! -e "$PATH_TO_GAME/d3d8.dll.b" ]] && mv -f "$PATH_TO_GAME/d3d8.dll" "$PATH_TO_GAME/d3d8.dll.b"
try_force_link_file "$root/d3d8.dll" "$PATH_TO_GAME/d3d8.dll"
var_winedlloverride_update "d3d8=n,b"
fi
printf '%s\n%s\n' "${PW_RESHADE_VER}_Addon" "$dll" > "$PATH_TO_GAME/reshade_version"
}
pw_prepare_gamescope_launch () {
[[ "$PW_USE_NATIVE_WAYLAND" == "1" ]] && export PW_GAMESCOPE="0"
@@ -2957,6 +3123,7 @@ start_portproton () {
pw_optiscaler_sync_files
pw_specialk_sync_files
pw_reshade_sync_files
if [[ "${PW_USE_RAY_TRACING}" == "1" ]] ; then
var_vkd3d_config_update dxr
@@ -4052,7 +4219,6 @@ steamplay_launch () {
pw_optiscaler_sync_files
pw_specialk_sync_files
PORT_WINE_PREFIX="$STEAM_COMPAT_DATA_PATH/pfx"
pw_prepare_gamescope_launch
@@ -4118,6 +4284,8 @@ steamplay_launch () {
fi
fi
pw_reshade_sync_files
# Winetricks
if [[ -n "${PW_DLL_INSTALL:-}" ]] ; then
update_winetricks
@@ -4331,7 +4499,9 @@ gui_edit_db () {
KEY_EDIT_DB_GUI=$RANDOM
DISABLE_EDIT_DB_LIST=""
PW_EDIT_DB_LIST=(PW_MANGOHUD PW_MANGOHUD_USER_CONF PW_VKBASALT PW_VKBASALT_USER_CONF
PW_VKBASALT_EFFECTS PW_VKBASALT_FFX_CAS PW_DGVOODOO2 PW_USE_SPECIALK
PW_VKBASALT_EFFECTS PW_VKBASALT_FFX_CAS PW_DGVOODOO2 PW_USE_SPECIALK PW_USE_RESHADE
PW_RESHADE_EXE PW_RESHADE_API PW_RESHADE_FONT PW_RESHADE_SCREENSHOT_PATH
PW_RESHADE_SCREENSHOT_KEY PW_RESHADE_OVERLAY_KEY
PW_USE_ESYNC PW_USE_FSYNC PW_USE_NTSYNC PW_USE_RAY_TRACING PW_USE_NVAPI_AND_DLSS PW_USE_OPTISCALER
PW_USE_LS_FRAME_GEN PW_WINE_FULLSCREEN_FSR PW_HIDE_NVIDIA_GPU PW_VIRTUAL_DESKTOP PW_USE_TERMINAL
PW_USE_GAMEMODE PW_USE_INHIBIT_SLEEP PW_USE_D3D_EXTRAS PW_FIX_VIDEO_IN_GAME PW_REDUCE_PULSE_LATENCY
@@ -4365,6 +4535,10 @@ gui_edit_db () {
DISABLE_EDIT_DB_LIST+=" PW_VKBASALT PW_VKBASALT_USER_CONF PW_USE_RAY_TRACING PW_USE_OBS_VKCAPTURE PW_USE_GALLIUM_ZINK PW_USE_WINED3D_VULKAN"
fi
[[ "${PW_USE_SPECIALK}" == "1" ]] && DISABLE_EDIT_DB_LIST+=" PW_DGVOODOO2"
[[ "${PW_USE_RESHADE}" == "1" ]] && DISABLE_EDIT_DB_LIST+=" PW_USE_OPTISCALER PW_USE_SPECIALK"
if [[ "${PW_USE_OPTISCALER}" == "1" || "${PW_USE_SPECIALK}" == "1" ]] ; then
DISABLE_EDIT_DB_LIST+=" PW_USE_RESHADE"
fi
[[ ! -e "/dev/ntsync" ]] && DISABLE_EDIT_DB_LIST+=" PW_USE_NTSYNC"
+9
View File
@@ -16,6 +16,8 @@ export VKD3D_SAREK_VER="sarek-2.6.0"
export PW_LSFG_VK_VER="2.0.0-dev25-p11"
export PW_OPTISCALER_VER="v03-0.9.0-pre8"
export PW_SPECIALK_VER="SK_26_5_15"
export PW_RESHADE_VER="6.8.0"
export PW_D3D8TO9_VER="1.15.1"
export PW_GALLIUM_NINE_VER="0.10"
export DOTPFX_VER="48v9"
export DEFPFX_VER="v1"
@@ -33,6 +35,13 @@ check_variables PROTON_USE_XALIA "0"
check_variables PW_USE_RUNTIME "1"
check_variables PW_USE_D7VK "0"
check_variables PW_USE_SPECIALK "0"
check_variables PW_USE_RESHADE "0"
check_variables PW_RESHADE_API "auto"
check_variables PW_RESHADE_EXE ""
check_variables PW_RESHADE_FONT ""
check_variables PW_RESHADE_SCREENSHOT_PATH ""
check_variables PW_RESHADE_SCREENSHOT_KEY "44,0,0,0"
check_variables PW_RESHADE_OVERLAY_KEY "36,0,0,0"
check_variables PW_USE_ESYNC "0"
check_variables PW_USE_FSYNC "0"
check_variables PW_USE_NTSYNC "0"
+9 -11
View File
@@ -807,14 +807,13 @@ def main():
return
QMessageBox.warning(window, _("Error"), _("Failed to update AppImage."))
if not is_silent_launch:
window.appimageUpdateWorker = AppImageUpdateWorker("check")
window.appimageUpdateWorker.update_available.connect(show_appimage_update_prompt)
window.appimageUpdateWorker.finished.connect(
lambda: setattr(window, "appimageUpdateWorker", None)
)
window.appimageUpdateWorker.finished.connect(window.appimageUpdateWorker.deleteLater)
QTimer.singleShot(APPIMAGE_UPDATE_START_DELAY_MS, window.appimageUpdateWorker.start)
window.appimageUpdateWorker = AppImageUpdateWorker("check")
window.appimageUpdateWorker.update_available.connect(show_appimage_update_prompt)
window.appimageUpdateWorker.finished.connect(
lambda: setattr(window, "appimageUpdateWorker", None)
)
window.appimageUpdateWorker.finished.connect(window.appimageUpdateWorker.deleteLater)
QTimer.singleShot(APPIMAGE_UPDATE_START_DELAY_MS, window.appimageUpdateWorker.start)
class GOGDLUpdateWorker(QThread):
"""Update gogdl without blocking the UI thread."""
@@ -901,15 +900,14 @@ def main():
finally:
self.finished.emit()
# Initial setup can stop Wine and must not race a silent game launch.
if start_sh and not silent_game_request:
if start_sh:
worker = InitialCommandWorker(start_sh)
window.initialCommandWorker = worker
worker.finished.connect(
lambda: setattr(window, "initialCommandWorker", None)
)
worker.start()
elif not start_sh:
else:
logger.warning("PortProton start command not available, skipping initial command")
# --- Cleanup ---
+27 -8
View File
@@ -55,6 +55,7 @@ from portprotonqt.dialogs.dialog_utils import create_dialog_hints_widget, update
from portprotonqt.dialogs.proton_manager import show_proton_manager
from portprotonqt.dialogs.settings_mangohud import MANGOHUD_ENV_KEYS, MangoHudSettingsMixin
from portprotonqt.dialogs.settings_gamescope import GAMESCOPE_ENV_KEYS, GamescopeSettingsMixin
from portprotonqt.dialogs.settings_reshade import RESHADE_ENV_KEYS, ReShadeSettingsMixin
from portprotonqt.dialogs.settings_vkbasalt import VKBASALT_ENV_KEYS, VkBasaltSettingsMixin
from portprotonqt.localization import _, format_setting_name_for_display
from portprotonqt.logger import get_logger
@@ -85,6 +86,8 @@ DEFAULT_WINE_SETTING_KEYS = {
'PW_PREFIX_NAME': 'PW_DEFAULT_PREFIX_NAME',
'PW_VULKAN_USE': 'PW_DEFAULT_VULKAN_USE',
}
def _normalize_prefix_directories(prefixes_dir):
if not os.path.isdir(prefixes_dir):
return
@@ -149,6 +152,7 @@ class ExeSettingsDialog(
DraggableDialog,
MangoHudSettingsMixin,
GamescopeSettingsMixin,
ReShadeSettingsMixin,
VkBasaltSettingsMixin,
):
"""Dialog for configuring executable-specific settings."""
@@ -207,6 +211,7 @@ class ExeSettingsDialog(
self.advanced_settings_by_key = {}
self.init_mangohud_state()
self.init_gamescope_state()
self.init_reshade_state()
self.init_vkbasalt_state()
self.blocked_keys = set()
self.numa_nodes = {}
@@ -332,6 +337,8 @@ class ExeSettingsDialog(
self.mangohud_tab_layout = QVBoxLayout(self.mangohud_tab)
self.vkbasalt_tab = QWidget()
self.vkbasalt_tab_layout = QVBoxLayout(self.vkbasalt_tab)
self.reshade_tab = QWidget()
self.reshade_tab_layout = QVBoxLayout(self.reshade_tab)
self.gamescope_tab = QWidget()
self.gamescope_tab_layout = QVBoxLayout(self.gamescope_tab)
@@ -339,6 +346,7 @@ class ExeSettingsDialog(
self.tab_widget.addTab(self.advanced_tab, _("Advanced"))
self.tab_widget.addTab(self.mangohud_tab, "MangoHud")
self.tab_widget.addTab(self.vkbasalt_tab, "vkBasalt")
self.tab_widget.addTab(self.reshade_tab, "ReShade")
if self.gamescope_available:
self.tab_widget.addTab(self.gamescope_tab, "Gamescope")
@@ -462,6 +470,7 @@ class ExeSettingsDialog(
self.setup_mangohud_tab()
self.setup_vkbasalt_tab()
self.setup_reshade_tab()
if self.gamescope_available:
self.setup_gamescope_tab()
@@ -557,6 +566,7 @@ class ExeSettingsDialog(
or key in MANGOHUD_ENV_KEYS
or key in GAMESCOPE_ENV_KEYS
or key in VKBASALT_ENV_KEYS
or key in RESHADE_ENV_KEYS
or key in TOGGLE_BOOL_KEYS
):
if val.startswith('"') and val.endswith('"') and len(val) >= 2:
@@ -591,6 +601,8 @@ class ExeSettingsDialog(
self.current_settings[key] = ''
for key in VKBASALT_ENV_KEYS:
self.current_settings[key] = ''
for key in RESHADE_ENV_KEYS:
self.current_settings[key] = ''
elif not self.user_conf:
self.current_settings.setdefault('PW_MANGOHUD', '0')
self.current_settings.setdefault('PW_VKBASALT', '0')
@@ -619,6 +631,7 @@ class ExeSettingsDialog(
self.populate_advanced()
self.populate_mangohud()
self.populate_vkbasalt()
self.populate_reshade()
if self.gamescope_available:
self.populate_gamescope()
self.populate_favorites(select_tab=True)
@@ -1391,6 +1404,7 @@ class ExeSettingsDialog(
self._filter_mangohud_settings(search_text)
self._filter_vkbasalt_settings(search_text)
self._filter_reshade_settings(search_text)
self._filter_gamescope_settings(search_text)
def apply_changes(self):
@@ -1465,6 +1479,7 @@ class ExeSettingsDialog(
if self.gamescope_available:
gamescope_changes = self._collect_gamescope_changes()
vkbasalt_changes = self._collect_vkbasalt_changes()
reshade_changes = self._collect_reshade_changes()
if self.user_conf:
if self.current_settings.get('PW_MANGOHUD') != '1':
mangohud_changes = [
@@ -1481,7 +1496,14 @@ class ExeSettingsDialog(
change for change in gamescope_changes
if change.startswith('PW_GAMESCOPE=')
]
specialized_changes = mangohud_changes + vkbasalt_changes + gamescope_changes
if self.current_settings.get('PW_USE_RESHADE') != '1':
reshade_changes = [
change for change in reshade_changes
if change.startswith('PW_USE_RESHADE=')
]
specialized_changes = (
mangohud_changes + vkbasalt_changes + reshade_changes + gamescope_changes
)
if self.user_conf:
defaults = ('', '0', '0.00', 'Home')
specialized_changes = [
@@ -1646,8 +1668,7 @@ class ExeSettingsDialog(
palette.setBrush(QPalette.ColorRole.Text, item.foreground())
label.setAutoFillBackground(selected)
label.setPalette(palette)
if label.foregroundRole() != QPalette.ColorRole.Text:
label.setForegroundRole(QPalette.ColorRole.Text)
label.setForegroundRole(QPalette.ColorRole.Text)
text = item.data(Qt.ItemDataRole.AccessibleTextRole)
name, separator, description = text.partition("\n")
formatted_text = escape(name)
@@ -1664,11 +1685,7 @@ class ExeSettingsDialog(
if label.text() != formatted_text:
label.setText(formatted_text)
width = max(1, table.columnWidth(0))
height_key = [width, formatted_text, label.font().toString()]
if label.property("ppqt_height_key") != height_key:
height = label.heightForWidth(width) + table.fontMetrics().height()
label.setProperty("ppqt_height_key", height_key)
table.setRowHeight(row, height)
table.setRowHeight(row, label.heightForWidth(width) + table.fontMetrics().height())
for row in range(table.rowCount()):
widget = table.cellWidget(row, 1)
if widget is None:
@@ -1682,6 +1699,8 @@ class ExeSettingsDialog(
)
return False
if isinstance(obj, QWidget) and self._handle_reshade_key_button_event(obj, event):
return True
if isinstance(obj, QWidget) and self._handle_vkbasalt_key_button_event(obj, event):
return True
+446
View File
@@ -0,0 +1,446 @@
"""ReShade settings support for executable settings dialog."""
import os
from typing import Any, cast
from PySide6.QtCore import QEvent, Qt
from PySide6.QtGui import QKeyEvent, QKeySequence
from PySide6.QtWidgets import (
QGridLayout,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QScrollArea,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from portprotonqt.custom_widgets import AutoSizeButton, CustomComboBox
from portprotonqt.localization import _
from portprotonqt.theme_manager import ThemeManager
RESHADE_ENV_KEYS = [
'PW_RESHADE_EXE',
'PW_RESHADE_API',
'PW_RESHADE_FONT',
'PW_RESHADE_SCREENSHOT_PATH',
'PW_RESHADE_SCREENSHOT_KEY',
'PW_RESHADE_OVERLAY_KEY',
]
RESHADE_API_OPTIONS = {
"Auto": "auto",
"DXGI": "dxgi.dll",
"DirectX 8/9": "d3d9.dll",
"OpenGL": "opengl32.dll",
"Vulkan": "vulkan",
}
RESHADE_SPECIAL_KEYS = {
Qt.Key.Key_Cancel: 3,
Qt.Key.Key_Backspace: 8,
Qt.Key.Key_Tab: 9,
Qt.Key.Key_Clear: 12,
Qt.Key.Key_Return: 13,
Qt.Key.Key_Enter: 13,
Qt.Key.Key_Pause: 19,
Qt.Key.Key_CapsLock: 20,
Qt.Key.Key_Escape: 27,
Qt.Key.Key_Space: 32,
Qt.Key.Key_PageUp: 33,
Qt.Key.Key_PageDown: 34,
Qt.Key.Key_End: 35,
Qt.Key.Key_Home: 36,
Qt.Key.Key_Left: 37,
Qt.Key.Key_Up: 38,
Qt.Key.Key_Right: 39,
Qt.Key.Key_Down: 40,
Qt.Key.Key_Print: 44,
Qt.Key.Key_Insert: 45,
Qt.Key.Key_Delete: 46,
Qt.Key.Key_Help: 47,
Qt.Key.Key_Meta: 91,
Qt.Key.Key_Menu: 93,
Qt.Key.Key_Sleep: 95,
Qt.Key.Key_NumLock: 144,
Qt.Key.Key_ScrollLock: 145,
Qt.Key.Key_Semicolon: 186,
Qt.Key.Key_Equal: 187,
Qt.Key.Key_Comma: 188,
Qt.Key.Key_Minus: 189,
Qt.Key.Key_Period: 190,
Qt.Key.Key_Slash: 191,
Qt.Key.Key_QuoteLeft: 192,
Qt.Key.Key_BracketLeft: 219,
Qt.Key.Key_Backslash: 220,
Qt.Key.Key_BracketRight: 221,
Qt.Key.Key_Apostrophe: 222,
Qt.Key.Key_Back: 166,
Qt.Key.Key_Forward: 167,
Qt.Key.Key_Refresh: 168,
Qt.Key.Key_Stop: 169,
Qt.Key.Key_Search: 170,
Qt.Key.Key_Favorites: 171,
Qt.Key.Key_HomePage: 172,
Qt.Key.Key_VolumeMute: 173,
Qt.Key.Key_VolumeDown: 174,
Qt.Key.Key_VolumeUp: 175,
Qt.Key.Key_MediaNext: 176,
Qt.Key.Key_MediaPrevious: 177,
Qt.Key.Key_MediaStop: 178,
Qt.Key.Key_MediaPlay: 179,
Qt.Key.Key_LaunchMail: 180,
Qt.Key.Key_LaunchMedia: 181,
Qt.Key.Key_Launch0: 182,
Qt.Key.Key_Launch1: 183,
}
RESHADE_KEYPAD_KEYS = {
Qt.Key.Key_Asterisk: 106,
Qt.Key.Key_Plus: 107,
Qt.Key.Key_Comma: 108,
Qt.Key.Key_Minus: 109,
Qt.Key.Key_Period: 110,
Qt.Key.Key_Slash: 111,
}
RESHADE_FIRST_FUNCTION_KEY = 112
RESHADE_LAST_FUNCTION_KEY = 135
RESHADE_FIRST_NUMPAD_KEY = 96
class ReShadeSettingsMixin:
"""Mixin with ReShade settings UI and serialization logic."""
theme: Any
exe_path: str | None
current_settings: dict[str, str]
original_values: dict[str, str]
reshade_tab: QWidget
reshade_tab_layout: QVBoxLayout
def init_reshade_state(self) -> None:
self.reshade_original_values = {}
self.reshade_key_target = None
def setup_reshade_tab(self) -> None:
"""Create ReShade tab widgets."""
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFocusPolicy(Qt.FocusPolicy.NoFocus)
scroll.setStyleSheet(self.theme.SCROLL_STYLE + self.theme.TRANSPARENT_BACKGROUND_STYLE)
container = QWidget()
layout = QVBoxLayout(container)
layout.setSpacing(self.theme.exeSettingsGroupBoxBlockSpacing)
actions = QGroupBox(_("Actions"))
actions.setStyleSheet(self.theme.QGROUP_BOX_STYLE)
actions_layout = QGridLayout(actions)
self.reshade_enable_button = self._create_reshade_button("")
self.reshade_enable_button.clicked.connect(self.toggle_reshade_enable)
actions_layout.addWidget(self.reshade_enable_button, 0, 0)
layout.addWidget(actions)
self._add_reshade_settings_group(layout)
layout.addStretch()
scroll.setWidget(container)
self.reshade_tab_layout.addWidget(scroll)
def _add_reshade_settings_group(self, parent_layout: QVBoxLayout) -> None:
self.reshade_settings_group = QGroupBox(_("Settings"))
self.reshade_settings_group.setStyleSheet(self.theme.QGROUP_BOX_STYLE)
settings_layout = QGridLayout(self.reshade_settings_group)
self.reshade_exe_edit = self._add_reshade_file_row(
settings_layout, 0, "EXE", ".exe"
)
self.reshade_api_combo = CustomComboBox(theme=self.theme)
self.reshade_api_combo.view().window().setWindowFlags(
Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint
)
self.reshade_api_combo.view().window().setAttribute(
Qt.WidgetAttribute.WA_TranslucentBackground
)
self.reshade_api_combo.setStyleSheet(
self.theme.COMBOBOX_STYLE + self.theme.SCROLL_STYLE
)
self.reshade_api_combo.addItems(list(RESHADE_API_OPTIONS))
settings_layout.addWidget(QLabel("API"), 1, 0)
settings_layout.addWidget(self.reshade_api_combo, 1, 1)
self.reshade_font_combo = CustomComboBox(theme=self.theme)
self.reshade_font_combo.view().window().setWindowFlags(
Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint
)
self.reshade_font_combo.view().window().setAttribute(
Qt.WidgetAttribute.WA_TranslucentBackground
)
self.reshade_font_combo.setStyleSheet(
self.theme.COMBOBOX_STYLE + self.theme.SCROLL_STYLE
)
self.reshade_font_combo.addItem(_("Default"), "")
settings_layout.addWidget(QLabel(_("Font")), 2, 0)
settings_layout.addWidget(self.reshade_font_combo, 2, 1)
self.reshade_screenshot_edit = self._add_reshade_directory_row(
settings_layout, 3, _("Screenshots")
)
self.reshade_screenshot_key_button = self._create_reshade_key_button(
"screenshot"
)
settings_layout.addWidget(QLabel(_("Screenshot key")), 4, 0)
settings_layout.addWidget(self.reshade_screenshot_key_button, 4, 1)
self.reshade_overlay_key_button = self._create_reshade_key_button("overlay")
settings_layout.addWidget(QLabel(_("Overlay key")), 5, 0)
settings_layout.addWidget(self.reshade_overlay_key_button, 5, 1)
parent_layout.addWidget(self.reshade_settings_group)
self._load_reshade_fonts()
def _create_reshade_button(self, label: str) -> QPushButton:
button = QPushButton(label)
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
return button
def _add_reshade_file_row(
self, layout: QGridLayout, row: int, label: str, file_filter: str
) -> QLineEdit:
line_edit = QLineEdit()
line_edit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
button = AutoSizeButton(
_("Browse…"), icon=ThemeManager().get_icon("folder", as_path=True)
)
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.clicked.connect(
lambda _checked=False: self._select_reshade_file(line_edit, file_filter)
)
field_layout = QHBoxLayout()
field_layout.addWidget(line_edit)
field_layout.addWidget(button)
layout.addWidget(QLabel(label), row, 0)
layout.addLayout(field_layout, row, 1)
return line_edit
def _select_reshade_file(self, line_edit: QLineEdit, file_filter: str) -> None:
from portprotonqt.dialogs.file_explorer import FileExplorer
current_path = line_edit.text().strip()
initial_path = os.path.dirname(current_path) if os.path.isfile(current_path) else None
explorer = FileExplorer(
self, theme=self.theme, file_filter=file_filter, initial_path=initial_path
)
explorer.file_signal.file_selected.connect(line_edit.setText)
explorer.exec()
def _add_reshade_directory_row(
self, layout: QGridLayout, row: int, label: str
) -> QLineEdit:
line_edit = QLineEdit()
line_edit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
button = AutoSizeButton(
_("Browse…"), icon=ThemeManager().get_icon("folder", as_path=True)
)
button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button.clicked.connect(
lambda _checked=False: self._select_reshade_directory(line_edit)
)
field_layout = QHBoxLayout()
field_layout.addWidget(line_edit)
field_layout.addWidget(button)
layout.addWidget(QLabel(label), row, 0)
layout.addLayout(field_layout, row, 1)
return line_edit
def _select_reshade_directory(self, line_edit: QLineEdit) -> None:
from portprotonqt.dialogs.file_explorer import FileExplorer
initial_path = line_edit.text().strip() or os.path.dirname(self.exe_path or "")
explorer = FileExplorer(
self, theme=self.theme, initial_path=initial_path, directory_only=True
)
explorer.file_signal.file_selected.connect(line_edit.setText)
explorer.exec()
def _create_reshade_key_button(self, target: str) -> QPushButton:
button = self._create_reshade_button("")
button.clicked.connect(lambda _checked=False: self._capture_reshade_key(target))
button.installEventFilter(cast(QWidget, self))
return button
def _capture_reshade_key(self, target: str) -> None:
self.reshade_key_target = target
button = self._get_reshade_key_button(target)
button.setText(_("Press a button to choose"))
button.grabKeyboard()
def _get_reshade_key_button(self, target: str) -> QPushButton:
if target == "screenshot":
return self.reshade_screenshot_key_button
return self.reshade_overlay_key_button
def _handle_reshade_key_button_event(self, obj: QWidget, event: QEvent) -> bool:
target = self.reshade_key_target
if target is None or obj is not self._get_reshade_key_button(target):
return False
if event.type() != QEvent.Type.KeyPress:
return False
key_event = cast(QKeyEvent, event)
key_value = self._reshade_virtual_key(
key_event.key(), key_event.modifiers()
)
if key_value is None:
return True
modifiers = key_event.modifiers()
value = ",".join(str(item) for item in (
key_value,
int(bool(modifiers & Qt.KeyboardModifier.ControlModifier)),
int(bool(modifiers & Qt.KeyboardModifier.ShiftModifier)),
int(bool(modifiers & Qt.KeyboardModifier.AltModifier)),
))
self.current_settings[f'PW_RESHADE_{target.upper()}_KEY'] = value
button = self._get_reshade_key_button(target)
button.setText(QKeySequence(key_event.keyCombination()).toString())
button.releaseKeyboard()
self.reshade_key_target = None
return True
def _reshade_virtual_key(
self, key: int, modifiers: Qt.KeyboardModifier
) -> int | None:
is_keypad = bool(modifiers & Qt.KeyboardModifier.KeypadModifier)
if is_keypad and Qt.Key.Key_0.value <= key <= Qt.Key.Key_9.value:
return RESHADE_FIRST_NUMPAD_KEY + key - Qt.Key.Key_0.value
if is_keypad:
try:
keypad_key = RESHADE_KEYPAD_KEYS.get(Qt.Key(key))
except ValueError:
keypad_key = None
if keypad_key is not None:
return keypad_key
if Qt.Key.Key_0.value <= key <= Qt.Key.Key_9.value:
return key
if Qt.Key.Key_A.value <= key <= Qt.Key.Key_Z.value:
return key
if Qt.Key.Key_F1.value <= key <= Qt.Key.Key_F24.value:
return RESHADE_FIRST_FUNCTION_KEY + key - Qt.Key.Key_F1.value
try:
return RESHADE_SPECIAL_KEYS.get(Qt.Key(key))
except ValueError:
return None
def _format_reshade_key(self, value: str) -> str:
try:
key, control, shift, alt = (int(item) for item in value.split(","))
except ValueError:
return value
qt_key = next(
(item for item, code in RESHADE_SPECIAL_KEYS.items() if code == key), None
)
if qt_key is not None:
name = QKeySequence(qt_key).toString()
elif RESHADE_FIRST_FUNCTION_KEY <= key <= RESHADE_LAST_FUNCTION_KEY:
name = f"F{key - RESHADE_FIRST_FUNCTION_KEY + 1}"
else:
name = chr(key)
modifiers = [
label for active, label in (
(control, "Ctrl"), (shift, "Shift"), (alt, "Alt")
) if active
]
return "+".join([*modifiers, name])
def _load_reshade_fonts(self) -> None:
selected_family = self.current_settings.get('PW_RESHADE_FONT', '')
font_options = cast(Any, self)._get_mangohud_font_options()
families = {name for name, _path in font_options}
self.reshade_font_combo.clear()
self.reshade_font_combo.addItem(_("Default"), "")
for family in sorted(families, key=str.casefold):
self.reshade_font_combo.addItem(family, family)
self._select_reshade_font(selected_family)
def _select_reshade_font(self, family: str) -> None:
for index in range(self.reshade_font_combo.count()):
if self.reshade_font_combo.itemData(index) == family:
self.reshade_font_combo.setCurrentIndex(index)
return
if family:
self.reshade_font_combo.addItem(family, family)
self.reshade_font_combo.setCurrentIndex(self.reshade_font_combo.count() - 1)
def populate_reshade(self) -> None:
"""Populate ReShade tab from current settings."""
reshade_exe = self.current_settings.get('PW_RESHADE_EXE', '') or self.exe_path or ''
self.reshade_exe_edit.setText(reshade_exe)
api_value = self.current_settings.get('PW_RESHADE_API', 'auto')
api_label = next(
(label for label, value in RESHADE_API_OPTIONS.items() if value == api_value),
"Auto",
)
self.reshade_api_combo.setCurrentText(api_label)
font_family = self.current_settings.get('PW_RESHADE_FONT', '')
self._select_reshade_font(font_family)
screenshot_path = self.current_settings.get('PW_RESHADE_SCREENSHOT_PATH', '')
screenshot_key = self.current_settings.get(
'PW_RESHADE_SCREENSHOT_KEY', '44,0,0,0'
)
overlay_key = self.current_settings.get(
'PW_RESHADE_OVERLAY_KEY', '36,0,0,0'
)
self.reshade_screenshot_edit.setText(screenshot_path)
self.reshade_screenshot_key_button.setText(
self._format_reshade_key(screenshot_key)
)
self.reshade_overlay_key_button.setText(self._format_reshade_key(overlay_key))
self.current_settings['PW_RESHADE_SCREENSHOT_KEY'] = screenshot_key
self.current_settings['PW_RESHADE_OVERLAY_KEY'] = overlay_key
self.reshade_original_values = {
key: self.current_settings.get(key, '') for key in RESHADE_ENV_KEYS
}
self.reshade_original_values['PW_RESHADE_EXE'] = reshade_exe
self.reshade_original_values['PW_RESHADE_SCREENSHOT_KEY'] = screenshot_key
self.reshade_original_values['PW_RESHADE_OVERLAY_KEY'] = overlay_key
self._update_reshade_widgets()
def toggle_reshade_enable(self) -> None:
enabled = self.current_settings.get('PW_USE_RESHADE') == '1'
self.current_settings['PW_USE_RESHADE'] = '0' if enabled else '1'
self._update_reshade_widgets()
def _update_reshade_widgets(self) -> None:
enabled = self.current_settings.get('PW_USE_RESHADE') == '1'
self.reshade_enable_button.setText(
_("Disable {0}").format("ReShade")
if enabled else _("Enable {0}").format("ReShade")
)
self.reshade_enable_button.setStyleSheet(
self.theme.ACTION_BUTTON_ACTIVE_STYLE if enabled else self.theme.ACTION_BUTTON_STYLE
)
self.reshade_settings_group.setVisible(enabled)
def _collect_reshade_changes(self) -> list[str]:
changes = []
enabled = '1' if self.current_settings.get('PW_USE_RESHADE') == '1' else '0'
original_enabled = '1' if self.original_values.get('PW_USE_RESHADE') == '1' else '0'
if enabled != original_enabled:
changes.append(f"PW_USE_RESHADE={enabled}")
values = {
'PW_RESHADE_EXE': self.reshade_exe_edit.text().strip(),
'PW_RESHADE_API': RESHADE_API_OPTIONS[self.reshade_api_combo.currentText()],
'PW_RESHADE_FONT': str(self.reshade_font_combo.currentData() or ''),
'PW_RESHADE_SCREENSHOT_PATH': self.reshade_screenshot_edit.text().strip(),
'PW_RESHADE_SCREENSHOT_KEY': self.current_settings['PW_RESHADE_SCREENSHOT_KEY'],
'PW_RESHADE_OVERLAY_KEY': self.current_settings['PW_RESHADE_OVERLAY_KEY'],
}
for key, value in values.items():
if value != self.reshade_original_values.get(key, ''):
changes.append(f"{key}={value}")
return changes
def _filter_reshade_settings(self, search_text: str) -> None:
enabled = self.current_settings.get('PW_USE_RESHADE') == '1'
self.reshade_settings_group.setVisible(
enabled and (
not search_text
or search_text in "exe api font screenshot overlay"
or search_text in _("Settings").lower()
)
)
+10 -14
View File
@@ -4,6 +4,7 @@ import hashlib
import os
import platform
import re
import shutil
import subprocess
import time
import zlib
@@ -24,8 +25,7 @@ GOG_LOGIN_URL = (
"redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&"
"response_type=code&layout=galaxy"
)
GOGDL_RELEASE_SOURCE = "linux-gaming-ru/gogdl"
GOGDL_RELEASE_URL = f"https://api.github.com/repos/{GOGDL_RELEASE_SOURCE}/releases/latest"
GOGDL_RELEASE_URL = "https://api.github.com/repos/Heroic-Games-Launcher/heroic-gogdl/releases/latest"
GOGDL_AUTH_TIMEOUT = 60
GOGDL_UPDATE_INTERVAL = 30 * 24 * 60 * 60
GOG_USER_TIMEOUT = 15
@@ -77,11 +77,11 @@ class GOGAPI:
self.games_dir = Path.home() / "Games"
def get_gogdl_path(self) -> str | None:
"""Return the managed gogdl fork executable when installed."""
"""Return an available gogdl executable."""
bundled = self.bin_dir / "gogdl"
if bundled.is_file() and os.access(bundled, os.X_OK) and self._get_gogdl_release_tag():
if bundled.is_file() and os.access(bundled, os.X_OK):
return str(bundled)
return None
return shutil.which("gogdl")
def build_command(self, arguments: list[str]) -> list[str]:
"""Build a gogdl command with private configuration paths."""
@@ -94,7 +94,7 @@ class GOGAPI:
return [gogdl, "--auth-config-path", str(self.auth_path), *arguments]
def ensure_gogdl(self) -> str:
"""Download and verify the latest linux-gaming-ru gogdl binary."""
"""Download and verify the latest official Heroic gogdl binary."""
existing = self.get_gogdl_path()
if existing:
return existing
@@ -102,9 +102,9 @@ class GOGAPI:
return self._install_gogdl_release(asset, release_tag)
def update_gogdl(self) -> str:
"""Replace other gogdl binaries and update the managed fork."""
"""Update the bundled gogdl binary to the latest official release."""
bundled = self.bin_dir / "gogdl"
if self.get_gogdl_path():
if bundled.is_file() and os.access(bundled, os.X_OK):
try:
last_check = self.gogdl_version_path.stat().st_mtime
except OSError:
@@ -143,17 +143,13 @@ class GOGAPI:
def _get_gogdl_release_tag(self) -> str:
try:
version = self.gogdl_version_path.read_text(encoding="utf-8").strip()
return self.gogdl_version_path.read_text(encoding="utf-8").strip()
except OSError:
return ""
source, _, tag = version.partition(":")
return tag if source == GOGDL_RELEASE_SOURCE else ""
def _install_gogdl_release(self, asset: dict, release_tag: str) -> str:
path = self._download_gogdl_asset(asset)
self.gogdl_version_path.write_text(
f"{GOGDL_RELEASE_SOURCE}:{release_tag}", encoding="utf-8"
)
self.gogdl_version_path.write_text(release_tag, encoding="utf-8")
return path
def _download_gogdl_asset(self, asset: dict) -> str:
+5 -1
View File
@@ -93,6 +93,10 @@ class KeyboardInputMixin(InputMixin):
keyboard.stop_backspace_repeat()
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
app = QApplication.instance()
if not app:
return QObject.eventFilter(cast(QObject, self), obj, event)
if event.type() == QEvent.Type.Show:
if isinstance(obj, QMenu):
SoundManager().play("open")
@@ -158,7 +162,7 @@ class KeyboardInputMixin(InputMixin):
logger.debug(f"Skipping event filter for non-QObject: {type(obj).__name__}")
return False
return False
return QObject.eventFilter(cast(QObject, self), obj, event)
def _qt_event_to_input_key(self, event: QEvent) -> int | None:
native_key = self._native_scan_to_input_key(event)
+1
View File
@@ -118,6 +118,7 @@ install_data(
'dialogs/settings_dialog.py',
'dialogs/settings_mangohud.py',
'dialogs/settings_gamescope.py',
'dialogs/settings_reshade.py',
'dialogs/settings_vkbasalt.py',
'dialogs/proton_manager.py',
'dialogs/prefix_backup.py',
+1
View File
@@ -149,6 +149,7 @@ def get_toggle_settings():
'PW_USE_SPECIALK': _("Enable SpecialK (injection library for fixing graphics, latency and more)"),
# Upscalers and Frame Generation
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
'PW_USE_RESHADE': _("Enable ReShade post-processing"),
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
# Ray Tracing
-32
View File
@@ -152,38 +152,6 @@ def test_silent_exe_uses_existing_instance(
startup.qt_app.exec.assert_not_called()
@pytest.mark.parametrize("silent", [False, True])
def test_silent_launch_skips_startup_workers(
silent_startup: SimpleNamespace, monkeypatch: pytest.MonkeyPatch, silent: bool,
) -> None:
from PySide6.QtCore import QThread
from portprotonqt import appimage_updater
startup = silent_startup
window = MagicMock()
startup.window.side_effect = None
startup.window.return_value = window
monkeypatch.setattr(startup.app, "QTimer", MagicMock())
monkeypatch.setattr(QThread, "start", MagicMock())
update_worker = MagicMock()
monkeypatch.setattr(appimage_updater, "AppImageUpdateWorker", update_worker)
monkeypatch.setattr(startup.app.display_config, "get_auto_fullscreen_gamepad", lambda: False)
monkeypatch.setattr(startup.app.display_config, "get_start_minimized", lambda: False)
monkeypatch.setattr(startup.app.display_config, "set_fullscreen", MagicMock())
flags = ["--silent"] if silent else []
monkeypatch.setattr(sys, "argv", ["portprotonqt", *flags, str(startup.exe)])
with pytest.raises(SystemExit) as result:
startup.app.main()
assert result.value.code == 0
assert window.silent_launch_mode is silent
assert ("initialCommandWorker" in vars(window)) is (not silent)
assert update_worker.called is (not silent)
assert ("appimageUpdateWorker" in vars(window)) is (not silent)
startup.window.assert_called_once()
@pytest.mark.parametrize("flags,target", [
([], "exe"),
(["--silent", "--log"], "exe"),
+6 -76
View File
@@ -1,6 +1,5 @@
import os
import zlib
from contextlib import nullcontext
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
@@ -8,7 +7,7 @@ from unittest.mock import Mock
import orjson
import pytest
from portprotonqt.gog_api import GOGAPI, GOGDL_RELEASE_SOURCE, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
from portprotonqt.gog_api import GOGAPI, GOGDL_UPDATE_INTERVAL, GOG_PRODUCT_LOCALES
from portprotonqt.localization import LOCALE_MAP
@@ -393,82 +392,13 @@ def test_ensure_gogdl_downloads_matching_architecture(tmp_path: Path, monkeypatc
response = Mock()
response.json.return_value = {"assets": [asset], "tag_name": "v1.2.1"}
monkeypatch.setattr("portprotonqt.gog_api.platform.machine", lambda: "x86_64")
request = Mock(return_value=response)
monkeypatch.setattr("portprotonqt.gog_api.requests.get", request)
monkeypatch.setattr("portprotonqt.gog_api.requests.get", lambda *args, **kwargs: response)
monkeypatch.setattr(
api, "_install_gogdl_release",
lambda selected, tag: f'{selected["name"]}:{tag}',
)
assert api.ensure_gogdl() == "gogdl_linux_x86_64:v1.2.1"
assert request.call_args.args[0] == f"https://api.github.com/repos/{GOGDL_RELEASE_SOURCE}/releases/latest"
@pytest.mark.parametrize("operation", ["ensure_gogdl", "update_gogdl"])
@pytest.mark.parametrize("version", ["", "v1.3.2", "Heroic-Games-Launcher/heroic-gogdl:v1.3.2"])
def test_gogdl_replaces_existing_upstream(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, operation: str, version: str
) -> None:
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
api = GOGAPI()
api.bin_dir.mkdir(parents=True)
binary = api.bin_dir / "gogdl"
binary.write_bytes(b"upstream")
binary.chmod(0o755)
if version:
api.gogdl_version_path.write_text(version)
latest = Mock(return_value=({"name": "gogdl_linux_x86_64"}, "v1.3.2"))
monkeypatch.setattr(api, "_get_latest_gogdl_release", latest)
def download(_asset: dict) -> str:
binary.write_bytes(b"fork")
return str(binary)
monkeypatch.setattr(api, "_download_gogdl_asset", download)
assert api.get_gogdl_path() is None
assert getattr(api, operation)() == str(binary)
latest.assert_called_once_with()
assert binary.read_bytes() == b"fork"
assert api.gogdl_version_path.read_text() == f"{GOGDL_RELEASE_SOURCE}:v1.3.2"
assert api.get_gogdl_path() == str(binary)
def test_gogdl_does_not_use_system_binary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
api = GOGAPI()
which = Mock(return_value="/usr/bin/gogdl")
monkeypatch.setattr("shutil.which", which)
install = Mock(return_value=str(api.bin_dir / "gogdl"))
monkeypatch.setattr(api, "_get_latest_gogdl_release", lambda: ({}, "v1.3.2"))
monkeypatch.setattr(api, "_install_gogdl_release", install)
assert api.get_gogdl_path() is None
assert api.ensure_gogdl() == str(api.bin_dir / "gogdl")
install.assert_called_once_with({}, "v1.3.2")
which.assert_not_called()
def test_failed_gogdl_migration_keeps_old_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path))
api = GOGAPI()
api.bin_dir.mkdir(parents=True)
binary = api.bin_dir / "gogdl"
binary.write_bytes(b"upstream")
binary.chmod(0o755)
api.gogdl_version_path.write_text("v1.3.2")
response = Mock()
response.iter_content.return_value = [b"corrupted download"]
monkeypatch.setattr("portprotonqt.gog_api.requests.get", lambda *args, **kwargs: nullcontext(response))
asset = {"browser_download_url": "https://github.com/linux-gaming-ru/gogdl/releases/download/v1.3.2/gogdl_linux_x86_64", "digest": "sha256:invalid"}
monkeypatch.setattr(api, "_get_latest_gogdl_release", lambda: (asset, "v1.3.2"))
with pytest.raises(OSError, match="checksum verification failed"):
api.update_gogdl()
assert binary.read_bytes() == b"upstream"
assert api.gogdl_version_path.read_text() == "v1.3.2"
assert not binary.with_suffix(".part").exists()
assert api.get_gogdl_path() is None
def test_update_gogdl_skips_current_release(tmp_path: Path, monkeypatch) -> None:
@@ -480,7 +410,7 @@ def test_update_gogdl_skips_current_release(tmp_path: Path, monkeypatch) -> None
gogdl_path.parent.mkdir()
gogdl_path.touch()
gogdl_path.chmod(0o755)
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
latest_release = Mock(return_value=({}, "v1.2.1"))
monkeypatch.setattr(api, "_get_latest_gogdl_release", latest_release)
install = Mock()
@@ -500,7 +430,7 @@ def test_update_gogdl_checks_release_after_month(tmp_path: Path, monkeypatch) ->
gogdl_path.parent.mkdir()
gogdl_path.touch()
gogdl_path.chmod(0o755)
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
os.utime(api.gogdl_version_path, (1, 1))
last_check = api.gogdl_version_path.stat().st_mtime
monkeypatch.setattr(
@@ -523,7 +453,7 @@ def test_update_gogdl_records_failed_monthly_check(tmp_path: Path, monkeypatch)
gogdl_path.parent.mkdir()
gogdl_path.touch()
gogdl_path.chmod(0o755)
api.gogdl_version_path.write_text(f"{GOGDL_RELEASE_SOURCE}:v1.2.1", encoding="utf-8")
api.gogdl_version_path.write_text("v1.2.1", encoding="utf-8")
os.utime(api.gogdl_version_path, (1, 1))
last_check = api.gogdl_version_path.stat().st_mtime
monkeypatch.setattr(
@@ -562,7 +492,7 @@ def test_install_gogdl_release_records_version(tmp_path: Path, monkeypatch) -> N
monkeypatch.setattr(api, "_download_gogdl_asset", lambda _asset: "/bin/gogdl")
assert api._install_gogdl_release({}, "v1.2.1") == "/bin/gogdl"
assert api.gogdl_version_path.read_text(encoding="utf-8") == f"{GOGDL_RELEASE_SOURCE}:v1.2.1"
assert api.gogdl_version_path.read_text(encoding="utf-8") == "v1.2.1"
def test_authenticate_returns_gogdl_error(monkeypatch) -> None:
+1 -30
View File
@@ -6,9 +6,8 @@ from typing import Any, cast
from unittest.mock import MagicMock
from PySide6.QtCore import QEvent, QObject, QStringListModel, Qt
from PySide6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, QFrame, QHBoxLayout, QLineEdit, QListView, QMenu, QPushButton, QSlider, QStackedWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget
from PySide6.QtWidgets import QApplication, QCheckBox, QComboBox, QDialog, QFrame, QHBoxLayout, QLineEdit, QListView, QMenu, QPushButton, QSlider, QStackedWidget, QTableWidget, QTableWidgetItem, QWidget
from pytest import MonkeyPatch, raises, mark
from shiboken6 import delete
import portprotonqt.input_manager as input_manager
import portprotonqt.input_manager.buttons as input_buttons
@@ -36,34 +35,6 @@ HIDDEN_CARD_X = 20
NEXT_CARD_X = 40
@mark.parametrize("event_type", [QEvent.Type.Destroy, QEvent.Type.ChildRemoved, QEvent.Type.DeferredDelete])
def test_keyboard_filter_ignores_deleted_layout(event_type: QEvent.Type) -> None:
app = QApplication.instance() or QApplication([])
manager = InputManager.__new__(InputManager)
QObject.__init__(manager)
layout = QVBoxLayout()
delete(layout)
assert manager.eventFilter(layout, QEvent(event_type)) is False
assert app is not None
@mark.parametrize("event_type", [QEvent.Type.Destroy, QEvent.Type.ChildRemoved, QEvent.Type.DeferredDelete])
def test_keyboard_filter_teardown_does_not_lookup_application(
monkeypatch: MonkeyPatch, event_type: QEvent.Type,
) -> None:
app = QApplication.instance() or QApplication([])
manager = InputManager.__new__(InputManager)
QObject.__init__(manager)
watched = QObject()
lookup = MagicMock(side_effect=AssertionError("Application lookup during teardown"))
monkeypatch.setattr(QApplication, "instance", lookup)
assert manager.eventFilter(watched, QEvent(event_type)) is False
lookup.assert_not_called()
assert app is not None
@mark.parametrize("initial", [True, False])
def test_gamepad_auto_fullscreen_shows_boot_on_hotplug(monkeypatch: MonkeyPatch, initial: bool) -> None:
app = QApplication.instance() or QApplication([])
+15 -9
View File
@@ -1,6 +1,8 @@
"""Progressive library loading, drawing and input regressions."""
from collections.abc import Callable, Generator
from pathlib import Path
from time import perf_counter
import logging
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import MagicMock
@@ -397,7 +399,7 @@ def real_cover_scene(
@pytest.mark.parametrize("animation_type", ["gradient", "glow"])
def test_real_cover_loading_yields_and_focuses_first_paint(
def test_real_cover_loading_and_first_paint_focus_latency(
real_cover_scene: tuple[GameLibraryManager, Path],
monkeypatch: pytest.MonkeyPatch, animation_type: str,
) -> None:
@@ -405,14 +407,14 @@ def test_real_cover_loading_yields_and_focuses_first_paint(
monkeypatch.setattr(manager.theme, "GAME_CARD_ANIMATION",
{**manager.theme.GAME_CARD_ANIMATION, "card_animation_type": animation_type})
threads: list[bool] = []
paints: list[bool] = []
ticks: list[tuple[int, int]] = []
paints: list[tuple[float, bool]] = []
ticks: list[float] = []
original_paint = GameCard.paintEvent
original_cover = GameCard.on_cover_loaded
def record_paint(card: GameCard, event: Any) -> None:
if card.name == "Game 0":
paints.append(card._focused or card._hovered)
paints.append((perf_counter(), card._focused or card._hovered))
original_paint(card, event)
def record_cover(card: GameCard, pixmap: QPixmap) -> None:
@@ -420,7 +422,7 @@ def test_real_cover_loading_yields_and_focuses_first_paint(
original_cover(card, pixmap)
def sample() -> None:
ticks.append((len(manager.game_card_cache), len(threads)))
ticks.append(perf_counter())
if len(threads) == len(games) and not manager._incremental_add_queue:
loop.quit()
@@ -432,14 +434,18 @@ def test_real_cover_loading_yields_and_focuses_first_paint(
timer.setInterval(5)
timer.timeout.connect(sample)
timer.start()
QTimer.singleShot(5000, loop.quit)
QTimer.singleShot(2000, loop.quit)
manager.set_games(games)
loop.exec()
timer.stop()
assert len(threads) == len(games) and all(threads), threads
assert paints and paints[0], paints
assert any(0 < cards < len(games) for cards, _ in ticks), ticks
assert any(0 < covers < len(games) for _, covers in ticks), ticks
highlighted = next((paint for paint in paints if paint[1]), None)
assert highlighted is not None
focus_delay = highlighted[0] - paints[0][0]
max_gap = max(after - before for before, after in zip(ticks, ticks[1:], strict=False))
logging.getLogger(__name__).info("%s: highlight %.1f ms, UI pause %.1f ms", animation_type, focus_delay * 1000, max_gap * 1000)
assert focus_delay < 0.05, paints
assert max_gap < 0.1, ticks
@pytest.mark.parametrize("scenario", [("grid", 0), ("grid", 1), ("vertical", 0), ("vertical", 1)])
+179 -1
View File
@@ -1,8 +1,9 @@
"""Tests for config/portproton.py — exec_line parsing, icon sanitization, launcher tail extraction."""
import os
import subprocess
from pathlib import Path
from pytest import MonkeyPatch
from pytest import MonkeyPatch, mark
from portprotonqt.config.portproton import (
extract_exec_target_path,
@@ -192,3 +193,180 @@ class TestThemedLaunchIconNames:
def test_reg_has_icon(self):
assert "reg" in THEMED_LAUNCH_ICON_NAMES[".reg"]
def test_run_after_batch_is_created_next_to_exe() -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
assert 'run_after_dir="$(dirname "${PW_EXE_FILE}")"' in helper
assert 'pw_exe_file_win="$("${WINELOADER}" winepath -w "${PW_EXE_FILE}"' in helper
assert "chcp 65001 >nul" in helper
assert 'start "" "${pw_exe_file_win}" ${LAUNCH_PARAMETERS}' in helper
assert 'start "" /unix "${PW_RUN_AFTER_EXE}"' in helper
assert 'LAUNCH_PARAMETERS="" proxy_launch_parameters="" \\' in helper
assert 'pw_run "${PW_VD_TMP[@]}" "${run_after_bat}"' in helper
def test_game_launch_marker_is_emitted_for_wine_and_proton() -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
marker = "printf '%s\\n' 'PORTPROTONQT_GAME_LAUNCH_STARTED'"
assert helper.count(marker) == 2
def test_user_conf_get_without_name_lists_active_values(tmp_path: Path) -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
definition = helper.split("manage_user_conf_value () {", 1)[1].split(
"use_exiftool () {", 1
)[0]
user_conf = tmp_path / "user.conf"
user_conf.write_text(
'# comment\nexport PW_USE_ESYNC="1"\n# export PW_USE_FSYNC="1"\n',
encoding="utf-8",
)
script = "manage_user_conf_value () {" + definition + '\nmanage_user_conf_value get\n'
result = subprocess.run(
["bash", "-c", script], capture_output=True, text=True,
env={"PATH": os.defpath, "USER_CONF": str(user_conf)}, check=False,
)
assert result.returncode == 0, result.stderr
assert result.stdout == 'PW_USE_ESYNC="1"\n'
@mark.parametrize("runtime,logging", [("0", "0"), ("0", "1"), ("1", "0"), ("1", "1")])
def test_wine_exit_code_survives_log_output_and_wineserver_wait(
tmp_path: Path, runtime: str, logging: str,
) -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text()
definition = helper.split("pw_run () {", 1)[1].split("export -f pw_run", 1)[0]
script = "pw_run () {" + definition + '''
check_variables () { :; }
pw_launch_wrapper () { return 37; }
wait_wineserver () { return 0; }
print_info () { :; }
PATH_TO_GAME="$1"
PW_TMPFS_PATH="$1"
PW_LOG_FILE="$1/wine.log"
PW_EXE_FILE="$1/game.exe"
PW_USE_RUNTIME="$2"
PW_LOG="$3"
pw_run "$PW_EXE_FILE"
exit $?
'''
(tmp_path / "game.exe").touch()
result = subprocess.run(
["bash", "-c", script, "bash", str(tmp_path), runtime, logging],
capture_output=True, text=True, env={"PATH": os.defpath}, check=False,
)
assert result.returncode == 37, result.stderr
assert result.stdout.count("PORTPROTONQT_GAME_EXIT_CODE=37") == 1
def test_vk_gpu_info_uses_build_aux_binary() -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
assert '/../../../bin/vk_gpu_info"' in helper
assert "dev-scripts/vk_gpu_info" not in helper
def test_reshade_prefers_directx_for_unity() -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
assert "source=$(echo \"$detect\" | grep '^SOURCE=' | cut -d= -f2-)" in helper
assert (
'[[ "${source##*/}" == UnityPlayer.dll && "$highest" != None ]] '
"&& vulkan=false"
) in helper
def test_reshade_and_optiscaler_are_mutually_exclusive() -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
assert 'DISABLE_EDIT_DB_LIST+=" PW_USE_OPTISCALER PW_USE_SPECIALK"' in helper
assert 'DISABLE_EDIT_DB_LIST+=" PW_USE_RESHADE"' in helper
def test_reshade_repeated_link_does_not_create_nested_shaders(tmp_path: Path) -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
link_function = helper.split("try_force_link_dir () {", 1)[1].split("\n}", 1)[0]
shaders = tmp_path / "shaders"
shaders.mkdir()
game = tmp_path / "game"
game.mkdir()
script = f'try_force_link_dir () {{{link_function}\n}}\n'
link_call = helper.split(' [[ "$PW_USE_RESHADE" != 1 ]] && return 0', 1)[1]
link_call = link_call.split(' pw_specialk_set_ini_value', 1)[0]
script += f'root="$1"\nPATH_TO_GAME="$2"\n{link_call * 2}'
subprocess.run(
["bash", "-c", script, "bash", str(tmp_path), str(game)],
check=True,
)
assert (game / "ReShade_shaders").resolve() == shaders
assert not (shaders / "shaders").exists()
def test_reshade_disable_cleans_unreal_shipping_directory(tmp_path: Path) -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
sync_function = helper.split("pw_reshade_sync_files () {", 1)[1].split("\n}", 1)[0]
game_root = tmp_path / "game"
game_root.mkdir()
game_exe = game_root / "Praest.exe"
game_exe.touch()
game = game_root / "Praest" / "Binaries" / "Win64"
game.mkdir(parents=True)
runtime = tmp_path / "plugins" / "reshade_windows" / "reshade" / "6.8.0_Addon"
runtime.mkdir(parents=True)
reshade_dll = runtime / "ReShade32.dll"
reshade_dll.touch()
(game / "opengl32.dll").symlink_to(reshade_dll)
(game / "opengl32.dll.b").symlink_to(reshade_dll)
(game / "ReShadePreset.ini").write_text("[GENERAL]\n")
(game / "reshade_version").write_text("6.8.0_Addon\nopengl32.dll\n")
script = f'pw_reshade_sync_files () {{{sync_function}\n}}\n'
script += 'try_remove_file () { rm -f "$1"; }\npw_reshade_sync_files\n'
subprocess.run(
["bash", "-c", script],
check=True,
env={
**os.environ,
"PW_EXE_FILE": str(game_exe),
"PW_PLUGINS_PATH": str(tmp_path / "plugins"),
"PW_USE_RESHADE": "0",
"PW_RESHADE_API": "",
"PW_RESHADE_EXE": "",
"PATH_TO_GAME": str(game),
},
)
assert not (game / "opengl32.dll").exists()
assert not (game / "opengl32.dll.b").exists()
assert not (game / "ReShadePreset.ini").exists()
def test_ini_writer_matches_spaces_around_equals() -> None:
helper = Path("build-aux/share/portproton/scripts/functions_helper").read_text(
encoding="utf-8",
)
assert '^[[:space:]]*$key[[:space:]]*=' in helper
+40 -45
View File
@@ -37,6 +37,7 @@ from portprotonqt.dialogs.settings_mangohud import (
MANGOHUD_TOGGLE_DESCRIPTIONS,
MangoHudSettingsMixin,
)
from portprotonqt.dialogs.settings_reshade import ReShadeSettingsMixin
from portprotonqt.dialogs.settings_vkbasalt import VkBasaltSettingsMixin
@@ -90,6 +91,45 @@ def test_settings_dialog_search_accepts_typing_without_search_focus() -> None:
QApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
def test_reshade_collects_enable_exe_api_and_font() -> None:
settings = ReShadeSettingsMixin()
settings.current_settings = {
"PW_USE_RESHADE": "1",
"PW_RESHADE_SCREENSHOT_KEY": "44,0,0,0",
"PW_RESHADE_OVERLAY_KEY": "36,0,0,0",
}
settings.original_values = {"PW_USE_RESHADE": "0"}
settings.reshade_original_values = {
"PW_RESHADE_EXE": "",
"PW_RESHADE_API": "auto",
"PW_RESHADE_FONT": "",
"PW_RESHADE_SCREENSHOT_PATH": "",
"PW_RESHADE_SCREENSHOT_KEY": "44,0,0,0",
"PW_RESHADE_OVERLAY_KEY": "36,0,0,0",
}
settings.reshade_exe_edit = QLineEdit("/games/game.exe")
settings.reshade_api_combo = CustomComboBox()
settings.reshade_api_combo.addItems(["Auto", "Vulkan"])
settings.reshade_api_combo.setCurrentText("Vulkan")
settings.reshade_font_combo = CustomComboBox()
settings.reshade_font_combo.addItem("DejaVu Sans", "DejaVu Sans")
settings.reshade_screenshot_edit = QLineEdit("/screenshots")
assert settings._collect_reshade_changes() == [
"PW_USE_RESHADE=1",
"PW_RESHADE_EXE=/games/game.exe",
"PW_RESHADE_API=vulkan",
"PW_RESHADE_FONT=DejaVu Sans",
"PW_RESHADE_SCREENSHOT_PATH=/screenshots",
]
assert settings._reshade_virtual_key(
Qt.Key.Key_Semicolon.value, Qt.KeyboardModifier.NoModifier
) == 186
assert settings._reshade_virtual_key(
Qt.Key.Key_1.value, Qt.KeyboardModifier.KeypadModifier
) == 97
def test_mangohud_search_ignores_unavailable_categories() -> None:
settings = MangoHudSettingsMixin()
settings.mangohud_category_groups = {}
@@ -631,48 +671,3 @@ def test_winetricks_centered_checkboxes_and_install_selection(
dialog.install_selected(force=True)
dialog._start_install_process.assert_called_once_with(["art2kmin"], True)
dialog.deleteLater()
@pytest.mark.parametrize("theme_name", ["standart", "standart-light", "classic", "classic-light"])
def test_settings_table_stops_repainting_when_idle(theme_name: str) -> None:
from PySide6.QtCore import QObject
from PySide6.QtWidgets import QTableWidgetItem
from portprotonqt.dialogs.settings_dialog import ExeSettingsDialog
from portprotonqt.theme_manager import ThemeWrapper, load_theme
theme = ThemeWrapper(load_theme(theme_name))
dialog = cast(ExeSettingsDialog, SimpleNamespace(theme=theme))
class PaintFilter(QObject):
paints = 0
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if event.type() == QEvent.Type.Paint:
self.paints += 1
return ExeSettingsDialog.eventFilter(dialog, obj, event)
return False
table = QTableWidget(3, 3)
table.resize(800, 400)
table.setStyleSheet(theme.WINETRICKS_TABBLE_STYLE)
table.setColumnWidth(0, 500)
table.setColumnHidden(2, True)
table.viewport().setProperty("ppqt_settings_values", True)
paint_filter = PaintFilter(table)
table.viewport().installEventFilter(paint_filter)
for row in range(3):
table.setItem(row, 0, QTableWidgetItem("Setting\nDescription " * 5))
combo = CustomComboBox(theme=theme)
combo.addItem("Value")
table.setCellWidget(row, 1, combo)
table.show()
for selected_row, width in ((0, 500), (1, 300)):
table.selectRow(selected_row)
table.setColumnWidth(0, width)
for _ in range(10):
QApplication.processEvents()
QTest.qWait(100)
paints = paint_filter.paints
QTest.qWait(100)
assert paint_filter.paints == paints
table.close()