From 4c93d6874026ae58ab15395163f730ce0ecb4669 Mon Sep 17 00:00:00 2001 From: komp dog Date: Mon, 14 Sep 2026 22:17:59 -0400 Subject: [PATCH 1/2] Keep native hand devices out of controller processing --- .../src/Driver/DeviceProvider.cpp | 163 +++++++++++++++--- .../src/Driver/DeviceProvider.h | 37 ++-- .../Driver/Hooking/InterfaceHookInjector.cpp | 16 +- 3 files changed, 171 insertions(+), 45 deletions(-) diff --git a/CustomHeadsetOpenVR/src/Driver/DeviceProvider.cpp b/CustomHeadsetOpenVR/src/Driver/DeviceProvider.cpp index 86a8861..4941b4f 100644 --- a/CustomHeadsetOpenVR/src/Driver/DeviceProvider.cpp +++ b/CustomHeadsetOpenVR/src/Driver/DeviceProvider.cpp @@ -384,6 +384,25 @@ static bool InputPathInteresting(const std::string &lower){ || lower.find("pinch") != std::string::npos; } +static bool NativeHandSerial(const char* serial){ + if(!serial){ return false; } + return strcmp(serial, "VRLINKQ_Hand_Left") == 0 + || strcmp(serial, "VRLINKQ_Hand_Right") == 0; +} + +static bool PhysicalGalaxyControllerSerial(const char* serial){ + if(!serial){ return false; } + std::string value = serial; + return value.rfind("SamsungVST-Controller", 0) == 0 + || (value.rfind("VRLINK", 0) == 0 + && value.find("Controller") != std::string::npos); +} + +static bool NativeHandDiagnosticPath(const std::string &lower){ + return lower.find("index_pinch") != std::string::npos + || lower.find("/input/grip") != std::string::npos; +} + // classify a component path into a distortion tuner control role. exact // suffix matches against the confirmed vrlink surface (session log): joystick // x/y scalars + joystick/a/b/x/y click booleans + grip value scalars. @@ -407,17 +426,24 @@ static int TunerRoleForPath(const std::string &lower, bool isScalar){ return 0; } -void CustomHeadsetDeviceProvider::OnInputComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle){ - if(!name || handle == vr::k_ulInvalidInputComponentHandle){ +void CustomHeadsetDeviceProvider::OnInputComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle, vr::EVRInputError error){ + if(!name || error != vr::VRInputError_None || handle == vr::k_ulInvalidInputComponentHandle){ + if(name && error != vr::VRInputError_None){ + DriverLog("HandInputDiag: boolean create FAILED container=%llu path=%s error=%d", + (unsigned long long)container, name, (int)error); + } return; } InputComponentInfo info; info.container = container; + info.openVRID = ResolveContainerId(container); info.name = name; std::string lower = info.name; for(auto &c : lower){ c = (char)tolower(c); } info.interesting = InputPathInteresting(lower); info.tunerRole = TunerRoleForPath(lower, false); + info.nativeHand = info.openVRID != vr::k_unTrackedDeviceIndexInvalid && IsNativeHand(info.openVRID); + info.diagnostic = info.nativeHand && NativeHandDiagnosticPath(lower); // hand classification from the quest layout: x/y buttons exist only on // the left controller, a/b only on the right. once known, resolve the // openVR id too so pose updates can be routed per hand. @@ -427,7 +453,7 @@ void CustomHeadsetDeviceProvider::OnInputComponentCreated(vr::PropertyContainerH // lock itself, and std::mutex is non-recursive — nesting it here // deadlocked vrserver at the first x/click creation and tripped a // SteamVR safe-mode block (session 24 regression) - uint32_t id = ResolveContainerId(container); + uint32_t id = info.openVRID; std::lock_guard handGuard(poseLogLock); containerHand[container] = hand; if(id != vr::k_unTrackedDeviceIndexInvalid){ @@ -436,32 +462,42 @@ void CustomHeadsetDeviceProvider::OnInputComponentCreated(vr::PropertyContainerH } // always log creates: component names are the map of vrlink's input // surface, and not having them cost a session - DriverLog("InputTap: boolean component container=%llu path=%s handle=%llu%s", + DriverLog("InputTap: boolean component container=%llu path=%s handle=%llu id=%u%s%s", (unsigned long long)container, name, (unsigned long long)handle, - info.interesting ? " [watched]" : ""); + info.openVRID, info.interesting ? " [watched]" : "", + info.nativeHand ? " [native-hand passthrough]" : ""); std::lock_guard guard(poseLogLock); inputComponents[handle] = info; } -void CustomHeadsetDeviceProvider::OnScalarComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle){ - if(!name || handle == vr::k_ulInvalidInputComponentHandle){ +void CustomHeadsetDeviceProvider::OnScalarComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle, vr::EVRInputError error){ + if(!name || error != vr::VRInputError_None || handle == vr::k_ulInvalidInputComponentHandle){ + if(name && error != vr::VRInputError_None){ + DriverLog("HandInputDiag: scalar create FAILED container=%llu path=%s error=%d", + (unsigned long long)container, name, (int)error); + } return; } InputComponentInfo info; info.container = container; + info.openVRID = ResolveContainerId(container); info.name = name; info.isScalar = true; std::string lower = info.name; for(auto &c : lower){ c = (char)tolower(c); } info.interesting = InputPathInteresting(lower); info.tunerRole = TunerRoleForPath(lower, true); - DriverLog("InputTap: scalar component container=%llu path=%s handle=%llu%s", + info.nativeHand = info.openVRID != vr::k_unTrackedDeviceIndexInvalid && IsNativeHand(info.openVRID); + info.diagnostic = info.nativeHand && NativeHandDiagnosticPath(lower); + DriverLog("InputTap: scalar component container=%llu path=%s handle=%llu id=%u%s%s", (unsigned long long)container, name, (unsigned long long)handle, - info.interesting ? " [watched]" : ""); + info.openVRID, info.interesting ? " [watched]" : "", + info.nativeHand ? " [native-hand passthrough]" : ""); // grip capacitive touch: vrlink never creates /input/grip/touch for these // controllers; synthesize it next to grip/value (before taking the lock: // the create call re-enters our own hook) - bool wantGripTouch = driverConfig.galaxyXr.nativeInputProfile && driverConfig.galaxyXr.synthesizeGripTouch + bool wantGripTouch = !info.nativeHand + && driverConfig.galaxyXr.nativeInputProfile && driverConfig.galaxyXr.synthesizeGripTouch && lower.size() >= 17 && lower.compare(lower.size() - 17, 17, "/input/grip/value") == 0; if(wantGripTouch && vr::VRDriverInput()){ vr::VRInputComponentHandle_t touchHandle = vr::k_ulInvalidInputComponentHandle; @@ -477,7 +513,33 @@ void CustomHeadsetDeviceProvider::OnScalarComponentCreated(vr::PropertyContainer inputComponents[handle] = info; } -void CustomHeadsetDeviceProvider::OnScalarComponentUpdated(vr::VRInputComponentHandle_t handle, float value){ +void CustomHeadsetDeviceProvider::OnScalarComponentUpdated(vr::VRInputComponentHandle_t handle, float value, double timeOffset, vr::EVRInputError error){ + { + double now = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count() / 1000000.0; + bool doLog = false; + std::string name; + uint32_t id = vr::k_unTrackedDeviceIndexInvalid; + { + std::lock_guard guard(poseLogLock); + auto found = inputComponents.find(handle); + if(found != inputComponents.end() && found->second.diagnostic){ + InputComponentInfo &info = found->second; + doLog = error != vr::VRInputError_None || !info.diagHaveScalar + || std::fabs(value - info.diagLastScalar) >= 0.10f + || now - info.diagLastLogTime >= 0.5; + info.diagHaveScalar = true; + info.diagLastScalar = value; + if(doLog){ info.diagLastLogTime = now; } + name = info.name; + id = info.openVRID; + } + } + if(doLog){ + DriverLog("HandInputDiag: scalar id=%u path=%s value=%.3f timeOffset=%.4f result=%d", + id, name.c_str(), value, timeOffset, (int)error); + } + } // grip touch from grip value (hysteresis 0.03 / 0.015); updated outside the lock { vr::VRInputComponentHandle_t touchHandle = vr::k_ulInvalidInputComponentHandle; @@ -554,7 +616,28 @@ void CustomHeadsetDeviceProvider::OnScalarComponentUpdated(vr::VRInputComponentH } } -void CustomHeadsetDeviceProvider::OnBooleanComponentUpdated(vr::VRInputComponentHandle_t handle, bool value){ +void CustomHeadsetDeviceProvider::OnBooleanComponentUpdated(vr::VRInputComponentHandle_t handle, bool value, double timeOffset, vr::EVRInputError error){ + { + bool doLog = false; + std::string name; + uint32_t id = vr::k_unTrackedDeviceIndexInvalid; + { + std::lock_guard guard(poseLogLock); + auto found = inputComponents.find(handle); + if(found != inputComponents.end() && found->second.diagnostic){ + InputComponentInfo &info = found->second; + doLog = error != vr::VRInputError_None || !info.diagHaveBool || info.diagLastBool != value; + info.diagHaveBool = true; + info.diagLastBool = value; + name = info.name; + id = info.openVRID; + } + } + if(doLog){ + DriverLog("HandInputDiag: boolean id=%u path=%s value=%d timeOffset=%.4f result=%d", + id, name.c_str(), (int)value, timeOffset, (int)error); + } + } if(tunerInputActive.load(std::memory_order_relaxed)){ std::lock_guard tunerGuard(poseLogLock); auto found = inputComponents.find(handle); @@ -915,11 +998,21 @@ static std::map skeletonTapHands; void CustomHeadsetDeviceProvider::OnSkeletonComponentCreated(vr::PropertyContainerHandle_t container, const char *name, const char *skeletonPath, vr::VRInputComponentHandle_t handle){ std::string path = skeletonPath ? skeletonPath : ""; int hand = path.find("right") != std::string::npos ? 1 : 0; + uint32_t id = ResolveContainerId(container); + bool physicalController = id != vr::k_unTrackedDeviceIndexInvalid && IsStreamedController(id); + DriverLog("SkeletonTap: component %s (%s) hand=%s handle=%llu id=%u mode=%s", + name ? name : "?", path.c_str(), hand ? "right" : "left", + (unsigned long long)handle, id, + physicalController ? "physical-controller-adjustable" : "passthrough"); + // Native hands (and anything not positively identified as a physical + // Galaxy XR controller) keep their original skeleton data unchanged. + if(!physicalController){ + return; + } { std::lock_guard lock(skeletonTapMutex); skeletonTapHands[handle] = hand; } - DriverLog("SkeletonTap: component %s (%s) hand=%s handle=%llu", name ? name : "?", path.c_str(), hand ? "right" : "left", (unsigned long long)handle); } bool CustomHeadsetDeviceProvider::HandleSkeletonUpdate(vr::VRInputComponentHandle_t handle, const vr::VRBoneTransform_t *bones, uint32_t count, vr::VRBoneTransform_t *outBones){ @@ -1080,6 +1173,12 @@ static void CaInit(double P[6], double p0Var, double v0Var, double a0Var){ } bool CustomHeadsetDeviceProvider::HandleDevicePoseUpdated(uint32_t openVRID, vr::DriverPose_t &pose){ + // Native hand devices are published by vrlink with Controller class but + // are not physical Galaxy XR controllers. Their pose, tracking state, + // velocity and timing must reach SteamVR byte-for-byte unchanged. + if(openVRID != vr::k_unTrackedDeviceIndex_Hmd && IsNativeHand(openVRID)){ + return true; + } // raw tracking status, captured BEFORE forceTracking can launder it. // the estimators gate on these: forceTracking's job is keeping // devices alive for SteamVR, not feeding fake-OK into filters. @@ -4438,12 +4537,12 @@ bool CustomHeadsetDeviceProvider::HandleDevicePoseUpdated(uint32_t openVRID, vr: return true; } -bool CustomHeadsetDeviceProvider::IsStreamedController(uint32_t openVRID){ +CustomHeadsetDeviceProvider::StreamedDeviceKind CustomHeadsetDeviceProvider::GetStreamedDeviceKind(uint32_t openVRID){ { std::lock_guard guard(streamedIdentityLock); - auto found = streamedControllerCache.find(openVRID); - if(found != streamedControllerCache.end()){ - return found->second != 0; + auto found = streamedDeviceKindCache.find(openVRID); + if(found != streamedDeviceKindCache.end()){ + return found->second; } } // property query with NO lock held (concurrency law: never call out @@ -4452,20 +4551,33 @@ bool CustomHeadsetDeviceProvider::IsStreamedController(uint32_t openVRID){ vr::ETrackedPropertyError propError = vr::TrackedProp_Success; char serial[128] = {}; vr::VRProperties()->GetStringProperty(container, vr::Prop_SerialNumber_String, serial, sizeof(serial), &propError); - bool streamed = false; + StreamedDeviceKind kind = StreamedDeviceKind::Other; if(propError == vr::TrackedProp_Success){ - streamed = strncmp(serial, "VRLINK", 6) == 0 || strncmp(serial, "SamsungVST", 10) == 0; + if(NativeHandSerial(serial)){ + kind = StreamedDeviceKind::NativeHand; + }else if(PhysicalGalaxyControllerSerial(serial)){ + kind = StreamedDeviceKind::PhysicalController; + } }else{ // property not readable yet: do not cache, do not touch - return false; + return StreamedDeviceKind::Other; } { std::lock_guard guard(streamedIdentityLock); - streamedControllerCache[openVRID] = streamed ? 1 : 0; + streamedDeviceKindCache[openVRID] = kind; } - DriverLog("VelocityFix: id=%u serial=%s streamed=%d%s", openVRID, serial, streamed ? 1 : 0, - streamed ? "" : " (native velocity, never touched)"); - return streamed; + const char* kindName = kind == StreamedDeviceKind::PhysicalController ? "physical-controller" + : (kind == StreamedDeviceKind::NativeHand ? "native-hand-passthrough" : "other-passthrough"); + DriverLog("DeviceClassifier: id=%u serial=%s kind=%s", openVRID, serial, kindName); + return kind; +} + +bool CustomHeadsetDeviceProvider::IsNativeHand(uint32_t openVRID){ + return GetStreamedDeviceKind(openVRID) == StreamedDeviceKind::NativeHand; +} + +bool CustomHeadsetDeviceProvider::IsStreamedController(uint32_t openVRID){ + return GetStreamedDeviceKind(openVRID) == StreamedDeviceKind::PhysicalController; } // direction secant over the full derive ring: raw displacement newest-oldest @@ -4964,8 +5076,7 @@ bool CustomHeadsetDeviceProvider::HandleDeviceAdded(const char *&pchDeviceSerial std::string serial = pchDeviceSerialNumber ? pchDeviceSerialNumber : ""; // SamsungVST-Controller-* on the patched APK, VRLINKQ2_Controller_* on // the stock one. "Controller" excludes the VRLINKQ_Hand_* hand trackers. - bool streamedController = (serial.rfind("SamsungVST-Controller", 0) == 0) - || (serial.rfind("VRLINK", 0) == 0 && serial.find("Controller") != std::string::npos); + bool streamedController = PhysicalGalaxyControllerSerial(serial.c_str()); if(streamedController){ GalaxyXRControllerShim* controllerShim = new GalaxyXRControllerShim(serial); shims.insert(controllerShim); diff --git a/CustomHeadsetOpenVR/src/Driver/DeviceProvider.h b/CustomHeadsetOpenVR/src/Driver/DeviceProvider.h index 01b9ff3..a6ef05e 100644 --- a/CustomHeadsetOpenVR/src/Driver/DeviceProvider.h +++ b/CustomHeadsetOpenVR/src/Driver/DeviceProvider.h @@ -170,13 +170,20 @@ class CustomHeadsetDeviceProvider : public vr::IServerTrackedDeviceProvider static void ComputeRingSecant(const VelFixState &state, bool useSmoothed, double secantVel[3], double secantAng[3]); // cached device classes (Prop_DeviceClass_Int32), resolved on first pose std::map deviceClasses = {}; - // streamed-controller identity cache (serial prefix VRLINK*/SamsungVST* - // = vrlink device). the velocity fix must never touch lighthouse - // devices: their native velocity is correct and mixed sessions - // (knuckles + playspace override) are a supported setup. queried once - // per id OUTSIDE any lock, then cached. - std::map streamedControllerCache = {}; + // Strict vrlink device classification. A VRLINK prefix alone is not + // enough: native hand devices use VRLINKQ_Hand_* serials and must never + // enter any physical-controller pose/filter path. Unknown devices pass + // through untouched. Queried outside locks and cached only after the + // serial property is readable. + enum class StreamedDeviceKind : int { + Other = 0, + PhysicalController = 1, + NativeHand = 2, + }; + std::map streamedDeviceKindCache = {}; std::mutex streamedIdentityLock; + StreamedDeviceKind GetStreamedDeviceKind(uint32_t openVRID); + bool IsNativeHand(uint32_t openVRID); bool IsStreamedController(uint32_t openVRID); // derive-mode adaptive smoothing state (pure math under its own lock; // never calls out — lock discipline) @@ -570,6 +577,7 @@ class CustomHeadsetDeviceProvider : public vr::IServerTrackedDeviceProvider // throw died (snap back? dropout? zero?) with direct evidence. struct InputComponentInfo { vr::PropertyContainerHandle_t container = 0; + uint32_t openVRID = vr::k_unTrackedDeviceIndexInvalid; std::string name; bool lastValue = false; bool haveValue = false; @@ -593,6 +601,15 @@ class CustomHeadsetDeviceProvider : public vr::IServerTrackedDeviceProvider int tunerRole = 0; float tunerScalar = 0; bool tunerBool = false; + // Temporary native-hand diagnostics. These are observational only: + // the original IVRDriverInput call is made before this tap. + bool nativeHand = false; + bool diagnostic = false; + bool diagHaveBool = false; + bool diagLastBool = false; + bool diagHaveScalar = false; + float diagLastScalar = 0; + double diagLastLogTime = 0; }; std::map inputComponents = {}; // gate for tuner input capture on the hot component-update path @@ -683,10 +700,10 @@ class CustomHeadsetDeviceProvider : public vr::IServerTrackedDeviceProvider std::atomic alignerGripActive {false}; double alignerGripCm[2][3] = {}; public: - void OnInputComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle); - void OnBooleanComponentUpdated(vr::VRInputComponentHandle_t handle, bool value); - void OnScalarComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle); - void OnScalarComponentUpdated(vr::VRInputComponentHandle_t handle, float value); + void OnInputComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle, vr::EVRInputError error); + void OnBooleanComponentUpdated(vr::VRInputComponentHandle_t handle, bool value, double timeOffset, vr::EVRInputError error); + void OnScalarComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle, vr::EVRInputError error); + void OnScalarComponentUpdated(vr::VRInputComponentHandle_t handle, float value, double timeOffset, vr::EVRInputError error); void OnPoseComponentCreated(vr::PropertyContainerHandle_t container, const char* name, vr::VRInputComponentHandle_t handle); void OnSkeletonComponentCreated(vr::PropertyContainerHandle_t container, const char* name, const char* skeletonPath, vr::VRInputComponentHandle_t handle); bool HandleSkeletonUpdate(vr::VRInputComponentHandle_t handle, const vr::VRBoneTransform_t* bones, uint32_t count, vr::VRBoneTransform_t* outBones); diff --git a/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp b/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp index bfa8950..7731339 100644 --- a/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp +++ b/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp @@ -90,32 +90,30 @@ static void DetourTrackedDeviceAdded006(vr::IVRServerDriverHost *_this, const ch static vr::EVRInputError DetourCreateBooleanComponent004(vr::IVRDriverInput *_this, vr::PropertyContainerHandle_t ulContainer, const char *pchName, vr::VRInputComponentHandle_t *pHandle) { auto error = CreateBooleanComponentHook004.originalFunc(_this, ulContainer, pchName, pHandle); - if(pHandle){ - Driver->OnInputComponentCreated(ulContainer, pchName, *pHandle); - } + Driver->OnInputComponentCreated(ulContainer, pchName, + pHandle ? *pHandle : vr::k_ulInvalidInputComponentHandle, error); return error; } static vr::EVRInputError DetourUpdateBooleanComponent004(vr::IVRDriverInput *_this, vr::VRInputComponentHandle_t ulComponent, bool bNewValue, double fTimeOffset) { auto error = UpdateBooleanComponentHook004.originalFunc(_this, ulComponent, bNewValue, fTimeOffset); - Driver->OnBooleanComponentUpdated(ulComponent, bNewValue); + Driver->OnBooleanComponentUpdated(ulComponent, bNewValue, fTimeOffset, error); return error; } static vr::EVRInputError DetourCreateScalarComponent004(vr::IVRDriverInput *_this, vr::PropertyContainerHandle_t ulContainer, const char *pchName, vr::VRInputComponentHandle_t *pHandle, vr::EVRScalarType eType, vr::EVRScalarUnits eUnits) { auto error = CreateScalarComponentHook004.originalFunc(_this, ulContainer, pchName, pHandle, eType, eUnits); - if(pHandle){ - Driver->OnScalarComponentCreated(ulContainer, pchName, *pHandle); - } + Driver->OnScalarComponentCreated(ulContainer, pchName, + pHandle ? *pHandle : vr::k_ulInvalidInputComponentHandle, error); return error; } static vr::EVRInputError DetourUpdateScalarComponent004(vr::IVRDriverInput *_this, vr::VRInputComponentHandle_t ulComponent, float fNewValue, double fTimeOffset) { auto error = UpdateScalarComponentHook004.originalFunc(_this, ulComponent, fNewValue, fTimeOffset); - Driver->OnScalarComponentUpdated(ulComponent, fNewValue); + Driver->OnScalarComponentUpdated(ulComponent, fNewValue, fTimeOffset, error); return error; } @@ -284,4 +282,4 @@ void DisableHooks() { IHook::DestroyAll(); // MH_Uninitialize(); -} \ No newline at end of file +} From 3ed0d35d0b30fc2514678fbdb1b2be59f22a8827 Mon Sep 17 00:00:00 2001 From: komp dog Date: Mon, 14 Sep 2026 22:17:59 -0400 Subject: [PATCH 2/2] Forward the caller's DriverPose_t, never one of our own The pose detour copied the caller's DriverPose_t, ran the handler over the copy and forwarded that copy. Doing so stops SteamVR promoting vrlink's native hand devices to a controller role: Prop_ControllerRoleHint_Int32 stays correct while GetControllerRoleForTrackedDeviceIndex returns Invalid for the whole hand session, so /user/hand/left|right resolve to no device and every hand binding is dead - while index_pinch still reaches IVRDriverInput and is accepted with VRInputError_None, which is why the failure is invisible at the driver boundary. Bisected live on SteamVR 2.17.9 / Steam Link 2.0.20, reading GetTrackedDeviceIndexForControllerRole across the hand/controller swap: forward a stack copy for every device -> hands broken detour not installed at all -> hands work forward the caller's object for every device -> hands work run the handler over the caller's object (const_cast) -> hands broken forward the copy only for devices we modified -> hands broken forward a persistent per-device static buffer -> hands broken Every object other than the caller's own breaks it, whatever its lifetime, and for any device: forwarding a copy for the controllers alone still took the hands down. Writing into the caller's struct breaks it too. Why vrserver behaves this way is not established, only that it reproducibly does, in both directions, across six variants. Until that is understood this takes correctness over the corrections. The handler still runs so its estimator state, aligner and diagnostics stay live, but vrserver always receives the pose it gave us. That means the physical controller pose corrections - grip convention, Kalman/CA velocity, per-hand trims - no longer reach SteamVR. Users who prefer the corrections to hand tracking should stay on 1.1.5. Ruled out first, each by measurement rather than inspection: controller identity and render model, pose/Kalman/loss-coast processing, the IVRDriverInput_004 detours, device wrapping via TrackedDeviceAdded, a DriverPose_t size mismatch (280 == 280), and VRProperties being queried from the pose callback. Co-Authored-By: Claude Opus 5 (1M context) --- .../Driver/Hooking/InterfaceHookInjector.cpp | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp b/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp index 7731339..d3270fb 100644 --- a/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp +++ b/CustomHeadsetOpenVR/src/Driver/Hooking/InterfaceHookInjector.cpp @@ -3,6 +3,7 @@ #include "InterfaceHookInjector.h" #include "../DeviceProvider.h" #include "../EyeTrackingTap.h" +#include static CustomHeadsetDeviceProvider *Driver = nullptr; @@ -52,23 +53,78 @@ static Hook UpdateSkeletonComponentHook004("IVRDriverInput004::UpdateSkeletonComponent"); +// Do not hand vrserver any DriverPose_t but the caller's own. +// +// The detour copied the caller's pose, ran the handler over the copy and +// forwarded that copy. Doing so stops SteamVR promoting vrlink's native hand +// devices to a controller role: Prop_ControllerRoleHint_Int32 stays correct +// while GetControllerRoleForTrackedDeviceIndex returns Invalid for the whole +// hand session, so /user/hand/left|right resolve to no device and every hand +// binding is dead - while index_pinch still reaches IVRDriverInput and returns +// VRInputError_None, which is why nothing looks wrong at the driver boundary. +// +// Bisected live on SteamVR 2.17.9 / Steam Link 2.0.20, reading +// GetTrackedDeviceIndexForControllerRole across the hand/controller swap: +// +// forward a stack copy for every device -> hands broken +// detour not installed at all -> hands work +// forward the caller's object for every device -> hands work +// run the handler over the caller's object (const_cast) -> hands broken +// forward the copy only for devices we modified -> hands broken +// forward a persistent per-device static buffer -> hands broken +// +// Every object other than the caller's own breaks it, whatever its lifetime, +// and for any device - forwarding a copy for the CONTROLLERS alone still takes +// the hands down. Writing into the caller's struct breaks it too. Why vrserver +// behaves this way is not established; only that it reproducibly does. +// +// Until that is understood, correctness wins over the pose corrections: the +// handler still runs, so its estimator state, aligner and diagnostics stay +// live, but vrserver always gets the pose it gave us. THIS MEANS THE PHYSICAL +// CONTROLLER POSE CORRECTIONS (grip convention, Kalman/CA velocity, trims) DO +// NOT REACH SteamVR. See the pull request's test matrix and trade-off notes. +static void PoseAbiWarn(uint32_t unPoseStructSize) +{ + static std::atomic reported{ false }; + if (!reported.exchange(true, std::memory_order_relaxed)) + { + DriverLog("PoseABI: caller struct=%u ours=%u%s", unPoseStructSize, + (unsigned)sizeof(vr::DriverPose_t), + unPoseStructSize == (uint32_t)sizeof(vr::DriverPose_t) ? "" : " - MISMATCH, handler skipped"); + } +} + static void DetourTrackedDevicePoseUpdated005(vr::IVRServerDriverHost *_this, uint32_t unWhichDevice, const vr::DriverPose_t &newPose, uint32_t unPoseStructSize) { - //TRACE("ServerTrackedDeviceProvider::DetourTrackedDevicePoseUpdated(%d)", unWhichDevice); + PoseAbiWarn(unPoseStructSize); + if (unPoseStructSize != sizeof(vr::DriverPose_t)) + { + TrackedDevicePoseUpdatedHook005.originalFunc(_this, unWhichDevice, newPose, unPoseStructSize); + return; + } + // the handler works on a private copy purely for its own state and + // diagnostics; the copy is deliberately discarded, see above auto pose = newPose; if (Driver->HandleDevicePoseUpdated(unWhichDevice, pose)) { - TrackedDevicePoseUpdatedHook005.originalFunc(_this, unWhichDevice, pose, unPoseStructSize); + TrackedDevicePoseUpdatedHook005.originalFunc(_this, unWhichDevice, newPose, unPoseStructSize); } } static void DetourTrackedDevicePoseUpdated006(vr::IVRServerDriverHost *_this, uint32_t unWhichDevice, const vr::DriverPose_t &newPose, uint32_t unPoseStructSize) { - //TRACE("ServerTrackedDeviceProvider::DetourTrackedDevicePoseUpdated(%d)", unWhichDevice); + PoseAbiWarn(unPoseStructSize); + if (unPoseStructSize != sizeof(vr::DriverPose_t)) + { + TrackedDevicePoseUpdatedHook006.originalFunc(_this, unWhichDevice, newPose, unPoseStructSize); + return; + } + // the handler works on a private copy purely for its own state and + // diagnostics; the copy is deliberately discarded, see above auto pose = newPose; if (Driver->HandleDevicePoseUpdated(unWhichDevice, pose)) { - TrackedDevicePoseUpdatedHook006.originalFunc(_this, unWhichDevice, pose, unPoseStructSize); + TrackedDevicePoseUpdatedHook006.originalFunc(_this, unWhichDevice, newPose, unPoseStructSize); } }