- Add AppDiscovery provider for running app enumeration - Implement AppDropdownView with auto-launch functionality - Create SignalAction models for 40+ yabai commands - Build ActionBuilderView with nested parameter controls - Add LiveShellPreview for real-time shell command generation - Implement ActionValidator for conflict detection - Add migration parser for existing raw action strings - Include feature flag for safe rollout - Maintain full backward compatibility
314 lines
11 KiB
Swift
314 lines
11 KiB
Swift
//
|
|
// AnimationPerformanceManager.swift
|
|
// YabaiPro
|
|
//
|
|
// Created by Jake Shore
|
|
// Copyright © 2024 Jake Shore. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import IOKit.ps
|
|
import Metal
|
|
|
|
class AnimationPerformanceManager {
|
|
static let shared = AnimationPerformanceManager()
|
|
|
|
private var performanceMode: PerformanceMode = .auto
|
|
private var metalCapability: MetalCapability = .unknown
|
|
private var lastAssessmentTime: Date = Date.distantPast
|
|
private var assessmentCacheTime: TimeInterval = 30.0 // Cache for 30 seconds
|
|
|
|
enum MetalCapability {
|
|
case ultraHighPerformance, highPerformance, mediumPerformance, lowPerformance, unsupported, unknown
|
|
}
|
|
|
|
enum PerformanceMode {
|
|
case auto, ultra, high, medium, low, minimal
|
|
}
|
|
|
|
private init() {
|
|
assessMetalCapability()
|
|
}
|
|
|
|
func optimalSettings(metalAnimationsEnabled: Bool = false, directWindowMetalEnabled: Bool = false) -> AnimationSettings {
|
|
// Re-assess if cache is stale
|
|
if Date().timeIntervalSince(lastAssessmentTime) > assessmentCacheTime {
|
|
assessMetalCapability()
|
|
lastAssessmentTime = Date()
|
|
}
|
|
|
|
let batteryLevel = getBatteryLevel()
|
|
let thermalState = ProcessInfo.processInfo.thermalState
|
|
let cpuUsage = getCPUUsage()
|
|
|
|
// Auto mode logic
|
|
if performanceMode == .auto {
|
|
var settings = AnimationSettings.default
|
|
|
|
// Check UI settings first - if Metal is disabled in UI, don't enable it
|
|
let canUseMetal = metalAnimationsEnabled
|
|
|
|
// Battery considerations
|
|
if batteryLevel < 20 {
|
|
settings.particleCount = min(settings.particleCount, 5)
|
|
settings.metalEffectsEnabled = false
|
|
settings.frameRateLimit = 30
|
|
}
|
|
|
|
// Thermal considerations
|
|
if thermalState == .critical {
|
|
settings.particleCount = 0
|
|
settings.metalEffectsEnabled = false
|
|
settings.frameRateLimit = 15
|
|
} else if thermalState == .serious {
|
|
settings.particleCount = min(settings.particleCount, 8)
|
|
settings.frameRateLimit = 30
|
|
}
|
|
|
|
// CPU usage considerations
|
|
if cpuUsage > 80 {
|
|
settings.particleCount = min(settings.particleCount, 10)
|
|
settings.frameRateLimit = 30
|
|
}
|
|
|
|
// Metal capability considerations - only enable if UI allows it
|
|
if canUseMetal {
|
|
switch metalCapability {
|
|
case .ultraHighPerformance, .highPerformance:
|
|
// High-performance Metal GPU - enable enhanced features
|
|
settings.metalEffectsEnabled = true
|
|
settings.particleCount = 20
|
|
settings.frameRateLimit = 120
|
|
case .mediumPerformance:
|
|
// Medium performance - still enable Metal but reduce particles
|
|
settings.metalEffectsEnabled = true
|
|
settings.particleCount = min(settings.particleCount, 12)
|
|
settings.frameRateLimit = 60
|
|
case .lowPerformance:
|
|
// Low performance - enable Metal with minimal effects
|
|
settings.metalEffectsEnabled = true
|
|
settings.particleCount = min(settings.particleCount, 5)
|
|
settings.frameRateLimit = 30
|
|
case .unsupported:
|
|
// No Metal support - fall back to Canvas
|
|
settings.metalEffectsEnabled = false
|
|
settings.fallbackToCanvas = true
|
|
case .unknown:
|
|
// Unknown performance - enable Metal optimistically but only if UI allows
|
|
settings.metalEffectsEnabled = false // Conservative approach - don't enable unknown Metal
|
|
settings.particleCount = min(settings.particleCount, 8)
|
|
}
|
|
} else {
|
|
// Metal animations disabled in UI
|
|
settings.metalEffectsEnabled = false
|
|
settings.fallbackToCanvas = true
|
|
}
|
|
|
|
return settings
|
|
|
|
} else {
|
|
// Manual mode - return preset for selected performance mode
|
|
return settingsForMode(performanceMode)
|
|
}
|
|
}
|
|
|
|
func setPerformanceMode(_ mode: PerformanceMode) {
|
|
performanceMode = mode
|
|
}
|
|
|
|
private func settingsForMode(_ mode: PerformanceMode) -> AnimationSettings {
|
|
switch mode {
|
|
case .auto:
|
|
// Auto mode returns the optimal settings based on current conditions
|
|
return optimalSettings()
|
|
|
|
case .ultra:
|
|
return AnimationSettings(
|
|
metalEffectsEnabled: true,
|
|
particleCount: 25,
|
|
rippleCount: 5,
|
|
morphingEnabled: true,
|
|
gradientsEnabled: true,
|
|
frameRateLimit: 60,
|
|
qualityScale: 1.0
|
|
)
|
|
|
|
case .high:
|
|
return AnimationSettings(
|
|
metalEffectsEnabled: true,
|
|
particleCount: 15,
|
|
rippleCount: 3,
|
|
morphingEnabled: true,
|
|
gradientsEnabled: true,
|
|
frameRateLimit: 60,
|
|
qualityScale: 0.9
|
|
)
|
|
|
|
case .medium:
|
|
return AnimationSettings(
|
|
metalEffectsEnabled: true,
|
|
particleCount: 8,
|
|
rippleCount: 2,
|
|
morphingEnabled: false,
|
|
gradientsEnabled: true,
|
|
frameRateLimit: 45,
|
|
qualityScale: 0.7
|
|
)
|
|
|
|
case .low:
|
|
return AnimationSettings(
|
|
metalEffectsEnabled: false,
|
|
particleCount: 3,
|
|
rippleCount: 1,
|
|
morphingEnabled: false,
|
|
gradientsEnabled: false,
|
|
frameRateLimit: 30,
|
|
qualityScale: 0.5
|
|
)
|
|
|
|
case .minimal:
|
|
return AnimationSettings(
|
|
metalEffectsEnabled: false,
|
|
particleCount: 0,
|
|
rippleCount: 0,
|
|
morphingEnabled: false,
|
|
gradientsEnabled: false,
|
|
frameRateLimit: 15,
|
|
qualityScale: 0.3
|
|
)
|
|
}
|
|
}
|
|
|
|
private func assessMetalCapability() {
|
|
guard let device = MTLCreateSystemDefaultDevice() else {
|
|
metalCapability = .unsupported
|
|
return
|
|
}
|
|
|
|
// Check GPU family support
|
|
let hasFeatureSet1 = device.supportsFamily(.apple1)
|
|
let hasFeatureSet2 = device.supportsFamily(.apple2)
|
|
|
|
// Enhanced Metal 2.0 capabilities (no Metal 3 on macOS 12)
|
|
|
|
// Check memory and performance characteristics
|
|
let recommendedMaxWorkingSet = device.recommendedMaxWorkingSetSize
|
|
let hasUnifiedMemory = device.hasUnifiedMemory
|
|
|
|
// Determine capability level
|
|
if hasFeatureSet2 && recommendedMaxWorkingSet >= 1_073_741_824 { // 1GB+
|
|
metalCapability = .highPerformance
|
|
} else if hasFeatureSet1 && (hasUnifiedMemory || recommendedMaxWorkingSet >= 536_870_912) { // 512MB+
|
|
metalCapability = .mediumPerformance
|
|
} else if hasFeatureSet1 {
|
|
metalCapability = .lowPerformance
|
|
} else {
|
|
metalCapability = .unsupported
|
|
}
|
|
}
|
|
|
|
func getBatteryLevel() -> Int {
|
|
// Use IOKit to get battery information
|
|
let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"))
|
|
guard service != 0 else { return 100 } // Assume plugged in if no battery
|
|
|
|
defer { IOObjectRelease(service) }
|
|
|
|
if let currentCapacity = IORegistryEntryCreateCFProperty(service, "CurrentCapacity" as CFString, kCFAllocatorDefault, 0),
|
|
let maxCapacity = IORegistryEntryCreateCFProperty(service, "MaxCapacity" as CFString, kCFAllocatorDefault, 0) {
|
|
|
|
// Properly extract integer values from CFTypeRef
|
|
let current = currentCapacity.takeRetainedValue() as? Int ?? 0
|
|
let max = maxCapacity.takeRetainedValue() as? Int ?? 100
|
|
|
|
if max > 0 {
|
|
return (current * 100) / max
|
|
}
|
|
}
|
|
|
|
return 100 // Default to full battery
|
|
}
|
|
|
|
private func getCPUUsage() -> Double {
|
|
// Simple CPU usage estimation
|
|
// In a real implementation, you'd use host_statistics or similar
|
|
// For now, return a conservative estimate
|
|
return 30.0
|
|
}
|
|
|
|
// MARK: - Performance Monitoring
|
|
|
|
func startPerformanceMonitoring() {
|
|
// Set up periodic performance checks
|
|
Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
|
|
self?.checkPerformanceThresholds()
|
|
}
|
|
}
|
|
|
|
private func checkPerformanceThresholds() {
|
|
let batteryLevel = getBatteryLevel()
|
|
let thermalState = ProcessInfo.processInfo.thermalState
|
|
|
|
// Auto-adjust performance if needed
|
|
if performanceMode == .auto {
|
|
if batteryLevel < 10 || thermalState == .critical {
|
|
// Force minimal mode
|
|
NotificationCenter.default.post(
|
|
name: .animationPerformanceChanged,
|
|
object: nil,
|
|
userInfo: ["mode": PerformanceMode.minimal]
|
|
)
|
|
} else if thermalState == .serious && batteryLevel < 20 {
|
|
// Force low mode
|
|
NotificationCenter.default.post(
|
|
name: .animationPerformanceChanged,
|
|
object: nil,
|
|
userInfo: ["mode": PerformanceMode.low]
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
func getCurrentCapability() -> MetalCapability {
|
|
if metalCapability == .unknown {
|
|
assessMetalCapability()
|
|
}
|
|
return metalCapability
|
|
}
|
|
|
|
func isMetalRecommended() -> Bool {
|
|
return metalCapability == .highPerformance || metalCapability == .mediumPerformance
|
|
}
|
|
|
|
func getPerformanceRecommendations() -> [String] {
|
|
var recommendations: [String] = []
|
|
|
|
if metalCapability == .unsupported {
|
|
recommendations.append("Metal not supported - using Canvas fallback")
|
|
}
|
|
|
|
let batteryLevel = getBatteryLevel()
|
|
if batteryLevel < 20 {
|
|
recommendations.append("Low battery - consider reducing animation quality")
|
|
}
|
|
|
|
let thermalState = ProcessInfo.processInfo.thermalState
|
|
if thermalState == .serious || thermalState == .critical {
|
|
recommendations.append("High temperature - animations automatically reduced")
|
|
}
|
|
|
|
return recommendations
|
|
}
|
|
}
|
|
|
|
// MARK: - Animation Settings Structure
|
|
// AnimationSettings is defined in AnimationTypes.swift
|
|
|
|
// MARK: - Notifications
|
|
|
|
extension Notification.Name {
|
|
static let animationPerformanceChanged = Notification.Name("animationPerformanceChanged")
|
|
}
|