# Phase 10.1: SetDisplayEyeToHead Spike - Context

**Gathered:** 2026-03-24
**Status:** Ready for planning

<domain>
## Phase Boundary

Test whether `VRServerDriverHost()->SetDisplayEyeToHead(0, leftMatrix, rightMatrix)` works from the sidecar driver for the HMD it doesn't own (device 0, owned by lighthouse driver). This is the critical gate for Phase 11's IPD implementation. Spike code that validates the approach — not production features.

</domain>

<decisions>
## Implementation Decisions

### Matrix computation
- Read current EyeToHead transforms at runtime: try property-based readback first, fall back to parsing lighthouse config JSON (`lhr-*/config.json` → `tracking_to_eye_transform[N].eye_to_head`)
- Deconstruct the existing 3x3 rotation matrix to extract Euler angles (XYZ convention per VAP code: pitch, yaw, roll)
- Expect ±6.17° yaw (HORIZONTAL_CANT from VAP alignment code) as built-in validation that the decomposition math is correct
- Rebuild the full HmdMatrix34_t: [3x3 rotation from deconstructed angles | 3x1 translation from new IPD (left=[-ipd/2,0,0], right=[+ipd/2,0,0])]
- Research phase MUST include proper study of rotation matrix decomposition — papers, published studies, mathematical foundations for extracting Euler angles from 3x3 rotation matrices

### Test verification
- Visual inspection in HMD is the primary verification method (not programmatic readback)
- Step through multiple IPD values: 48mm → 55mm → 63mm → 75mm to confirm unmistakable visual changes at each step
- Log deconstructed Euler angles (pitch/yaw/roll) for both eyes before and after SetDisplayEyeToHead calls — the ±6.17° yaw serves as a sanity check that the matrix math is correct
- If direct call works, test repeated calls in sequence to confirm live-change capability (not a one-shot thing)

### Pipe command design
- Single command: `eyetohead_set <mm>` — reads current rotation, deconstructs angles, logs them, rebuilds matrix with new IPD translation, calls SetDisplayEyeToHead
- Also calls SetFloatProperty(Prop_UserIpdMeters_Float) alongside SetDisplayEyeToHead to keep property system in sync for GUI/event consistency
- Response includes full diagnostics: IPD set, extracted Euler angles (pitch/yaw/roll) for both eyes
- Example response: `OK ipd=65.0mm left_yaw=6.17 right_yaw=-6.17 left_pitch=0.00 right_pitch=0.00`
- IPD validation: 48-75mm range (matching Beyond 2 physical range)

### Failure handling
- SetDisplayEyeToHead returns void — no error code available
- Detection of success/failure is visual-only: if rendering doesn't change across the 48→55→63→75mm test sequence, the call silently failed
- If direct call fails: document the BeyondEyetracking `IVRServerDriverHost_006` hook approach as the fallback strategy for Phase 11 (higher complexity but proven by the ET driver)
- Go/no-go criteria:
  - **PASS**: Visual eye separation changes when calling SetDisplayEyeToHead from sidecar with device 0
  - **FAIL**: No visual change — sidecar cannot call SetDisplayEyeToHead for HMD it doesn't own

### Claude's Discretion
- Exact approach for reading current EyeToHead transforms (which properties to query, which lighthouse config paths to check)
- Euler angle decomposition implementation details (library choice, edge case handling)
- Order of IPD test values during visual verification
- Log format and verbosity beyond the required Euler angle output

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### OpenVR API
- `extern/openvr/headers/openvr_driver.h` — SetDisplayEyeToHead signature (line 3806): `void SetDisplayEyeToHead(uint32_t unWhichDevice, const HmdMatrix34_t& eyeToHeadLeft, const HmdMatrix34_t& eyeToHeadRight)`. Also: HmdMatrix34_t type, IVRServerDriverHost_006 interface version
- `extern/openvr/docs/Driver_API_Documentation.md` — IVRServerDriverHost section, device ownership semantics

### Existing driver code
- `src/driver/device_provider.cpp` — Current pipe command handler (HandlePipeCommand), existing spike methods (SetHmdIpd, HandleEyeToHeadCheck, HandleSliderTest), VRServerDriverHost() usage pattern, HMD container access via TrackedDeviceToPropertyContainer
- `src/driver/device_provider.h` — Class structure, member variables

### VAP alignment reference (rotation matrix computation)
- `code_samples/vertical_alignment_proximity/alignment.py` — `align_write_config_values` (line 545): shows how eye_to_head rotation is built from Euler XYZ [pitch, ±6.17° yaw, 0]. `align_reset_config_values` (line 569): baseline rotation. HORIZONTAL_CANT = 6.17° (line 73), DOWNWARD_CANT = -5° (line 72)
- `code_samples/vertical_alignment_proximity/dist/main/_internal/steamvr/steamvr.py` — `set_ipd_default_mm` (line 73), `set_eye_to_head` (line 81): config structure for IPD and rotation

### Phase 10 findings
- `.planning/phases/10-feasibility-spike/10-FINDINGS.md` — FEAS-01/02/03 results, go/no-go rationale, root cause analysis of why SetDisplayEyeToHead is needed
- `.planning/phases/10-feasibility-spike/10-CONTEXT.md` — Prior spike methodology and decisions

### Requirements
- `.planning/REQUIREMENTS.md` — FEAS-01/02/03 (completed), IPD-01 through IPD-05 (Phase 11, dependent on this spike)

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `DeviceProvider::SetHmdIpd()` (device_provider.cpp:358-372) — SetFloatProperty pattern for IPD. Spike's `eyetohead_set` extends this with SetDisplayEyeToHead
- `DeviceProvider::HandleEyeToHeadCheck()` (device_provider.cpp:374-390) — Property readback pattern for IPD. Can be extended to read matrix-related properties
- `DeviceProvider::HandlePipeCommand()` (device_provider.cpp:195-320) — Pipe command dispatcher. New `eyetohead_set` follows same pattern
- `VRServerDriverHost()->PollNextEvent()` (device_provider.cpp:99) — Already used for VREvent_IpdChanged detection

### Established Patterns
- HMD container access: `VRProperties()->TrackedDeviceToPropertyContainer(k_unTrackedDeviceIndex_Hmd)` — proven pattern, reuse for SetDisplayEyeToHead's device index (0)
- Pipe command format: `"command arg"` in, `"OK key=value"` or `"ERR message"` out
- State-change guard in SetHmdProximity (only write on change) — consider for IPD dedup

### Integration Points
- `DeviceProvider::HandlePipeCommand()` — Where `eyetohead_set` command handler is added
- `VRServerDriverHost()` — The interface that provides SetDisplayEyeToHead (line 3806 in openvr_driver.h)
- `k_unTrackedDeviceIndex_Hmd` (= 0) — The device index to pass to SetDisplayEyeToHead

</code_context>

<specifics>
## Specific Ideas

- The ±6.17° yaw angle from the VAP alignment code (HORIZONTAL_CANT) is the known Beyond 2 lens cant — extracting this value from the runtime matrix serves as proof that the decomposition math is correct
- Research phase must cover rotation matrix decomposition rigorously — published papers, mathematical foundations, not just "looks right"
- The spike tests repeated SetDisplayEyeToHead calls to confirm live-change capability, not just one-shot
- Spike code evolves into Phase 11's production implementation — the `eyetohead_set` command becomes the basis for the production `ipd` command

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope

</deferred>

---

*Phase: 10.1-setdisplayeyetohead-spike*
*Context gathered: 2026-03-24*
