mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-11-03 21:46:32 +00:00
120 lines
6.7 KiB
Python
120 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare the frame rate of ClassiCube's game under Raven and under Wine on the same compositor and Vulkan driver.
|
|
|
|
GAME_EXE is a Direct3D 11 build of ClassiCube compiled with -DCC_BENCH_FPS: it draws without a frame limit, counts the frames between
|
|
five and fifteen seconds after its first loaded-world frame and writes the frames per second to bench_fps.txt beside itself, then exits. The program
|
|
starts straight into singleplayer (--singleplayer) with the texture and sound packs of GAME_DIR, the frame limit off and DXVK for
|
|
Direct3D. Each runtime runs REPEATS times on its own isolated headless compositor; the table shows the median.
|
|
|
|
usage: benchmark-classicube.py RAVEN WINE GAME_EXE GAME_DIR DXVK_PE64_DIRECTORY WORKDIR [REPEATS]
|
|
|
|
The measured ClassiCube source revision is 33891a4e7e586941240261bc911312f228be57d3 from
|
|
https://github.com/ClassiCube/ClassiCube. In that checkout, normalize src/Game.c and src/Server.c to LF before applying
|
|
testdata/classicube/benchmark.patch from this repository, then build with:
|
|
make windows CC=x86_64-w64-mingw32-gcc 'CFLAGS+=-DCC_GFX_BACKEND=CC_GFX_BACKEND_D3D11 -DCC_BENCH_FPS' -j2
|
|
Supply the game's texture and sound packs separately in GAME_DIR. The patch's upstream context keeps the
|
|
ClassiCube license in testdata/classicube/LICENSE; the game executable and packs are not distributed here.
|
|
The benchmark build fixes the generated world's seed to 1; runs use a 640 by 480 window and view distance 128.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import statistics
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
def wineserver(wine):
|
|
return next((str(path) for path in (wine.with_name("wineserver"), Path("/usr/lib/wine/wineserver"),
|
|
Path("/usr/lib/x86_64-linux-gnu/wine/wineserver")) if path.exists()), "wineserver")
|
|
|
|
|
|
def run_once(runtime, raven, wine, game, prefix, work):
|
|
runtime_dir = Path(f"/run/user/{os.getuid()}/raven-labwc-bench")
|
|
runtime_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
for name in ("wayland-0", "wayland-0.lock"):
|
|
(runtime_dir / name).unlink(missing_ok=True)
|
|
env = {"PATH": "/usr/bin:/bin", "HOME": os.environ["HOME"], "TMPDIR": os.environ.get("TMPDIR", str(work)), "XDG_RUNTIME_DIR": str(runtime_dir),
|
|
"WAYLAND_DISPLAY": "wayland-0", "VK_DRIVER_FILES": "/usr/share/vulkan/icd.d/lvp_icd.json", "LP_NUM_THREADS": "2"}
|
|
compositor = subprocess.Popen(["labwc"], env=env | {"WLR_BACKENDS": "headless", "WLR_RENDERER": "pixman", "WLR_HEADLESS_OUTPUTS": "1"},
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
try:
|
|
for _ in range(100):
|
|
if (runtime_dir / "wayland-0").exists():
|
|
break
|
|
time.sleep(0.1)
|
|
time.sleep(0.5)
|
|
if runtime == "raven":
|
|
env["RAVEN_PREFIX"] = str(prefix)
|
|
# The game reads its resources from its working directory; drive Z is the host's root.
|
|
directory = "--directory=Z:" + str(game.parent).replace("/", "\\")
|
|
command = [str(raven), "--run-process", "--execution=native", "--timeout-ms=120000", "--desktop=wayland", directory, str(game), "--",
|
|
"--singleplayer"]
|
|
else:
|
|
env |= {"WINEPREFIX": str(prefix), "WINEDEBUG": "-all", "WINEDLLOVERRIDES": "mscoree=;mshtml=;d3d11,dxgi,d3d10core=n"}
|
|
command = [str(wine), str(game), "--singleplayer"]
|
|
result = game.parent / "bench_fps.txt"
|
|
result.unlink(missing_ok=True)
|
|
process = subprocess.Popen(command, env=env, cwd=game.parent, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
try:
|
|
process.wait(timeout=float(os.environ.get("RAVEN_BENCH_TIMEOUT", "120")))
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait()
|
|
if runtime == "wine":
|
|
subprocess.run([wineserver(wine), "-k"], env=env, capture_output=True, timeout=60)
|
|
text = result.read_text().strip() if result.exists() else ""
|
|
config = game.parent / "bench_config.txt"
|
|
configured = config.is_file() and config.read_text().strip() == "1 128 64 128 640 480"
|
|
return int(text) if configured and process.returncode == 0 and re.fullmatch(r"[1-9]\d*", text) else None
|
|
finally:
|
|
compositor.terminate()
|
|
compositor.wait(10)
|
|
|
|
|
|
def main():
|
|
raven, wine, game_exe, game_dir, dxvk, work = (Path(value).resolve() for value in sys.argv[1:7])
|
|
repeats = int(sys.argv[7]) if len(sys.argv) > 7 else 3
|
|
shutil.rmtree(work, ignore_errors=True)
|
|
work.mkdir(parents=True)
|
|
warm = work / "wine-warm"
|
|
warm_env = {"PATH": "/usr/bin:/bin", "HOME": os.environ["HOME"], "WINEPREFIX": str(warm), "WINEDEBUG": "-all"}
|
|
subprocess.run([str(wine), "wineboot", "-u"], env=warm_env, capture_output=True, timeout=300)
|
|
subprocess.run([str(wine), "reg", "add", "HKCU\\Software\\Wine\\Drivers", "/v", "Graphics", "/d", "wayland", "/f"], env=warm_env,
|
|
capture_output=True, timeout=120)
|
|
subprocess.run([wineserver(wine), "-w"], env=warm_env, capture_output=True, timeout=120)
|
|
subprocess.run([wineserver(wine), "-k"], env=warm_env, capture_output=True, timeout=60)
|
|
rates = {"raven": [], "wine": []}
|
|
measurements = []
|
|
for runtime in ("raven", "wine"):
|
|
for index in range(repeats):
|
|
# A fresh copy of the game for every run: the program writes its options and the result beside itself.
|
|
game = work / f"game-{runtime}-{index}"
|
|
game.mkdir()
|
|
for name in ("texpacks", "audio"):
|
|
if (game_dir / name).exists():
|
|
shutil.copytree(game_dir / name, game / name)
|
|
(game / "options.txt").write_text("fpslimit=LimitNone\nwindow-width=640\nwindow-height=480\nviewdist=128\n")
|
|
shutil.copy(game_exe, game / "ClassiCube.exe")
|
|
for name in ("d3d11.dll", "dxgi.dll", "d3d10core.dll"):
|
|
shutil.copy(dxvk / name, game / name)
|
|
prefix = work / f"prefix-{runtime}-{index}"
|
|
if runtime == "wine":
|
|
shutil.copytree(warm, prefix, symlinks=True)
|
|
rate = run_once(runtime, raven, wine, game / "ClassiCube.exe", prefix, work)
|
|
measurements.append(dict(runtime=runtime, trial=index + 1, frames_per_second=rate))
|
|
print(runtime, index, rate, flush=True)
|
|
if rate:
|
|
rates[runtime].append(rate)
|
|
(work / "samples.json").write_text(json.dumps(measurements, indent=2) + "\n")
|
|
print(f"{'runtime':8} {'frames per second':>18} runs")
|
|
for runtime, values in rates.items():
|
|
print(f"{runtime:8} {statistics.median(values):18.1f} {len(values)}/{repeats}" if values else f"{runtime:8} {'-':>18} 0/{repeats}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|