From 8d4ce2d7c6407feb5252b6c7b8f523e93b49a174 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Sun, 6 Sep 2026 10:04:09 +0200 Subject: [PATCH] Add DXVK FPS limit helpers --- dxvk_fps.py | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 dxvk_fps.py diff --git a/dxvk_fps.py b/dxvk_fps.py new file mode 100644 index 0000000..def8a5f --- /dev/null +++ b/dxvk_fps.py @@ -0,0 +1,200 @@ +import os +import re + + +DEFAULT_REFRESH_RATE = 60 +_MAX_FRAME_RATE_RE = re.compile( + r"^\s*(?P#\s*)?d3d9\.maxFrameRate\s*=\s*(?P\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_ACTIVE = 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_ACTIVE): + 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 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 so only one 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