diff --git a/docs/development/game-environment.md b/docs/development/game-environment.md index 57c88bd92..060a7fbae 100644 --- a/docs/development/game-environment.md +++ b/docs/development/game-environment.md @@ -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 diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index 025d84d26..6f665494e 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -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": diff --git a/tools/tests/test_esync_packs.py b/tools/tests/test_esync_packs.py index 7ba20b448..25a0de711 100644 --- a/tools/tests/test_esync_packs.py +++ b/tools/tests/test_esync_packs.py @@ -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): diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index ebb611036..0585fc57f 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -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}