202 lines
6.6 KiB
Python
202 lines
6.6 KiB
Python
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
|