mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
Detect Samsung's Vulkan HAL (vulkan.samsung.so) and its sgpu render node alongside PowerVR, and run Xclipse through the same libhybris bridge: - VK_LAYER_DROIDDECK_xclipse (tools/linuxfs/xclipse-layer), built with the glibc preloads and enabled only on Xclipse: sets bionic's environ for the driver's shader compiler, avoids immutable YCbCr samplers in compute layouts, forwards the KHR calibrated-timestamp entry points to the EXT ones, and reports textureCompressionBC and BC4/BC5 filtering over the compat layer's BC emulation. - Upstream DXVK 2.6.2, pinned as a tar.gz component and selected on Xclipse: the driver has 128 bytes of push constants, DXVK 3 needs 256. - android_wlegl accepts a zero stride and passes it through unchanged: Samsung's gralloc records none for GPU-only buffers. Tested on a Galaxy A56 (Xclipse 540, driver 24.0.560): Steam Big Picture and PICO PARK (D3D9).
229 lines
9.9 KiB
Python
229 lines
9.9 KiB
Python
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
TOOLS = Path(__file__).resolve().parents[1]
|
|
WRAPPER_SOURCE = TOOLS / "hybris/run-proton.py"
|
|
DLL_NAMES = ("d3d8.dll", "d3d9.dll", "d3d10core.dll", "d3d11.dll", "dxgi.dll")
|
|
DXVK_X64_ANCHOR = 'g_proton.arch_pe_dir("wine/dxvk", False) + f + ".dll"'
|
|
DXVK_X32_ANCHOR = 'g_proton.arch_pe_dir("wine/dxvk", True) + f + ".dll"'
|
|
PREFIX_ANCHOR = " # check whether any prefix config has changed"
|
|
|
|
|
|
PROTON_SOURCE = '''import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
from neighbor import NEIGHBOR
|
|
|
|
|
|
class Proton:
|
|
def arch_pe_dir(self, directory, is32):
|
|
return str(Path(__file__).parent / "stock" / ("x32" if is32 else "x64")) + "/"
|
|
|
|
|
|
g_proton = Proton()
|
|
|
|
|
|
def prefix_marker():
|
|
if True:
|
|
prefix_info = "stock"
|
|
# check whether any prefix config has changed
|
|
return prefix_info
|
|
|
|
|
|
def payloads():
|
|
result = {"x64": {}, "x32": {}}
|
|
for f in ("d3d8", "d3d9", "d3d10core", "d3d11", "dxgi"):
|
|
x64 = g_proton.arch_pe_dir("wine/dxvk", False) + f + ".dll"
|
|
x32 = g_proton.arch_pe_dir("wine/dxvk", True) + f + ".dll"
|
|
result["x64"][f + ".dll"] = Path(x64).read_text()
|
|
result["x32"][f + ".dll"] = Path(x32).read_text()
|
|
return result
|
|
|
|
|
|
def main():
|
|
record = {
|
|
"argv0": sys.argv[0],
|
|
"argv": sys.argv[1:],
|
|
"file": __file__,
|
|
"neighbor": NEIGHBOR,
|
|
"prefix": prefix_marker(),
|
|
"payloads": payloads(),
|
|
"openvr": g_proton.arch_pe_dir("wine/dxvk", False) + "openvr_api_dxvk.dll",
|
|
}
|
|
Path(os.environ["PROBE"]).write_text(json.dumps(record))
|
|
raise SystemExit(int(os.environ.get("PROTON_EXIT", "0")))
|
|
|
|
|
|
main()
|
|
'''
|
|
|
|
|
|
class ProtonWrapperTest(unittest.TestCase):
|
|
def fixture(self, root):
|
|
wrapper_dir = root / 'DXVK profile "quotes" $dollar `backticks`'
|
|
wrapper_dir.mkdir(parents=True)
|
|
wrapper = wrapper_dir / "run-proton.py"
|
|
shutil.copy2(WRAPPER_SOURCE, wrapper)
|
|
|
|
dxvk = wrapper_dir / "dxvk"
|
|
for architecture in ("x32", "x64"):
|
|
payload = dxvk / architecture
|
|
payload.mkdir(parents=True)
|
|
for name in DLL_NAMES:
|
|
(payload / name).write_text(f"{architecture}:{name}")
|
|
|
|
depot = root / "Proton depot with spaces"
|
|
depot.mkdir()
|
|
proton = depot / "selected proton.py"
|
|
proton.write_text(PROTON_SOURCE)
|
|
(depot / "neighbor.py").write_text('NEIGHBOR = "imported from selected depot"\n')
|
|
return {"wrapper": wrapper, "dxvk": dxvk, "depot": depot, "proton": proton,
|
|
"probe": root / "proton-ran.json"}
|
|
|
|
def run_wrapper(self, fixture, *proton_args, check=False, exit_status=0, env=None):
|
|
command = [sys.executable, str(fixture["wrapper"])]
|
|
if check:
|
|
command.append("--check")
|
|
command.extend((str(fixture["proton"]), *proton_args))
|
|
return subprocess.run(
|
|
command,
|
|
cwd=fixture["wrapper"].parent,
|
|
env={"PATH": os.defpath, "PROBE": str(fixture["probe"]),
|
|
"PROTON_EXIT": str(exit_status), **(env or {})},
|
|
text=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
def test_launch_uses_both_staged_payloads_and_selected_proton_context(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
fixture = self.fixture(Path(tmp))
|
|
original_source = fixture["proton"].read_bytes()
|
|
arguments = ("waitforexitandrun", "game with spaces.exe", 'quoted "value"',
|
|
"$literal-dollar", "`literal-backticks`")
|
|
result = self.run_wrapper(fixture, *arguments)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(fixture["proton"].read_bytes(), original_source)
|
|
record = json.loads(fixture["probe"].read_text())
|
|
self.assertEqual(record["argv0"], str(fixture["proton"]))
|
|
self.assertEqual(record["argv"], list(arguments))
|
|
self.assertEqual(record["file"], str(fixture["proton"]))
|
|
self.assertEqual(record["neighbor"], "imported from selected depot")
|
|
self.assertEqual(record["prefix"], "stock\ndroiddeck-dxvk-profile-v1")
|
|
self.assertEqual(record["openvr"], str(fixture["depot"] / "stock/x64/openvr_api_dxvk.dll"))
|
|
for architecture in ("x32", "x64"):
|
|
self.assertEqual(
|
|
record["payloads"][architecture],
|
|
{name: f"{architecture}:{name}" for name in DLL_NAMES},
|
|
)
|
|
|
|
def test_named_sibling_payload_replaces_the_default(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
fixture = self.fixture(Path(tmp))
|
|
for architecture in ("x32", "x64"):
|
|
payload = fixture["wrapper"].parent / "dxvk-2.6.2" / architecture
|
|
payload.mkdir(parents=True)
|
|
for name in DLL_NAMES:
|
|
(payload / name).write_text(f"2.6.2 {architecture}:{name}")
|
|
result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe",
|
|
env={"DROIDDECK_HYBRIS_DXVK": "dxvk-2.6.2"})
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
record = json.loads(fixture["probe"].read_text())
|
|
for architecture in ("x32", "x64"):
|
|
self.assertEqual(
|
|
record["payloads"][architecture],
|
|
{name: f"2.6.2 {architecture}:{name}" for name in DLL_NAMES},
|
|
)
|
|
|
|
def test_payload_names_outside_the_wrapper_directory_are_refused(self):
|
|
for name in ("../dxvk", "dxvk/../../x", "/tmp/dxvk", "other", ".dxvk"):
|
|
with self.subTest(name=name), tempfile.TemporaryDirectory() as tmp:
|
|
fixture = self.fixture(Path(tmp))
|
|
result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe",
|
|
env={"DROIDDECK_HYBRIS_DXVK": name})
|
|
|
|
self.assertEqual(result.returncode, 1)
|
|
self.assertIn("invalid payload name", result.stderr)
|
|
self.assertFalse(fixture["probe"].exists())
|
|
|
|
def test_check_validates_without_executing_proton(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
fixture = self.fixture(Path(tmp))
|
|
result = self.run_wrapper(fixture, check=True)
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn("payload present and Proton layout supported", result.stdout)
|
|
self.assertFalse(fixture["probe"].exists())
|
|
|
|
def test_check_rejects_malformed_source_before_its_top_level_code_runs(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
fixture = self.fixture(Path(tmp))
|
|
source = fixture["proton"].read_text()
|
|
source = source.replace(
|
|
"import sys\n", 'import sys\n\nPath(os.environ["PROBE"]).write_text("top-level")\n', 1)
|
|
fixture["proton"].write_text(source + "\nthis is not valid =\n")
|
|
result = self.run_wrapper(fixture, check=True)
|
|
|
|
self.assertEqual(result.returncode, 1, result.stderr)
|
|
self.assertIn("DroidDeck DXVK profile:", result.stderr)
|
|
self.assertFalse(fixture["probe"].exists())
|
|
|
|
def test_preflight_rejects_each_missing_or_empty_payload_before_proton_runs(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
for state in ("missing", "empty"):
|
|
for architecture in ("x32", "x64"):
|
|
for name in DLL_NAMES:
|
|
with self.subTest(state=state, architecture=architecture, name=name):
|
|
fixture = self.fixture(root / f"{state}-{architecture}-{name}")
|
|
payload = fixture["dxvk"] / architecture / name
|
|
if state == "missing":
|
|
payload.unlink()
|
|
else:
|
|
payload.write_bytes(b"")
|
|
result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe")
|
|
self.assertEqual(result.returncode, 1, result.stderr)
|
|
self.assertIn(f"{state} DXVK file", result.stderr)
|
|
self.assertFalse(fixture["probe"].exists())
|
|
|
|
def test_preflight_rejects_changed_proton_layout_or_syntax_before_proton_runs(self):
|
|
mutations = []
|
|
for label, anchor in (("x64", DXVK_X64_ANCHOR), ("x32", DXVK_X32_ANCHOR),
|
|
("prefix", PREFIX_ANCHOR)):
|
|
mutations.extend(((f"missing-{label}", lambda text, anchor=anchor: text.replace(anchor, "missing_anchor", 1)),
|
|
(f"duplicate-{label}", lambda text, anchor=anchor: text + "\n# " + anchor + "\n")))
|
|
mutations.append(("syntax-error", lambda text: text + "\nthis is not valid =\n"))
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
for label, mutate in mutations:
|
|
with self.subTest(layout=label):
|
|
fixture = self.fixture(root / label)
|
|
fixture["proton"].write_text(mutate(fixture["proton"].read_text()))
|
|
result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe")
|
|
self.assertEqual(result.returncode, 1, result.stderr)
|
|
self.assertIn("DroidDeck DXVK profile:", result.stderr)
|
|
self.assertFalse(fixture["probe"].exists())
|
|
|
|
def test_proton_exit_status_is_propagated(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
fixture = self.fixture(Path(tmp))
|
|
result = self.run_wrapper(fixture, "waitforexitandrun", "game.exe", exit_status=23)
|
|
|
|
self.assertEqual(result.returncode, 23, result.stderr)
|
|
self.assertTrue(fixture["probe"].exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|