Add configurable DXVK FPS limiting #14
@@ -51,7 +51,7 @@ jobs:
|
||||
run: python scripts/prepare_remote_fallbacks.py
|
||||
|
||||
- name: Syntax check
|
||||
run: python -m py_compile setup_tool.py setup_tool_dynamic.py setup_tool_responsive.py remote_packages.py tests/test_safety.py scripts/prepare_remote_fallbacks.py
|
||||
run: python -m py_compile setup_tool.py setup_tool_dynamic.py setup_tool_responsive.py dxvk_fps.py remote_packages.py tests/test_safety.py tests/test_dxvk_fps_limit.py scripts/prepare_remote_fallbacks.py
|
||||
|
||||
- name: Unit safety tests
|
||||
run: python -m unittest discover -s tests -v
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
DEFAULT_REFRESH_RATE = 60
|
||||
_MAX_FRAME_RATE_RE = re.compile(
|
||||
r"^\s*(?P<comment>#\s*)?d3d9\.maxFrameRate\s*=\s*(?P<value>[+-]?\d+)\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def detect_max_refresh_rate(default=DEFAULT_REFRESH_RATE):
|
||||
"""Return the highest refresh rate reported by active Windows displays."""
|
||||
try:
|
||||
fallback = max(1, int(default))
|
||||
except (TypeError, ValueError):
|
||||
fallback = DEFAULT_REFRESH_RATE
|
||||
|
||||
if os.name != "nt":
|
||||
return fallback
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class DISPLAY_DEVICEW(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cb", wintypes.DWORD),
|
||||
("DeviceName", wintypes.WCHAR * 32),
|
||||
("DeviceString", wintypes.WCHAR * 128),
|
||||
("StateFlags", wintypes.DWORD),
|
||||
("DeviceID", wintypes.WCHAR * 128),
|
||||
("DeviceKey", wintypes.WCHAR * 128),
|
||||
]
|
||||
|
||||
class POINTL(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("x", wintypes.LONG),
|
||||
("y", wintypes.LONG),
|
||||
]
|
||||
|
||||
class DEVMODEW(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dmDeviceName", wintypes.WCHAR * 32),
|
||||
("dmSpecVersion", wintypes.WORD),
|
||||
("dmDriverVersion", wintypes.WORD),
|
||||
("dmSize", wintypes.WORD),
|
||||
("dmDriverExtra", wintypes.WORD),
|
||||
("dmFields", wintypes.DWORD),
|
||||
("dmPosition", POINTL),
|
||||
("dmDisplayOrientation", wintypes.DWORD),
|
||||
("dmDisplayFixedOutput", wintypes.DWORD),
|
||||
("dmColor", wintypes.SHORT),
|
||||
("dmDuplex", wintypes.SHORT),
|
||||
("dmYResolution", wintypes.SHORT),
|
||||
("dmTTOption", wintypes.SHORT),
|
||||
("dmCollate", wintypes.SHORT),
|
||||
("dmFormName", wintypes.WCHAR * 32),
|
||||
("dmLogPixels", wintypes.WORD),
|
||||
("dmBitsPerPel", wintypes.DWORD),
|
||||
("dmPelsWidth", wintypes.DWORD),
|
||||
("dmPelsHeight", wintypes.DWORD),
|
||||
("dmDisplayFlags", wintypes.DWORD),
|
||||
("dmDisplayFrequency", wintypes.DWORD),
|
||||
("dmICMMethod", wintypes.DWORD),
|
||||
("dmICMIntent", wintypes.DWORD),
|
||||
("dmMediaType", wintypes.DWORD),
|
||||
("dmDitherType", wintypes.DWORD),
|
||||
("dmReserved1", wintypes.DWORD),
|
||||
("dmReserved2", wintypes.DWORD),
|
||||
("dmPanningWidth", wintypes.DWORD),
|
||||
("dmPanningHeight", wintypes.DWORD),
|
||||
]
|
||||
|
||||
enum_display_devices = ctypes.windll.user32.EnumDisplayDevicesW
|
||||
enum_display_settings = ctypes.windll.user32.EnumDisplaySettingsW
|
||||
enum_display_devices.argtypes = [
|
||||
wintypes.LPCWSTR,
|
||||
wintypes.DWORD,
|
||||
ctypes.POINTER(DISPLAY_DEVICEW),
|
||||
wintypes.DWORD,
|
||||
]
|
||||
enum_display_devices.restype = wintypes.BOOL
|
||||
enum_display_settings.argtypes = [
|
||||
wintypes.LPCWSTR,
|
||||
wintypes.DWORD,
|
||||
ctypes.POINTER(DEVMODEW),
|
||||
]
|
||||
enum_display_settings.restype = wintypes.BOOL
|
||||
|
||||
DISPLAY_DEVICE_ATTACHED_TO_DESKTOP = 0x00000001
|
||||
ENUM_CURRENT_SETTINGS = 0xFFFFFFFF
|
||||
rates = []
|
||||
index = 0
|
||||
|
||||
while True:
|
||||
device = DISPLAY_DEVICEW()
|
||||
device.cb = ctypes.sizeof(DISPLAY_DEVICEW)
|
||||
if not enum_display_devices(None, index, ctypes.byref(device), 0):
|
||||
break
|
||||
index += 1
|
||||
|
||||
if not (device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP):
|
||||
continue
|
||||
|
||||
mode = DEVMODEW()
|
||||
mode.dmSize = ctypes.sizeof(DEVMODEW)
|
||||
if not enum_display_settings(
|
||||
device.DeviceName,
|
||||
ENUM_CURRENT_SETTINGS,
|
||||
ctypes.byref(mode),
|
||||
):
|
||||
continue
|
||||
|
||||
refresh_rate = int(mode.dmDisplayFrequency)
|
||||
# Windows may report 0/1 when the rate is unknown/default.
|
||||
if 1 < refresh_rate < 10000:
|
||||
rates.append(refresh_rate)
|
||||
|
||||
if rates:
|
||||
return max(rates)
|
||||
except (AttributeError, OSError, TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return fallback
|
||||
|
||||
|
||||
def read_dxvk_fps_limit(config_path):
|
||||
"""Return (enabled, fps) for the first positive DXVK maxFrameRate line, or None."""
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8", errors="ignore") as handle:
|
||||
lines = handle.read().splitlines()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
for line in lines:
|
||||
match = _MAX_FRAME_RATE_RE.match(line)
|
||||
if match is None:
|
||||
continue
|
||||
try:
|
||||
value = int(match.group("value"))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if value <= 0:
|
||||
continue
|
||||
return match.group("comment") is None, value
|
||||
return None
|
||||
|
||||
|
||||
def apply_dxvk_fps_limit(config_path, enabled, fps):
|
||||
"""Atomically set or comment DXVK's d3d9.maxFrameRate option."""
|
||||
try:
|
||||
value = int(fps)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError("DXVK FPS limit must be a positive integer.") from exc
|
||||
if value <= 0:
|
||||
raise ValueError("DXVK FPS limit must be a positive integer.")
|
||||
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8", errors="ignore") as handle:
|
||||
existing = handle.read()
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"Could not read dxvk.conf: {exc}") from exc
|
||||
|
||||
setting = (
|
||||
f"d3d9.maxFrameRate = {value}"
|
||||
if enabled
|
||||
else f"# d3d9.maxFrameRate = {value}"
|
||||
)
|
||||
|
||||
output = []
|
||||
replaced = False
|
||||
for line in existing.splitlines():
|
||||
if _MAX_FRAME_RATE_RE.match(line):
|
||||
if not replaced:
|
||||
output.append(setting)
|
||||
replaced = True
|
||||
# Drop duplicate maxFrameRate lines, including legacy signed values,
|
||||
# so only the user's selected setting can win.
|
||||
continue
|
||||
output.append(line)
|
||||
|
||||
if not replaced:
|
||||
if output and output[-1] != "":
|
||||
output.append("")
|
||||
output.append(setting)
|
||||
|
||||
updated = "\n".join(output) + "\n"
|
||||
staged = config_path + ".modernization-new"
|
||||
try:
|
||||
with open(staged, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(updated)
|
||||
os.replace(staged, config_path)
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"Could not update dxvk.conf: {exc}") from exc
|
||||
finally:
|
||||
if os.path.exists(staged):
|
||||
try:
|
||||
os.remove(staged)
|
||||
except OSError:
|
||||
pass
|
||||
+214
-2
@@ -1,15 +1,38 @@
|
||||
import json
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import messagebox, ttk
|
||||
|
||||
import dxvk_fps
|
||||
import setup_tool_dynamic
|
||||
|
||||
|
||||
class ResponsiveModernWowSetupTool(setup_tool_dynamic.ModernWowSetupTool):
|
||||
"""Production entry point with DPI/display-scaling-safe window behavior."""
|
||||
"""Production entry point with display-scaling-safe UI and DXVK FPS control."""
|
||||
|
||||
def __init__(self, root):
|
||||
# Define these before the parent constructor: WowSetupTool.__init__ calls
|
||||
# overridden UI/settings methods while it is still initializing.
|
||||
self.detected_refresh_rate = dxvk_fps.detect_max_refresh_rate()
|
||||
self.limit_dxvk_fps = tk.BooleanVar(master=root, value=True)
|
||||
self.dxvk_fps_limit = tk.IntVar(
|
||||
master=root,
|
||||
value=self.detected_refresh_rate,
|
||||
)
|
||||
self.dxvk_fps_checkbox = None
|
||||
self.dxvk_fps_entry = None
|
||||
self.dxvk_fps_unit_label = None
|
||||
|
||||
super().__init__(root)
|
||||
|
||||
# Keep the FPS controls synchronized when the renderer radio selection
|
||||
# changes or when saved settings restore a different renderer.
|
||||
self._rendering_mode_trace = self.rendering_mode.trace_add(
|
||||
"write",
|
||||
lambda *_args: self._update_dxvk_fps_state(),
|
||||
)
|
||||
self._update_dxvk_fps_state()
|
||||
|
||||
# Preserve all existing installer behavior and only change how the
|
||||
# top-level Tk window allocates and exposes its controls.
|
||||
root.resizable(True, True)
|
||||
@@ -44,6 +67,195 @@ class ResponsiveModernWowSetupTool(setup_tool_dynamic.ModernWowSetupTool):
|
||||
root.geometry(f"{width}x{height}")
|
||||
root.minsize(min(640, max_width), min(520, max_height))
|
||||
|
||||
def build_main_tab(self, parent):
|
||||
super().build_main_tab(parent)
|
||||
|
||||
optional_label = None
|
||||
for child in parent.winfo_children():
|
||||
try:
|
||||
if child.cget("text") == "Optional Mods:":
|
||||
optional_label = child
|
||||
break
|
||||
except tk.TclError:
|
||||
continue
|
||||
|
||||
fps_frame = ttk.Frame(parent)
|
||||
pack_options = {
|
||||
"fill": "x",
|
||||
"padx": 20,
|
||||
"pady": (8, 2),
|
||||
}
|
||||
if optional_label is not None:
|
||||
pack_options["before"] = optional_label
|
||||
fps_frame.pack(**pack_options)
|
||||
|
||||
controls = ttk.Frame(fps_frame)
|
||||
controls.pack(anchor="w")
|
||||
|
||||
self.dxvk_fps_checkbox = ttk.Checkbutton(
|
||||
controls,
|
||||
text="Limit DXVK FPS",
|
||||
variable=self.limit_dxvk_fps,
|
||||
command=self._update_dxvk_fps_state,
|
||||
)
|
||||
self.dxvk_fps_checkbox.pack(side="left")
|
||||
|
||||
self.dxvk_fps_entry = ttk.Entry(
|
||||
controls,
|
||||
textvariable=self.dxvk_fps_limit,
|
||||
width=7,
|
||||
)
|
||||
self.dxvk_fps_entry.pack(side="left", padx=(10, 4))
|
||||
|
||||
self.dxvk_fps_unit_label = ttk.Label(controls, text="FPS")
|
||||
self.dxvk_fps_unit_label.pack(side="left")
|
||||
|
||||
help_label = ttk.Label(
|
||||
fps_frame,
|
||||
text=(
|
||||
"Auto-detected from the fastest active display at startup. "
|
||||
"You can change it manually. DXVK only."
|
||||
),
|
||||
font=("Segoe UI", 8, "italic"),
|
||||
)
|
||||
help_label.pack(anchor="w", padx=(22, 0), pady=(1, 0))
|
||||
|
||||
tooltip = (
|
||||
"Limits DXVK with d3d9.maxFrameRate. The initial value uses the "
|
||||
f"highest active display refresh rate detected at startup "
|
||||
f"({self.detected_refresh_rate} Hz). You can enter another positive "
|
||||
"integer. DirectX 9 is never modified by this option."
|
||||
)
|
||||
setup_tool_dynamic.ToolTip(self.dxvk_fps_checkbox, tooltip)
|
||||
setup_tool_dynamic.ToolTip(self.dxvk_fps_entry, tooltip)
|
||||
|
||||
self._update_dxvk_fps_state()
|
||||
|
||||
def _update_dxvk_fps_state(self):
|
||||
is_dxvk = getattr(self, "rendering_mode", None) is not None and (
|
||||
self.rendering_mode.get() == "dxvk"
|
||||
)
|
||||
enabled = bool(self.limit_dxvk_fps.get())
|
||||
|
||||
checkbox = getattr(self, "dxvk_fps_checkbox", None)
|
||||
entry = getattr(self, "dxvk_fps_entry", None)
|
||||
unit_label = getattr(self, "dxvk_fps_unit_label", None)
|
||||
|
||||
if checkbox is not None:
|
||||
checkbox.configure(state="normal" if is_dxvk else "disabled")
|
||||
if entry is not None:
|
||||
entry.configure(state="normal" if is_dxvk and enabled else "disabled")
|
||||
if unit_label is not None:
|
||||
if is_dxvk and enabled:
|
||||
unit_label.state(["!disabled"])
|
||||
else:
|
||||
unit_label.state(["disabled"])
|
||||
|
||||
def _collect_settings(self):
|
||||
settings = super()._collect_settings()
|
||||
try:
|
||||
fps_value = int(self.dxvk_fps_limit.get())
|
||||
except (tk.TclError, TypeError, ValueError):
|
||||
fps_value = int(self.detected_refresh_rate)
|
||||
|
||||
settings["dxvk_fps_limit"] = {
|
||||
"enabled": bool(self.limit_dxvk_fps.get()),
|
||||
"value": fps_value,
|
||||
}
|
||||
return settings
|
||||
|
||||
def _apply_settings_dict(self, saved):
|
||||
super()._apply_settings_dict(saved)
|
||||
|
||||
fps_settings = saved.get("dxvk_fps_limit") if isinstance(saved, dict) else None
|
||||
if isinstance(fps_settings, dict):
|
||||
enabled = fps_settings.get("enabled")
|
||||
value = fps_settings.get("value")
|
||||
if isinstance(enabled, bool):
|
||||
self.limit_dxvk_fps.set(enabled)
|
||||
if (
|
||||
isinstance(value, int)
|
||||
and not isinstance(value, bool)
|
||||
and value > 0
|
||||
):
|
||||
self.dxvk_fps_limit.set(value)
|
||||
|
||||
self._update_dxvk_fps_state()
|
||||
|
||||
def load_settings(self, target_dir):
|
||||
# Existing v2.3 installations predate this option. Preserve their
|
||||
# current DXVK limit state instead of silently enabling a new cap.
|
||||
has_saved_fps_setting = False
|
||||
settings_path = self._settings_path(target_dir)
|
||||
try:
|
||||
with open(settings_path, "r", encoding="utf-8") as handle:
|
||||
saved = json.load(handle)
|
||||
has_saved_fps_setting = isinstance(
|
||||
saved.get("dxvk_fps_limit") if isinstance(saved, dict) else None,
|
||||
dict,
|
||||
)
|
||||
except (OSError, json.JSONDecodeError, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
loaded = super().load_settings(target_dir)
|
||||
|
||||
if (
|
||||
not has_saved_fps_setting
|
||||
and self._looks_like_managed_install(target_dir)
|
||||
and self.rendering_mode.get() == "dxvk"
|
||||
):
|
||||
existing = dxvk_fps.read_dxvk_fps_limit(
|
||||
os.path.join(target_dir, "dxvk.conf")
|
||||
)
|
||||
if existing is None:
|
||||
self.limit_dxvk_fps.set(False)
|
||||
else:
|
||||
enabled, value = existing
|
||||
self.limit_dxvk_fps.set(enabled)
|
||||
self.dxvk_fps_limit.set(value)
|
||||
|
||||
self._update_dxvk_fps_state()
|
||||
return loaded
|
||||
|
||||
def validate_limits(self):
|
||||
if not super().validate_limits():
|
||||
return False
|
||||
|
||||
if self.rendering_mode.get() != "dxvk" or not self.limit_dxvk_fps.get():
|
||||
return True
|
||||
|
||||
try:
|
||||
value = int(self.dxvk_fps_limit.get())
|
||||
except (tk.TclError, TypeError, ValueError):
|
||||
messagebox.showerror(
|
||||
"Input Error",
|
||||
"DXVK FPS Limit must contain a positive whole number.",
|
||||
)
|
||||
return False
|
||||
|
||||
if value <= 0:
|
||||
messagebox.showerror(
|
||||
"Input Error",
|
||||
"DXVK FPS Limit must be greater than 0.",
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def configure_dxvk(self, target):
|
||||
super().configure_dxvk(target)
|
||||
|
||||
# DirectX 9 follows the existing renderer cleanup path above. Never
|
||||
# create or modify dxvk.conf when DXVK is not selected.
|
||||
if self.rendering_mode.get() != "dxvk":
|
||||
return
|
||||
|
||||
dxvk_fps.apply_dxvk_fps_limit(
|
||||
os.path.join(target, "dxvk.conf"),
|
||||
bool(self.limit_dxvk_fps.get()),
|
||||
int(self.dxvk_fps_limit.get()),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import ctypes
|
||||
import os
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import dxvk_fps
|
||||
import setup_tool_dynamic
|
||||
import setup_tool_responsive
|
||||
|
||||
|
||||
class FakeVar:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
def set(self, value):
|
||||
self.value = value
|
||||
|
||||
|
||||
class DxvkFpsLimitTests(unittest.TestCase):
|
||||
def write_config(self, root, text):
|
||||
path = os.path.join(root, "dxvk.conf")
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(text)
|
||||
return path
|
||||
|
||||
def read_config(self, path):
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
def detect_with_fake_displays(self, devices, default=77):
|
||||
"""Run Windows display detection against deterministic fake Win32 APIs."""
|
||||
# Each record is (device name, state flags, refresh rate, settings_ok).
|
||||
def enum_display_devices(_device, index, device_ptr, _flags):
|
||||
if index >= len(devices):
|
||||
return False
|
||||
name, state_flags, _rate, _settings_ok = devices[index]
|
||||
device_ptr._obj.DeviceName = name
|
||||
device_ptr._obj.StateFlags = state_flags
|
||||
return True
|
||||
|
||||
def enum_display_settings(device_name, _mode, mode_ptr):
|
||||
for name, _state_flags, rate, settings_ok in devices:
|
||||
if name != device_name:
|
||||
continue
|
||||
if not settings_ok:
|
||||
return False
|
||||
mode_ptr._obj.dmDisplayFrequency = rate
|
||||
return True
|
||||
return False
|
||||
|
||||
fake_user32 = types.SimpleNamespace(
|
||||
EnumDisplayDevicesW=enum_display_devices,
|
||||
EnumDisplaySettingsW=enum_display_settings,
|
||||
)
|
||||
fake_windll = types.SimpleNamespace(user32=fake_user32)
|
||||
|
||||
with mock.patch.object(dxvk_fps.os, "name", "nt"), mock.patch.object(
|
||||
ctypes,
|
||||
"windll",
|
||||
fake_windll,
|
||||
create=True,
|
||||
):
|
||||
return dxvk_fps.detect_max_refresh_rate(default=default)
|
||||
|
||||
def test_enable_replaces_bundled_commented_value_and_preserves_other_options(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
path = self.write_config(
|
||||
root,
|
||||
"# DXVK configuration\n"
|
||||
"# d3d9.maxFrameRate = 1000\n"
|
||||
"dxvk.allowFse = False\n",
|
||||
)
|
||||
|
||||
dxvk_fps.apply_dxvk_fps_limit(path, True, 165)
|
||||
updated = self.read_config(path)
|
||||
|
||||
self.assertIn("d3d9.maxFrameRate = 165\n", updated)
|
||||
self.assertNotIn("# d3d9.maxFrameRate = 1000", updated)
|
||||
self.assertIn("dxvk.allowFse = False\n", updated)
|
||||
self.assertEqual(updated.count("d3d9.maxFrameRate"), 1)
|
||||
self.assertEqual(dxvk_fps.read_dxvk_fps_limit(path), (True, 165))
|
||||
|
||||
def test_disable_comments_the_setting(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
path = self.write_config(
|
||||
root,
|
||||
"d3d9.maxFrameRate = 144\n"
|
||||
"dxvk.numCompilerThreads = 4\n",
|
||||
)
|
||||
|
||||
dxvk_fps.apply_dxvk_fps_limit(path, False, 144)
|
||||
updated = self.read_config(path)
|
||||
|
||||
self.assertIn("# d3d9.maxFrameRate = 144\n", updated)
|
||||
self.assertNotIn("\nd3d9.maxFrameRate = 144\n", "\n" + updated)
|
||||
self.assertEqual(dxvk_fps.read_dxvk_fps_limit(path), (False, 144))
|
||||
|
||||
def test_duplicate_limit_lines_are_collapsed_to_one(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
path = self.write_config(
|
||||
root,
|
||||
"d3d9.maxFrameRate = 60\n"
|
||||
"# d3d9.maxFrameRate = 120\n"
|
||||
"dxvk.allowFse = False\n",
|
||||
)
|
||||
|
||||
dxvk_fps.apply_dxvk_fps_limit(path, True, 240)
|
||||
updated = self.read_config(path)
|
||||
|
||||
self.assertEqual(updated.count("d3d9.maxFrameRate"), 1)
|
||||
self.assertIn("d3d9.maxFrameRate = 240\n", updated)
|
||||
self.assertIn("dxvk.allowFse = False\n", updated)
|
||||
|
||||
def test_signed_legacy_values_are_replaced_without_duplicates(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
path = self.write_config(
|
||||
root,
|
||||
"d3d9.maxFrameRate = -120\n"
|
||||
"# d3d9.maxFrameRate = +90\n"
|
||||
"dxvk.allowFse = False\n",
|
||||
)
|
||||
|
||||
dxvk_fps.apply_dxvk_fps_limit(path, True, 165)
|
||||
updated = self.read_config(path)
|
||||
|
||||
self.assertEqual(updated.count("d3d9.maxFrameRate"), 1)
|
||||
self.assertIn("d3d9.maxFrameRate = 165\n", updated)
|
||||
self.assertNotIn("-120", updated)
|
||||
self.assertNotIn("+90", updated)
|
||||
self.assertIn("dxvk.allowFse = False\n", updated)
|
||||
|
||||
def test_missing_limit_line_is_appended(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
path = self.write_config(root, "dxvk.allowFse = False\n")
|
||||
|
||||
dxvk_fps.apply_dxvk_fps_limit(path, True, 75)
|
||||
updated = self.read_config(path)
|
||||
|
||||
self.assertIn("dxvk.allowFse = False\n", updated)
|
||||
self.assertTrue(updated.endswith("d3d9.maxFrameRate = 75\n"))
|
||||
|
||||
def test_invalid_values_are_rejected_without_modifying_file(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
original = "# d3d9.maxFrameRate = 1000\n"
|
||||
path = self.write_config(root, original)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
dxvk_fps.apply_dxvk_fps_limit(path, True, 0)
|
||||
|
||||
self.assertEqual(self.read_config(path), original)
|
||||
|
||||
def test_refresh_detection_uses_fastest_attached_valid_display(self):
|
||||
detected = self.detect_with_fake_displays(
|
||||
[
|
||||
("DISPLAY1", 0x00000001, 60, True),
|
||||
# Faster but not attached to the desktop: must be ignored.
|
||||
("DISPLAY2", 0x00000000, 360, True),
|
||||
("DISPLAY3", 0x00000001, 165, True),
|
||||
# Windows unknown/default frequency: must be ignored.
|
||||
("DISPLAY4", 0x00000001, 1, True),
|
||||
# Current settings query failed: must be ignored.
|
||||
("DISPLAY5", 0x00000001, 240, False),
|
||||
],
|
||||
default=77,
|
||||
)
|
||||
self.assertEqual(detected, 165)
|
||||
|
||||
def test_refresh_detection_falls_back_when_no_usable_display_rate_exists(self):
|
||||
detected = self.detect_with_fake_displays(
|
||||
[
|
||||
("DISPLAY1", 0x00000000, 240, True),
|
||||
("DISPLAY2", 0x00000001, 0, True),
|
||||
("DISPLAY3", 0x00000001, 1, True),
|
||||
("DISPLAY4", 0x00000001, 165, False),
|
||||
],
|
||||
default=77,
|
||||
)
|
||||
self.assertEqual(detected, 77)
|
||||
|
||||
def test_refresh_detection_always_returns_a_positive_integer(self):
|
||||
detected = dxvk_fps.detect_max_refresh_rate(default=77)
|
||||
self.assertIsInstance(detected, int)
|
||||
self.assertGreater(detected, 0)
|
||||
|
||||
@mock.patch.object(setup_tool_dynamic.ModernWowSetupTool, "configure_dxvk")
|
||||
@mock.patch("setup_tool_responsive.dxvk_fps.apply_dxvk_fps_limit")
|
||||
def test_directx9_never_applies_fps_config(self, apply_limit, parent_configure):
|
||||
tool = setup_tool_responsive.ResponsiveModernWowSetupTool.__new__(
|
||||
setup_tool_responsive.ResponsiveModernWowSetupTool
|
||||
)
|
||||
tool.rendering_mode = FakeVar("directx9")
|
||||
tool.limit_dxvk_fps = FakeVar(True)
|
||||
tool.dxvk_fps_limit = FakeVar(165)
|
||||
|
||||
tool.configure_dxvk("C:/WoW")
|
||||
|
||||
parent_configure.assert_called_once_with("C:/WoW")
|
||||
apply_limit.assert_not_called()
|
||||
|
||||
@mock.patch.object(setup_tool_dynamic.ModernWowSetupTool, "configure_dxvk")
|
||||
@mock.patch("setup_tool_responsive.dxvk_fps.apply_dxvk_fps_limit")
|
||||
def test_dxvk_applies_selected_fps_value(self, apply_limit, parent_configure):
|
||||
tool = setup_tool_responsive.ResponsiveModernWowSetupTool.__new__(
|
||||
setup_tool_responsive.ResponsiveModernWowSetupTool
|
||||
)
|
||||
tool.rendering_mode = FakeVar("dxvk")
|
||||
tool.limit_dxvk_fps = FakeVar(True)
|
||||
tool.dxvk_fps_limit = FakeVar(144)
|
||||
|
||||
tool.configure_dxvk("C:/WoW")
|
||||
|
||||
parent_configure.assert_called_once_with("C:/WoW")
|
||||
apply_limit.assert_called_once_with(
|
||||
os.path.join("C:/WoW", "dxvk.conf"),
|
||||
True,
|
||||
144,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user