diff options
| author | Tomasz Kramkowski <tomasz@kramkow.ski> | 2026-11-03 20:01:42 +0000 |
|---|---|---|
| committer | Tomasz Kramkowski <tomasz@kramkow.ski> | 2026-11-03 20:01:42 +0000 |
| commit | 098bd37f355a77abbb5751fc55a91f7ebe74cc6b (patch) | |
| tree | d3bc93cb427276a4936d79903b62e156d5c538e7 /steamvr | |
| parent | 64c349eb1dc0fde65fd4e6ddf525faa0a4c64a91 (diff) | |
| download | steamvr-patches-master.tar.gz steamvr-patches-master.tar.xz steamvr-patches-master.zip | |
Diffstat (limited to 'steamvr')
| -rw-r--r-- | steamvr/image-wait/image_wait.c | 148 | ||||
| -rw-r--r-- | steamvr/image-wait/trampoline.S | 26 | ||||
| -rwxr-xr-x | steamvr/patch-steamvr | 216 | ||||
| -rw-r--r-- | steamvr/patches.toml | 82 |
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"], +] + |
