‹ projects

reverb-m1

Native wired VR on Apple silicon: Reverb G1 display bring-up and stereo demo
git clone https://umhau.org/code/reverb-m1.git
Log | Files | Refs

NOTES.md (12862B)


      1 # HP Reverb G1 on M1 Mac: findings
      2 
      3 ## Root cause of the dark panel: HDCP
      4 When the DP link comes up, the headset turns the panel on (status `05 01 ...`). macOS's DCP then enables
      5 HDCP 2.x encryption about 2-3 s after hotplug; the headset blanks within ~300 ms, drops HPD, and retries every
      6 ~8 s forever. Ruled out: power (same hub works on Linux), refresh rate/mode, bit depth (8-bit RGB), our HID commands.
      7 
      8 ## Fix (CONFIRMED 2026-10-20): boot-arg `hdcp-fuse-mask=0`. Desktop output confirmed in hardware testing; full recipe in SETUP.md
      9 From disassembling this machine's arm64 kernelcache (macOS 15.6.1):
     10 - DPTX init: `PE_parse_boot_argn("hdcp-fuse-mask", &mask, 4)`, default = fuse & 3.
     11 - Then checks device-tree `/chosen/disable-av-content-protection` (iBoot-set, not a boot-arg). If true it logs
     12   "HDCP suppressed per disable-av-content-protection" and sets mask = 0. `-restore` does the same.
     13 - So `hdcp-fuse-mask=0` reproduces Apple's own HDCP-suppressed state. Parsed at 2 call sites.
     14 - Other HDCP boot-args (semantics not decoded): av_hdcp_protocol_mask, av_hdcp_auth_policy,
     15   av_protection_delay_ms, av_cp_failure_mitigation_enabled.
     16 - `amfi_get_out_of_my_way=1` affects code signing, not HDCP.
     17 
     18 ## HID (works)
     19 - Control 03f0:0c6a: 4x feature [0x50,0x01]+get, get 0x09/0x08/0x06, feature [0x04,0x01] = screen on.
     20 - IMU 045e:0659: output [0x02,0x07] starts 1000 Hz IMU (381-byte reports).
     21 - Status 0x05 (11 bytes): [1],[4] appear to track panel on. [5] is NOT a reliable refresh measurement (nonzero in some
     22   sessions, 0 while video was visibly running in others); do not infer refresh from it.
     23 
     24 ## After reboot
     25 Run `tools/verify.sh` (boot-args + log summaries only; no ioreg/system_profiler). Panel lit must be confirmed physically.
     26 
     27 ## Link reliability (2026-10-20, vr1 + vr0)
     28 - Periodic ~70-80 s drops ([time redacted]-[time redacted]) happened while `ioreg -c IOPortTransportStateDisplayPort` was polled at 1 Hz and a
     29   287 s run without polling had none, but causation is UNPROVEN. Signature: sink pulls HPD fully low (DP [HPD=0,IRQ=0]).
     30   Counter-evidence: an HPD=0 drop at [time redacted] with NO polling running, 18.6 s after a 90 Hz mode-set link-up.
     31   Still: never poll that class.
     32 - Post-DRST transient (2 of 3 trials, +5.05 s after link-up): headset raises IRQ_HPD (DP [HPD=1,IRQ=1]); DCP
     33   DCPDP13Service::handleCheckLink reports INTERLANE_ALIGN_DONE=0 -> "link unstable, retraining" -> terminates the video
     34   interface (macOS sees REMOVE) -> retrains at HBR3 x4 -> ADD ~0.63 s later. A link-integrity failure, different from
     35   the HPD=0 drops. Hypothesis: HBR3 margin through the adapter; 60 Hz fits HBR2 (~15.2 of 17.28 Gbps).
     36 - DCP firmware has per-port link limits (max_link_rate, max_link_rate_4_lanes, min_link_rate, max_lane_count) read
     37   from boot-args with default prefix "appledptx" (format strings "%s-%s" present, so likely `appledptx-max_link_rate=`);
     38   name format VERIFIED (DCP fw 0x708b0: snprintf(key[0]=='-' ? "-%s%s" : "%s-%s", prefix, key)) => boot-arg is exactly
     39   `appledptx-max_link_rate`; value is 1 byte in DisplayPort link-rate codes (VERIFIED: min_link_rate defaults to 6 =
     40   RBR at DCP fw 0x56914; compared against the logged linkRate at 0x52a7c). HBR2 cap = `appledptx-max_link_rate=0x14`
     41   (hex vs decimal parsing of the value not verified; 20 = 0x14). Untested; needs a reboot.
     42 - Stuck state (headset display gone, adapter reports HPD=0 to the Mac): **recoverable in software** with a USB-PD
     43   Data Reset on port 2's controller: `sudo tools/hpmctl 1 cmd DRST` (4CC passed big-endian, see
     44   tools/kernel/HPM_RE.md section 8). First success: display back 2.2 s after the command, no replug.
     45 - RETRACTED: "only a physical replug recovers it"; "deeper off state"; "HRST untested/unsupported" claims made
     46   before the byte order was fixed. With correct encoding HRST is rejected by the firmware (!CMD), DRST works.
     47 - HID screen_off (0x04 0x00) reliably induces the stuck state; screen_on / full wake alone do not undo it.
     48 
     49 ## App recovery path (2026-10-20, vr0 implementation, vr1 review)
     50 - macOS SkyLight blocks the app's MAIN thread for exactly ~10.0 s right after delivering a display REMOVE
     51   (sample: CGSPostLocalNotification -> SLSWaitThroughReconfigurationCycleForcingNotificationHandling ->
     52   connection_fetch_updated_event_queue). Not triggered by our code; IMU callbacks on main stall with it.
     53 - Fix: recovery state machine on its own serial queue, fed directly from the CG callback before the main hop;
     54   rendering on a dedicated CAMetalDisplayLink thread. Forced outage 14.4 s -> 4.4 s.
     55 - Tested, not supported (vr1 hypothesis): "the window server waits for us to release the captured display".
     56   CGDisplayRelease inside the REMOVE callback returns 1001 = kCGErrorIllegalArgument (the id is no longer valid there)
     57   and outages stayed ~4.6 s, so that release was removed. This does NOT conclusively rule out every capture-related
     58   cause; the root cause of the ~10 s SkyLight wait remains unknown.
     59 - Helper result: DATA1 byte0 = 0x04 observed after a DRST whose IOReturn was 0 and after which the display returned.
     60   Meaning of 0x04 unknown (TI documents 0 = success, 1 = timeout/abort, 3 = rejected). The helper conservatively
     61   exits 1; the app relies on display ADD.
     62 - Remaining outage = 2 s grace (avoids resetting during 0.6-0.8 s self-heals) + ~2.2 s DRST/retrain/re-add.
     63   Millisecond continuity would need the HPD-blip ride-through below (untested, needs a reboot).
     64 
     65 ## Candidate: ride through brief HPD drops (millisecond-level, no display REMOVE) — static, UNTESTED
     66 DCP firmware (FUD/DCP.img4, t8103) AppleDCPDPTXController config, verified in disassembly:
     67 - 0x56c0c: key `hot_unplug_delay_ms` -> boot-arg `appledptx-hot_unplug_delay_ms` (prefix + "-" + key, see above).
     68 - 0x56c1c-0x56c28: default = (0x4bc5c(this) ? 0 : 200) ms; 0x4bc5c = (port-type vmethod +0x1a0 == 1), true for our
     69   USB-C alt-mode port => default 0 => macOS removes the display the instant HPD drops ("HPD Lost period over").
     70 - 0x573f0: stores the boot-arg's numeric value if present, else the default. Property `hot-unplug-delay-ms` (8 bytes)
     71   may override afterwards if present (it is not in our device tree).
     72 - handleUnplugEvent (0x55134) logs "HPD Lost, delay = %llu" from this field when non-zero.
     73 The headset's HPD=0 self-heals restored HPD in ~120 ms ([time redacted] -> .997). With
     74 `appledptx-hot_unplug_delay_ms=300` those would not reach macOS at all. Does NOT cover the IRQ_HPD/INTERLANE retrain
     75 mode (that path calls handleRemoveInterfaces directly). Needs a reboot; proposed boot-args:
     76 `amfi_get_out_of_my_way=1 hdcp-fuse-mask=0 appledptx-hot_unplug_delay_ms=300` (optionally `appledptx-max_link_rate=0x14`
     77 for 60 Hz-only operation).
     78 
     79 ## Milestones
     80 - 2026-10-20 ~[time redacted]: the stereo 3D test scene was visible inside the headset, after an automatic app
     81   recovery. App: captured display, native 4320x2160 @ 90 Hz, strict presented-frame telemetry 89.90 fps
     82   (presentAge 0.01 s, GPU 1.75 ms/frame, 0 GPU errors, 7 skipped frames at startup only). No boot-arg changes needed.
     83 - Display-loss detection latency in the app: 10.7 s -> 0.98 s after moving callback work to CFRunLoopPerformBlock.
     84 - Still open: millisecond-level continuity (candidate boot-arg appledptx-hot_unplug_delay_ms, parked), head tracking,
     85   lens-distortion calibration.
     86 - 2026-10-20 ~[time redacted]: RELIABILITY MILESTONE PASSED. 650 s no-fault soak at native 90 Hz: 89.9987 presented fps,
     87   skipped +3, 0 GPU errors, clean exit, zero DP resets (evidence/verified-90hz-soak.json). Fault recovery (separate
     88   trials): 4.4 s outage via recovery queue + DRST.
     89 - Factory calibration was read successfully. Measured per-unit optical geometry is omitted from this public history; the demo reads each headset at runtime.
     90 - Factory chromatic calibration motivated the per-channel correction pass. Per-unit measurements are omitted.
     91 - GPU warp check (Tests/LensWarpChecks.swift, evidence/lenswarp-gpu-checks.txt): 1632 channel samples, max error
     92   0.0041511 against the Double CPU model. Resolution is limited by 8-bit coordinate gradients (~12 texels per step);
     93   catches gross/channel-swap errors, not sub-12-texel ones; blue position is not checked.
     94 
     95 ## Factory optical correction (2026-10-20)
     96 
     97 Optical compensation is separate from head tracking. The factory store was read twice (10,737 bytes),
     98 and both reads matched byte for byte. The saved JSON supplies the per-eye affine transform, asymmetric field of view,
     99 and separate Polynomial-3K coefficients for red, green, and blue. Source: Monado WMR driver and distortion utilities;
    100 see DEPENDENCIES.md and LICENSES/BSL-1.0.txt for provenance.
    101 
    102 The renderer now always uses that calibration: 6048x3024 offscreen stereo rendering, then a per-color correction pass
    103 into native 4320x2160. The optics grid uses angular coordinates, so its straight-ahead cross agrees between eyes.
    104 Raw output snapshots intentionally show curved lines and separated colors: the physical lenses should cancel that
    105 pre-compensation. That is a software/model expectation, not proof that all observed optical defocus is corrected.
    106 
    107 Validation: offline calibration fixtures pass; the production Metal coordinate function agrees with the Double CPU
    108 model over 3,750 RGB coordinate pairs (max normalized error 4.574e-7). The additional end-to-end 8-bit gradient sampling
    109 check passes (1,632 channel samples, max error 0.004152, limited by 8-bit quantization). Asymmetric projection edges
    110 also pass. The 180 s calibrated hardware trial exits cleanly: 89.996 presented fps in the 160 s settled interval,
    111 zero display removes, one additional skipped frame, 0 GPU errors, 5.97 ms GPU/frame. Detailed results are in evidence/.
    112 Physical optical validation remains separate from software/model checks. Optical focus/fit and the headset's intrinsic peripheral limits
    113 may remain after correct geometric/chromatic compensation; do not claim blur has been eliminated based on GPU tests.
    114 
    115 Offline checks:
    116 ```sh
    117 swiftc -O Sources/VRWorkstation/Calibration.swift Tests/CalibrationChecks.swift -o /tmp/vr-calibration-checks
    118 /tmp/vr-calibration-checks
    119 swiftc -O -D LENS_WARP_CHECKS Sources/VRWorkstation/Calibration.swift Sources/VRWorkstation/LensWarp.swift Tests/LensWarpChecks.swift -framework Metal -o /tmp/vr-lenswarp-checks
    120 /tmp/vr-lenswarp-checks "$HOME/Library/Application Support/ReverbM1Demo/factory.json"
    121 ```
    122 
    123 The calibrated live build also passed a forced panel-off recovery: display ADD after 4.576 s, then the known
    124 0.654 s post-reset retrain healed automatically; native 90 Hz rendering resumed with no GPU errors.
    125 See `evidence/calibrated-recovery-trial.json` and `evidence/calibrated-recovery-live.txt`.
    126 The running demo logs to `/tmp/vr-calibrated-live.txt`; Q or Escape in its window quits it.
    127 
    128 ## Live optical tuning and floor-grid filtering (2026-10-21)
    129 
    130 The Mac has a Headset optics window with
    131 scene/white-grid/green-grid patterns, red strength, and separate horizontal/vertical blue strengths (-100% to 400%).
    132 100% is the original factory compensation; zero uses green geometry for that channel. The controls pass settings to
    133 the render worker under a lock, and debounce-save them to Application Support/ReverbM1Demo/optics-adjustments.json.
    134 Factory JSON remains unchanged. Green-only mode disables the color sliders. Reset restores factory color strengths.
    135 
    136 Separately, the floor's 1.5 cm geometric grid strips projected below one panel pixel at distance. They were replaced
    137 with one floor surface and a derivative-filtered grid in its fragment shader: minimum readable pixel footprint,
    138 smoothed coverage, and per-axis fading when line spacing becomes unresolved. This reduces different aliasing between
    139 the two eyes, but does not change the stereo view matrices. An isolated infinity-alignment target was discussed but
    140 has NOT been added; the white/green patterns are angular grids.
    141 
    142 Operator feedback confirmed an improvement with optical tuning. Personal adjustment values and verbatim feedback are omitted.
    143 Current evidence/optics-controls-live.txt shows approximately 90 presented fps and zero GPU errors.
    144 
    145 The optical tuning shader passes float-coordinate comparisons including independent blue axes; the test also retains
    146 the end-to-end quantized sampling check. Screenshots show pre-compensated output before the physical lenses, so they
    147 cannot establish the blur/fringe that reaches a user's eye. Software rendering, optical compensation, and physical
    148 lens/eye placement are separate contributors; do not infer a hardware defect from the remaining residuals.
    149 
    150 ## Desktop coexistence
    151 
    152 The demo uses a non-activating headset panel without exclusive display capture. Its session-only display layout places the headset to the right of other displays. The application does not change Dock preferences. Native 90 Hz rendering and clean shutdown were checked; see evidence/desktop-coexistence.txt.