fix(game-env): OpenGL games stop waiting for each screen refresh (#1715)

With no frame limit, AIO's Direct3D 7 (DirectDraw) test ran at exactly
144 fps on a 144 Hz panel: every frame waited one refresh (6.94 ms).
Wine asks for a swap interval of 1 on every OpenGL window, and WineD3D's
front-buffer drawing used by DirectDraw and Direct3D 1-7 never lowers
it; it only sets the interval in its present path. Mesa's Zink then
presents in FIFO.

droiddeck-game-env sets vblank_mode=0 for games launched through Proton
before the game's environment edits, so a value set in the launch
options or the editor wins and an editor Remove leaves Mesa's default.
Steam's own runs in compatdata/0 are unchanged. Steam's Frame Limit
still holds a game: gamescope's WSI layer presents every swapchain in
FIFO while its limiter is on.

On the AYANEO Pocket FIT, AIO Direct3D 7 went from 144 to 2,345 and
2,265 fps (1 % low 99 -> 620) with no limit; with the Quick Access
limit at 144 and 72 it held at 143.76 and 71.92.
This commit is contained in:
The412Banner authored and GitHub committed 2026-10-28 13:34:45 +00:00
1 parent 213c2da57e
commit 9d82df6981
4 files changed
+58 -1

No files matched your search

+9
View File
@@ -96,6 +96,15 @@ A Unity game under FEX (`UnityPlayer.dll` for Windows builds under Proton,
turns it off: without TSO such games can deadlock or crash (seen with Megabonk
and Crab Game). A FEX preset chosen for the game itself still applies.
Games launched through Proton get `vblank_mode=0`, so OpenGL does not wait for
each screen refresh. Wine asks for a swap interval of 1 on every OpenGL window, and WineD3D's front-buffer
drawing (DirectDraw, Direct3D 1–7) never lowers it, so those games ran at the
refresh rate even with no frame limit (AIO Direct3D 7: 144 fps at 144 Hz, about
2,260 with the wait off). Steam's Frame Limit still holds a game: while it is on,
gamescope's WSI layer presents every swapchain in FIFO at the limit. A
`vblank_mode` from the launch options or the environment editor is kept, and
**Remove** in the editor leaves Mesa's own default.
## Defaults and available suggestions
The initial configuration enables Mesa shader caching with
@@ -432,6 +432,16 @@ def cpu_topology(env, command=()):
print("droiddeck-game-env: Windows sees %d cores (%s)" % (len(cores), env["WINE_CPU_TOPOLOGY"]), file=sys.stderr)
def gl_vsync(env):
"""OpenGL stops waiting for each screen refresh; Steam's Frame Limit still holds the game through gamescope."""
# Wine asks for interval 1 on every GL window, and WineD3D's front-buffer drawing (DirectDraw,
# Direct3D 1-7) never lowers it, so those games ran at the refresh rate with no limit set.
if "vblank_mode" in env or game_id(env) is None:
return False
env["vblank_mode"] = "0"
return True
def apply_config(env, config, app_id, fixes=None):
if config.get("version") != 1:
raise ValueError("unsupported environment configuration")
@@ -729,6 +739,7 @@ def main():
digest = None
app_id = game_id(env)
if app_id:
gl_default = gl_vsync(env)
waiting = len(command) > 1 and command[1] == "waitforexitandrun"
fixes = {}
if waiting:
@@ -761,6 +772,8 @@ def main():
env = apply_game_debug_logs(env, config, debug_dir)
if env is not before_debug:
print("droiddeck-game-env: game debug logs enabled for this launch", file=sys.stderr)
if gl_default and env.get("vblank_mode") == "0":
print("droiddeck-game-env: OpenGL does not wait for the screen refresh (vblank_mode=0)", file=sys.stderr)
if app_id and len(command) > 1 and command[1] == "waitforexitandrun":
command = offline_without_anticheat(command, env)
if mic_enabled == "0":
+2 -1
View File
@@ -1085,7 +1085,8 @@ class LauncherTest(SyncTestCase):
self.assertEqual([call[:2] for call in calls], [(str(dist / "proton"), [str(dist / "proton")] + command[1:]),
(str(tool / "proton"), command)])
self.assertEqual((calls[0][2]["WINEESYNC"], calls[0][2]["PROTON_NO_ESYNC"], calls[0][2]["BL_SYNC_PACK"]), ("0", "1", pack["id"]))
self.assertEqual(calls[1][2], self.env(STEAM_COMPAT_APP_ID="2630", PROTON_NO_NTSYNC="1", BL_SYNC="0", WINEESYNC="1"))
self.assertEqual(calls[1][2], self.env(STEAM_COMPAT_APP_ID="2630", PROTON_NO_NTSYNC="1", BL_SYNC="0", WINEESYNC="1",
vblank_mode="0"))
self.assertIn("could not start (", said.getvalue())
def test_generated_launchers_run_the_dist_proton(self):
+34
View File
@@ -1,4 +1,5 @@
import hashlib
import io
import json
import os
import shutil
@@ -586,6 +587,39 @@ class GameEnvironmentTest(unittest.TestCase):
self.assertEqual(env.get("WINE_CPU_TOPOLOGY"), expected, env)
self.assertEqual(env.get("PROTON_CPU_TOPOLOGY"), proton, env)
def test_opengl_games_do_not_wait_for_the_screen_refresh(self):
cases = [
({}, "0"),
({"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/-686078602"}, "0"),
({"vblank_mode": "2"}, "2"),
({"vblank_mode": ""}, ""),
({"STEAM_COMPAT_DATA_PATH": "/steam/compatdata/0"}, None),
({"STEAM_COMPAT_DATA_PATH": ""}, None),
]
for env, expected in cases:
env.setdefault("STEAM_COMPAT_DATA_PATH", "/steam/compatdata/3608888694")
added = expected == "0" and "vblank_mode" not in env
self.assertEqual(MODULE["gl_vsync"](env), added, env)
self.assertEqual(env.get("vblank_mode"), expected, env)
said = "OpenGL does not wait for the screen refresh"
with tempfile.TemporaryDirectory() as tmp:
probe = Path(tmp) / "probe"
probe.write_text("#!" + sys.executable + "\nimport os\nprint(os.environ.get('vblank_mode', 'unset'))\n")
probe.chmod(0o755)
config = Path(tmp) / ".config/droiddeck/game-environment.json"
config.parent.mkdir(parents=True)
for inherited, edits, expected in (({}, None, "0"), ({"vblank_mode": "3"}, None, "3"),
({}, {"vblank_mode": None}, "unset"), ({}, {"vblank_mode": "2"}, "2"),
({"vblank_mode": "3"}, {"vblank_mode": None}, "unset")):
with self.subTest(inherited=inherited, edits=edits):
config.write_text(json.dumps({"version": 1, "games": {"42": edits or {}}}))
env = {name: value for name, value in os.environ.items() if name != "vblank_mode"}
env.update(HOME=tmp, STEAM_COMPAT_DATA_PATH="/compatdata/42", **inherited)
result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-env"), str(probe), "waitforexitandrun"],
env=env, text=True, capture_output=True, check=True)
self.assertEqual(result.stdout.strip(), expected)
self.assertEqual(said in result.stderr, expected == "0" and not inherited, result.stderr)
def test_windows_cpu_0_is_the_fastest_core(self):
os_mod = MODULE["os"]
pocket_fit = {0: 379, 1: 379, 2: 923, 3: 923, 4: 923, 5: 867, 6: 867, 7: 1024}