aboutsummaryrefslogtreecommitdiffstats
path: root/steamvr
diff options
context:
space:
mode:
Diffstat (limited to 'steamvr')
-rwxr-xr-xsteamvr/patch-steamvr226
1 files changed, 226 insertions, 0 deletions
diff --git a/steamvr/patch-steamvr b/steamvr/patch-steamvr
new file mode 100755
index 0000000..2565974
--- /dev/null
+++ b/steamvr/patch-steamvr
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: 2026 Tomasz Kramkowski <tomasz@kramkow.ski>
+# SPDX-License-Identifier: CC0-1.0
+
+"""A steamvr launch command wrapper that patches various parts of steamvr
+to fix bugs.
+
+For SteamVR Beta 2.18.4, build 25998080.
+
+Instructions:
+ 1. Put this script somewhere, e.g. `~/.local/libexec/patch-steamvr`
+ 2. Make it executable
+ 3. Change the SteamVR launch options to:
+ `/absolute/path/to/patch-steamvr %command%`.
+ If you already have something in there, I'm sure you can figure it
+ out.
+ 4. Launch SteamVR normally. You should see a desktop notification with
+ some diagnostics whenever the script actually does/tries to do
+ something.
+
+Use at your own risk!
+"""
+
+import hashlib
+import os
+import stat
+import subprocess
+import sys
+import tempfile
+from dataclasses import dataclass
+from pathlib import Path
+
+NAME = Path(__file__).name
+MARKER = b"Valve, please fix."
+
+
+@dataclass(frozen=True)
+class Edit:
+ offset: int
+ replacement: bytes
+
+
+@dataclass(frozen=True)
+class Patch:
+ original_hash: bytes
+ patched_hash: bytes
+ edits: tuple[Edit, ...]
+
+ def apply_to(self, data: bytes) -> bytes:
+ payload = data.removesuffix(MARKER)
+ digest = sha256(payload)
+ if digest == self.patched_hash:
+ return payload + MARKER
+ if digest != self.original_hash:
+ if payload != data:
+ raise LookupError(
+ "already patched with a another set of patches: verify file integrity and restart"
+ )
+ raise LookupError("unrecognized build: this wrapper may need updating")
+ result = bytearray(payload)
+ for edit in self.edits:
+ end = edit.offset + len(edit.replacement)
+ if edit.offset < 0 or end > len(result):
+ raise ValueError("patch extends outside the file")
+ result[edit.offset : end] = edit.replacement
+ if sha256(result) != self.patched_hash:
+ raise ValueError("patched file checksum mismatch")
+ return bytes(result) + MARKER
+
+
+def patch(
+ original_hash: str, patched_hash: str, edits: tuple[tuple[int, str], ...]
+) -> Patch:
+ return Patch(
+ bytes.fromhex(original_hash),
+ bytes.fromhex(patched_hash),
+ tuple(
+ Edit(offset, bytes.fromhex(replacement)) for offset, replacement in edits
+ ),
+ )
+
+
+PATCHES = {
+ "bin/linux64/vrcompositor": patch(
+ "ebdffa9a8309d9596201cb385ceded35b71a1f16fb8f123fc0196c245cb8186c",
+ "47c721d16d4da568e8d6344d9e7ef79b155afe557998780491f25e554f9761af",
+ (
+ # Wait for GPU to finish using its resources (see #952)
+ (0x3E7FC1, "84c00f84cf560000eb06"),
+ (0x3E7FD1, "488dbbe00d0000e9aa560000"),
+ (0x3ED67C, "f0c1"),
+ (0x3ED680, "e93ca9ffff9090"),
+ # Stop reusing GPU work identifier across frames
+ # (allows above patch to work reliably)
+ (0x40049B, "31c09090909090"),
+ (0x400BBE, "31c09090909090"),
+ (0x400BF2, "909090909090"),
+ # Fix shader descriptor texture array counts (should be 3, not 1)
+ # for bindings 9, 17, and 20. Affected shaders:
+ # - distort_none_ps
+ # - distort_none_ps_layered
+ # - distort_panorama_ps
+ # - distort_panorama_solid_color_ps
+ # - distort_ps_achromatic_nd
+ (0x3E7FE1, "498b5500b80300000089424ceb22"),
+ (0x3E8011, "89828c0000008982a4000000eb12"),
+ (0x3E8031, "4c8bbd18feffffe9e21d0100"),
+ (0x3F9E18, "e9c4e1feff9090"),
+ ),
+ ),
+ "bin/linux64/vrserver": patch(
+ "fb87eefff0cad90c69c73cfe6affdb3c752f2047c4fa3bb63e478bcec05d9d18",
+ "28a91a9c677052d6fb62cfd015fe0e4bbf946a57e0f8195abf221e434ac9cbf2",
+ (
+ # Remove logger calls in forked processes (can cause a deadlock and
+ # make the process linger preventing SteamVR from starting again)
+ (0x2BD5F6, "9090909090"),
+ (0x2BD7E7, "9090909090"),
+ (0x2BD87C, "9090909090"),
+ (0x2BD976, "9090909090"),
+ (0x2BDA4F, "9090909090"),
+ ),
+ ),
+ "drivers/vrlink/bin/linux64/driver_vrlink.so": patch(
+ "19db86058ca8243bf9e9f4cad456ec64f2038709c1570afbda60f82694fd6319",
+ "2eb30ceceba4e10d412ea7f5a54430600b25090e92b79bcf65d59cff49cac045",
+ (
+ # Keep increasing (mod(2^16)) the ACK cutoff instead of clearing it
+ # (#965)
+ (0xA1F4D, "89836c0d000090909090"),
+ # Wait for encoder to finish before reconfiguring it
+ # (ulVideoTimeline == ulTimelineSequence) (#965)
+ (0x0FDDE2, "83bb4c090000007458e9702c0000"),
+ (0x0FDE43, "488b8328090000e9ec210000"),
+ (0x100034, "e9a9ddffff9090"),
+ # Clean up failed encoded frames so the above fix doesn't wait
+ # forever when these trigger a reset
+ (0xFF246, "27000000"),
+ (0xFF271, "488b9d70ffffff4c01f3e9d6feffff"),
+ # Reset tracking and encoding when starting a new streaming session
+ (0xD5A31, "57488b7f184885ff740feb04"),
+ (0xD5A41, "488b076a015eff50285fe95055feff"),
+ (0xDD171, "4c89a30004000031c0894308c3"),
+ (0xDD1B7, "e8b5ffffff6690"),
+ (0xDD1CB, "6288ff"),
+ # Ignore stale tracking IDs and repeated older attempts
+ (0xDE1CE, "89d02b47084189c6c1e004"),
+ (0xDE1DA, "88"),
+ (0xDE6C1, "eb"),
+ (0xDE6F8, "88430eebc69090"),
+ ),
+ ),
+}
+
+
+def sha256(data):
+ return hashlib.sha256(data).digest()
+
+
+def notify(message):
+ print(f"{NAME}: {message}", file=sys.stderr)
+ try:
+ subprocess.run(
+ ["notify-send", NAME, message],
+ check=False,
+ timeout=3,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ )
+ except (OSError, subprocess.TimeoutExpired):
+ pass
+
+
+def replace(path, data):
+ mode = stat.S_IMODE(path.stat().st_mode)
+ with tempfile.NamedTemporaryFile(
+ dir=path.parent, prefix=path.name + ".", delete=False
+ ) as f:
+ temporary = Path(f.name)
+ try:
+ f.write(data)
+ f.flush()
+ os.fchmod(f.fileno(), mode)
+ os.fsync(f.fileno())
+ os.replace(temporary, path)
+ finally:
+ temporary.unlink(missing_ok=True)
+
+
+def main(command):
+ diagnostics = []
+ if not command:
+ print(f"Usage: {NAME} command [args...]", file=sys.stderr)
+ return 2
+ try:
+ root = Path.cwd()
+ pending = []
+ for relative, patch in PATCHES.items():
+ path = (root / relative).resolve()
+ try:
+ original = path.read_bytes()
+ except FileNotFoundError as e:
+ diagnostics.append(f"Skipped `{relative}`: {e}")
+ continue
+ try:
+ updated = patch.apply_to(original)
+ except ValueError as e:
+ raise ValueError(f"Patching `{relative}` failed: {e}") from e
+ except LookupError as e:
+ diagnostics.append(f"Skipped `{relative}`: {e}")
+ continue
+ if updated != original:
+ pending.append((path, updated))
+ for path, updated in pending:
+ replace(path, updated)
+ diagnostics.append(f"Patched {path}")
+ if diagnostics:
+ notify("\n".join(diagnostics))
+ except (OSError, ValueError) as e:
+ notify("\n".join([f"Encountered an error: {e}", *diagnostics]))
+ return 1
+ os.execvp(command[0], command)
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))