# Phase 10: Feasibility Spike - Findings

**Date:** 2026-03-24
**SteamVR version:** 2.15.6
**HMD:** Beyond 2
**Driver:** BeyondProximity sidecar (no tracked devices)

## FEAS-01: SetFloatProperty(Prop_UserIpdMeters_Float) from Sidecar

**Result:** PASS

**Evidence:**
- Baseline IPD (before any changes): `OK ipd=0.0630m (63.0mm) err=0`
- After `ipd_test 65`: SetFloatProperty returned err=0
  - CLI response: `OK ipd_set=65.0mm (0.0650m)`
  - Log: `IPD: SetFloatProperty(HMD, UserIpdMeters, 0.0650) = 0`
- Readback after set: `OK ipd=0.0650m (65.0mm) err=0`
- VREvent_IpdChanged fired: **yes** — `IPD Event: VREvent_IpdChanged ipd=0.0650m (65.0mm)` logged immediately after SetFloatProperty
- Boundary tests:
  - 48mm (min): `OK ipd_set=48.0mm (0.0480m)` — readback confirmed 0.0480m, VREvent fired
  - 75mm (max): `OK ipd_set=75.0mm (0.0750m)` — readback confirmed 0.0750m, VREvent fired
  - 47mm (below range): `ERR ipd out of range (48-75mm)` — correctly rejected
  - 76mm (above range): `ERR ipd out of range (48-75mm)` — correctly rejected
- Startup events observed: VREvent_IpdChanged fires twice at boot — first `ipd=0.0000m`, then `ipd=0.0630m` (system sets initial IPD)

**Human verification (visual):**
- IPD change GUI notification: **appears** with each ipd_test call (SteamVR shows the IPD value changing)
- Visual eye separation change in HMD: **NO** — no observable rendering change despite property write succeeding
- This matches Research Pitfall 4: "Property Write Succeeds But No Observable Effect"

**Conclusion:** Property write from sidecar successfully changes the IPD value in the OpenVR property system (err=0, readback confirmed, VREvent fires, GUI notification appears). However, the actual rendering eye separation does NOT change. This strongly indicates that `SetDisplayEyeToHead` has been called by the lighthouse driver (or BeyondEyetracking shim), permanently disabling property-based IPD auto-computation. The property system still works as metadata, but the compositor uses fixed EyeToHead matrices instead of auto-computing from the property.

**Revised result: PARTIAL PASS** — property writes work at the API level but do not affect rendering.

## FEAS-02: Lighthouse Driver SetDisplayEyeToHead Behavior

**Result:** LIKELY BLOCKS (via eye_to_head config, not logged)

**Evidence:**
- "SetDisplayEyeToHead" found in vrserver.txt: **no** — zero occurrences logged. However, absence of log entry does not mean the call wasn't made.
- Property readback shows new value after ipd_test: **yes** — property system works correctly
- VREvent_IpdChanged fires after property write: **yes** — fires synchronously
- **Visual eye separation change: NO** — this is the key evidence that SetDisplayEyeToHead HAS been called
- Beyond 2 lighthouse config (`lhr-1f8e25f1/config.json`) contains non-trivial `eye_to_head` rotation matrices in `tracking_to_eye_transform` — the lighthouse driver applies these at init
- BeyondEyetracking shim driver hooks `IVRServerDriverHost_006` and creates an `HmdShimDriver` that wraps the lighthouse HMD — this shim may also call SetDisplayEyeToHead for eye tracking adjustments

**Root cause analysis:**
The Beyond 2's lighthouse JSON config defines per-eye `eye_to_head` 3x3 rotation matrices with off-diagonal elements (lens cant geometry). The lighthouse driver must apply these via `SetDisplayEyeToHead` during initialization. Per OpenVR docs: "Once you call SetDisplayEyeToHead, SteamVR will no longer automatically update the HeadFromEyePose based on changes to Prop_UserIpdMeters_Float." This is why property writes succeed but rendering doesn't change.

**Conclusion:** The lighthouse driver (and/or BeyondEyetracking shim) calls SetDisplayEyeToHead at initialization, permanently disabling property-based IPD for rendering. The sidecar CANNOT change eye separation via SetFloatProperty alone. To actually change rendering IPD, the sidecar must call SetDisplayEyeToHead itself with correct matrices (rotation from config + new IPD translation).

## FEAS-03: IpdUIRange Slider Trigger

**Result:** PROPERTIES SET SUCCESSFULLY

**Evidence:**
- slider_test response: `OK range_min=0.048 err=0 range_max=0.075 err=0 displays_ipd=true err=0`
- Property write errors: min=0 (success), max=0 (success), displays_ipd=0 (success)
- Log: `Slider Test: IpdUIRangeMin=0 IpdUIRangeMax=0 DriverDisplaysIPD=0` (all err=0)
- DriverDisplaysIPDChanges_Bool value tested: true
- Note: Whether the SteamVR slider UI visually appears in the dashboard requires human verification (see Task 3 checkpoint)
- Note: Testing with DriverDisplaysIPDChanges_Bool=false deferred to Phase 12

**Human verification (visual):**
- SteamVR IPD slider visible in dashboard settings: **NO** — slider does not appear after slider_test

**Conclusion:** All three slider properties are accepted by the OpenVR property system (err=0), but setting them from the sidecar does NOT trigger the SteamVR slider UI. Possible reasons: (1) SteamVR only reads these at startup, not dynamically; (2) properties must be set by the HMD-owning driver; (3) `DriverDisplaysIPDChanges_Bool=true` may mean "driver handles IPD display" which suppresses the slider rather than enabling it; (4) additional driver manifest or capability flags may be required.

**Revised result: FAIL** — properties accepted but slider UI does not appear.

### FEAS-03 Alternative Approaches to Research

| Approach | Description | Effort | Risk |
|----------|-------------|--------|------|
| Try DriverDisplaysIPDChanges_Bool=false | The name suggests `true` = "driver handles IPD display" — may suppress slider | Low | May not help if ownership is the issue |
| Set properties at driver Init() | SteamVR may only read slider config at startup | Low | Requires restart to test |
| SteamVR dashboard overlay | Build custom overlay with our own IPD slider | Medium | Full control, but more code to maintain |
| Desktop GUI / tray app | Simple Windows UI since pipe interface exists | Medium | No VR integration, but simple and reliable |
| steamvr.vrsettings manipulation | Write IPD to settings file, trigger reload | Low | May not trigger runtime change |
| IVRDriverInput or action manifest | Alternative input mechanism for IPD control | Medium | Undocumented for this use case |

## Go/No-Go Decision

**Decision:** PARTIAL GO

**Rationale:**
- FEAS-01 (property IPD): **PARTIAL PASS** — property writes work at API level, VREvent fires, GUI notification appears, BUT rendering eye separation does not change
- FEAS-02 (lighthouse blocking): **LIKELY BLOCKS** — lighthouse config has eye_to_head matrices, rendering unaffected by property writes
- FEAS-03 (slider trigger): **FAIL** — properties accepted (err=0) but slider UI does not appear

**Critical finding:** The sidecar can write to the OpenVR property system, and the system acknowledges the changes (VREvent, GUI notification). But the rendering pipeline uses `SetDisplayEyeToHead` matrices set at init, not the property value. To actually change rendering IPD, the sidecar must call `SetDisplayEyeToHead` with correct matrices.

**Impact on roadmap:**
- Phase 11 (Core IPD Pipe Command): **GO with revised approach** — must use `SetDisplayEyeToHead` instead of property-only. Need to: (1) read current eye_to_head rotation from lighthouse config or runtime, (2) compute translation from desired IPD, (3) call `SetDisplayEyeToHead` from sidecar. Also set property for metadata/GUI consistency.
- Phase 12 (SteamVR IPD Slider UI): **DESCOPED or REVISED** — native slider won't work from sidecar. Consider: custom SteamVR overlay, desktop tray app, or alternative UI approach.
- Phase 13 (Slider Persistence): **GO** — persistence mechanism is independent of UI approach. Save/restore IPD value on driver startup.

**Alternative strategies for rendering IPD (Phase 11):**
1. **SetDisplayEyeToHead from sidecar (PRIMARY):** Call `VRServerDriverHost()->SetDisplayEyeToHead()` with device index 0 (HMD). Compute HmdMatrix34_t from lighthouse config rotation + IPD-based translation (left=[-ipd/2,0,0], right=[+ipd/2,0,0]). Risk: sidecar may not have permission to call this for a device it doesn't own — needs spike.
2. **Hook approach (FALLBACK):** Similar to BeyondEyetracking's `IVRServerDriverHost_006` hook — intercept or extend the shim to update eye transforms. Higher complexity but proven by ET driver.
3. **VREvent_IpdChanged listener in lighthouse (LONG SHOT):** If lighthouse driver listens for VREvent_IpdChanged and re-applies transforms — but this depends on Valve's implementation and is unlikely.

**Next spike needed:** Test whether `VRServerDriverHost()->SetDisplayEyeToHead(0, leftMatrix, rightMatrix)` works from the sidecar driver (device 0 = HMD owned by lighthouse). This is the critical gate for Phase 11.
