mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-10-26 21:17:40 +00:00
149 lines
7.9 KiB
Python
149 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Present a Direct3D 11 swap chain through DXVK on Raven to a Wayland window and compare what the compositor shows.
|
|
|
|
testdata/d3d11/window.exe renders sixty frames into a DXGI swap chain and presents each one. It reads back its first and last back
|
|
buffers with the GPU. This script runs it on an isolated headless compositor, waits until every frame was presented, captures the
|
|
compositor output with grim and requires that:
|
|
|
|
- the read-back last frame matches the reference rasterizer (check-d3d11-triangle.py);
|
|
- the read-back first frame differs from the last one in the colour the program ramps (frames really change);
|
|
- the captured window contains the read-back last frame, pixel for pixel, at one position (the swap chain content reached the screen);
|
|
- after the program resizes its window and swap chain to 320 by 200, a second capture shows the new, larger frame the GPU rendered
|
|
(read back with a different clear colour), again pixel for pixel, and the window outside it is not left holding the old picture.
|
|
|
|
With the extra argument "gpu" the host's default Vulkan driver is used instead of llvmpipe and the compositor renders with GLES so that it
|
|
offers buffer sharing; the test then also requires DXVK to report a driver other than llvmpipe.
|
|
|
|
usage: native-d3d11-present.py RAVEN WINDOW_EXE DXVK_PE64_DIRECTORY WORKDIR [gpu]
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from PIL import Image
|
|
|
|
SIZE = 256
|
|
GPU = len(sys.argv) > 5 and sys.argv[5] == "gpu"
|
|
RUNTIME = Path(f"/run/user/{os.getuid()}/raven-labwc-{'gpu' if GPU else 'runtime'}")
|
|
|
|
|
|
def environment():
|
|
return {"PATH": "/usr/bin:/bin", "XDG_RUNTIME_DIR": str(RUNTIME), "WAYLAND_DISPLAY": "wayland-0"}
|
|
|
|
|
|
def start_compositor(log):
|
|
if (RUNTIME / "wayland-0").exists():
|
|
probe = subprocess.run(["wlrctl", "toplevel", "list"], env=environment(), capture_output=True, timeout=10)
|
|
if probe.returncode == 0:
|
|
return None
|
|
(RUNTIME / "wayland-0").unlink(missing_ok=True)
|
|
(RUNTIME / "wayland-0.lock").unlink(missing_ok=True)
|
|
RUNTIME.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
env = environment() | {"WLR_BACKENDS": "headless", "WLR_RENDERER": "gles2" if GPU else "pixman", "WLR_HEADLESS_OUTPUTS": "1"}
|
|
process = subprocess.Popen(["labwc"], env=env, stdout=log, stderr=log)
|
|
for _ in range(50):
|
|
if (RUNTIME / "wayland-0").exists():
|
|
time.sleep(0.5)
|
|
return process
|
|
time.sleep(0.1)
|
|
raise RuntimeError("isolated compositor did not start")
|
|
|
|
|
|
def rgb(raw, width=SIZE, height=SIZE):
|
|
image = Image.frombytes("RGBA", (width, height), raw)
|
|
return image.convert("RGB")
|
|
|
|
|
|
def compare(capture, raw, width, height, label):
|
|
"""Find the frame in the capture by its top-left pixel and require every pixel of it to match."""
|
|
screen = Image.open(capture).convert("RGB")
|
|
expected = rgb(raw, width, height)
|
|
clear = tuple(expected.getpixel((0, 0)))
|
|
origin = next(((x, y) for y in range(screen.height) for x in range(screen.width) if screen.getpixel((x, y)) == clear), None)
|
|
if origin is None:
|
|
raise SystemExit(f"{label}: the compositor output holds no pixel of the clear colour: the swap chain never reached the screen")
|
|
shown = screen.crop((origin[0], origin[1], origin[0] + width, origin[1] + height))
|
|
shown_bytes, expected_bytes = shown.tobytes(), expected.tobytes()
|
|
differing = sum(1 for index in range(0, len(expected_bytes), 3) if shown_bytes[index:index + 3] != expected_bytes[index:index + 3])
|
|
print(f"{label}: compositor capture at {origin}: {differing} of {width * height} pixels differ from the read-back frame")
|
|
if differing:
|
|
shown.save(capture.with_name(label + "-shown.png"))
|
|
raise SystemExit(f"{label}: the window on screen does not show the frame the GPU rendered")
|
|
return origin
|
|
|
|
|
|
def main():
|
|
raven, workload, dxvk, work = map(Path, sys.argv[1:5])
|
|
shutil.rmtree(work, ignore_errors=True)
|
|
run, prefix = work / "run", work / "prefix"
|
|
run.mkdir(parents=True)
|
|
for name in ("d3d11.dll", "dxgi.dll", "d3d10core.dll"):
|
|
shutil.copy(dxvk / name, run / name)
|
|
shutil.copy(workload, run / "window.exe")
|
|
with open(work / "compositor.log", "wb") as log:
|
|
compositor = start_compositor(log)
|
|
env = os.environ | environment() | {"RAVEN_PREFIX": str(prefix)}
|
|
env["PATH"] = os.environ["PATH"]
|
|
lavapipe = Path("/usr/share/vulkan/icd.d/lvp_icd.json")
|
|
if "VK_DRIVER_FILES" not in env and lavapipe.exists() and not GPU:
|
|
env["VK_DRIVER_FILES"] = str(lavapipe)
|
|
process = subprocess.Popen([str(raven), "--run-process", "--execution=native", "--timeout-ms=100000", "--desktop=wayland", str(run / "window.exe")],
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
|
|
status = prefix / "drive_c" / "window.txt"
|
|
try:
|
|
deadline = time.time() + 90
|
|
while time.time() < deadline and process.poll() is None and not status.exists():
|
|
time.sleep(0.5)
|
|
if not status.exists() or status.read_text() != "OK":
|
|
raise SystemExit(f"workload did not present all frames: {status.read_text() if status.exists() else 'missing'}")
|
|
time.sleep(1)
|
|
capture = work / "capture.png"
|
|
subprocess.run(["grim", str(capture)], env=environment(), check=True, timeout=20)
|
|
resized_marker = prefix / "drive_c" / "window_resized.txt"
|
|
deadline = time.time() + 90
|
|
while time.time() < deadline and process.poll() is None and not resized_marker.exists():
|
|
time.sleep(0.5)
|
|
if not resized_marker.exists() or resized_marker.read_text() != "OK":
|
|
raise SystemExit("the workload did not finish rendering after resizing")
|
|
time.sleep(1)
|
|
resized = work / "capture-resized.png"
|
|
subprocess.run(["grim", str(resized)], env=environment(), check=True, timeout=20)
|
|
finally:
|
|
process.terminate()
|
|
try:
|
|
process.wait(10)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
if compositor:
|
|
compositor.terminate()
|
|
drive = prefix / "drive_c"
|
|
driver = next((line.split(":", 1)[1].strip() for line in (drive / "window_d3d11.log").read_text().splitlines() if line.startswith("info: Driver")), "")
|
|
print(f"DXVK reports the driver: {driver}")
|
|
if GPU and (not driver or "llvmpipe" in driver.lower()):
|
|
raise SystemExit("the hardware variant ran on a software driver")
|
|
check = subprocess.run([sys.executable, "-I", str(Path(__file__).with_name("check-d3d11-triangle.py")), str(drive / "window_last.raw")],
|
|
capture_output=True, text=True, timeout=120)
|
|
sys.stdout.write(check.stdout)
|
|
if check.returncode:
|
|
raise SystemExit(check.stderr.strip() or "read-back frame does not match the reference")
|
|
first, last = (drive / "window_first.raw").read_bytes(), (drive / "window_last.raw").read_bytes()
|
|
if first[:4] == last[:4]:
|
|
raise SystemExit("the first and last frames have the same clear colour: the frames did not change")
|
|
print(f"first frame corner {tuple(first[:4])}, last frame corner {tuple(last[:4])}")
|
|
compare(capture, last, SIZE, SIZE, "first window")
|
|
check = subprocess.run([sys.executable, "-I", str(Path(__file__).with_name("check-d3d11-triangle.py")), str(drive / "window_resized.raw"), "320", "200",
|
|
"26", "77", "26"], capture_output=True, text=True, timeout=120)
|
|
sys.stdout.write(check.stdout)
|
|
if check.returncode:
|
|
raise SystemExit(check.stderr.strip() or "resized read-back frame does not match the reference")
|
|
compare(resized, (drive / "window_resized.raw").read_bytes(), 320, 200, "resized window")
|
|
print("a Direct3D 11 frame rendered by DXVK and Raven's Vulkan bridge is on screen, matches the GPU read-back, and keeps doing so after a resize")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|