‹ 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

HPM_RE.md (23319B)


      1 # HPM / USB-C "Ace" (TI CD3217 / TPS6598x) DisplayPort alt-mode restart — RE notes
      2 
      3 Static analysis only. No hardware was touched. Target: decompressed arm64e fileset
      4 kernelcache macOS 15.6.1 (24G90), `tools/kernel/kernelcache.macho`.
      5 
      6 Fixup base (for chained ptrs): `BASE = 0xfffffe0007004000`; decoded pointer =
      7 `BASE + (raw & 0xffffffff)`. Helper scripts used live in the session scratchpad;
      8 all addresses below are runtime VAs.
      9 
     10 Kext text ranges:
     11 - `com.apple.driver.AppleHPM` `__TEXT_EXEC` = `0xfffffe0009b3d8c0` .. `+0x51f00`
     12 - `com.apple.iokit.IOAccessoryManager` `__TEXT_EXEC` = `0xfffffe000aba2ab0` .. `+0xf2d9c`
     13 - `com.apple.iokit.IOPortFamily` `__TEXT_EXEC` = **0x84 bytes (stub)** — all logic lives in IOAccessoryManager.
     14 
     15 ---
     16 
     17 ## 0. TL;DR — ranked recipes (gentlest/fastest → most drastic)
     18 
     19 All go through `AppleHPMLib.plugin` on the **AppleHPMDevice** node for Port-USB-C@2
     20 (RID 1), i.e. the same connection you already use for `iecsRead`. Opening the user
     21 client requires **root AND the `com.apple.USBCEntitlement` entitlement** (see §1);
     22 `hpmdiagnose` has that entitlement, your own tool must be signed with it or injected
     23 some other way.
     24 
     25 `route` = 0 (the controller's own port, as you already use). 4CCs are **little-endian
     26 ASCII u32**.
     27 
     28 | # | Action | Call (plugin vtable → kernel selector) | Confidence it restarts DP |
     29 |---|--------|----------------------------------------|---------------------------|
     30 | 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 |
     31 | 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 |
     32 | 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) |
     33 | 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 |
     34 | 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 |
     35 | 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 |
     36 
     37 Notes that de-risk this:
     38 - **There is NO 4CC allow/deny list in the kernel AppleHPM user client.** Selector 2
     39   passes `(route, cmd, flags)` straight through to the provider (§2). I scanned every
     40   `IOReturn` construction in AppleHPM: **`kIOReturnBadMessageID` (0xe00002c6) is never
     41   built anywhere in the kext.** So the `0xe00002c6` you got for `'Gaid'` did **not**
     42   come from a kernel 4CC filter — it came from the plugin's own userspace wrapper or
     43   from somewhere else on the path (unresolved).
     44   **CORRECTION (vr1, verified on hardware 2026-10-20 22:2x):** the earlier advice here to
     45   "re-try on the AppleHPMDevice node" is WRONG. `IOCreatePlugInInterfaceForService` on the
     46   AppleHPMDevice (RID 1) node returns 0xe00002c7: AppleHPMLib's personality is
     47   `IOProviderClass = AppleHPM`, which only the `AppleHPMARMI2C` parent is. `iecsRead` works
     48   on the PARENT (not the Device node). On the parent, `iecsCommand` 'Gaid' and 'DRST' both
     49   return 0xe00002c6, with and without the com.apple.USBCEntitlement entitlement, and with no
     50   kernel log output. Where 0xe00002c6 originates is still being traced.
     51 - Start gentle (#1/#2) and keep #6 as the fallback you already wanted.
     52 
     53 ---
     54 
     55 ## 1. Access control (who may open the user client)
     56 
     57 `AppleHPMUserClient::initWithTask` @ **0xfffffe0009b49570**:
     58 
     59 ```
     60 0x…495b4  x1="root"                              ; IOUserClient::clientHasPrivilege(task,"root")
     61 0x…495bc  bl  clientHasPrivilege
     62 0x…495c8  x1="com.apple.USBCEntitlement"         ; copyClientEntitlement(task, …)
     63 0x…495d0  bl  copyClientEntitlement
     64 0x…495e8  cmp x0, kOSBooleanTrue                  ; entitlement must == true
     65 …         requires BOTH → else logs and returns 0 (open fails)
     66 ```
     67 Strings: `com.apple.USBCEntitlement` @0x47b6bb, "does NOT have
     68 kIOClientPrivilegeAdministrator" @0x47b6d5, "does NOT have entitlement…" @0x47b733.
     69 
     70 **Requirement: run as root *and* ship `com.apple.USBCEntitlement` (= true).** No
     71 per-command privilege check beyond this.
     72 
     73 ---
     74 
     75 ## 2. AppleHPMUserClient external-method dispatch
     76 
     77 Dispatch is **not** an `IOExternalMethodDispatch` array — it is a
     78 `getTargetAndMethodForIndex`-style switch @ **0xfffffe0009b497dc** (vtable idx 272)
     79 that tail-calls one of the class methods by vtable offset. Argument-count validation
     80 is a shared helper at UC vtable **+0x990** (`0xfffffe0009b49b10`,
     81 `checkArguments(args, scalarIn, structIn, scalarOut, structOut)`) that returns
     82 **`kIOReturnBadArgument` 0xe00002c2** on count mismatch (reads
     83 `IOExternalMethodArguments` fields at +0x28 scalarInCnt, +0x38/+0x40 structIn,
     84 +0x50 scalarOutCnt, +0x60/+0x68 structOut).
     85 
     86 Selector map (selector → UC handler → forwards to provider vtable offset):
     87 
     88 | sel | plugin vtbl | name | handler | checkArguments | forwards to provider |
     89 |----:|-------------|------|---------|----------------|----------------------|
     90 | 0 | +0x28 | iecsRead | 0x9b49c24 | struct-out path | reg read |
     91 | 1 | +0x30 | **iecsWrite** | 0x9b49e3c | scalarIn **5** | provider +0x218 (mem) then write |
     92 | 2 | +0x38 | **iecsCommand** | 0x9b4a044 | scalarIn **3** | **provider +0x8f0**(route,cmd,flags,5) |
     93 | 3 | +0x40 | **sendVDM** | 0x9b4a118 | scalarIn **5** | **provider +0x990**(route,SOP,md,len,flags) |
     94 | 4 | +0x48 | receiveVDM | 0x9b4a314 | — | provider +0x998-ish |
     95 | 5 | +0x50 | receiveVDMAttention | 0x9b4a540 | — | — |
     96 | 6 | +0x58 | **forceMode** | 0x9b4a76c | scalarIn **2** | provider +0x938(mode) |
     97 | 7 | +0x60 | **forceUpdateMode** | 0x9b4a834 | scalarIn **4** | provider +0x940 |
     98 | 8 | +0x68 | IECSAtomicCommand | 0x9b4a908 | scalarIn **2** | provider +0x948(route,u8) |
     99 | 9 | +0x70 | EnableOptions | 0x9b4a9d4 | scalarIn **0xb** | up to 4 copyin buffers |
    100 | 10| +0x78 | (reserved) | 0x9b4b0e4 | — | — |
    101 | default | | | | | **kIOReturnUnsupported 0xe00002c7** @0x9b49a88 |
    102 
    103 Provider object = UC+0xe0, set in `start` @0x9b49660: casts the IOService provider to
    104 the required metaclass (GOT `0xfffffe0007f7fcd0` → runtime metaclass `0xc334688`; name
    105 is runtime-initialized, this is the **AppleHPMDevice** RID node), then calls
    106 `provider->vtbl[+0x6b0]()` to get the command target and stores it at +0xe0. Per-call
    107 lock at UC+0xe8 via `IOLockLock`=0x8f1dafc / `IOLockUnlock`=0x8f1da7c. copyin helper =
    108 `0x8f3decc`.
    109 
    110 ### Exact ABIs (all `IOConnectCallScalarMethod`, selector as above)
    111 
    112 - **iecsRead (sel 0):** as you use it — `(route, reg u8, …)`, data returned in the
    113   struct-output buffer + `outLen`.
    114 - **iecsWrite (sel 1):** scalars `[route u32, reg u8, len, userBufPtr, flags]`.
    115   Handler (0x9b49e3c): `len` must be **≤ 0x40 (64)**; copies `len` bytes from the user
    116   buffer and writes them to register `reg` on `route`. **This is the raw host-interface
    117   write** — lets you poke CMD1 (0x08) / DATA1 (0x09) directly.
    118 - **iecsCommand (sel 2):** scalars `[route u32, cmd u32(4CC), flags u32]`. Handler
    119   (0x9b4a044) only validates "3 scalars" then calls `provider+0x8f0(route,cmd,flags,5)`.
    120   No 4CC filtering in the UC. The constant `5` is a fixed source/SOP selector the driver
    121   always passes here.
    122 - **sendVDM (sel 3):** scalars `[route u32, SOPtype u32, len, userBufPtr, flags u32]`.
    123   Handler (0x9b4a118) range-checks `len`, copies the VDM byte buffer in, and calls
    124   `provider+0x990(route, SOPtype, IOMemoryDescriptor*, len&0xff, flags)`. `SOPtype` is an
    125   `AppleHPMSOPType` passed straight through (the driver's own code used value **5** when
    126   issuing a Discover-Identity to SOP′, so the enum is *not* the raw USB-PD SOP ordinal —
    127   determine the partner-SOP value empirically; 0 is the natural first guess for SOP).
    128 - **forceMode (sel 6):** scalars `[route, mode]` → `provider+0x938(mode)`
    129   (drives the HPM mode register / `overrideHPMMode`).
    130 - **forceUpdateMode (sel 7):** scalars `[route, u8, u32, u32]` → `provider+0x940`
    131   (firmware/update-mode forcing; less relevant to DP).
    132 
    133 ---
    134 
    135 ## 3. 4CC commands and the raw host-interface encoding
    136 
    137 4CCs the kernel itself materializes (movz/movk sites in AppleHPM `__text`):
    138 `'VDMs'`=0x734D4456, `'DRST'`=0x54535244, `'SSPS'`=0x53505353, `'SCfg'`=0x67664353,
    139 `'MCDL'`=0x4C44434D, `'HChk'`=0x6B684348, `'DFUf'`=0x66554644, `'CRYm'`, `'!CMD'`,
    140 `'Gaid'`=0x64696147, `'GAID'`=0x44494147, `'ADFU'` (@0x483a73, used by `sendHPMReset`
    141 "resetting port / out of ADFU"). `AppleHCPM::hpmCmdWrite` @0x9b6cc44 is the software
    142 *emulation* path and enumerates the accepted set (incl. VDMs, Gaid/GAID) — it is NOT
    143 the live I2C path, but it confirms which 4CCs are "known".
    144 
    145 **Two ways to send a 4CC from user space:**
    146 
    147 1. **Structured:** `iecsCommand` (selector 2) — `[route, 4CC, flags]`. Simplest for
    148    data-less commands (`'DRST'`, `'Gaid'`, `'GAID'`, `'SSPS'`).
    149 
    150 2. **Raw TPS6598x host interface** via `iecsWrite` (selector 1), mirroring the TI Ace
    151    register model you already see (reg 0x03 = mode "APP ", reg 0x58 = DP SID status):
    152    - write the **DATA1 register `0x09`** with the command payload (VDM header + VDOs,
    153      or command args), then
    154    - write the **CMD1 register `0x08`** with the 4CC (e.g. `'VDMs'` / `'DRST'`).
    155    The Ace executes and clears CMD1 on completion; poll reg 0x08 / read result from
    156    DATA1 (reg 0x09) or the relevant status register (reg 0x58 for DP).
    157 
    158 ---
    159 
    160 ## 4. How the driver itself drives DP / VDMs (templates to copy)
    161 
    162 ### 4a. The driver's own VDM send ('VDMs') — payload layout
    163 `AppleHPMInterface` @ **0xfffffe0009b5d7bc** (log: "role is wrong, issuing discovery on
    164 sop′"):
    165 ```
    166 mov w8,'VDMs'(0x734D4456) ; str  [sp+0x3c]   ; the 4CC → CMD1
    167 mov w8,#5                 ; strb [sp+0x38]    ; SOP type byte = 5
    168 mov w8,0xACA00121         ; str  [sp+0x34]    ; VDM header dword (Discover Identity here)
    169 …
    170 blraa provider+0xb10 (this, reg=**8**, &fourcc, &dataStruct{header,SOP,…}, count=1)
    171 ```
    172 So the generic executor is **AppleHPMInterface vtbl +0xb10** =
    173 `cmdExecWithData(reg=0x08/CMD1, &4CC, &data, dataLen/VDOcount)`, with the DATA1 payload =
    174 `[u32 VDM header][u8 SOP type][VDOs…]`. For a DisplayPort VDM you replace the header/VDOs
    175 with SVID **0xFF01** (DisplayPort) structured VDMs:
    176 - **DP Status Update** (cmd 0x6) to re-request HPD/status from the partner;
    177 - **Exit Mode** (cmd 0x5) then **Enter Mode** (cmd 0x4) to restart the alt-mode session;
    178 - **DP Configure** (cmd 0x7) to re-apply pin assignment.
    179 
    180 From user space, reproduce this either via `sendVDM` (selector 3, buffer = `[VDM header
    181 dword][VDOs]`, `SOPtype` = partner) or raw via `iecsWrite` DATA1+CMD1='VDMs'.
    182 
    183 ### 4b. 'DRST' / Data Reset — `AppleHPMInterface::exitPDMode` @ 0xfffffe0009b5bc44
    184 Reads two IORegistry properties into 0x40-byte buffers — **"Exit PD Mode TX Identity"**
    185 (@0x47fd66) and **"Exit PD Mode DRST Data"** (@0x47fd7f) — then:
    186 ```
    187 blraa provider+0xb10 (this, reg=8, &'DRST', drstData(0x40 bytes), len=0x40)
    188 … log "exitPDMode … DRST complete - status = 0x%08x"
    189 ```
    190 `'DRST'` performs a **USB-PD Data Reset**: it tears down and re-negotiates the PD
    191 contract and re-enters alt modes = a clean soft replug of the data path. Reachable from
    192 user space as `iecsCommand(route, 'DRST', 0)` (data-less form) — the property-driven
    193 TX-Identity/DRST-Data payloads are optional refinements the driver supplies internally.
    194 
    195 ### 4c. DP status / HPD plumbing (for understanding, not a direct trigger)
    196 - `configureDP` @0x9b40160 / `createDPPortObject` / `configureDPPortObject` — build/activate
    197   the DP transport; re-run on port re-evaluation.
    198 - `sendDpStatusUpdates` @ **0xfffffe0009b58490** — reads the cached DP status word at
    199   `this+0x1244`, **HPD = bit 15** (`ubfx …,#0xf,#1`), and on change sets IORegistry
    200   property **"HPDAsserted"** (`this+0x298 setProperty`) and pushes HPD down to the DP
    201   transport (`this+0x770`). This is consistent with your reg 0x58 decode (partner DP
    202   Status VDO, HPD bit).
    203 - `communicateDPHPDChange` @0x9b3fc14, `clearDpIRQ`, `resetDataControl`, `repeaterReset`,
    204   `disablePort`, `forcePortEvaluation` (@0x9b4764c and others) — the internal re-eval /
    205   teardown primitives; `forceMode`/`forceUpdateMode` (selectors 6/7) are the
    206   user-reachable entry points that can re-drive this logic.
    207 
    208 ---
    209 
    210 ## 5. Boot-args and settable properties
    211 
    212 PE_parse_boot_argn = `0xfffffe0008fed024`. AppleHPM boot-args (all config/diagnostic —
    213 **none force a DP reset or change HPD timing**):
    214 `hpmI2CRetries`, `hpmI2CRetryInterval`, `hpmI2CNonCompliant`, `hpmDisableAppleUVDMSOP`,
    215 `hpmDisableAppleUVDMSOPp`, `-hpmUnplugForceUpdateMode`, `hpmlog` (log mask). There is no
    216 "HPD Lost period" / "hot plug delay" boot-arg in this kext (searched; absent).
    217 `hpm-exit-dfu` (@0x9b68514) and `boot-command` are IORegistry/NVRAM properties, not
    218 boot-args. No `setProperties` key in AppleHPM was found that triggers a DP re-entry from
    219 user space — use the selectors instead.
    220 
    221 ---
    222 
    223 ## 6. IOAccessoryManager / IOPortFamily user clients (item 4)
    224 
    225 `IOPortFamily` is a stub; everything is in **IOAccessoryManager**. Relevant class
    226 `IOPortTransportStateDisplayPort` exposes:
    227 - `setHPDState` @0xabe5adc (log "hpdState: %d"), `HPD_State` property, `triggerHPDIRQ`
    228   @0xabe5f98 (log "HPD IRQ triggered!"), `setDPOverdriveActive` @0xabf17bc / `DPOverdrive`,
    229   `setDisplayPortPinAssignment` @0xac13718 / `DisplayPortPinAssignment`,
    230   `getDPDNConfig` / `configureDPDNConfigOverrideGated`.
    231 
    232 **Important:** these operate on the *OS-side* DP transport node — they set/propagate the
    233 HPD state and pin config that the display pipeline sees. They are driven **from the HPM
    234 side** (AppleHPM → `communicateDPHPDChange` → `setHPDState`). Faking HPD here makes macOS
    235 re-examine its view of the link, but it does **not** make the Ace re-run alt-mode
    236 negotiation with the physical adapter. For the VR "the adapter dropped DP" case, the
    237 AppleHPM paths in §4 (which talk to the controller/partner) are the right lever;
    238 `triggerHPDIRQ` is at best a complementary "make the GPU re-read EDID / re-train" nudge.
    239 
    240 User clients and their gates:
    241 - `IOPortTransportUserClient` — entitlement **`com.apple.iokit.IOPort.user-access`**
    242   (@0x898584; TRM variant `com.apple.iokit.IOPort.TRM.user-access` @0x89855d). Class refs
    243   @0xabc7984/0xabdf764/0xabdf898/0xabe041c; entitlement check @0xabdfba8.
    244 - `IOPortFeatureLDCMUserClient` — `IOExternalMethodDispatch2022` array @
    245   **0xfffffe00082890f0**, 5 methods, each requiring entitlement
    246   **`com.apple.iokit.IOPortFeature.LDCM`**, each `scalarIn=1`. (LDCM = the SBU/pin
    247   low-data-rate config path; ties to `setLDCMPin` in AppleHPM.)
    248 - `IOPortUserClient`, `IOAccessoryManagerUserClient`, plus AID-bus reset primitives
    249   `resetAccessoryPowerGated` @0xabc1c24, `sendBusReset` @0xac245f0, `goldbondDisablePort`
    250   — these reset the **accessory-ID bus**, not the USB-C/DP data path; not useful here.
    251 
    252 No external method in these clients was found that re-runs USB-C DP alt-mode negotiation;
    253 they manipulate OS-side transport/HPD/pin state only. The AppleHPM user client (§2–§4)
    254 remains the correct path.
    255 
    256 ---
    257 
    258 ## 7. Suggested concrete attempts (in order)
    259 
    260 1. On the **AppleHPMDevice** node (where `iecsRead` works), retry
    261    `iecsCommand(route=0, 'DRST', 0)` — data-less Data Reset. Watch reg 0x58 HPD bit.
    262 2. If that is too heavy or no-ops, `sendVDM(route=0, SOP=partner, DP Status Update VDM)`;
    263    then `sendVDM` Exit Mode + Enter Mode (SVID 0xFF01) for a full alt-mode replug.
    264 3. If structured sendVDM mis-encodes, do it raw: `iecsWrite(0x09, <VDM bytes>)` then
    265    `iecsWrite(0x08, 'VDMs')`, mirroring §4a (SOP byte + header + VDOs).
    266 4. `forceMode` / `forceUpdateMode` to force the driver's port re-evaluation.
    267 5. Fallback: `iecsCommand(route=0, 'Gaid', 0)` (or `'GAID'`) — full Ace reset = guaranteed
    268    replug of the whole port, slow; expect USB + charging + DP to drop and re-enumerate.
    269 
    270 Caveat: everything here is static analysis; argument *values* that are enum-like
    271 (`SOPtype` for sendVDM, `mode` for forceMode, exact DP VDM VDOs) could not be verified
    272 without touching hardware. Start with the data-less `'DRST'` and `forceMode` paths,
    273 which have the least ambiguity.
    274 
    275 ---
    276 
    277 ## 8. CORRECTION: selector 2 expects BIG-endian (C multichar) 4CCs (vr1, static, 2026-10-20 ~[time redacted])
    278 
    279 - `0xfffffe0009b8c830(this, x1=route, x2=cmd, x3=flags, x4)` matches the user client's
    280   `provider+0x8f0(route, cmd, flags, 5)` call by signature (VERIFIED: prologue moves x1..x4 into
    281   x21/x23/x20/x22). Reached via vtable only (no direct BL callers), so the +0x8f0 -> 0x9b8c830 link
    282   is CONJECTURE until the vtable is resolved.
    283 - At `0x9b8c970`: `rev w8, w23` (byte-swap cmd), then `this->vtbl[+0x8c0](this, route, 8, &w8, 4)`,
    284   i.e. a 4-byte write of the SWAPPED value to register 0x08 (CMD1). VERIFIED in disassembly.
    285 - Neighbouring code builds 4CCs as BE constants (LE-read 'sMDV' = BE 'VDMs', 'pelS' = 'Slep').
    286 - So on this path pass 'HRST' = 0x48525354, 'DRST' = 0x44525354, 'Gaid' = 0x47616964.
    287   The earlier "4CCs are little-endian ASCII" advice (section 2 / summary) holds for code that stores
    288   4CC bytes directly (e.g. exitPDMode's 'DRST' at 0x9b5bbd4), NOT for selector 2.
    289 - Consequence: the 'Gaid'/'DRST' (vr1) and 'HRST' (vr0) attempts were sent LE, arrived byte-reversed
    290   ('diaG', 'TSRD', 'TSRH'), and !CMD in CMD1 is the chip rejecting those. They remain UNTESTED.
    291 - Flags bit0 (x3) gates a branch at 0x9b8c8b0 that skips a pre-check; use flags 0.
    292 
    293 ### 8a. Endianness boundary, CONFIRMED on hardware (vr0, 2026-10-20 [time redacted])
    294 
    295 | layer | 4CC representation | evidence |
    296 |---|---|---|
    297 | AppleHPMLib plugin `iecsCommand` (vtbl +0x38) | passes the u32 through unchanged as scalar #2 of selector 2 | plugin disasm 0x2614-0x2668 (vr1, vr0 independently) |
    298 | AppleHPMUserClient selector 2 handler 0x9b4a044 | passes u32 unchanged to provider+0x8f0(route, cmd, flags, 5) | static (subagent) |
    299 | provider+0x8f0 = 0x9b8c830 | **byte-swaps** (`rev w8,w23` @0x9b8c970) then writes 4 bytes to CMD1 (reg 0x08) | static (vr1); vtable slot -> 0x9b8c830 confirmed by vr0 chained-pointer scan (7 slots, PAC discriminator 0x191d matching the UC +0x8f0 call) |
    300 | driver-internal helpers that store 4CC bytes to memory first (e.g. exitPDMode 'DRST' @0x9b5bbd4: `mov #0x5244; movk #0x5453` -> 0x54535244 stored LE) | little-endian ASCII in memory | static |
    301 
    302 **Rule for user space (selector 2): pass the C multichar big-endian value**, e.g. `'DRST'` = 0x44525354,
    303 `'HRST'` = 0x48525354, `'Gaid'` = 0x47616964. Do NOT copy the little-endian constants seen in the
    304 driver's internal helpers.
    305 
    306 Hardware results with corrected BE encoding (vr0):
    307 - `DRST` 0x44525354 -> IOReturn 0x00000000 in 50 ms (140.855422). Accepted.
    308 - `HRST` 0x48525354 -> 0xe00002c6 (126.592422). Rejected (reason TBD: see 0x9b8c830 return paths).
    309 - All earlier LE attempts (Gaid, DRST by vr1; HRST by vr0) were byte-reversed on the wire.
    310 
    311 ### 8b. Return codes of provider+0x8f0 = 0x9b8c830 (vr1, static, VERIFIED in disassembly)
    312 - Pre-check: reads CMD1 (vtbl+0x8d0, reg 8); if a previous command is still pending it clears CMD1 first.
    313 - Writes rev(cmd) to CMD1 (vtbl+0x8c0, reg 8, 4 bytes).
    314 - flags bit1 set: sleep 100 us, return 0 (fire-and-forget).
    315 - Otherwise polls CMD1 every 100 us (w0=0x64 delay), up to 1000 tries (flags bit0 clear) or 100000 (bit0 set):
    316   - CMD1 == 0x444d4321 (LE u32 of "!CMD") -> returns 0xe00002d8 - 0x12 = **0xe00002c6 (kIOReturnBadMessageID)**
    317     = firmware did not recognize the command.
    318   - CMD1 == 0 -> returns 0 (accepted/completed). Task result is then in DATA1 (reg 0x09) byte 0.
    319   - tries exhausted -> 0xe00002d8 - 2 = **0xe00002d6** (timeout).
    320 So with correct BE encoding: HRST -> 0xe00002c6 means this CD3217 APP firmware does not implement HRST;
    321 DRST -> 0 means it does.
    322 
    323 ### 8c. SOFTWARE RECOVERY CONFIRMED (vr0 hardware, 2026-10-20)
    324 Stuck state (adapter partner status HPD=0 since [time redacted], induced by HID screen_off) recovered by
    325 `iecsCommand(route 0, 'DRST' = 0x44525354, flags 0)` on RID 1 (Port-USB-C@2):
    326 returned 0 in 50 ms at 140.855422; CMD1 = 0, DATA1 byte0 = 0; display ADD 4320x2160@60 at
    327 143.064422 (+2.209 s). No physical replug. Repeatability trials in progress (vr0).
    328 
    329 ## 9. User-client access gate is OR, not AND (vr1, static, VERIFIED in disassembly)
    330 `AppleHPMUserClient::initWithTask` @ 0xfffffe0009b49570:
    331 - super::initWithTask; on failure -> false.
    332 - w21 = IOUserClient::clientHasPrivilege(token, "root")  (0 = caller is root)
    333 - ent = copyClientEntitlement(task, "com.apple.USBCEntitlement"); w22 = (ent == kOSBooleanTrue)
    334 - allow if (w21 == 0) || w22   (branches 0x9b49614 / 0x9b4961c / 0x9b49620 / 0x9b49624)
    335 - deny: logs two strings (0x747f6d5, 0x747f733), returns false -> plugin creation fails (seen as 0xe00002be).
    336 => An unprivileged (non-root user) process ad-hoc signed with com.apple.USBCEntitlement=true is admitted (AMFI is off,
    337    so the entitlement is honoured). No sudo needed. Matches hardware: unsigned non-root user failed; signed non-root user read OK.
    338 Per-selector: handlers seen (0, 2, 3) only validate argument counts; no extra privilege check observed on the
    339 selector-2 path. Not every handler audited.
    340 
    341 ## 10. Unattended recovery: integration review (vr1, design, no hardware)
    342 - Privilege: either sign the app itself with com.apple.USBCEntitlement, or (narrower) a small signed helper the
    343   app execs. No root, no sudo cache. Both rely on AMFI staying off (amfi_get_out_of_my_way=1).
    344 - Target selection, once per attempt, never polled: AppleHPMDevice with RID == 1 whose child is
    345   `Port-USB-C@2` (PortNumber 2), and open the plug-in on its AppleHPMARMI2C parent. Refuse RID 0 (hub, headset
    346   USB, Mac power). Optional: confirm reg 0x03 == "APP " and reg 0x58 byte0 bit1 (DP alt mode active) before DRST.
    347 - Command: iecsCommand(route 0, 0x44525354 'DRST', flags 0). Driver itself polls CMD1 for ~100 ms.
    348   Check: return 0 -> then read DATA1 (reg 0x09) byte0 == 0. 0xe00002c6 = firmware !CMD; 0xe00002d6 = timeout.
    349 - Bound it: whole helper run <= ~1 s; app waits for CGDisplay ADD of vendor 8718 with a timeout (e.g. 5 s;
    350   observed +2.2 s), then re-captures. Rate-limit (e.g. max 3 DRST per minute, exponential backoff) and log
    351   every attempt with epoch timestamps + return codes.
    352 - Trigger: headset display REMOVE (track the captured CGDirectDisplayID, not vendor of a removed id) and no
    353   ADD within a short grace window. Self-heals were ADD within ~0.7-0.8 s, so a ~1 s grace avoids DRST
    354   racing a self-heal; tune with trial data. HID 0x05 panel-off is a secondary signal only.
    355 - Never read IOPortTransportStateDisplayPort properties in a loop; one-off reads only, or none.