axiom-display-performance

0
0
Source

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.

Install

mkdir -p .claude/skills/axiom-display-performance && curl -L -o skill.zip "https://mcp.directory/api/skills/download/4989" && unzip -o skill.zip -d .claude/skills/axiom-display-performance && rm skill.zip

Installs to .claude/skills/axiom-display-performance

About this skill

Display Performance

Systematic 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.

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.


Part 1: Why You're Stuck at 60fps

Diagnostic Order

Check these in order when stuck at 60fps on ProMotion:

  1. Info.plist key missing? (iPhone only) → Part 2
  2. Render loop configured for 60? (MTKView defaults, CADisplayLink) → Part 3
  3. System caps enabled? (Low Power Mode, Limit Frame Rate, Thermal) → Part 5
  4. Frame time > 8.33ms? (Can't sustain 120fps) → Part 6
  5. Frame pacing issues? (Micro-stuttering despite good FPS) → Part 7
  6. Measuring wrong thing? (UIScreen vs actual presentation) → Part 9

Part 2: Enabling ProMotion on iPhone

Critical: Core Animation won't access frame rates above 60Hz on iPhone unless you add this key.

<!-- Info.plist -->
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>

Without this key:

  • Your preferredFrameRateRange hints are ignored above 60Hz
  • Other animations may affect your CADisplayLink callback rate
  • iPad Pro does NOT require this key

When to add: Any iPhone app that needs >60Hz for games, animations, or smooth scrolling.


Part 3: Render Loop Configuration

MTKView Defaults to 60fps

This is the most common cause. MTKView's preferredFramesPerSecond defaults to 60.

// ❌ WRONG: Implicit 60fps (default)
let mtkView = MTKView(frame: frame, device: device)
mtkView.delegate = self
// Running at 60fps even on ProMotion!

// ✅ CORRECT: Explicit 120fps request
let mtkView = MTKView(frame: frame, device: device)
mtkView.preferredFramesPerSecond = 120
mtkView.isPaused = false
mtkView.enableSetNeedsDisplay = false  // Continuous, not on-demand
mtkView.delegate = self

Critical settings for continuous high-rate rendering:

PropertyValueWhy
preferredFramesPerSecond120Request max rate
isPausedfalseDon't pause the render loop
enableSetNeedsDisplayfalseContinuous mode, not on-demand

CADisplayLink Configuration (iOS 15+)

Apple explicitly recommends CADisplayLink (not timers) for custom render loops.

// ❌ WRONG: Timer-based render loop (drifts, wastes frame time)
Timer.scheduledTimer(withTimeInterval: 1.0/120.0, repeats: true) { _ in
    self.render()
}

// ❌ WRONG: Default CADisplayLink (may hint 60)
let displayLink = CADisplayLink(target: self, selector: #selector(render))
displayLink.add(to: .main, forMode: .common)

// ✅ CORRECT: Explicit frame rate range
let displayLink = CADisplayLink(target: self, selector: #selector(render))
displayLink.preferredFrameRateRange = CAFrameRateRange(
    minimum: 80,      // Minimum acceptable
    maximum: 120,     // Preferred maximum
    preferred: 120    // What you want
)
displayLink.add(to: .main, forMode: .common)

Special priority for games: iOS 15+ gives 30Hz and 60Hz special priority. If targeting these rates:

// 30Hz and 60Hz get priority scheduling
let prioritizedRange = CAFrameRateRange(
    minimum: 30,
    maximum: 60,
    preferred: 60
)
displayLink.preferredFrameRateRange = prioritizedRange

Suggested Frame Rates by Content Type

Content TypeSuggested RateNotes
Video playback24-30 HzMatch content frame rate
Scrolling UI60-120 HzHigher = smoother
Fast games60-120 HzMatch rendering capability
Slow animations30-60 HzSave power
Static content10-24 HzMinimal updates needed

Part 4: CAMetalDisplayLink (iOS 17+)

For Metal apps needing precise timing control, CAMetalDisplayLink provides more control than CADisplayLink.

class MetalRenderer: NSObject, CAMetalDisplayLinkDelegate {
    var displayLink: CAMetalDisplayLink?
    var metalLayer: CAMetalLayer!

    func setupDisplayLink() {
        displayLink = CAMetalDisplayLink(metalLayer: metalLayer)
        displayLink?.delegate = self
        displayLink?.preferredFrameRateRange = CAFrameRateRange(
            minimum: 60,
            maximum: 120,
            preferred: 120
        )
        // Control render latency (in frames)
        displayLink?.preferredFrameLatency = 2
        displayLink?.add(to: .main, forMode: .common)
    }

    func metalDisplayLink(_ link: CAMetalDisplayLink, needsUpdate update: CAMetalDisplayLink.Update) {
        // update.drawable - The drawable to render to
        // update.targetTimestamp - Deadline to finish rendering
        // update.targetPresentationTimestamp - When frame will display

        guard let drawable = update.drawable else { return }

        let workingTime = update.targetTimestamp - CACurrentMediaTime()
        // workingTime = seconds available before deadline

        // Render to drawable...
        renderFrame(to: drawable)
    }
}

Key differences from CADisplayLink:

FeatureCADisplayLinkCAMetalDisplayLink
Drawable accessManual via layerProvided in callback
Latency controlNonepreferredFrameLatency
Target timingtimestamp/targetTimestamp+ targetPresentationTimestamp
Use caseGeneral animationMetal-specific rendering

When to use CAMetalDisplayLink:

  • Need precise control over render timing window
  • Want to minimize input latency
  • Building games or intensive Metal apps
  • iOS 17+ only deployment

Part 5: System Caps

System states can force 60fps even when your code requests 120:

Low Power Mode

Caps ProMotion devices to 60fps.

// Check programmatically
if ProcessInfo.processInfo.isLowPowerModeEnabled {
    // System caps display to 60Hz
}

// Observe changes
NotificationCenter.default.addObserver(
    forName: .NSProcessInfoPowerStateDidChange,
    object: nil,
    queue: .main
) { _ in
    let isLowPower = ProcessInfo.processInfo.isLowPowerModeEnabled
    self.adjustRenderingForPowerState(isLowPower)
}

Limit Frame Rate (Accessibility)

Settings → Accessibility → Motion → Limit Frame Rate caps to 60fps.

No API to detect. If user reports 60fps despite configuration, have them check this setting.

Thermal Throttling

System restricts 120Hz when device overheats.

// Check thermal state
switch ProcessInfo.processInfo.thermalState {
case .nominal, .fair:
    preferredFramesPerSecond = 120
case .serious, .critical:
    preferredFramesPerSecond = 60  // Reduce proactively
@unknown default:
    break
}

// Observe thermal changes
NotificationCenter.default.addObserver(
    forName: ProcessInfo.thermalStateDidChangeNotification,
    object: nil,
    queue: .main
) { _ in
    self.adjustForThermalState()
}

Adaptive Power (iOS 26+, iPhone 17)

New in iOS 26: Adaptive Power is ON by default on iPhone 17/17 Pro. Can throttle even at 60% battery.

User action for testing: Settings → Battery → Power Mode → disable Adaptive Power.

No public API to detect Adaptive Power state.


Part 6: Performance Budget

Frame Time Budgets

Target FPSFrame BudgetVsync Interval
1208.33msEvery vsync
9011.11ms
6016.67msEvery 2nd vsync
3033.33msEvery 4th vsync

If you consistently exceed budget, system drops to next sustainable rate.

Measuring GPU Frame Time

func draw(in view: MTKView) {
    guard let commandBuffer = commandQueue.makeCommandBuffer() else { return }

    // Your rendering code...

    commandBuffer.addCompletedHandler { buffer in
        let gpuTime = buffer.gpuEndTime - buffer.gpuStartTime
        let gpuMs = gpuTime * 1000

        if gpuMs > 8.33 {
            print("⚠️ GPU: \(String(format: "%.2f", gpuMs))ms exceeds 120Hz budget")
        }
    }

    commandBuffer.commit()
}

Can't Sustain 120? Target Lower Rate Evenly

Critical: Uneven frame pacing looks worse than consistent lower rate.

// If you can't sustain 8.33ms, explicitly target 60 for smooth cadence
if averageGpuTime > 8.33 && averageGpuTime <= 16.67 {
    mtkView.preferredFramesPerSecond = 60
}

Part 7: Frame Pacing

The Micro-Stuttering Problem

Even with good average FPS, inconsistent frame timing causes visible jitter.

// BAD: Inconsistent intervals despite ~40 FPS average
Frame 1: 25ms
Frame 2: 40ms  ← stutter
Frame 3: 25ms
Frame 4: 40ms  ← stutter

// GOOD: Consistent intervals at 30 FPS
Frame 1: 33ms
Frame 2: 33ms
Frame 3: 33ms
Frame 4: 33ms

Presenting immediately after rendering causes this. Use explicit timing control.

Frame Pacing APIs

present(afterMinimumDuration:) — Recommended

Ensures consistent spacing between frames:

func draw(in view: MTKView) {
    guard let commandBuffer = commandQueue.makeCommandBuffer(),
          let drawable = view.currentDrawable else { return }

    // Render to drawable...

    // Present with minimum 33ms between frames (30 FPS target)
    commandBuffer.present(drawable, afterMinimumDuration: 0.033)
    commandBuffer.commit()
}

present(at:) — Precise Timing

Schedule presentation at specific time:

// Present at specific Mach absolute time
let presentTime = CACurrentMediaTime() + 0.033
commandBuffer.present(drawable, atTime: presentTime)

presentedTime — Verify Actual Presentation

Check when frames actually appeared:

drawable.addPresentedHandler { drawable in
    let actualTime = drawable.presentedTime
    if actualTime == 0.0 {
        // Frame was dropped!
        print("⚠️ Frame dropped")
    } else {
        print("Frame presented at: \(actualTime)")
    }
}

Frame


Content truncated.

axiom-ios-build

CharlesWiltgen

Use when ANY iOS build fails, test crashes, Xcode misbehaves, or environment issue occurs before debugging code. Covers build failures, compilation errors, dependency conflicts, simulator problems, environment-first diagnostics.

91

axiom-getting-started

CharlesWiltgen

Use when first installing Axiom, unsure which skill to use, want an overview of available skills, or need help finding the right skill for your situation — interactive onboarding that recommends skills based on your project and current focus

00

axiom-ui-testing

CharlesWiltgen

Use when writing UI tests, recording interactions, tests have race conditions, timing dependencies, inconsistent pass/fail behavior, or XCTest UI tests are flaky - covers Recording UI Automation (WWDC 2025), condition-based waiting, network conditioning, multi-factor testing, crash debugging, and accessibility-first testing patterns

00

axiom-core-spotlight-ref

CharlesWiltgen

Use when indexing app content for Spotlight search, using NSUserActivity for prediction/handoff, or choosing between CSSearchableItem and IndexedEntity - covers Core Spotlight framework and NSUserActivity integration for iOS 9+

00

axiom-vision-diag

CharlesWiltgen

subject not detected, hand pose missing landmarks, low confidence observations, Vision performance, coordinate conversion, VisionKit errors, observation nil, text not recognized, barcode not detected, DataScannerViewController not working, document scan issues

00

axiom-now-playing-carplay

CharlesWiltgen

CarPlay Now Playing integration patterns. Use when implementing CarPlay audio controls, CPNowPlayingTemplate customization, or debugging CarPlay-specific issues.

00

You might also like

flutter-development

aj-geddes

Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.

643969

drawio-diagrams-enhanced

jgtolentino

Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.

591705

ui-ux-pro-max

nextlevelbuilder

"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."

318398

godot

bfollington

This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.

339397

nano-banana-pro

garg-aayush

Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.

451339

fastapi-templates

wshobson

Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.

304231

Stay ahead of the MCP ecosystem

Get weekly updates on new skills and servers.