[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6E69HPCSBVRPSNZ9K759S78":3,"skill-same:01M6E69HPCSBVRPSNZ9K759S78":55},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":25,"linkOnly":26,"markdownContent":27,"markdownBody":28,"frontMatter":29,"refName":32,"commitSha":33,"skillDirectory":34,"forksCount":35,"repositoryHtmlUrl":36,"repositoryHomepageUrl":37,"ownerHtmlUrl":38,"ownerBlogUrl":6,"ownerDescription":39,"updatedAt":40,"install":41,"resources":54},"01M6E69HPCSBVRPSNZ9K759S78","axiom-display-performance",null,"Use when app runs at unexpected frame rate, stuck at 60fps on ProMotion, frame pacing issues, or configuring render loops. Covers MTKView, CADisplayLink, CAMetalDisplayLink, frame pacing, hitches, system caps.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-display-performance\u002FSKILL.md",[],[19,20,21,22,23,24],"performance","axiom display performance","issues","system","use","app","2026-11-10 10:12:30",false,"---\nname: axiom-display-performance\ndescription: Use when app runs at unexpected frame rate, stuck at 60fps on ProMotion, frame pacing issues, or configuring render loops. Covers MTKView, CADisplayLink, CAMetalDisplayLink, frame pacing, hitches, system caps.\nlicense: MIT\nmetadata:\n  version: \"1.0.0\"\n---\n\n# Display Performance\n\nSystematic diagnosis for frame rate issues on variable refresh rate displays (ProMotion, iPad Pro, future devices). Covers render loop configuration, frame pacing, hitch mechanics, and production telemetry.\n\n**Key insight**: \"ProMotion available\" does NOT mean your app automatically runs at 120Hz. You must configure it correctly, account for system caps, and ensure proper frame pacing.\n\n---\n\n## Part 1: Why You're Stuck at 60fps\n\n### Diagnostic Order\n\nCheck these in order when stuck at 60fps on ProMotion:\n\n1. **Info.plist key missing?** (iPhone only) → Part 2\n2. **Render loop configured for 60?** (MTKView defaults, CADisplayLink) → Part 3\n3. **System caps enabled?** (Low Power Mode, Limit Frame Rate, Thermal) → Part 5\n4. **Frame time > 8.33ms?** (Can't sustain 120fps) → Part 6\n5. **Frame pacing issues?** (Micro-stuttering despite good FPS) → Part 7\n6. **Measuring wrong thing?** (UIScreen vs actual presentation) → Part 9\n\n---\n\n## Part 2: Enabling ProMotion on iPhone\n\n**Critical**: Core Animation won't access frame rates above 60Hz on iPhone unless you add this key.\n\n```xml\n\u003C!-- Info.plist -->\n\u003Ckey>CADisableMinimumFrameDurationOnPhone\u003C\u002Fkey>\n\u003Ctrue\u002F>\n```\n\nWithout this key:\n- Your `preferredFrameRateRange` hints are ignored above 60Hz\n- Other animations may affect your CADisplayLink callback rate\n- iPad Pro does NOT require this key\n\n**When to add**: Any iPhone app that needs >60Hz for games, animations, or smooth scrolling.\n\n---\n\n## Part 3: Render Loop Configuration\n\n### MTKView Defaults to 60fps\n\n**This is the most common cause.** MTKView's `preferredFramesPerSecond` defaults to 60.\n\n```swift\n\u002F\u002F ❌ WRONG: Implicit 60fps (default)\nlet mtkView = MTKView(frame: frame, device: device)\nmtkView.delegate = self\n\u002F\u002F Running at 60fps even on ProMotion!\n\n\u002F\u002F ✅ CORRECT: Explicit 120fps request\nlet mtkView = MTKView(frame: frame, device: device)\nmtkView.preferredFramesPerSecond = 120\nmtkView.isPaused = false\nmtkView.enableSetNeedsDisplay = false  \u002F\u002F Continuous, not on-demand\nmtkView.delegate = self\n```\n\n**Critical settings for continuous high-rate rendering:**\n\n| Property | Value | Why |\n|----------|-------|-----|\n| `preferredFramesPerSecond` | `120` | Request max rate |\n| `isPaused` | `false` | Don't pause the render loop |\n| `enableSetNeedsDisplay` | `false` | Continuous mode, not on-demand |\n\n### CADisplayLink Configuration (iOS 15+)\n\nApple explicitly recommends CADisplayLink (not timers) for custom render loops.\n\n```swift\n\u002F\u002F ❌ WRONG: Timer-based render loop (drifts, wastes frame time)\nTimer.scheduledTimer(withTimeInterval: 1.0\u002F120.0, repeats: true) { _ in\n    self.render()\n}\n\n\u002F\u002F ❌ WRONG: Default CADisplayLink (may hint 60)\nlet displayLink = CADisplayLink(target: self, selector: #selector(render))\ndisplayLink.add(to: .main, forMode: .common)\n\n\u002F\u002F ✅ CORRECT: Explicit frame rate range\nlet displayLink = CADisplayLink(target: self, selector: #selector(render))\ndisplayLink.preferredFrameRateRange = CAFrameRateRange(\n    minimum: 80,      \u002F\u002F Minimum acceptable\n    maximum: 120,     \u002F\u002F Preferred maximum\n    preferred: 120    \u002F\u002F What you want\n)\ndisplayLink.add(to: .main, forMode: .common)\n```\n\n**Special priority for games**: iOS 15+ gives 30Hz and 60Hz special priority. If targeting these rates:\n\n```swift\n\u002F\u002F 30Hz and 60Hz get priority scheduling\nlet prioritizedRange = CAFrameRateRange(\n    minimum: 30,\n    maximum: 60,\n    preferred: 60\n)\ndisplayLink.preferredFrameRateRange = prioritizedRange\n```\n\n### Suggested Frame Rates by Content Type\n\n| Content Type | Suggested Rate | Notes |\n|--------------|----------------|-------|\n| Video playback | 24-30 Hz | Match content frame rate |\n| Scrolling UI | 60-120 Hz | Higher = smoother |\n| Fast games | 60-120 Hz | Match rendering capability |\n| Slow animations | 30-60 Hz | Save power |\n| Static content | 10-24 Hz | Minimal updates needed |\n\n---\n\n## Part 4: CAMetalDisplayLink (iOS 17+)\n\nFor Metal apps needing precise timing control, `CAMetalDisplayLink` provides more control than CADisplayLink.\n\n```swift\nclass MetalRenderer: NSObject, CAMetalDisplayLinkDelegate {\n    var displayLink: CAMetalDisplayLink?\n    var metalLayer: CAMetalLayer!\n\n    func setupDisplayLink() {\n        displayLink = CAMetalDisplayLink(metalLayer: metalLayer)\n        displayLink?.delegate = self\n        displayLink?.preferredFrameRateRange = CAFrameRateRange(\n            minimum: 60,\n            maximum: 120,\n            preferred: 120\n        )\n        \u002F\u002F Control render latency (in frames)\n        displayLink?.preferredFrameLatency = 2\n        displayLink?.add(to: .main, forMode: .common)\n    }\n\n    func metalDisplayLink(_ link: CAMetalDisplayLink, needsUpdate update: CAMetalDisplayLink.Update) {\n        \u002F\u002F update.drawable - The drawable to render to\n        \u002F\u002F update.targetTimestamp - Deadline to finish rendering\n        \u002F\u002F update.targetPresentationTimestamp - When frame will display\n\n        guard let drawable = update.drawable else { return }\n\n        let workingTime = update.targetTimestamp - CACurrentMediaTime()\n        \u002F\u002F workingTime = seconds available before deadline\n\n        \u002F\u002F Render to drawable...\n        renderFrame(to: drawable)\n    }\n}\n```\n\n**Key differences from CADisplayLink:**\n\n| Feature | CADisplayLink | CAMetalDisplayLink |\n|---------|---------------|-------------------|\n| Drawable access | Manual via layer | Provided in callback |\n| Latency control | None | `preferredFrameLatency` |\n| Target timing | timestamp\u002FtargetTimestamp | + targetPresentationTimestamp |\n| Use case | General animation | Metal-specific rendering |\n\n**When to use CAMetalDisplayLink:**\n- Need precise control over render timing window\n- Want to minimize input latency\n- Building games or intensive Metal apps\n- iOS 17+ only deployment\n\n---\n\n## Part 5: System Caps\n\nSystem states can force 60fps even when your code requests 120:\n\n### Low Power Mode\n\n**Caps ProMotion devices to 60fps.**\n\n```swift\n\u002F\u002F Check programmatically\nif ProcessInfo.processInfo.isLowPowerModeEnabled {\n    \u002F\u002F System caps display to 60Hz\n}\n\n\u002F\u002F Observe changes\nNotificationCenter.default.addObserver(\n    forName: .NSProcessInfoPowerStateDidChange,\n    object: nil,\n    queue: .main\n) { _ in\n    let isLowPower = ProcessInfo.processInfo.isLowPowerModeEnabled\n    self.adjustRenderingForPowerState(isLowPower)\n}\n```\n\n### Limit Frame Rate (Accessibility)\n\n**Settings → Accessibility → Motion → Limit Frame Rate** caps to 60fps.\n\nNo API to detect. If user reports 60fps despite configuration, have them check this setting.\n\n### Thermal Throttling\n\nSystem restricts 120Hz when device overheats.\n\n```swift\n\u002F\u002F Check thermal state\nswitch ProcessInfo.processInfo.thermalState {\ncase .nominal, .fair:\n    preferredFramesPerSecond = 120\ncase .serious, .critical:\n    preferredFramesPerSecond = 60  \u002F\u002F Reduce proactively\n@unknown default:\n    break\n}\n\n\u002F\u002F Observe thermal changes\nNotificationCenter.default.addObserver(\n    forName: ProcessInfo.thermalStateDidChangeNotification,\n    object: nil,\n    queue: .main\n) { _ in\n    self.adjustForThermalState()\n}\n```\n\n### Adaptive Power (iOS 26+, iPhone 17)\n\n**New in iOS 26**: Adaptive Power is ON by default on iPhone 17\u002F17 Pro. Can throttle even at 60% battery.\n\n**User action for testing**: Settings → Battery → Power Mode → disable **Adaptive Power**.\n\nNo public API to detect Adaptive Power state.\n\n---\n\n## Part 6: Performance Budget\n\n### Frame Time Budgets\n\n| Target FPS | Frame Budget | Vsync Interval |\n|------------|--------------|----------------|\n| 120 | 8.33ms | Every vsync |\n| 90 | 11.11ms | — |\n| 60 | 16.67ms | Every 2nd vsync |\n| 30 | 33.33ms | Every 4th vsync |\n\n**If you consistently exceed budget, system drops to next sustainable rate.**\n\n### Measuring GPU Frame Time\n\n```swift\nfunc draw(in view: MTKView) {\n    guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }\n\n    \u002F\u002F Your rendering code...\n\n    commandBuffer.addCompletedHandler { buffer in\n        let gpuTime = buffer.gpuEndTime - buffer.gpuStartTime\n        let gpuMs = gpuTime * 1000\n\n        if gpuMs > 8.33 {\n            print(\"⚠️ GPU: \\(String(format: \"%.2f\", gpuMs))ms exceeds 120Hz budget\")\n        }\n    }\n\n    commandBuffer.commit()\n}\n```\n\n### Can't Sustain 120? Target Lower Rate Evenly\n\n**Critical**: Uneven frame pacing looks worse than consistent lower rate.\n\n```swift\n\u002F\u002F If you can't sustain 8.33ms, explicitly target 60 for smooth cadence\nif averageGpuTime > 8.33 && averageGpuTime \u003C= 16.67 {\n    mtkView.preferredFramesPerSecond = 60\n}\n```\n\n---\n\n## Part 7: Frame Pacing\n\n### The Micro-Stuttering Problem\n\nEven with good average FPS, inconsistent frame timing causes visible jitter.\n\n```\n\u002F\u002F BAD: Inconsistent intervals despite ~40 FPS average\nFrame 1: 25ms\nFrame 2: 40ms  ← stutter\nFrame 3: 25ms\nFrame 4: 40ms  ← stutter\n\n\u002F\u002F GOOD: Consistent intervals at 30 FPS\nFrame 1: 33ms\nFrame 2: 33ms\nFrame 3: 33ms\nFrame 4: 33ms\n```\n\n**Presenting immediately after rendering causes this.** Use explicit timing control.\n\n### Frame Pacing APIs\n\n#### present(afterMinimumDuration:) — Recommended\n\nEnsures consistent spacing between frames:\n\n```swift\nfunc draw(in view: MTKView) {\n    guard let commandBuffer = commandQueue.makeCommandBuffer(),\n          let drawable = view.currentDrawable else { return }\n\n    \u002F\u002F Render to drawable...\n\n    \u002F\u002F Present with minimum 33ms between frames (30 FPS target)\n    commandBuffer.present(drawable, afterMinimumDuration: 0.033)\n    commandBuffer.commit()\n}\n```\n\n#### present(at:) — Precise Timing\n\nSchedule presentation at specific time:\n\n```swift\n\u002F\u002F Present at specific Mach absolute time\nlet presentTime = CACurrentMediaTime() + 0.033\ncommandBuffer.present(drawable, atTime: presentTime)\n```\n\n#### presentedTime — Verify Actual Presentation\n\nCheck when frames actually appeared:\n\n```swift\ndrawable.addPresentedHandler { drawable in\n    let actualTime = drawable.presentedTime\n    if actualTime == 0.0 {\n        \u002F\u002F Frame was dropped!\n        print(\"⚠️ Frame dropped\")\n    } else {\n        print(\"Frame presented at: \\(actualTime)\")\n    }\n}\n```\n\n### Frame Pacing Pattern\n\n```swift\nclass SmoothRenderer: NSObject, MTKViewDelegate {\n    private var targetFrameDuration: CFTimeInterval = 1.0 \u002F 60.0  \u002F\u002F 60 FPS target\n\n    func draw(in view: MTKView) {\n        guard let commandBuffer = commandQueue.makeCommandBuffer(),\n              let drawable = view.currentDrawable else { return }\n\n        renderScene(to: drawable)\n\n        \u002F\u002F Use frame pacing to ensure consistent intervals\n        commandBuffer.present(drawable, afterMinimumDuration: targetFrameDuration)\n        commandBuffer.commit()\n    }\n\n    func adjustTargetFrameRate(canSustain fps: Int) {\n        switch fps {\n        case 90...:\n            targetFrameDuration = 1.0 \u002F 120.0\n        case 50...:\n            targetFrameDuration = 1.0 \u002F 60.0\n        default:\n            targetFrameDuration = 1.0 \u002F 30.0\n        }\n    }\n}\n```\n\n---\n\n## Part 8: Understanding Hitches\n\n### Render Loop Phases\n\nFrame lifecycle: **Begin Time → Commit Deadline → Presentation Time**\n\n1. **App Process (CPU)**: Handle events, compute UI updates, Core Animation commit\n2. **Render Server (CPU+GPU)**: Transform UI to bitmap, render to buffer\n3. **Display Driver**: Swap buffer to screen at vsync\n\nAt 120Hz, each phase has ~8.33ms. Miss any deadline = hitch.\n\n### Commit Hitch vs Render Hitch\n\n**Commit Hitch**: App process misses commit deadline\n- Cause: Main thread work takes too long\n- Fix: Move work off main thread, reduce view complexity\n\n**Render Hitch**: Render server misses presentation deadline\n- Cause: GPU work too complex (blur, shadows, layers)\n- Fix: Simplify visual effects, reduce overdraw\n\n### Double vs Triple Buffering\n\n**Double Buffer (default)**:\n- Frame lifetime: 2 vsync intervals\n- Tighter deadlines\n- Lower latency\n\n**Triple Buffer (system may enable)**:\n- Frame lifetime: 3 vsync intervals\n- Render server gets 2 vsync intervals\n- Higher latency but more headroom\n\nThe system automatically switches to triple buffering to recover from render hitches.\n\n### Hitch Duration\n\n```\nExpected Frame Lifetime = Begin Time → Presentation Time\nActual Frame Lifetime = Begin Time → Actual Vsync\n\nHitch Duration = Actual - Expected\n```\n\nIf hitch duration > 0, the frame was late and previous frame stayed onscreen longer.\n\n---\n\n## Part 9: Measurement\n\n### UIScreen Lies, Actual Presentation Tells Truth\n\n```swift\n\u002F\u002F ❌ This says 120 even when system caps you to 60\nlet maxFPS = UIScreen.main.maximumFramesPerSecond\n\u002F\u002F Reports capability, not actual rate!\n\n\u002F\u002F ✅ Measure from CADisplayLink timing\n@objc func displayLinkCallback(_ link: CADisplayLink) {\n    \u002F\u002F Time available to prepare next frame\n    let workingTime = link.targetTimestamp - CACurrentMediaTime()\n\n    \u002F\u002F Actual interval since last callback\n    if lastTimestamp > 0 {\n        let interval = link.timestamp - lastTimestamp\n        let actualFPS = 1.0 \u002F interval\n    }\n    lastTimestamp = link.timestamp\n}\n```\n\n### Metal Performance HUD\n\nEnable on-device real-time performance overlay:\n\n**Via Xcode scheme:**\n1. Edit Scheme → Run → Diagnostics\n2. Enable \"Show Graphics Overview\"\n3. Optionally enable \"Log Graphics Overview\"\n\n**Via environment variable:**\n```bash\nMTL_HUD_ENABLED=1\n```\n\n**Via device settings:**\nSettings → Developer → Graphics HUD → Show Graphics HUD\n\n**HUD shows:**\n- FPS (average)\n- GPU time per frame\n- Frame interval chart (last 120 frames)\n- Memory usage\n\n### Production Telemetry with MetricKit\n\nMonitor hitches in production:\n\n```swift\nimport MetricKit\n\nclass MetricsManager: NSObject, MXMetricManagerSubscriber {\n    func didReceive(_ payloads: [MXMetricPayload]) {\n        for payload in payloads {\n            if let animationMetrics = payload.animationMetrics {\n                \u002F\u002F Ratio of time spent hitching during scroll\n                let scrollHitchRatio = animationMetrics.scrollHitchTimeRatio\n\n                \u002F\u002F Ratio of time spent hitching in all animations\n                if #available(iOS 17.0, *) {\n                    let hitchRatio = animationMetrics.hitchTimeRatio\n                }\n\n                analyzeHitchMetrics(scrollHitchRatio: scrollHitchRatio)\n            }\n        }\n    }\n}\n\n\u002F\u002F Register for metrics\nMXMetricManager.shared.add(metricsManager)\n```\n\n**What to track:**\n- `scrollHitchTimeRatio`: Time spent hitching while scrolling (UIScrollView only)\n- `hitchTimeRatio` (iOS 17+): Time spent hitching in all tracked animations\n\n---\n\n## Part 10: Quick Diagnostic Checklist\n\nWhen debugging frame rate issues:\n\n| Step | Check | Fix |\n|------|-------|-----|\n| 1 | Info.plist key present? (iPhone) | Add `CADisableMinimumFrameDurationOnPhone` |\n| 2 | Limit Frame Rate off? | Settings → Accessibility → Motion |\n| 3 | Low Power Mode off? | Settings → Battery |\n| 4 | Adaptive Power off? (iPhone 17+) | Settings → Battery → Power Mode |\n| 5 | preferredFramesPerSecond = 120? | Set explicitly on MTKView |\n| 6 | preferredFrameRateRange set? | Configure on CADisplayLink |\n| 7 | GPU frame time \u003C 8.33ms? | Profile with Metal HUD or Instruments |\n| 8 | Frame pacing consistent? | Use present(afterMinimumDuration:) |\n| 9 | Hitches in production? | Monitor with MetricKit |\n\n---\n\n## Part 11: Common Patterns\n\n### Pattern: Adaptive Frame Rate with Thermal Awareness\n\n```swift\nclass AdaptiveRenderer: NSObject, MTKViewDelegate {\n    private var recentFrameTimes: [Double] = []\n    private let sampleCount = 30\n    private var targetFrameDuration: CFTimeInterval = 1.0 \u002F 60.0\n\n    func draw(in view: MTKView) {\n        guard let commandBuffer = commandQueue.makeCommandBuffer(),\n              let drawable = view.currentDrawable else { return }\n\n        let startTime = CACurrentMediaTime()\n        renderScene(to: drawable)\n        let frameTime = (CACurrentMediaTime() - startTime) * 1000\n\n        updateTargetRate(frameTime: frameTime, view: view)\n\n        commandBuffer.present(drawable, afterMinimumDuration: targetFrameDuration)\n        commandBuffer.commit()\n    }\n\n    private func updateTargetRate(frameTime: Double, view: MTKView) {\n        recentFrameTimes.append(frameTime)\n        if recentFrameTimes.count > sampleCount {\n            recentFrameTimes.removeFirst()\n        }\n\n        let avgFrameTime = recentFrameTimes.reduce(0, +) \u002F Double(recentFrameTimes.count)\n        let thermal = ProcessInfo.processInfo.thermalState\n        let lowPower = ProcessInfo.processInfo.isLowPowerModeEnabled\n\n        \u002F\u002F Constrain based on what we can sustain AND system state\n        if lowPower || thermal >= .serious {\n            view.preferredFramesPerSecond = 30\n            targetFrameDuration = 1.0 \u002F 30.0\n        } else if avgFrameTime \u003C 7.0 && thermal == .nominal {\n            view.preferredFramesPerSecond = 120\n            targetFrameDuration = 1.0 \u002F 120.0\n        } else if avgFrameTime \u003C 14.0 {\n            view.preferredFramesPerSecond = 60\n            targetFrameDuration = 1.0 \u002F 60.0\n        } else {\n            view.preferredFramesPerSecond = 30\n            targetFrameDuration = 1.0 \u002F 30.0\n        }\n    }\n}\n```\n\n### Pattern: Frame Drop Detection\n\n```swift\nclass FrameDropMonitor {\n    private var expectedPresentTime: CFTimeInterval = 0\n    private var dropCount = 0\n\n    func trackFrame(drawable: MTLDrawable, expectedInterval: CFTimeInterval) {\n        drawable.addPresentedHandler { [weak self] drawable in\n            guard let self = self else { return }\n\n            if drawable.presentedTime == 0.0 {\n                self.dropCount += 1\n                print(\"⚠️ Frame dropped (total: \\(self.dropCount))\")\n            } else if self.expectedPresentTime > 0 {\n                let actualInterval = drawable.presentedTime - self.expectedPresentTime\n                let variance = abs(actualInterval - expectedInterval)\n\n                if variance > expectedInterval * 0.5 {\n                    print(\"⚠️ Frame timing variance: \\(variance * 1000)ms\")\n                }\n            }\n\n            self.expectedPresentTime = drawable.presentedTime\n        }\n    }\n}\n```\n\n---\n\n## Resources\n\n**WWDC**: 2021-10147, 2018-612, 2022-10083, 2023-10123\n\n**Tech Talks**: 10855, 10856, 10857 (Hitch deep dives)\n\n**Docs**: \u002Fquartzcore\u002Fcadisplaylink, \u002Fquartzcore\u002Fcametaldisplaylink, \u002Fquartzcore\u002Foptimizing-iphone-and-ipad-apps-to-support-promotion-displays, \u002Fxcode\u002Funderstanding-hitches-in-your-app, \u002Fmetal\u002Fmtldrawable\u002Fpresent(afterminimumduration:), \u002Fmetrickit\u002Fmxanimationmetric\n\n**Skills**: axiom-energy, axiom-ios-graphics, axiom-metal-migration-ref, axiom-performance-profiling\n\n","# Display Performance\n\nSystematic diagnosis for frame rate issues on variable refresh rate displays (ProMotion, iPad Pro, future devices). Covers render loop configuration, frame pacing, hitch mechanics, and production telemetry.\n\n**Key insight**: \"ProMotion available\" does NOT mean your app automatically runs at 120Hz. You must configure it correctly, account for system caps, and ensure proper frame pacing.\n\n---\n\n## Part 1: Why You're Stuck at 60fps\n\n### Diagnostic Order\n\nCheck these in order when stuck at 60fps on ProMotion:\n\n1. **Info.plist key missing?** (iPhone only) → Part 2\n2. **Render loop configured for 60?** (MTKView defaults, CADisplayLink) → Part 3\n3. **System caps enabled?** (Low Power Mode, Limit Frame Rate, Thermal) → Part 5\n4. **Frame time > 8.33ms?** (Can't sustain 120fps) → Part 6\n5. **Frame pacing issues?** (Micro-stuttering despite good FPS) → Part 7\n6. **Measuring wrong thing?** (UIScreen vs actual presentation) → Part 9\n\n---\n\n## Part 2: Enabling ProMotion on iPhone\n\n**Critical**: Core Animation won't access frame rates above 60Hz on iPhone unless you add this key.\n\n```xml\n\u003C!-- Info.plist -->\n\u003Ckey>CADisableMinimumFrameDurationOnPhone\u003C\u002Fkey>\n\u003Ctrue\u002F>\n```\n\nWithout this key:\n- Your `preferredFrameRateRange` hints are ignored above 60Hz\n- Other animations may affect your CADisplayLink callback rate\n- iPad Pro does NOT require this key\n\n**When to add**: Any iPhone app that needs >60Hz for games, animations, or smooth scrolling.\n\n---\n\n## Part 3: Render Loop Configuration\n\n### MTKView Defaults to 60fps\n\n**This is the most common cause.** MTKView's `preferredFramesPerSecond` defaults to 60.\n\n```swift\n\u002F\u002F ❌ WRONG: Implicit 60fps (default)\nlet mtkView = MTKView(frame: frame, device: device)\nmtkView.delegate = self\n\u002F\u002F Running at 60fps even on ProMotion!\n\n\u002F\u002F ✅ CORRECT: Explicit 120fps request\nlet mtkView = MTKView(frame: frame, device: device)\nmtkView.preferredFramesPerSecond = 120\nmtkView.isPaused = false\nmtkView.enableSetNeedsDisplay = false  \u002F\u002F Continuous, not on-demand\nmtkView.delegate = self\n```\n\n**Critical settings for continuous high-rate rendering:**\n\n| Property | Value | Why |\n|----------|-------|-----|\n| `preferredFramesPerSecond` | `120` | Request max rate |\n| `isPaused` | `false` | Don't pause the render loop |\n| `enableSetNeedsDisplay` | `false` | Continuous mode, not on-demand |\n\n### CADisplayLink Configuration (iOS 15+)\n\nApple explicitly recommends CADisplayLink (not timers) for custom render loops.\n\n```swift\n\u002F\u002F ❌ WRONG: Timer-based render loop (drifts, wastes frame time)\nTimer.scheduledTimer(withTimeInterval: 1.0\u002F120.0, repeats: true) { _ in\n    self.render()\n}\n\n\u002F\u002F ❌ WRONG: Default CADisplayLink (may hint 60)\nlet displayLink = CADisplayLink(target: self, selector: #selector(render))\ndisplayLink.add(to: .main, forMode: .common)\n\n\u002F\u002F ✅ CORRECT: Explicit frame rate range\nlet displayLink = CADisplayLink(target: self, selector: #selector(render))\ndisplayLink.preferredFrameRateRange = CAFrameRateRange(\n    minimum: 80,      \u002F\u002F Minimum acceptable\n    maximum: 120,     \u002F\u002F Preferred maximum\n    preferred: 120    \u002F\u002F What you want\n)\ndisplayLink.add(to: .main, forMode: .common)\n```\n\n**Special priority for games**: iOS 15+ gives 30Hz and 60Hz special priority. If targeting these rates:\n\n```swift\n\u002F\u002F 30Hz and 60Hz get priority scheduling\nlet prioritizedRange = CAFrameRateRange(\n    minimum: 30,\n    maximum: 60,\n    preferred: 60\n)\ndisplayLink.preferredFrameRateRange = prioritizedRange\n```\n\n### Suggested Frame Rates by Content Type\n\n| Content Type | Suggested Rate | Notes |\n|--------------|----------------|-------|\n| Video playback | 24-30 Hz | Match content frame rate |\n| Scrolling UI | 60-120 Hz | Higher = smoother |\n| Fast games | 60-120 Hz | Match rendering capability |\n| Slow animations | 30-60 Hz | Save power |\n| Static content | 10-24 Hz | Minimal updates needed |\n\n---\n\n## Part 4: CAMetalDisplayLink (iOS 17+)\n\nFor Metal apps needing precise timing control, `CAMetalDisplayLink` provides more control than CADisplayLink.\n\n```swift\nclass MetalRenderer: NSObject, CAMetalDisplayLinkDelegate {\n    var displayLink: CAMetalDisplayLink?\n    var metalLayer: CAMetalLayer!\n\n    func setupDisplayLink() {\n        displayLink = CAMetalDisplayLink(metalLayer: metalLayer)\n        displayLink?.delegate = self\n        displayLink?.preferredFrameRateRange = CAFrameRateRange(\n            minimum: 60,\n            maximum: 120,\n            preferred: 120\n        )\n        \u002F\u002F Control render latency (in frames)\n        displayLink?.preferredFrameLatency = 2\n        displayLink?.add(to: .main, forMode: .common)\n    }\n\n    func metalDisplayLink(_ link: CAMetalDisplayLink, needsUpdate update: CAMetalDisplayLink.Update) {\n        \u002F\u002F update.drawable - The drawable to render to\n        \u002F\u002F update.targetTimestamp - Deadline to finish rendering\n        \u002F\u002F update.targetPresentationTimestamp - When frame will display\n\n        guard let drawable = update.drawable else { return }\n\n        let workingTime = update.targetTimestamp - CACurrentMediaTime()\n        \u002F\u002F workingTime = seconds available before deadline\n\n        \u002F\u002F Render to drawable...\n        renderFrame(to: drawable)\n    }\n}\n```\n\n**Key differences from CADisplayLink:**\n\n| Feature | CADisplayLink | CAMetalDisplayLink |\n|---------|---------------|-------------------|\n| Drawable access | Manual via layer | Provided in callback |\n| Latency control | None | `preferredFrameLatency` |\n| Target timing | timestamp\u002FtargetTimestamp | + targetPresentationTimestamp |\n| Use case | General animation | Metal-specific rendering |\n\n**When to use CAMetalDisplayLink:**\n- Need precise control over render timing window\n- Want to minimize input latency\n- Building games or intensive Metal apps\n- iOS 17+ only deployment\n\n---\n\n## Part 5: System Caps\n\nSystem states can force 60fps even when your code requests 120:\n\n### Low Power Mode\n\n**Caps ProMotion devices to 60fps.**\n\n```swift\n\u002F\u002F Check programmatically\nif ProcessInfo.processInfo.isLowPowerModeEnabled {\n    \u002F\u002F System caps display to 60Hz\n}\n\n\u002F\u002F Observe changes\nNotificationCenter.default.addObserver(\n    forName: .NSProcessInfoPowerStateDidChange,\n    object: nil,\n    queue: .main\n) { _ in\n    let isLowPower = ProcessInfo.processInfo.isLowPowerModeEnabled\n    self.adjustRenderingForPowerState(isLowPower)\n}\n```\n\n### Limit Frame Rate (Accessibility)\n\n**Settings → Accessibility → Motion → Limit Frame Rate** caps to 60fps.\n\nNo API to detect. If user reports 60fps despite configuration, have them check this setting.\n\n### Thermal Throttling\n\nSystem restricts 120Hz when device overheats.\n\n```swift\n\u002F\u002F Check thermal state\nswitch ProcessInfo.processInfo.thermalState {\ncase .nominal, .fair:\n    preferredFramesPerSecond = 120\ncase .serious, .critical:\n    preferredFramesPerSecond = 60  \u002F\u002F Reduce proactively\n@unknown default:\n    break\n}\n\n\u002F\u002F Observe thermal changes\nNotificationCenter.default.addObserver(\n    forName: ProcessInfo.thermalStateDidChangeNotification,\n    object: nil,\n    queue: .main\n) { _ in\n    self.adjustForThermalState()\n}\n```\n\n### Adaptive Power (iOS 26+, iPhone 17)\n\n**New in iOS 26**: Adaptive Power is ON by default on iPhone 17\u002F17 Pro. Can throttle even at 60% battery.\n\n**User action for testing**: Settings → Battery → Power Mode → disable **Adaptive Power**.\n\nNo public API to detect Adaptive Power state.\n\n---\n\n## Part 6: Performance Budget\n\n### Frame Time Budgets\n\n| Target FPS | Frame Budget | Vsync Interval |\n|------------|--------------|----------------|\n| 120 | 8.33ms | Every vsync |\n| 90 | 11.11ms | — |\n| 60 | 16.67ms | Every 2nd vsync |\n| 30 | 33.33ms | Every 4th vsync |\n\n**If you consistently exceed budget, system drops to next sustainable rate.**\n\n### Measuring GPU Frame Time\n\n```swift\nfunc draw(in view: MTKView) {\n    guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }\n\n    \u002F\u002F Your rendering code...\n\n    commandBuffer.addCompletedHandler { buffer in\n        let gpuTime = buffer.gpuEndTime - buffer.gpuStartTime\n        let gpuMs = gpuTime * 1000\n\n        if gpuMs > 8.33 {\n            print(\"⚠️ GPU: \\(String(format: \"%.2f\", gpuMs))ms exceeds 120Hz budget\")\n        }\n    }\n\n    commandBuffer.commit()\n}\n```\n\n### Can't Sustain 120? Target Lower Rate Evenly\n\n**Critical**: Uneven frame pacing looks worse than consistent lower rate.\n\n```swift\n\u002F\u002F If you can't sustain 8.33ms, explicitly target 60 for smooth cadence\nif averageGpuTime > 8.33 && averageGpuTime \u003C= 16.67 {\n    mtkView.preferredFramesPerSecond = 60\n}\n```\n\n---\n\n## Part 7: Frame Pacing\n\n### The Micro-Stuttering Problem\n\nEven with good average FPS, inconsistent frame timing causes visible jitter.\n\n```\n\u002F\u002F BAD: Inconsistent intervals despite ~40 FPS average\nFrame 1: 25ms\nFrame 2: 40ms  ← stutter\nFrame 3: 25ms\nFrame 4: 40ms  ← stutter\n\n\u002F\u002F GOOD: Consistent intervals at 30 FPS\nFrame 1: 33ms\nFrame 2: 33ms\nFrame 3: 33ms\nFrame 4: 33ms\n```\n\n**Presenting immediately after rendering causes this.** Use explicit timing control.\n\n### Frame Pacing APIs\n\n#### present(afterMinimumDuration:) — Recommended\n\nEnsures consistent spacing between frames:\n\n```swift\nfunc draw(in view: MTKView) {\n    guard let commandBuffer = commandQueue.makeCommandBuffer(),\n          let drawable = view.currentDrawable else { return }\n\n    \u002F\u002F Render to drawable...\n\n    \u002F\u002F Present with minimum 33ms between frames (30 FPS target)\n    commandBuffer.present(drawable, afterMinimumDuration: 0.033)\n    commandBuffer.commit()\n}\n```\n\n#### present(at:) — Precise Timing\n\nSchedule presentation at specific time:\n\n```swift\n\u002F\u002F Present at specific Mach absolute time\nlet presentTime = CACurrentMediaTime() + 0.033\ncommandBuffer.present(drawable, atTime: presentTime)\n```\n\n#### presentedTime — Verify Actual Presentation\n\nCheck when frames actually appeared:\n\n```swift\ndrawable.addPresentedHandler { drawable in\n    let actualTime = drawable.presentedTime\n    if actualTime == 0.0 {\n        \u002F\u002F Frame was dropped!\n        print(\"⚠️ Frame dropped\")\n    } else {\n        print(\"Frame presented at: \\(actualTime)\")\n    }\n}\n```\n\n### Frame Pacing Pattern\n\n```swift\nclass SmoothRenderer: NSObject, MTKViewDelegate {\n    private var targetFrameDuration: CFTimeInterval = 1.0 \u002F 60.0  \u002F\u002F 60 FPS target\n\n    func draw(in view: MTKView) {\n        guard let commandBuffer = commandQueue.makeCommandBuffer(),\n              let drawable = view.currentDrawable else { return }\n\n        renderScene(to: drawable)\n\n        \u002F\u002F Use frame pacing to ensure consistent intervals\n        commandBuffer.present(drawable, afterMinimumDuration: targetFrameDuration)\n        commandBuffer.commit()\n    }\n\n    func adjustTargetFrameRate(canSustain fps: Int) {\n        switch fps {\n        case 90...:\n            targetFrameDuration = 1.0 \u002F 120.0\n        case 50...:\n            targetFrameDuration = 1.0 \u002F 60.0\n        default:\n            targetFrameDuration = 1.0 \u002F 30.0\n        }\n    }\n}\n```\n\n---\n\n## Part 8: Understanding Hitches\n\n### Render Loop Phases\n\nFrame lifecycle: **Begin Time → Commit Deadline → Presentation Time**\n\n1. **App Process (CPU)**: Handle events, compute UI updates, Core Animation commit\n2. **Render Server (CPU+GPU)**: Transform UI to bitmap, render to buffer\n3. **Display Driver**: Swap buffer to screen at vsync\n\nAt 120Hz, each phase has ~8.33ms. Miss any deadline = hitch.\n\n### Commit Hitch vs Render Hitch\n\n**Commit Hitch**: App process misses commit deadline\n- Cause: Main thread work takes too long\n- Fix: Move work off main thread, reduce view complexity\n\n**Render Hitch**: Render server misses presentation deadline\n- Cause: GPU work too complex (blur, shadows, layers)\n- Fix: Simplify visual effects, reduce overdraw\n\n### Double vs Triple Buffering\n\n**Double Buffer (default)**:\n- Frame lifetime: 2 vsync intervals\n- Tighter deadlines\n- Lower latency\n\n**Triple Buffer (system may enable)**:\n- Frame lifetime: 3 vsync intervals\n- Render server gets 2 vsync intervals\n- Higher latency but more headroom\n\nThe system automatically switches to triple buffering to recover from render hitches.\n\n### Hitch Duration\n\n```\nExpected Frame Lifetime = Begin Time → Presentation Time\nActual Frame Lifetime = Begin Time → Actual Vsync\n\nHitch Duration = Actual - Expected\n```\n\nIf hitch duration > 0, the frame was late and previous frame stayed onscreen longer.\n\n---\n\n## Part 9: Measurement\n\n### UIScreen Lies, Actual Presentation Tells Truth\n\n```swift\n\u002F\u002F ❌ This says 120 even when system caps you to 60\nlet maxFPS = UIScreen.main.maximumFramesPerSecond\n\u002F\u002F Reports capability, not actual rate!\n\n\u002F\u002F ✅ Measure from CADisplayLink timing\n@objc func displayLinkCallback(_ link: CADisplayLink) {\n    \u002F\u002F Time available to prepare next frame\n    let workingTime = link.targetTimestamp - CACurrentMediaTime()\n\n    \u002F\u002F Actual interval since last callback\n    if lastTimestamp > 0 {\n        let interval = link.timestamp - lastTimestamp\n        let actualFPS = 1.0 \u002F interval\n    }\n    lastTimestamp = link.timestamp\n}\n```\n\n### Metal Performance HUD\n\nEnable on-device real-time performance overlay:\n\n**Via Xcode scheme:**\n1. Edit Scheme → Run → Diagnostics\n2. Enable \"Show Graphics Overview\"\n3. Optionally enable \"Log Graphics Overview\"\n\n**Via environment variable:**\n```bash\nMTL_HUD_ENABLED=1\n```\n\n**Via device settings:**\nSettings → Developer → Graphics HUD → Show Graphics HUD\n\n**HUD shows:**\n- FPS (average)\n- GPU time per frame\n- Frame interval chart (last 120 frames)\n- Memory usage\n\n### Production Telemetry with MetricKit\n\nMonitor hitches in production:\n\n```swift\nimport MetricKit\n\nclass MetricsManager: NSObject, MXMetricManagerSubscriber {\n    func didReceive(_ payloads: [MXMetricPayload]) {\n        for payload in payloads {\n            if let animationMetrics = payload.animationMetrics {\n                \u002F\u002F Ratio of time spent hitching during scroll\n                let scrollHitchRatio = animationMetrics.scrollHitchTimeRatio\n\n                \u002F\u002F Ratio of time spent hitching in all animations\n                if #available(iOS 17.0, *) {\n                    let hitchRatio = animationMetrics.hitchTimeRatio\n                }\n\n                analyzeHitchMetrics(scrollHitchRatio: scrollHitchRatio)\n            }\n        }\n    }\n}\n\n\u002F\u002F Register for metrics\nMXMetricManager.shared.add(metricsManager)\n```\n\n**What to track:**\n- `scrollHitchTimeRatio`: Time spent hitching while scrolling (UIScrollView only)\n- `hitchTimeRatio` (iOS 17+): Time spent hitching in all tracked animations\n\n---\n\n## Part 10: Quick Diagnostic Checklist\n\nWhen debugging frame rate issues:\n\n| Step | Check | Fix |\n|------|-------|-----|\n| 1 | Info.plist key present? (iPhone) | Add `CADisableMinimumFrameDurationOnPhone` |\n| 2 | Limit Frame Rate off? | Settings → Accessibility → Motion |\n| 3 | Low Power Mode off? | Settings → Battery |\n| 4 | Adaptive Power off? (iPhone 17+) | Settings → Battery → Power Mode |\n| 5 | preferredFramesPerSecond = 120? | Set explicitly on MTKView |\n| 6 | preferredFrameRateRange set? | Configure on CADisplayLink |\n| 7 | GPU frame time \u003C 8.33ms? | Profile with Metal HUD or Instruments |\n| 8 | Frame pacing consistent? | Use present(afterMinimumDuration:) |\n| 9 | Hitches in production? | Monitor with MetricKit |\n\n---\n\n## Part 11: Common Patterns\n\n### Pattern: Adaptive Frame Rate with Thermal Awareness\n\n```swift\nclass AdaptiveRenderer: NSObject, MTKViewDelegate {\n    private var recentFrameTimes: [Double] = []\n    private let sampleCount = 30\n    private var targetFrameDuration: CFTimeInterval = 1.0 \u002F 60.0\n\n    func draw(in view: MTKView) {\n        guard let commandBuffer = commandQueue.makeCommandBuffer(),\n              let drawable = view.currentDrawable else { return }\n\n        let startTime = CACurrentMediaTime()\n        renderScene(to: drawable)\n        let frameTime = (CACurrentMediaTime() - startTime) * 1000\n\n        updateTargetRate(frameTime: frameTime, view: view)\n\n        commandBuffer.present(drawable, afterMinimumDuration: targetFrameDuration)\n        commandBuffer.commit()\n    }\n\n    private func updateTargetRate(frameTime: Double, view: MTKView) {\n        recentFrameTimes.append(frameTime)\n        if recentFrameTimes.count > sampleCount {\n            recentFrameTimes.removeFirst()\n        }\n\n        let avgFrameTime = recentFrameTimes.reduce(0, +) \u002F Double(recentFrameTimes.count)\n        let thermal = ProcessInfo.processInfo.thermalState\n        let lowPower = ProcessInfo.processInfo.isLowPowerModeEnabled\n\n        \u002F\u002F Constrain based on what we can sustain AND system state\n        if lowPower || thermal >= .serious {\n            view.preferredFramesPerSecond = 30\n            targetFrameDuration = 1.0 \u002F 30.0\n        } else if avgFrameTime \u003C 7.0 && thermal == .nominal {\n            view.preferredFramesPerSecond = 120\n            targetFrameDuration = 1.0 \u002F 120.0\n        } else if avgFrameTime \u003C 14.0 {\n            view.preferredFramesPerSecond = 60\n            targetFrameDuration = 1.0 \u002F 60.0\n        } else {\n            view.preferredFramesPerSecond = 30\n            targetFrameDuration = 1.0 \u002F 30.0\n        }\n    }\n}\n```\n\n### Pattern: Frame Drop Detection\n\n```swift\nclass FrameDropMonitor {\n    private var expectedPresentTime: CFTimeInterval = 0\n    private var dropCount = 0\n\n    func trackFrame(drawable: MTLDrawable, expectedInterval: CFTimeInterval) {\n        drawable.addPresentedHandler { [weak self] drawable in\n            guard let self = self else { return }\n\n            if drawable.presentedTime == 0.0 {\n                self.dropCount += 1\n                print(\"⚠️ Frame dropped (total: \\(self.dropCount))\")\n            } else if self.expectedPresentTime > 0 {\n                let actualInterval = drawable.presentedTime - self.expectedPresentTime\n                let variance = abs(actualInterval - expectedInterval)\n\n                if variance > expectedInterval * 0.5 {\n                    print(\"⚠️ Frame timing variance: \\(variance * 1000)ms\")\n                }\n            }\n\n            self.expectedPresentTime = drawable.presentedTime\n        }\n    }\n}\n```\n\n---\n\n## Resources\n\n**WWDC**: 2021-10147, 2018-612, 2022-10083, 2023-10123\n\n**Tech Talks**: 10855, 10856, 10857 (Hitch deep dives)\n\n**Docs**: \u002Fquartzcore\u002Fcadisplaylink, \u002Fquartzcore\u002Fcametaldisplaylink, \u002Fquartzcore\u002Foptimizing-iphone-and-ipad-apps-to-support-promotion-displays, \u002Fxcode\u002Funderstanding-hitches-in-your-app, \u002Fmetal\u002Fmtldrawable\u002Fpresent(afterminimumduration:), \u002Fmetrickit\u002Fmxanimationmetric\n\n**Skills**: axiom-energy, axiom-ios-graphics, axiom-metal-migration-ref, axiom-performance-profiling\n\n",{"name":5,"license":15,"metadata":30,"description":7},{"version":31},"1.0.0","main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-display-performance",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":42,"prompt":52,"zipAvailable":53},[43,46,49],{"tool":44,"command":45},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill axiom-display-performance",{"tool":47,"command":48},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill axiom-display-performance",{"tool":50,"command":51},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill axiom-display-performance","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-display-performance\u002FSKILL.md",true,[],[56,70,80,89,95,102],{"skillNo":57,"name":58,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":59,"categories":60,"keywords":66,"publishedAt":25},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[61],{"categoryNo":62,"categoryName":63,"taxonomyType":64,"taxonomyTypeDesc":65},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[67,68,69],"model","model hierarchy","model hierarchy skill",{"skillNo":71,"name":72,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":76,"publishedAt":25},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[75],{"categoryNo":62,"categoryName":63,"taxonomyType":64,"taxonomyTypeDesc":65},[77,78,79],"MCP","zoom","zoom mcp",{"skillNo":81,"name":82,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":85,"publishedAt":25},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[86,87,88,78],"sdk","zoom apps sdk","apps",{"skillNo":90,"name":91,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":94,"publishedAt":25},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[91],{"skillNo":96,"name":97,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":98,"categories":99,"keywords":101,"publishedAt":25},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[100],{"categoryNo":62,"categoryName":63,"taxonomyType":64,"taxonomyTypeDesc":65},[97],{"skillNo":103,"name":104,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":105,"categories":106,"keywords":107,"publishedAt":25},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[104]]