合盖瞬间的物理视界:Mac Duo 铰链传感器逆向与 Metal 实时透视流水线

合盖瞬间的物理视界:Mac Duo 铰链传感器逆向与 Metal 实时透视流水线

合上 MacBook 屏幕的动作,在过去二十年里一直是一个枯燥的二元状态切换:屏幕亮起或者瞬间黑屏休眠。然而,开源项目 Mac Duo(由开发者 Makito 打造)打破了这种死板的体验。当你缓缓压下屏幕时,屏幕上的所有窗口与壁纸会像一张立体的纸张一样,随着物理铰链的折叠向后倾斜下沉,产生拟真的 3D 景深模糊与阴影衰减;当你重新掀开屏幕,画面又如丝般顺滑地回弹复原。

这并非基于摄像头面部追踪或拙劣的假定时器,而是一套严谨打通了 MacBook 隐藏硬件传感器、ScreenCaptureKit 零拷贝显存捕获、Heckbert 单应性几何投影 以及 Metal 硬件级三线性高斯金字塔着色器 的高性能系统工程。

本文将从硬件逆向、状态估算、显存管道到片元着色器,全景拆解 Mac Duo 背后的软硬件协同流水线。


系统核心全景架构

在深入代码之前,我们先通过架构流转图俯瞰 Mac Duo 的双通道驱动模型:传感器数据流与屏幕像素流如何解耦并行,并在 Metal 管线中完成实时合流。

flowchart TD
    subgraph 硬件与传感器域
        Sensor["AppleSPUHIDDevice (0x05AC:0x8104)"]
        IOKit["IOHIDManager / IOHIDDeviceGetReport"]
        Poller["自适应轮询器 (8Hz 闲置 ~ 30Hz 临界)"]
        Predictor["角速度与前瞻预测算法 (Staleness Compensation)"]
        Spring["临界阻尼弹簧 (Semi-implicit Euler)"]
    end

    subgraph 零拷贝屏幕流域
        Screen["MacBook 内置 Retina 屏幕"]
        SCK["ScreenCaptureKit (SCStream)"]
        IOSurface["IOSurface 显存缓冲区"]
        CVMetal["CVMetalTextureCache (0 内存拷贝映射)"]
        Presence["1x1 隐形窗口 (keepPresence 过滤守卫)"]
    end

    subgraph 几何与渲染引擎域
        CADisplay["CADisplayLink (60Hz / 120Hz ProMotion)"]
        Homography["Heckbert 四边形单应性透视变换"]
        Pyramid["MPSImageGaussianPyramid (Mipmap 硬件生成)"]
        MetalShader["DepthFragment (单 Pass 逆向采样 + 景深模糊 + Gamma 调光)"]
        Overlay["OverlayWindow (CGShieldingWindowLevel)"]
    end

    Sensor --> IOKit
    IOKit --> Poller
    Poller --> Predictor
    Predictor --> Spring
    Spring --> CADisplay

    Presence -.->|诱导系统过滤| SCK
    Screen --> SCK
    SCK --> IOSurface
    IOSurface --> CVMetal
    CVMetal --> Pyramid

    CADisplay --> Homography
    Homography --> MetalShader
    Pyramid --> MetalShader
    MetalShader --> Overlay

一、 隐秘的硬件资产:Apple Silicon 铰链角度传感器逆向

很多人不知道,从搭载 Apple Silicon 的 MacBook 开始(以及部分配备 T2 芯片的机型),苹果在转轴处集成了一颗物理铰链角度传感器(Lid Angle Sensor)。系统利用它来辅助实现开盖即开机、精准闭合休眠、微小缝隙低功耗检测以及原彩显示(True Tone)的角度校准。

1. 硬件定位与 HID 描述符

在 LidAngleKit/LidAngleSensor.swift 中,作者揭示了这颗传感器的底层身份:

  • 设备类型:AppleSPUHIDDevice(运行在 Apple 传感器处理单元 SPU 上的 HID 节点)
  • Vendor ID:0x05AC(Apple Inc.)
  • Product ID:0x8104
  • HID Usage Page:0x20(Sensor Page)
  • HID Usage:0x8A(Hinge / Orientation Sensor)

最令人惊喜的是:读取该传感器无需 root 特权,无需内核扩展(KEXT),也无需向系统申请任何隐私弹窗权限。任何普通用户态 App 均可直接借助 IOKit.hid 通信。

2. 协议报文结构与分辨率差异

Mac Duo 通过 kIOHIDReportTypeFeature 接口拉取报文。不同硬件批次和机型会提供不同的报文 ID:

报文类型 长度 字节布局 精度 说明
Report 7 5 字节 [0x07, b0, b1, b2, b3] 0.01° 小端 32 位整型,除以 100 得到开合角。Apple Silicon Pro/Max 主流规格
Report 1 3 字节 [0x01, lo, hi] 1.0° 小端 16 位整型,直接表示角度整数。降级容错方案

核心解析代码极其干净轻量:

public func angle() -> Double? {
    guard let resolution else { return nil }
    guard let bytes = read(reportID: resolution.reportID) else { return nil }

    let degrees: Double
    switch resolution {
    case .hundredthsOfADegree:
        guard bytes.count >= 5 else { return nil }
        let raw = UInt32(bytes[1])
            | UInt32(bytes[2]) << 8
            | UInt32(bytes[3]) << 16
            | UInt32(bytes[4]) << 24
        degrees = Double(raw) / 100.0
    case .wholeDegrees:
        guard bytes.count >= 3 else { return nil }
        degrees = Double(UInt16(bytes[1]) | UInt16(bytes[2]) << 8)
    }

    guard degrees >= 0, degrees <= 360 else {
        lastRead.rejectedDegrees = degrees
        return nil
    }
    return degrees
}

传感器读数为 0 表示完全闭合,MacBook 完全掀开通常在 120° 至 135° 之间。


二、 物理鸿沟攻坚:用 10Hz 硬件喂饱 120Hz ProMotion 屏幕

获取传感器角度只是第一步。在工程实现上,团队遇到了极其严苛的物理矛盾:

  1. 硬件刷新率限制:转轴传感器在硬件内部的刷新周期约为 100ms(即采样率只有 10 Hz);
  2. 现代屏幕高刷标准:MacBook Pro 的 Liquid Retina XDR 屏幕刷新率高达 120 Hz(ProMotion);
  3. 陈旧度滞后(Staleness Lag):如果用户快速合盖,手指动作耗时仅约 200~300ms。100ms 的采样延迟会导致动画严重“脱节”,产生肉眼可见的物理滞后感。

Mac Duo 设计了一套优雅的三段式状态估算管道,完美消解了这一断层。

sequenceDiagram
    autonumber
    participant Sensor as 铰链传感器 (10Hz)
    participant Poller as 自适应定时器 (8~30Hz)
    participant Predictor as 前瞻速度预测器
    participant Spring as 临界阻尼弹簧
    participant CADisplay as CADisplayLink (120Hz)
    participant Screen as 屏幕渲染 (Metal)

    Note over Sensor,Poller: 1. 自适应降频与拉升
    Poller->>Sensor: 读取角读数 (Raw Angle)
    Note over Poller,Predictor: 2. 算得角速度并预估硬件滞后
    Predictor->>Predictor: rawAngle + angularVelocity * (staleness + latency)
    Note over Predictor,Spring: 3. 半隐式欧拉平滑
    CADisplay->>Spring: advance(to: predicted, dt: 1/120s)
    Spring->>Screen: 输出连续无抖动视觉角

1. 前瞻外推预测(Predictive Extrapolation)

在 LidController.swift 中,系统会持续计算合盖的瞬时角速度(angularVelocity)。如果检测到快速合盖(角速度超过 -40°/s),算法就会启动前瞻补偿:

private func predictedAngle() -> Double {
    guard angularVelocity < -Self.predictionSpeedFloor else { return rawAngle }
    let staleness = min(CACurrentMediaTime() - lastChangeTime, 0.12)
    return rawAngle + angularVelocity * (staleness + Self.predictionLatency)
}

通过引入硬件陈旧时间(staleness)与传感器固有响应延迟(predictionLatency = 0.04s),让几何投影预测到“未来 40 毫秒后盖子到达的位置”,彻底抹平传感器带来的体感滞后。

2. 临界阻尼物理弹簧(Critically Damped Spring)

仅有预测还不够,10Hz 的离散数据点如果在 120Hz 下直接线性插值,遇到速度突变依然会产生机械顿挫。

Mac Duo 引入了基于半隐式欧拉积分(Semi-implicit Euler)的 临界阻尼弹簧模型(CriticallyDampedSpring.swift):

struct CriticallyDampedSpring {
    var value: Double
    var velocity: Double = 0
    var frequency: Double = 16 // 弧度/秒

    mutating func advance(to target: Double, dt: Double) {
        let acceleration = frequency * frequency * (target - value) - 2 * frequency * velocity
        velocity += acceleration * dt
        value += velocity * dt
    }
}

在二阶力学系统中,阻尼比为 1(临界阻尼)时,系统能以最短时间收敛到目标值,且 绝对不会发生回弹超调(No Overshoot)。这保证了合盖与开盖过程中,视觉视界始终像具有质量与阻尼的物理实体一般稳健自然。

3. 功耗保护与阶梯轮询

为了杜绝后台空转耗电,轮询定时器具备自适应功耗机制:

  • 闲置状态(Idle):当屏幕正常打开且静止时,定时器以 8 Hz 低频休眠轮询,CPU 占用率几近于 0;
  • 临界预警状态(Fast Poll):当检测到合盖角速度,或当前角度进入触发前沿(阈值 + 20° 边缘区),轮询频率自动飙升至 30 Hz,为即将到来的动画做好充分采样准备。

三、 零拷贝显存直通:ScreenCaptureKit 管道与 1x1 隐形窗口

为了让倾斜下沉的画面具备生命力,Mac Duo 必须把当前屏幕内容捕捉下来。在 macOS 14+ 上,任何基于 CPU 内存拷贝的传统截屏方案(如 CGWindowListCreateImage)在 4K Retina 屏幕上都会引发数十毫秒的掉帧与巨大的内存吞吐。

Mac Duo 采用的是基于显存的零拷贝直通架构。

flowchart LR
    Display["MacBook 屏幕输出"] -->|ScreenCaptureKit| SCStream["SCStream 捕获队列"]
    SCStream -->|CMSampleBuffer| IOSurface["IOSurface 显存句柄"]
    IOSurface -->|CVMetalTextureCache| MTLTexture["MTLTexture (Metal 纹理)"]
    MTLTexture -->|零拷贝直接绑定| Shader["片元着色器输入 texture(0)"]

1. 显存直通映射

在 ScreenStreamer.swift 中,SCStream 回调返回的 CMSampleBuffer 直接包裹着底层的 CVPixelBuffer(其由显存 IOSurface 支撑)。通过 CoreVideo 与 Metal 的深度打通:

CVMetalTextureCacheCreateTextureFromImage(
    kCFAllocatorDefault,
    cache,
    pixels,
    nil,
    .bgra8Unorm_srgb,
    CVPixelBufferGetWidth(pixels),
    CVPixelBufferGetHeight(pixels),
    0,
    &wrapped
)

这段代码利用 CVMetalTextureCache,直接将操作系统已有的显存地址包装为 MTLTexture。整个过程耗时不足 0.1ms,实现真正的 0 字节 CPU 拷贝 与 0 显存重分配。

2. “1x1 隐形窗口”神级避坑黑客技巧

在开发全屏捕获与实时覆盖类工具时,每一个开发者都会遇到“死循环梦魇”:全屏 Overlay 窗口本身会被录制流捕获,导致无限套娃的镜像反馈或画面撕裂。

虽然 ScreenCaptureKit 提供了 SCContentFilter 允许通过应用程序标识排除本 App 的所有窗口,但作者在工程实操中踩中了一个极其晦涩的系统级特性:

macOS 底层机制:ScreenCaptureKit 仅在系统 WindowServer 中维护了“至少拥有一个可见窗口”的应用程序列表。如果你的应用只是一个常驻后台菜单栏工具(Menu Bar Only),在尚未弹出全屏 Overlay 之前,系统过滤器列表中根本找不到你的 App,因而无法将其提前加入排除规则;而当你弹出全屏窗口时,录制流已经将它绘制进去了!

为了破解这个先有鸡还是先有蛋的死锁,Mac Duo 祭出了一招令人拍案叫绝的架构设计——常驻 1x1 隐形窗口(keepPresence):

private func keepPresence() {
    guard presenceWindow == nil else { return }
    let window = OverlayWindow(
        contentRect: NSRect(x: 0, y: 0, width: 1, height: 1),
        styleMask: .borderless,
        backing: .buffered,
        defer: false
    )
    window.isOpaque = false
    window.backgroundColor = .clear
    window.ignoresMouseEvents = true
    window.alphaValue = 0.004  // 几近全透,骗过系统窗口注册表
    window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
    window.orderFrontRegardless()
    presenceWindow = window
}

通过常驻一个几乎完全透明、无交互、仅 1 像素大的占位窗口,使应用在系统级窗口注册表中稳稳锚定。这样,ScreenCaptureKit 随时都能将其精准列入排除白名单,彻底终结录屏死循环。


四、 空间折叠与高斯金字塔:Metal 单 Pass 景深着色器

当屏幕被拉进显存、角度被弹簧平滑之后,最核心的视觉魔法便交给了 Metal 图形管线。

1. Heckbert 任意四边形单应性透视(Homography)

在 3D 观察空间中,MacBook 的屏幕底边贴在转轴处保持静止,而顶边随着合盖向内上方或后方旋转,在人的双眼看来,矩形屏幕投影成了一个等腰梯形。

在 Homography.swift 中,Mac Duo 采用了图形学经典的 Paul Heckbert 1989 四边形到矩形单应性逆解法。算法通过四对坐标点(原矩形四角与投影后梯形四角),解出齐次变换矩阵:

将平面的屏幕矩形映射到四角透视梯形,把三维空间的旋转投影转化为二维仿射与透视除法的齐次单应性矩阵。这个 3x3 矩阵直接作为 Uniform 传入片元着色器,用于像素逆向查询。

2. 全屏单三角形顶点技巧(Fullscreen Triangle Trick)

在 DepthShaders.swift 中,看不到繁琐的 Vertex Buffer 绑定。代码直接利用硬件 Vertex ID 生成一个覆盖整屏的大三角形:

vertex float4 depthVertex(uint vertexID [[vertex_id]]) {
    const float2 corners[3] = { 
        float2(-1.0, -3.0), 
        float2(-1.0,  1.0), 
        float2( 3.0,  1.0) 
    };
    return float4(corners[vertexID], 0.0, 1.0);
}

这种经典的图形学优化手法避免了创建、上传顶点数据的开销,顶点流水线完全在 GPU 硬件调度器中以纳秒级完成。

3. 突破性能极限:基于 Mipmap 金字塔的单 Pass 景深模糊

实现物理景深(Depth of Field)最棘手的是大半径模糊。对于 16 英寸 MacBook Pro 接近 4K 的高分辨率而言,如果使用传统的多 Pass 可分离双向高斯模糊卷积,即便在 Apple Silicon GPU 上跑满 120 FPS 也需要消耗不可忽视的算力与功耗。

Mac Duo 采用了一种极其聪明的 硬件级 Mipmap 三线性采样策略:

  1. 当每帧抓取到画面后,调用 Metal Performance Shaders 的 MPSImageGaussianPyramid 一键建立硬件 Mipmap 链;
  2. 在片元着色器中,根据像素在透视空间中的相对高度(越靠顶部,距离虚拟镜头越远,景深弥散圆越发膨胀),计算出需要的模糊等级;
  3. 直接调用 Metal 硬件采样的 level(mipLevel),硬件在相邻两个 Mipmap 层级之间进行自动三线性插值(Trilinear Filtering)!
fragment float4 depthFragment(float4 position [[position]],
                               constant Uniforms &uniforms [[buffer(0)]],
                               texture2d<float> picture [[texture(0)]]) {
    constexpr sampler linearSampler(filter::linear, mip_filter::linear, address::clamp_to_edge);

    // 1. 逆透视矩阵映射
    float2 screenPoint = float2(position.x / pixelScale, screenSize.y - position.y / pixelScale);
    float3 mapped = screenToPicture * float3(screenPoint, 1.0);
    if (abs(mapped.z) < 1e-6) { return float4(0.0, 0.0, 0.0, 1.0); }
    float2 picturePoint = mapped.xy / mapped.z;

    // 2. 超出原画面边界填充为纯黑
    float2 unit = (picturePoint - paddedOrigin) / paddedSize;
    if (unit.x < 0.0 || unit.x > 1.0 || unit.y < 0.0 || unit.y > 1.0) {
        return float4(0.0, 0.0, 0.0, 1.0);
    }
    float2 texCoord = float2(unit.x, 1.0 - unit.y);

    // 3. 依据景深深度计算 Mipmap 层级(0 额外卷积开销!)
    float height = clamp(picturePoint.y / screenSize.y, 0.0, 1.0);
    float blur = strength * (blurFloor + (1.0 - blurFloor) * height);
    float mipLevel = clamp(log2(max(blur * maxRadius, 1.0)), 0.0, maxLevel);

    float4 colour = picture.sample(linearSampler, texCoord, level(mipLevel));

    // 4. 基于 Gamma 2.2 的平滑自然暗角衰减
    float spread = smoothstep(0.0, max(dimReach, 0.02), height);
    float fade = dimStrength * (dimFloor + (1.0 - dimFloor) * spread);
    colour.rgb *= pow(1.0 - maxDim * fade, 2.2);

    return float4(colour.rgb, 1.0);
}

单 Pass、单纹理、硬件三线性采样——这一设计让复杂的景深计算蜕变为极其轻量的单次纹理查询,无论多大范围的模糊,GPU 耗时均恒定维持在微秒级别。


五、 窗口层级、事件穿透与优雅复位(Closing Out)

为了呈现极致的拟物感,覆盖窗口必须凌驾于系统所有的既有视觉元素之上,同时绝不能破坏用户的操作流程。

1. 视界层级与全屏 Space 穿透

在 DepthOverlay.swift 中,OverlayWindow 被赋予了极高的系统层级:

window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary, .ignoresCycle]
window.ignoresMouseEvents = true
  • CGShieldingWindowLevel():这个层级是系统在捕捉显示器或锁屏时使用的防护盾层级,高于常规的 Floating、Status 以及菜单栏层级;
  • fullScreenAuxiliary:确保即使屏幕当前处于独立的全屏应用 Space,覆盖层依然能够无缝浮现;
  • ignoresMouseEvents = true:透视层不拦截任何鼠标事件,后方的按钮、Dock 和应用窗口依然能够接收到点击。

2. 防视觉突变的平滑归位(Closing Out Easing)

如果在合盖过程中,用户突然停住或把屏幕往上抬,动画该如何结束?

如果简单粗暴地销毁窗口,用户就会看到一个已经倾斜扭曲的画面瞬间“闪退”,暴露背后平整的原生桌面,产生严重的不连续感。

Mac Duo 引入了 归位过渡机制(Closing Out):

stateDiagram-v2
    [*] --> Tracking: 满足合盖触发阈值
    Tracking --> ClosingOut: 角度回升或静止超时
    state ClosingOut {
        [*] --> SpringToZero: 目标角强制重设为初始平角
        SpringToZero --> SettleCheck: 逐帧拉平并判断 epsilon < 0.05°
        SettleCheck --> FadeOut: 达到完全平贴状态,渐隐窗口
    }
    FadeOut --> [*]: 彻底释放 Metal 显存资源

当触发结束条件时,控制器并不立即关闭 Overlay,而是将物理弹簧的目标值平滑引导回平面(0 偏移)。CADisplayLink 驱动画面先在视觉上“回弹”至完全平贴的原生屏幕画面,此时几何误差小于 0.05°,两层画面在像素级完全重合;随后执行极短的淡出动画,让视觉过渡毫无痕迹。


总结与启示

Mac Duo 的精妙之处,不在于堆叠庞大的第三方框架,而在于对 Apple 原生底层能力(Native Primaries) 的深刻挖掘与优雅组合:

  1. 对未公开硬件的敏锐观察:利用 IOKit HID 直击 Apple Silicon 专属的铰链传感器,为拟物交互提供了最纯粹的第一物理变量;
  2. 现代视音频管线的极简复用:以零拷贝思路整合 ScreenCaptureKit 与 Metal,杜绝一切不必要的内存分配与总线拷贝;
  3. 图形学经验的举重若轻:摒弃沉重的离屏多 Pass 高斯卷积,用 Heckbert 逆单应性矩阵 + Mipmap 硬件三线性过滤搞定全套景深渲染;
  4. 极致严谨的边界状态处理:从 1x1 占位窗口、自适应能耗轮询,到临界阻尼弹簧与 Closing Out 归位,每个细节都体现了资深客户端架构师的功底。

当你再次合上 MacBook 时,不妨细细品味一下那道随着转轴缓缓折叠下沉的像素光影——那不仅是一场视觉魔术,更是软硬件协同美学在现代 macOS 平台上的一次璀璨绽放。