commit 953367e42d9f600488269c20d5d32ca592ed3cc1
parent c265ec7a54fac4ebb3b87718c6fb9cd535c868e0
Author: umhau <umhau@example.invalid>
Date: Wed, 21 Oct 2026 12:00:02 +0000
App owns the headset: display capture at 4320x2160@60, passive health watch; hpmctl cmd mode; HPM RE notes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat:
6 files changed, 403 insertions(+), 41 deletions(-)
diff --git a/Sources/VRWorkstation/App.swift b/Sources/VRWorkstation/App.swift
@@ -1,4 +1,9 @@
-// Fullscreen window on the headset display running the stereo renderer.
+// The app owns the headset: it wakes it over HID, captures its display (no desktop, no other windows),
+// renders the stereo scene fullscreen at 4320x2160 @ 60 Hz, and re-takes control the moment the display returns.
+//
+// Health is watched PASSIVELY only (macOS display callbacks + the headset's own 0x05 status reports).
+// Never poll USB-C/DisplayPort port state (ioreg IOPortTransportStateDisplayPort): that polling made the
+// headset drop its link every ~70 s.
import AppKit
import MetalKit
@@ -9,46 +14,111 @@ final class HeadsetWindow: NSWindow {
}
}
-/// The headset shows up as an external display from vendor 0x220E (8718).
-func findHeadsetScreen() -> NSScreen? {
- NSScreen.screens.first { s in
- let id = (s.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0
- return CGDisplayVendorNumber(id) == 8718
- }
+/// The headset's display: vendor 0x220E (8718).
+func isHeadsetDisplay(_ id: CGDirectDisplayID) -> Bool { CGDisplayVendorNumber(id) == 8718 }
+
+func onlineHeadsetDisplay() -> CGDirectDisplayID? {
+ var ids = [CGDirectDisplayID](repeating: 0, count: 16)
+ var n: UInt32 = 0
+ CGGetOnlineDisplayList(16, &ids, &n)
+ return ids.prefix(Int(n)).first(where: isHeadsetDisplay)
}
-func runApp(windowed: Bool) {
- let app = NSApplication.shared
- app.setActivationPolicy(.regular)
+func log(_ s: String) { print(String(format: "%.3f ", Date().timeIntervalSince1970) + s) }
+
+final class HeadsetSession {
+ let hmd = HMD()
+ let renderer: Renderer
+ let view: MTKView
+ private var window: HeadsetWindow?
+ private(set) var display: CGDirectDisplayID?
+ private var lostAt: Date?
- // Wake the headset only if its display isn't up yet (e.g. tools/hmdwake already running).
- var hmd: HMD?
- if !windowed && findHeadsetScreen() == nil {
- let h = HMD()
- h.onStatus = { print("[hmd]", $0) }
- do { try h.start(); hmd = h; print("[hmd] woke headset, waiting for its display...") }
- catch { print("[hmd] not available:", error) }
- let deadline = Date().addingTimeInterval(10)
- while findHeadsetScreen() == nil && Date() < deadline { RunLoop.main.run(until: Date().addingTimeInterval(0.25)) }
+ init() throws {
+ view = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice())
+ view.colorPixelFormat = .bgra8Unorm
+ view.depthStencilPixelFormat = .depth32Float
+ view.preferredFramesPerSecond = 60
+ renderer = try Renderer(view: view)
+ view.delegate = renderer
}
- let screen = windowed ? nil : findHeadsetScreen()
- if screen == nil && !windowed { print("Headset display not found; showing a preview window instead.") }
- let frame = screen?.frame ?? NSRect(x: 100, y: 100, width: 1080, height: 540)
- let win = HeadsetWindow(contentRect: frame, styleMask: screen == nil ? [.titled, .closable, .resizable] : .borderless,
- backing: .buffered, defer: false, screen: screen)
- win.title = "VR Workstation preview"
- let view = MTKView(frame: NSRect(origin: .zero, size: frame.size), device: MTLCreateSystemDefaultDevice())
- view.colorPixelFormat = .bgra8Unorm
- view.depthStencilPixelFormat = .depth32Float
- view.preferredFramesPerSecond = 90
- let renderer: Renderer
- do { renderer = try Renderer(view: view) } catch { print("Renderer failed:", error); exit(1) }
- view.delegate = renderer
- win.contentView = view
- if screen != nil { win.level = .screenSaver; win.setFrame(frame, display: true) }
- win.makeKeyAndOrderFront(nil)
+ func start() {
+ hmd.onStatus = { log("[hmd] \($0)") }
+ hmd.onPanel = { [weak self] on in if !on { self?.panelOff() } }
+ do { try hmd.start() } catch { log("[hmd] \(error)") }
+
+ let me = Unmanaged.passUnretained(self).toOpaque()
+ CGDisplayRegisterReconfigurationCallback({ id, flags, ctx in
+ guard isHeadsetDisplay(id), !flags.contains(.beginConfigurationFlag) else { return }
+ let s = Unmanaged<HeadsetSession>.fromOpaque(ctx!).takeUnretainedValue()
+ if flags.contains(.removeFlag) { DispatchQueue.main.async { s.displayLost(id) } }
+ if flags.contains(.addFlag) { DispatchQueue.main.async { s.displayArrived(id) } }
+ }, me)
+ if let id = onlineHeadsetDisplay() { displayArrived(id) } else { log("waiting for the headset display...") }
+ }
+
+ /// Takes exclusive control of the headset display and puts the renderer on it.
+ func displayArrived(_ id: CGDirectDisplayID) {
+ if let t = lostAt { log(String(format: "headset display back after %.0f ms", Date().timeIntervalSince(t) * 1000)) }
+ lostAt = nil
+ display = id
+ let r = CGDisplayCapture(id)
+ log("captured display \(id): \(r == .success ? "ok" : "error \(r.rawValue)")")
+ setNativeMode(id)
+
+ // A captured display is covered by the shielding window; ours must sit above it.
+ let bounds = CGDisplayBounds(id)
+ let mainH = CGDisplayBounds(CGMainDisplayID()).height
+ let frame = NSRect(x: bounds.minX, y: mainH - bounds.maxY, width: bounds.width, height: bounds.height)
+ let win = window ?? HeadsetWindow(contentRect: frame, styleMask: .borderless, backing: .buffered, defer: false)
+ win.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
+ win.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
+ win.contentView = view
+ win.setFrame(frame, display: true)
+ win.makeKeyAndOrderFront(nil)
+ window = win
+ view.isPaused = false
+ log("rendering on headset \(frame.size), drawable \(view.drawableSize)")
+ }
+
+ func displayLost(_ id: CGDirectDisplayID) {
+ guard id == display else { return }
+ lostAt = Date()
+ display = nil
+ view.isPaused = true
+ log("headset display LOST")
+ recover()
+ }
+
+ func panelOff() { log("headset reports panel off") }
+
+ /// Recovery hook. The display normally returns by itself in under a second; when the USB-C DP adapter gets
+ /// stuck reporting HPD low, the fix is restarting port 2's DisplayPort alt-mode session (see NOTES.md).
+ func recover() {
+ log("recovery: waiting for the display to return")
+ }
+
+ /// 4320x2160 pixels at 60 Hz, unscaled.
+ private func setNativeMode(_ id: CGDirectDisplayID) {
+ let opts = [kCGDisplayShowDuplicateLowResolutionModes: true] as CFDictionary
+ let modes = (CGDisplayCopyAllDisplayModes(id, opts) as? [CGDisplayMode]) ?? []
+ guard let m = modes.first(where: { $0.pixelWidth == 4320 && $0.pixelHeight == 2160 && $0.refreshRate == 60 && $0.width == 4320 })
+ ?? modes.first(where: { $0.pixelWidth == 4320 && $0.pixelHeight == 2160 && $0.refreshRate == 60 }) else {
+ log("no 4320x2160@60 mode"); return
+ }
+ if CGDisplayCopyDisplayMode(id)?.ioDisplayModeID == m.ioDisplayModeID { return }
+ let r = CGDisplaySetDisplayMode(id, m, nil)
+ log("mode 4320x2160@60 (ui \(m.width)x\(m.height)): \(r == .success ? "ok" : "error \(r.rawValue)")")
+ }
+}
+
+func runApp() {
+ let app = NSApplication.shared
+ app.setActivationPolicy(.regular)
+ let session: HeadsetSession
+ do { session = try HeadsetSession() } catch { print("renderer failed:", error); exit(1) }
+ session.start()
app.activate(ignoringOtherApps: true)
- print("Rendering on \(screen?.localizedName ?? "preview window") \(frame.size), drawable \(view.drawableSize)")
- withExtendedLifetime((hmd, renderer)) { app.run() }
+ withExtendedLifetime(session) { app.run() }
}
diff --git a/Sources/VRWorkstation/HMD.swift b/Sources/VRWorkstation/HMD.swift
@@ -14,6 +14,8 @@ struct IMUSample {
final class HMD {
var onIMU: ((IMUSample) -> Void)?
var onStatus: ((String) -> Void)?
+ /// Panel power state from the headset's 0x05 status report (true = lit).
+ var onPanel: ((Bool) -> Void)?
private var managers: [IOHIDManager] = [] // must outlive the devices, or they get closed
private var control: IOHIDDevice?
@@ -71,7 +73,9 @@ final class HMD {
}
private func handleControl(_ p: UnsafeMutablePointer<UInt8>, _ n: Int) {
- if n >= 6 && p[0] == 0x05 { onStatus?("panel \(p[1] == 1 ? "on" : "off"), measured refresh \(p[5] == 0 ? 0 : Int(p[5]) + 1) Hz") }
+ guard n >= 6 && p[0] == 0x05 else { return }
+ onStatus?("panel \(p[1] == 1 ? "on" : "off"), measured refresh \(p[5] == 0 ? 0 : Int(p[5]) + 1) Hz")
+ onPanel?(p[1] == 1)
}
private func handleSensors(_ p: UnsafeMutablePointer<UInt8>, _ n: Int) {
diff --git a/Sources/VRWorkstation/main.swift b/Sources/VRWorkstation/main.swift
@@ -2,7 +2,7 @@ import Foundation
import MetalKit
import simd
setvbuf(stdout, nil, _IOLBF, 0)
-// Usage: VRWorkstation [--windowed] | --snapshot out.png [--snapshot-size WxH] [--snapshot-yaw deg]
+// Usage: VRWorkstation | --snapshot out.png [--snapshot-size WxH] [--snapshot-yaw deg]
// Options: --snapshot-size WxH (default 4320x2160), --snapshot-yaw <degrees> (head turn, positive = left).
func option(_ name: String) -> String? {
guard let i = CommandLine.arguments.firstIndex(of: name), CommandLine.arguments.count > i + 1 else { return nil }
@@ -23,4 +23,4 @@ if let i = CommandLine.arguments.firstIndex(of: "--snapshot") {
} catch { print("snapshot failed:", error) }
exit(0)
}
-runApp(windowed: CommandLine.arguments.contains("--windowed"))
+runApp()
diff --git a/tools/hpmctl.c b/tools/hpmctl.c
@@ -2,6 +2,8 @@
// Apple's AppleHPMLib IOKit plug-in, the same path /usr/bin/hpmdiagnose uses.
//
// Usage: hpmctl <rid> read <reg> read one controller register (hex), e.g. `hpmctl 1 read 0x58`
+// hpmctl <rid> cmd <4cc> run a 4-char controller command, e.g. `hpmctl 1 cmd DRST` (USB-PD Data Reset)
+// RID 0 is refused: it carries the hub, the headset's USB and the Mac's power.
//
// RID 0 = Port-USB-C@1 (hub, headset USB), RID 1 = Port-USB-C@2 (DP adapter) on this MacBook Air.
// Plug-in function table layout (from dyld_info fixups of AppleHPMLib, macOS 15.6.1):
@@ -57,8 +59,21 @@ static HPMInterface **openHPM(int rid) {
}
int main(int argc, char **argv) {
- if (argc < 4 || strcmp(argv[2], "read") != 0) { fprintf(stderr, "usage: hpmctl <rid> read <reg>\n"); return 2; }
+ if (argc < 4) { fprintf(stderr, "usage: hpmctl <rid> read <reg> | hpmctl <rid> cmd <4cc>\n"); return 2; }
int rid = atoi(argv[1]);
+ if (strcmp(argv[2], "cmd") == 0) {
+ if (rid == 0) { fprintf(stderr, "refusing to send commands to RID 0 (hub + power)\n"); return 2; }
+ if (strlen(argv[3]) != 4) { fprintf(stderr, "4cc must be 4 characters\n"); return 2; }
+ // The driver takes the 4CC as its ASCII bytes in string order, little-endian ('DRST' -> 0x54535244).
+ uint32_t cc = (uint32_t)argv[3][0] | (uint32_t)argv[3][1] << 8 | (uint32_t)argv[3][2] << 16 | (uint32_t)argv[3][3] << 24;
+ HPMInterface **hpm = openHPM(rid);
+ if (!hpm) return 1;
+ IOReturn r = (*hpm)->iecsCommand(hpm, 0, cc, 0);
+ printf("rid %d cmd '%s' (0x%08x): 0x%08x\n", rid, argv[3], cc, r);
+ (*hpm)->Release(hpm);
+ return r == kIOReturnSuccess ? 0 : 1;
+ }
+ if (strcmp(argv[2], "read") != 0) { fprintf(stderr, "unknown op %s\n", argv[2]); return 2; }
uint8_t reg = (uint8_t)strtoul(argv[3], NULL, 0);
HPMInterface **hpm = openHPM(rid);
if (!hpm) return 1;
diff --git a/tools/hpmctl.entitlements b/tools/hpmctl.entitlements
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0"><dict>
+ <key>com.apple.USBCEntitlement</key><true/>
+</dict></plist>
diff --git a/tools/kernel/HPM_RE.md b/tools/kernel/HPM_RE.md
@@ -0,0 +1,268 @@
+# HPM / USB-C "Ace" (TI CD3217 / TPS6598x) DisplayPort alt-mode restart — RE notes
+
+Static analysis only. No hardware was touched. Target: decompressed arm64e fileset
+kernelcache macOS 15.6.1 (24G90), `tools/kernel/kernelcache.macho`.
+
+Fixup base (for chained ptrs): `BASE = 0xfffffe0007004000`; decoded pointer =
+`BASE + (raw & 0xffffffff)`. Helper scripts used live in the session scratchpad;
+all addresses below are runtime VAs.
+
+Kext text ranges:
+- `com.apple.driver.AppleHPM` `__TEXT_EXEC` = `0xfffffe0009b3d8c0` .. `+0x51f00`
+- `com.apple.iokit.IOAccessoryManager` `__TEXT_EXEC` = `0xfffffe000aba2ab0` .. `+0xf2d9c`
+- `com.apple.iokit.IOPortFamily` `__TEXT_EXEC` = **0x84 bytes (stub)** — all logic lives in IOAccessoryManager.
+
+---
+
+## 0. TL;DR — ranked recipes (gentlest/fastest → most drastic)
+
+All go through `AppleHPMLib.plugin` on the **AppleHPMDevice** node for Port-USB-C@2
+(RID 1), i.e. the same connection you already use for `iecsRead`. Opening the user
+client requires **root AND the `com.apple.USBCEntitlement` entitlement** (see §1);
+`hpmdiagnose` has that entitlement, your own tool must be signed with it or injected
+some other way.
+
+`route` = 0 (the controller's own port, as you already use). 4CCs are **little-endian
+ASCII u32**.
+
+| # | Action | Call (plugin vtable → kernel selector) | Confidence it restarts DP |
+|---|--------|----------------------------------------|---------------------------|
+| 1 | **Re-send DP Status / re-assert HPD** by replaying the controller's own DP-status VDM to the partner | `sendVDM` (vtable +0x40 → **selector 3**), SOP=port-partner, payload = DP VDM (see §4) | Medium — mirrors the driver's own `sendDpStatusUpdates`; cheapest, no teardown |
+| 2 | **DP alt-mode Exit then Enter** (true "replug" of just the alt mode) | two `sendVDM` calls: Exit Mode VDM then Enter Mode VDM, SVID 0xFF01 (§4) | Medium-High — this is the literal software unplug/replug of DP |
+| 3 | **'DRST' USB-PD Data Reset** (renegotiate PD + re-enter all alt modes) | `iecsCommand` (vtable +0x38 → **selector 2**), cmd=`'DRST'`=0x54535244, flags=0 — or write it raw via `iecsWrite` (§3) | Medium-High — this is exactly what the driver's `exitPDMode` issues (§4) |
+| 4 | **Force port re-evaluation / re-run mode logic** (driver re-runs `configureDP`) | `forceMode` (+0x58 → **selector 6**) or `forceUpdateMode` (+0x60 → **selector 7**) | Medium — re-drives mode flags, may re-configure DP |
+| 5 | **Raw host-interface VDM**: write DATA1(0x09)+CMD1(0x08)='VDMs' yourself | `iecsWrite` (+0x30 → **selector 1**) ×2 (§3) | Medium — most control, you supply exact VDM bytes |
+| 6 | **Full controller reset 'Gaid'/'GAID'** = reboot the Ace = hard replug of the whole port | `iecsCommand` selector 2, cmd=`'Gaid'`=0x64696147 (or `'GAID'`=0x44494147), flags=0 | High that it replugs, but **drastic/slow**: drops USB + power + DP on that port, ~seconds |
+
+Notes that de-risk this:
+- **There is NO 4CC allow/deny list in the kernel AppleHPM user client.** Selector 2
+ passes `(route, cmd, flags)` straight through to the provider (§2). I scanned every
+ `IOReturn` construction in AppleHPM: **`kIOReturnBadMessageID` (0xe00002c6) is never
+ built anywhere in the kext.** So the `0xe00002c6` you got for `'Gaid'` did **not**
+ come from a kernel 4CC filter — it came from the plugin's own userspace wrapper or
+ from calling it on the wrong node (you noted you ran it against the `AppleHPMARMI2C`
+ parent, whose user client is a different class). Re-try 4CC commands on the
+ **AppleHPMDevice** node, the same one where `iecsRead` works.
+- Start gentle (#1/#2) and keep #6 as the fallback you already wanted.
+
+---
+
+## 1. Access control (who may open the user client)
+
+`AppleHPMUserClient::initWithTask` @ **0xfffffe0009b49570**:
+
+```
+0x…495b4 x1="root" ; IOUserClient::clientHasPrivilege(task,"root")
+0x…495bc bl clientHasPrivilege
+0x…495c8 x1="com.apple.USBCEntitlement" ; copyClientEntitlement(task, …)
+0x…495d0 bl copyClientEntitlement
+0x…495e8 cmp x0, kOSBooleanTrue ; entitlement must == true
+… requires BOTH → else logs and returns 0 (open fails)
+```
+Strings: `com.apple.USBCEntitlement` @0x47b6bb, "does NOT have
+kIOClientPrivilegeAdministrator" @0x47b6d5, "does NOT have entitlement…" @0x47b733.
+
+**Requirement: run as root *and* ship `com.apple.USBCEntitlement` (= true).** No
+per-command privilege check beyond this.
+
+---
+
+## 2. AppleHPMUserClient external-method dispatch
+
+Dispatch is **not** an `IOExternalMethodDispatch` array — it is a
+`getTargetAndMethodForIndex`-style switch @ **0xfffffe0009b497dc** (vtable idx 272)
+that tail-calls one of the class methods by vtable offset. Argument-count validation
+is a shared helper at UC vtable **+0x990** (`0xfffffe0009b49b10`,
+`checkArguments(args, scalarIn, structIn, scalarOut, structOut)`) that returns
+**`kIOReturnBadArgument` 0xe00002c2** on count mismatch (reads
+`IOExternalMethodArguments` fields at +0x28 scalarInCnt, +0x38/+0x40 structIn,
++0x50 scalarOutCnt, +0x60/+0x68 structOut).
+
+Selector map (selector → UC handler → forwards to provider vtable offset):
+
+| sel | plugin vtbl | name | handler | checkArguments | forwards to provider |
+|----:|-------------|------|---------|----------------|----------------------|
+| 0 | +0x28 | iecsRead | 0x9b49c24 | struct-out path | reg read |
+| 1 | +0x30 | **iecsWrite** | 0x9b49e3c | scalarIn **5** | provider +0x218 (mem) then write |
+| 2 | +0x38 | **iecsCommand** | 0x9b4a044 | scalarIn **3** | **provider +0x8f0**(route,cmd,flags,5) |
+| 3 | +0x40 | **sendVDM** | 0x9b4a118 | scalarIn **5** | **provider +0x990**(route,SOP,md,len,flags) |
+| 4 | +0x48 | receiveVDM | 0x9b4a314 | — | provider +0x998-ish |
+| 5 | +0x50 | receiveVDMAttention | 0x9b4a540 | — | — |
+| 6 | +0x58 | **forceMode** | 0x9b4a76c | scalarIn **2** | provider +0x938(mode) |
+| 7 | +0x60 | **forceUpdateMode** | 0x9b4a834 | scalarIn **4** | provider +0x940 |
+| 8 | +0x68 | IECSAtomicCommand | 0x9b4a908 | scalarIn **2** | provider +0x948(route,u8) |
+| 9 | +0x70 | EnableOptions | 0x9b4a9d4 | scalarIn **0xb** | up to 4 copyin buffers |
+| 10| +0x78 | (reserved) | 0x9b4b0e4 | — | — |
+| default | | | | | **kIOReturnUnsupported 0xe00002c7** @0x9b49a88 |
+
+Provider object = UC+0xe0, set in `start` @0x9b49660: casts the IOService provider to
+the required metaclass (GOT `0xfffffe0007f7fcd0` → runtime metaclass `0xc334688`; name
+is runtime-initialized, this is the **AppleHPMDevice** RID node), then calls
+`provider->vtbl[+0x6b0]()` to get the command target and stores it at +0xe0. Per-call
+lock at UC+0xe8 via `IOLockLock`=0x8f1dafc / `IOLockUnlock`=0x8f1da7c. copyin helper =
+`0x8f3decc`.
+
+### Exact ABIs (all `IOConnectCallScalarMethod`, selector as above)
+
+- **iecsRead (sel 0):** as you use it — `(route, reg u8, …)`, data returned in the
+ struct-output buffer + `outLen`.
+- **iecsWrite (sel 1):** scalars `[route u32, reg u8, len, userBufPtr, flags]`.
+ Handler (0x9b49e3c): `len` must be **≤ 0x40 (64)**; copies `len` bytes from the user
+ buffer and writes them to register `reg` on `route`. **This is the raw host-interface
+ write** — lets you poke CMD1 (0x08) / DATA1 (0x09) directly.
+- **iecsCommand (sel 2):** scalars `[route u32, cmd u32(4CC), flags u32]`. Handler
+ (0x9b4a044) only validates "3 scalars" then calls `provider+0x8f0(route,cmd,flags,5)`.
+ No 4CC filtering in the UC. The constant `5` is a fixed source/SOP selector the driver
+ always passes here.
+- **sendVDM (sel 3):** scalars `[route u32, SOPtype u32, len, userBufPtr, flags u32]`.
+ Handler (0x9b4a118) range-checks `len`, copies the VDM byte buffer in, and calls
+ `provider+0x990(route, SOPtype, IOMemoryDescriptor*, len&0xff, flags)`. `SOPtype` is an
+ `AppleHPMSOPType` passed straight through (the driver's own code used value **5** when
+ issuing a Discover-Identity to SOP′, so the enum is *not* the raw USB-PD SOP ordinal —
+ determine the partner-SOP value empirically; 0 is the natural first guess for SOP).
+- **forceMode (sel 6):** scalars `[route, mode]` → `provider+0x938(mode)`
+ (drives the HPM mode register / `overrideHPMMode`).
+- **forceUpdateMode (sel 7):** scalars `[route, u8, u32, u32]` → `provider+0x940`
+ (firmware/update-mode forcing; less relevant to DP).
+
+---
+
+## 3. 4CC commands and the raw host-interface encoding
+
+4CCs the kernel itself materializes (movz/movk sites in AppleHPM `__text`):
+`'VDMs'`=0x734D4456, `'DRST'`=0x54535244, `'SSPS'`=0x53505353, `'SCfg'`=0x67664353,
+`'MCDL'`=0x4C44434D, `'HChk'`=0x6B684348, `'DFUf'`=0x66554644, `'CRYm'`, `'!CMD'`,
+`'Gaid'`=0x64696147, `'GAID'`=0x44494147, `'ADFU'` (@0x483a73, used by `sendHPMReset`
+"resetting port / out of ADFU"). `AppleHCPM::hpmCmdWrite` @0x9b6cc44 is the software
+*emulation* path and enumerates the accepted set (incl. VDMs, Gaid/GAID) — it is NOT
+the live I2C path, but it confirms which 4CCs are "known".
+
+**Two ways to send a 4CC from user space:**
+
+1. **Structured:** `iecsCommand` (selector 2) — `[route, 4CC, flags]`. Simplest for
+ data-less commands (`'DRST'`, `'Gaid'`, `'GAID'`, `'SSPS'`).
+
+2. **Raw TPS6598x host interface** via `iecsWrite` (selector 1), mirroring the TI Ace
+ register model you already see (reg 0x03 = mode "APP ", reg 0x58 = DP SID status):
+ - write the **DATA1 register `0x09`** with the command payload (VDM header + VDOs,
+ or command args), then
+ - write the **CMD1 register `0x08`** with the 4CC (e.g. `'VDMs'` / `'DRST'`).
+ The Ace executes and clears CMD1 on completion; poll reg 0x08 / read result from
+ DATA1 (reg 0x09) or the relevant status register (reg 0x58 for DP).
+
+---
+
+## 4. How the driver itself drives DP / VDMs (templates to copy)
+
+### 4a. The driver's own VDM send ('VDMs') — payload layout
+`AppleHPMInterface` @ **0xfffffe0009b5d7bc** (log: "role is wrong, issuing discovery on
+sop′"):
+```
+mov w8,'VDMs'(0x734D4456) ; str [sp+0x3c] ; the 4CC → CMD1
+mov w8,#5 ; strb [sp+0x38] ; SOP type byte = 5
+mov w8,0xACA00121 ; str [sp+0x34] ; VDM header dword (Discover Identity here)
+…
+blraa provider+0xb10 (this, reg=**8**, &fourcc, &dataStruct{header,SOP,…}, count=1)
+```
+So the generic executor is **AppleHPMInterface vtbl +0xb10** =
+`cmdExecWithData(reg=0x08/CMD1, &4CC, &data, dataLen/VDOcount)`, with the DATA1 payload =
+`[u32 VDM header][u8 SOP type][VDOs…]`. For a DisplayPort VDM you replace the header/VDOs
+with SVID **0xFF01** (DisplayPort) structured VDMs:
+- **DP Status Update** (cmd 0x6) to re-request HPD/status from the partner;
+- **Exit Mode** (cmd 0x5) then **Enter Mode** (cmd 0x4) to restart the alt-mode session;
+- **DP Configure** (cmd 0x7) to re-apply pin assignment.
+
+From user space, reproduce this either via `sendVDM` (selector 3, buffer = `[VDM header
+dword][VDOs]`, `SOPtype` = partner) or raw via `iecsWrite` DATA1+CMD1='VDMs'.
+
+### 4b. 'DRST' / Data Reset — `AppleHPMInterface::exitPDMode` @ 0xfffffe0009b5bc44
+Reads two IORegistry properties into 0x40-byte buffers — **"Exit PD Mode TX Identity"**
+(@0x47fd66) and **"Exit PD Mode DRST Data"** (@0x47fd7f) — then:
+```
+blraa provider+0xb10 (this, reg=8, &'DRST', drstData(0x40 bytes), len=0x40)
+… log "exitPDMode … DRST complete - status = 0x%08x"
+```
+`'DRST'` performs a **USB-PD Data Reset**: it tears down and re-negotiates the PD
+contract and re-enters alt modes = a clean soft replug of the data path. Reachable from
+user space as `iecsCommand(route, 'DRST', 0)` (data-less form) — the property-driven
+TX-Identity/DRST-Data payloads are optional refinements the driver supplies internally.
+
+### 4c. DP status / HPD plumbing (for understanding, not a direct trigger)
+- `configureDP` @0x9b40160 / `createDPPortObject` / `configureDPPortObject` — build/activate
+ the DP transport; re-run on port re-evaluation.
+- `sendDpStatusUpdates` @ **0xfffffe0009b58490** — reads the cached DP status word at
+ `this+0x1244`, **HPD = bit 15** (`ubfx …,#0xf,#1`), and on change sets IORegistry
+ property **"HPDAsserted"** (`this+0x298 setProperty`) and pushes HPD down to the DP
+ transport (`this+0x770`). This is consistent with your reg 0x58 decode (partner DP
+ Status VDO, HPD bit).
+- `communicateDPHPDChange` @0x9b3fc14, `clearDpIRQ`, `resetDataControl`, `repeaterReset`,
+ `disablePort`, `forcePortEvaluation` (@0x9b4764c and others) — the internal re-eval /
+ teardown primitives; `forceMode`/`forceUpdateMode` (selectors 6/7) are the
+ user-reachable entry points that can re-drive this logic.
+
+---
+
+## 5. Boot-args and settable properties
+
+PE_parse_boot_argn = `0xfffffe0008fed024`. AppleHPM boot-args (all config/diagnostic —
+**none force a DP reset or change HPD timing**):
+`hpmI2CRetries`, `hpmI2CRetryInterval`, `hpmI2CNonCompliant`, `hpmDisableAppleUVDMSOP`,
+`hpmDisableAppleUVDMSOPp`, `-hpmUnplugForceUpdateMode`, `hpmlog` (log mask). There is no
+"HPD Lost period" / "hot plug delay" boot-arg in this kext (searched; absent).
+`hpm-exit-dfu` (@0x9b68514) and `boot-command` are IORegistry/NVRAM properties, not
+boot-args. No `setProperties` key in AppleHPM was found that triggers a DP re-entry from
+user space — use the selectors instead.
+
+---
+
+## 6. IOAccessoryManager / IOPortFamily user clients (item 4)
+
+`IOPortFamily` is a stub; everything is in **IOAccessoryManager**. Relevant class
+`IOPortTransportStateDisplayPort` exposes:
+- `setHPDState` @0xabe5adc (log "hpdState: %d"), `HPD_State` property, `triggerHPDIRQ`
+ @0xabe5f98 (log "HPD IRQ triggered!"), `setDPOverdriveActive` @0xabf17bc / `DPOverdrive`,
+ `setDisplayPortPinAssignment` @0xac13718 / `DisplayPortPinAssignment`,
+ `getDPDNConfig` / `configureDPDNConfigOverrideGated`.
+
+**Important:** these operate on the *OS-side* DP transport node — they set/propagate the
+HPD state and pin config that the display pipeline sees. They are driven **from the HPM
+side** (AppleHPM → `communicateDPHPDChange` → `setHPDState`). Faking HPD here makes macOS
+re-examine its view of the link, but it does **not** make the Ace re-run alt-mode
+negotiation with the physical adapter. For the VR "the adapter dropped DP" case, the
+AppleHPM paths in §4 (which talk to the controller/partner) are the right lever;
+`triggerHPDIRQ` is at best a complementary "make the GPU re-read EDID / re-train" nudge.
+
+User clients and their gates:
+- `IOPortTransportUserClient` — entitlement **`com.apple.iokit.IOPort.user-access`**
+ (@0x898584; TRM variant `com.apple.iokit.IOPort.TRM.user-access` @0x89855d). Class refs
+ @0xabc7984/0xabdf764/0xabdf898/0xabe041c; entitlement check @0xabdfba8.
+- `IOPortFeatureLDCMUserClient` — `IOExternalMethodDispatch2022` array @
+ **0xfffffe00082890f0**, 5 methods, each requiring entitlement
+ **`com.apple.iokit.IOPortFeature.LDCM`**, each `scalarIn=1`. (LDCM = the SBU/pin
+ low-data-rate config path; ties to `setLDCMPin` in AppleHPM.)
+- `IOPortUserClient`, `IOAccessoryManagerUserClient`, plus AID-bus reset primitives
+ `resetAccessoryPowerGated` @0xabc1c24, `sendBusReset` @0xac245f0, `goldbondDisablePort`
+ — these reset the **accessory-ID bus**, not the USB-C/DP data path; not useful here.
+
+No external method in these clients was found that re-runs USB-C DP alt-mode negotiation;
+they manipulate OS-side transport/HPD/pin state only. The AppleHPM user client (§2–§4)
+remains the correct path.
+
+---
+
+## 7. Suggested concrete attempts (in order)
+
+1. On the **AppleHPMDevice** node (where `iecsRead` works), retry
+ `iecsCommand(route=0, 'DRST', 0)` — data-less Data Reset. Watch reg 0x58 HPD bit.
+2. If that is too heavy or no-ops, `sendVDM(route=0, SOP=partner, DP Status Update VDM)`;
+ then `sendVDM` Exit Mode + Enter Mode (SVID 0xFF01) for a full alt-mode replug.
+3. If structured sendVDM mis-encodes, do it raw: `iecsWrite(0x09, <VDM bytes>)` then
+ `iecsWrite(0x08, 'VDMs')`, mirroring §4a (SOP byte + header + VDOs).
+4. `forceMode` / `forceUpdateMode` to force the driver's port re-evaluation.
+5. Fallback: `iecsCommand(route=0, 'Gaid', 0)` (or `'GAID'`) — full Ace reset = guaranteed
+ replug of the whole port, slow; expect USB + charging + DP to drop and re-enumerate.
+
+Caveat: everything here is static analysis; argument *values* that are enum-like
+(`SOPtype` for sendVDM, `mode` for forceMode, exact DP VDM VDOs) could not be verified
+without touching hardware. Start with the data-less `'DRST'` and `forceMode` paths,
+which have the least ambiguity.