aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--LICENSE131
-rw-r--r--README.md5
-rwxr-xr-xsteamvr/patch-steamvr226
3 files changed, 362 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6f73ac4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,131 @@
+# Creative Commons CC0 1.0 Universal
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL
+SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT
+RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS"
+BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS
+DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS
+LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE
+INFORMATION OR WORKS PROVIDED HEREUNDER.
+
+## Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator and
+subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in part
+through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily elects
+to apply CC0 to the Work and publicly distribute the Work under its terms,
+with knowledge of his or her Copyright and Related Rights in the Work and
+the meaning and intended legal effect of CC0 on those rights.
+
+1. __Copyright and Related Rights.__ A Work made available under CC0 may be
+ protected by copyright and related or neighboring rights ("Copyright and
+ Related Rights"). Copyright and Related Rights include, but are not
+ limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+
+ ii. moral rights retained by the original author(s) and/or
+ performer(s);
+
+ iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+
+ v. rights protecting the extraction, dissemination, use and reuse of
+ data in a Work;
+
+ vi. database rights (such as those arising under Directive 96/9/EC of
+ the European Parliament and of the Council of 11 March 1996 on the
+ legal protection of databases, and under any national
+ implementation thereof, including any amended or successor version
+ of such directive); and
+
+ vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. __Waiver.__ To the greatest extent permitted by, but not in
+ contravention of, applicable law, Affirmer hereby overtly, fully,
+ permanently, irrevocably and unconditionally waives, abandons, and
+ surrenders all of Affirmer's Copyright and Related Rights and associated
+ claims and causes of action, whether now known or unknown (including
+ existing as well as future claims and causes of action), in the Work (i)
+ in all territories worldwide, (ii) for the maximum duration provided by
+ applicable law or treaty (including future time extensions), (iii) in
+ any current or future medium and for any number of copies, and (iv) for
+ any purpose whatsoever, including without limitation commercial,
+ advertising or promotional purposes (the "Waiver"). Affirmer makes the
+ Waiver for the benefit of each member of the public at large and to the
+ detriment of Affirmer's heirs and successors, fully intending that such
+ Waiver shall not be subject to revocation, rescission, cancellation,
+ termination, or any other legal or equitable action to disrupt the quiet
+ enjoyment of the Work by the public as contemplated by Affirmer's
+ express Statement of Purpose.
+
+3. __Public License Fallback.__ Should any part of the Waiver for any
+ reason be judged legally invalid or ineffective under applicable law,
+ then the Waiver shall be preserved to the maximum extent permitted
+ taking into account Affirmer's express Statement of Purpose. In
+ addition, to the extent the Waiver is so judged Affirmer hereby grants
+ to each affected person a royalty-free, non transferable, non
+ sublicensable, non exclusive, irrevocable and unconditional license to
+ exercise Affirmer's Copyright and Related Rights in the Work (i) in all
+ territories worldwide, (ii) for the maximum duration provided by
+ applicable law or treaty (including future time extensions), (iii) in
+ any current or future medium and for any number of copies, and (iv) for
+ any purpose whatsoever, including without limitation commercial,
+ advertising or promotional purposes (the "License"). The License shall
+ be deemed effective as of the date CC0 was applied by Affirmer to the
+ Work. Should any part of the License for any reason be judged legally
+ invalid or ineffective under applicable law, such partial invalidity or
+ ineffectiveness shall not invalidate the remainder of the License, and
+ in such case Affirmer hereby affirms that he or she will not (i)
+ exercise any of his or her remaining Copyright and Related Rights in the
+ Work or (ii) assert any associated claims and causes of action with
+ respect to the Work, in either case contrary to Affirmer's express
+ Statement of Purpose.
+
+4. __Limitations and Disclaimers.__
+
+ a. No trademark or patent rights held by Affirmer are waived,
+ abandoned, surrendered, licensed or otherwise affected by this
+ document.
+
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy,
+ or the present or absence of errors, whether or not discoverable,
+ all to the greatest extent permissible under applicable law.
+
+ c. Affirmer disclaims responsibility for clearing rights of other
+ persons that may apply to the Work or any use thereof, including
+ without limitation any person's Copyright and Related Rights in the
+ Work. Further, Affirmer disclaims responsibility for obtaining any
+ necessary consents, permissions or other rights required for any use
+ of the Work.
+
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..17fadfe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# Patches for SteamVR on Linux
+
+See [the project page on my
+website](https://kramkow.ski/project/steamvr-patches.html) for more
+information.
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:]))