aboutsummaryrefslogtreecommitdiffstats
path: root/steamvr
diff options
context:
space:
mode:
authorTomasz Kramkowski <tomasz@kramkow.ski>2026-11-03 20:01:42 +0000
committerTomasz Kramkowski <tomasz@kramkow.ski>2026-11-03 20:01:42 +0000
commit098bd37f355a77abbb5751fc55a91f7ebe74cc6b (patch)
treed3bc93cb427276a4936d79903b62e156d5c538e7 /steamvr
parent64c349eb1dc0fde65fd4e6ddf525faa0a4c64a91 (diff)
downloadsteamvr-patches-master.tar.gz
steamvr-patches-master.tar.xz
steamvr-patches-master.zip
use patch generator to build patchersHEADmaster
Diffstat (limited to 'steamvr')
-rw-r--r--steamvr/image-wait/image_wait.c148
-rw-r--r--steamvr/image-wait/trampoline.S26
-rwxr-xr-xsteamvr/patch-steamvr216
-rw-r--r--steamvr/patches.toml82
4 files changed, 256 insertions, 216 deletions
diff --git a/steamvr/image-wait/image_wait.c b/steamvr/image-wait/image_wait.c
new file mode 100644
index 0000000..e8112c3
--- /dev/null
+++ b/steamvr/image-wait/image_wait.c
@@ -0,0 +1,148 @@
+// SPDX-FileCopyrightText: 2026 Tomasz Kramkowski <tomasz@kramkow.ski>
+// SPDX-License-Identifier: CC0-1.0
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#define ASSERT_SIZE(type, expected) \
+ static_assert(sizeof (type) == (expected), #type " size must be " #expected)
+#define ASSERT_OFFSET(type, field, expected) \
+ static_assert( \
+ offsetof(type, field) == (expected), \
+ #type "." #field " offset must be " #expected \
+ )
+
+ASSERT_SIZE(void *, 8);
+
+enum { VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000 };
+
+typedef struct {
+ uintptr_t semaphore;
+ uint64_t value;
+} SemaphoreEntry;
+ASSERT_SIZE(SemaphoreEntry, 16);
+ASSERT_OFFSET(SemaphoreEntry, value, 8);
+
+// vr::IVRSharedResource
+struct SharedResource {
+ const struct SharedResourceVtable *vtable;
+};
+
+typedef struct Semaphore *GetSemaphore(struct SharedResource *);
+
+struct SharedResourceVtable {
+ unsigned char unknown_000[0x30];
+ GetSemaphore *get_semaphore;
+};
+ASSERT_OFFSET(struct SharedResourceVtable, get_semaphore, 0x30);
+
+typedef uint64_t SemaphoreGetU64(struct Semaphore *);
+
+struct SemaphoreVtable {
+ unsigned char unknown_000[0x40];
+ SemaphoreGetU64 *get_handle;
+ unsigned char unknown_048[0x70 - 0x48];
+ SemaphoreGetU64 *get_value;
+};
+ASSERT_OFFSET(struct SemaphoreVtable, get_handle, 0x40);
+ASSERT_OFFSET(struct SemaphoreVtable, get_value, 0x70);
+
+// vr::IVRSharedSemaphore
+struct Semaphore {
+ const struct SemaphoreVtable *vtable;
+};
+
+// vr::IVRSharedImage
+struct Image {
+ const void *vtable;
+};
+
+struct Texture {
+ unsigned char unknown_000[0x48];
+ struct Image *image;
+};
+ASSERT_OFFSET(struct Texture, image, 0x48);
+
+struct Batch {
+ unsigned char unknown_000[0xf0];
+ SemaphoreEntry *waits;
+ SemaphoreEntry *waits_end;
+ unsigned char unknown_100[0x140 - 0x100];
+ uint32_t *wait_stages;
+ unsigned char unknown_148[0x398 - 0x148];
+ SemaphoreEntry *signals;
+ SemaphoreEntry *signals_end;
+};
+ASSERT_OFFSET(struct Batch, waits, 0xf0);
+ASSERT_OFFSET(struct Batch, waits_end, 0xf8);
+ASSERT_OFFSET(struct Batch, wait_stages, 0x140);
+ASSERT_OFFSET(struct Batch, signals, 0x398);
+ASSERT_OFFSET(struct Batch, signals_end, 0x3a0);
+
+struct RendererContext {
+ unsigned char unknown_000[8];
+ struct Batch *batch;
+};
+ASSERT_OFFSET(struct RendererContext, batch, 8);
+
+// CFacetVRRenderer
+struct Renderer {
+ unsigned char unknown_000[0x12b0];
+ struct RendererContext *context;
+};
+ASSERT_OFFSET(struct Renderer, context, 0x12b0);
+
+extern void append_wait(
+ struct Batch *batch, uintptr_t semaphore, uint32_t stages, uint64_t value
+) __attribute__((visibility("hidden")));
+
+// The patch calls this function at the beginning of a method in the "Facet"
+// `renderer` which is called, among other things, on `texture`s that the current
+// submission batch will use as shader inputs (e.g. eye textures).
+void register_image_wait(struct Renderer *renderer, struct Texture *texture)
+{
+ // First we need the completion semaphore for the texture.
+ struct Image *image = texture->image;
+ if (!image) return;
+
+ // The vbase offset entry for converting `vr::IVRSharedImage *` to
+ // `vr::IVRSharedResource *` is located at `image->vtable - 0x50`.
+ ptrdiff_t resource_offset =
+ *(const ptrdiff_t *)((const char *)image->vtable - 0x50);
+ struct SharedResource *resource = (void *)((char *)image + resource_offset);
+
+ struct Semaphore *semaphore = resource->vtable->get_semaphore(resource);
+ if (!semaphore) return;
+
+ uintptr_t handle = semaphore->vtable->get_handle(semaphore);
+ if (!handle) return;
+
+ // The current batch that is being prepared, and that may need the image
+ // semaphore as a dependency.
+ struct Batch *batch = renderer->context->batch;
+
+ // If the texture we got called on has the same completion semaphore as one
+ // of the completion signals of this batch, then making the batch depend on
+ // the semaphore could create a loop, and that would be bad.
+ for (SemaphoreEntry *p = batch->signals; p != batch->signals_end; p++) {
+ if (p->semaphore == handle) return;
+ }
+
+ // If there's already a wait on this texture's semaphore, ensure it will
+ // block all work in this batch and that the value is at least as high as the
+ // semaphore value required for this texture to be ready.
+ uint64_t value = semaphore->vtable->get_value(semaphore);
+ uint32_t *stages = batch->wait_stages;
+ for (SemaphoreEntry *p = batch->waits; p != batch->waits_end;
+ p++, stages++) {
+ if (p->semaphore != handle) continue;
+
+ if (p->value < value) p->value = value;
+ *stages |= VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
+ return;
+ }
+
+ // Otherwise append a wait on this texture's completion semaphore.
+ append_wait(batch, handle, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, value);
+}
diff --git a/steamvr/image-wait/trampoline.S b/steamvr/image-wait/trampoline.S
new file mode 100644
index 0000000..6c7dbc4
--- /dev/null
+++ b/steamvr/image-wait/trampoline.S
@@ -0,0 +1,26 @@
+/* SPDX-FileCopyrightText: 2026 Tomasz Kramkowski <tomasz@kramkow.ski>
+ * SPDX-License-Identifier: CC0-1.0
+ */
+
+.intel_syntax noprefix
+.section .text.entry,"ax",@progbits
+.global input_wait
+input_wait:
+ push rdi
+ push rsi
+ push rdx
+ push rcx
+ sub rsp, 8
+ call register_image_wait
+ add rsp, 8
+ pop rcx
+ pop rdx
+ pop rsi
+ pop rdi
+
+ # Replay the three displaced instructions, then resume the original function.
+ push rbp
+ mov rax, rsi
+ mov r9d, edx
+ jmp resume_transition
+.section .note.GNU-stack,"",@progbits
diff --git a/steamvr/patch-steamvr b/steamvr/patch-steamvr
deleted file mode 100755
index f176b0a..0000000
--- a/steamvr/patch-steamvr
+++ /dev/null
@@ -1,216 +0,0 @@
-#!/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.19.1, build 26389675.
-
-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(
- "4c380adc27832db9afaf83e446080354338bcb4c96f0f0cc561c90cd4d79168f",
- "a6d073c17489919e838c3d2ac4941a923bc18172a372f3c32bae3433e95f9dbc",
- (
- # Wait for GPU to finish using its resources (see #952)
- (0x404941, "84c00f842f570000eb06"),
- (0x404951, "488dbbe00d0000e90a570000"),
- (0x40A05B, "e830db0600"),
- (0x40A060, "e9dca8ffff9090"),
- # Stop reusing GPU work identifier across frames
- # (allows above patch to work reliably)
- (0x41D224, "31c09090909090"),
- (0x41DB39, "31c09090909090"),
- (0x41DB74, "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
- (0x404961, "498b5500b80300000089424ceb22"),
- (0x404991, "89828c0000008982a4000000eb12"),
- (0x4049B1, "4c8bbd18feffffe902200100"),
- (0x4169B8, "e9a4dffeff9090"),
- ),
- ),
- "bin/linux64/vrserver": patch(
- "a7c9246f8fe36749caa0bfe3984efbb5fa7dbbb28e5263a5c1744f5ba6b0c10b",
- "b14314f1fbfe8acb164583853e32c9a6870317bb9cbb278d9cea21862aaebd8f",
- (
- # Remove logger calls in forked processes (can cause a deadlock and
- # make the process linger preventing SteamVR from starting again)
- (0x2C1386, "9090909090"),
- (0x2C1577, "9090909090"),
- (0x2C160C, "9090909090"),
- (0x2C1726, "9090909090"),
- (0x2C17FF, "9090909090"),
- (0x2C1699, "9090909090"),
- ),
- ),
- "drivers/vrlink/bin/linux64/driver_vrlink.so": patch(
- "a33d1da2d1c9851a61e19b87ffc89bbbb6e8b1e503e189e3e19bccd2e6cd3b7f",
- "c7d8adbbcd44d577f7891c15e53d6000eaf0b257fde642b09ff286bd00be9468",
- (
- # Keep increasing (mod(2^16)) the ACK cutoff instead of clearing it
- # (#965)
- (0xD91DD, "89836c0d000090909090"),
- # Wait for encoder to finish before reconfiguring it
- # (ulVideoTimeline == ulTimelineSequence) (#965)
- (0x13C2D2, "83bb4c090000007458e9702c0000"),
- (0x13C333, "488b8328090000e9ec210000"),
- (0x13E524, "e9a9ddffff9090"),
- # Clean up failed encoded frames so the above fix doesn't wait
- # forever when these trigger a reset
- (0x13D735, "e927000000"),
- (0x13D761, "488b9d70ffffff4c01f3e9d6feffff"),
- # Reset tracking and encoding when starting a new streaming session
- (0x1129F1, "57488b7f184885ff740feb04"),
- (0x112A01, "488b076a015eff50285fe91013feff"),
- (0x11A251, "4c89a30804000031c0894308c3"),
- (0x11A297, "e8b5ffffff6690"),
- (0x11A2AA, "e84287ffff"),
- # Ignore stale tracking IDs and repeated older attempts
- (0x11B301, "89d02b47084189c6c1e004"),
- (0x11B30C, "0f8853050000"),
- (0x11B614, "0f841719fdff"),
- (0xECF31, "410fb644240988430ee926e90200"),
- ),
- ),
-}
-
-
-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:]))
diff --git a/steamvr/patches.toml b/steamvr/patches.toml
new file mode 100644
index 0000000..a4e618e
--- /dev/null
+++ b/steamvr/patches.toml
@@ -0,0 +1,82 @@
+# SPDX-FileCopyrightText: 2026 Tomasz Kramkowski <tomasz@kramkow.ski>
+# SPDX-License-Identifier: CC0-1.0
+
+target = "Beta 2.19.1, build 26389675"
+
+[[targets]]
+path = "bin/linux64/vrcompositor"
+sha256 = "4c380adc27832db9afaf83e446080354338bcb4c96f0f0cc561c90cd4d79168f"
+edits = [
+ # Wait for GPU to finish using its resources (see #952)
+ [0x404941, "84c00f842f570000eb06"],
+ [0x404951, "488dbbe00d0000e90a570000"],
+ [0x40a05b, "e830db0600"],
+ [0x40a060, "e9dca8ffff9090"],
+ # Stop reusing GPU work identifier across frames (allows above patch to
+ # work reliably)
+ [0x41d224, "31c09090909090"],
+ [0x41db39, "31c09090909090"],
+ [0x41db74, "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
+ [0x404961, "498b5500b80300000089424ceb22"],
+ [0x404991, "89828c0000008982a4000000eb12"],
+ [0x4049b1, "4c8bbd18feffffe902200100"],
+ [0x4169b8, "e9a4dffeff9090"],
+]
+[targets.compiled]
+sources = ["image-wait/trampoline.S", "image-wait/image_wait.c"]
+imports = { append_wait = 0x465dd0, resume_transition = 0x40e9d7 }
+hooks = [
+ # Make the compositor wait for frames submitted by the application to
+ # complete rendering before trying to encode them etc. (#1046)
+ { address = 0x40e9d0, length = 7, target = "input_wait" },
+]
+
+[[targets]]
+path = "bin/linux64/vrserver"
+sha256 = "a7c9246f8fe36749caa0bfe3984efbb5fa7dbbb28e5263a5c1744f5ba6b0c10b"
+edits = [
+ # Remove logger calls in forked processes (can cause a deadlock and make
+ # the process linger preventing SteamVR from starting again)
+ [0x2c1386, "9090909090"],
+ [0x2c1577, "9090909090"],
+ [0x2c160c, "9090909090"],
+ [0x2c1726, "9090909090"],
+ [0x2c17ff, "9090909090"],
+ [0x2c1699, "9090909090"],
+]
+
+[[targets]]
+path = "drivers/vrlink/bin/linux64/driver_vrlink.so"
+sha256 = "a33d1da2d1c9851a61e19b87ffc89bbbb6e8b1e503e189e3e19bccd2e6cd3b7f"
+edits = [
+ # Keep increasing (mod(2^16)) the ACK cutoff instead of clearing it (#965)
+ [0xd91dd, "89836c0d000090909090"],
+ # Wait for encoder to finish before reconfiguring it
+ # (ulVideoTimeline == ulTimelineSequence) (#965)
+ [0x13c2d2, "83bb4c090000007458e9702c0000"],
+ [0x13c333, "488b8328090000e9ec210000"],
+ [0x13e524, "e9a9ddffff9090"],
+ # Clean up failed encoded frames so the above fix doesn't wait forever when
+ # these trigger a reset
+ [0x13d735, "e927000000"],
+ [0x13d761, "488b9d70ffffff4c01f3e9d6feffff"],
+ # Reset tracking and encoding when starting a new streaming session
+ [0x1129f1, "57488b7f184885ff740feb04"],
+ [0x112a01, "488b076a015eff50285fe91013feff"],
+ [0x11a251, "4c89a30804000031c0894308c3"],
+ [0x11a297, "e8b5ffffff6690"],
+ [0x11a2aa, "e84287ffff"],
+ # Ignore stale tracking IDs and repeated older attempts
+ [0x11b301, "89d02b47084189c6c1e004"],
+ [0x11b30c, "0f8853050000"],
+ [0x11b614, "0f841719fdff"],
+ [0xecf31, "410fb644240988430ee926e90200"],
+]
+