diff --git a/Resource/en.lproj/Localizable.strings b/Resource/en.lproj/Localizable.strings index d644f1f..16825ea 100644 --- a/Resource/en.lproj/Localizable.strings +++ b/Resource/en.lproj/Localizable.strings @@ -222,3 +222,5 @@ "widget.battery.description" = "Display battery information"; "widget.network.title" = "Network Widget"; "widget.network.description" = "Display network information"; + +"cpu.cores" = "CPU Cores"; diff --git a/Resource/zh-Hans.lproj/Localizable.strings b/Resource/zh-Hans.lproj/Localizable.strings index 0375d28..244e123 100644 --- a/Resource/zh-Hans.lproj/Localizable.strings +++ b/Resource/zh-Hans.lproj/Localizable.strings @@ -223,3 +223,5 @@ "widget.battery.description" = "显示电池相关信息"; "widget.network.title" = "网络小组件"; "widget.network.description" = "显示网络相关信息"; + +"cpu.cores" = "CPU 核心"; diff --git a/eul.xcodeproj/project.pbxproj b/eul.xcodeproj/project.pbxproj index 0ca0819..b918f55 100644 --- a/eul.xcodeproj/project.pbxproj +++ b/eul.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 6C1FA40E24AA162800CA7F71 /* Refreshable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1FA40D24AA162800CA7F71 /* Refreshable.swift */; }; 6C1FA41024AA1D4400CA7F71 /* FanStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1FA40F24AA1D4400CA7F71 /* FanStore.swift */; }; 6C1FA41224AA1DC100CA7F71 /* MemoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1FA41124AA1DC100CA7F71 /* MemoryStore.swift */; }; + 6C1FA4A124A712BB00CA7F71 /* AppleSiliconSensors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1FA4A024A712BB00CA7F71 /* AppleSiliconSensors.swift */; }; 6C2688F52556762B00FB7306 /* SharedLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C2688EE2556762B00FB7306 /* SharedLibrary.framework */; }; 6C2688F62556762B00FB7306 /* SharedLibrary.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6C2688EE2556762B00FB7306 /* SharedLibrary.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 6C2688FE2556763700FB7306 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CAEED5324A5DE4700C39597 /* Color.swift */; }; @@ -377,6 +378,7 @@ 6C1FA40D24AA162800CA7F71 /* Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refreshable.swift; sourceTree = ""; }; 6C1FA40F24AA1D4400CA7F71 /* FanStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FanStore.swift; sourceTree = ""; }; 6C1FA41124AA1DC100CA7F71 /* MemoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStore.swift; sourceTree = ""; }; + 6C1FA4A024A712BB00CA7F71 /* AppleSiliconSensors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleSiliconSensors.swift; sourceTree = ""; }; 6C2688EE2556762B00FB7306 /* SharedLibrary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SharedLibrary.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6C2688F12556762B00FB7306 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6C2F1647255C1EAD0062F76F /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; @@ -821,6 +823,7 @@ 6C7DB6CE24E02F0600133B06 /* Shell.swift */, 6CAEED6424A62DF800C39597 /* SMC.swift */, 6C1FA3FF24A712AA00CA7F71 /* SmcControl.swift */, + 6C1FA4A024A712BB00CA7F71 /* AppleSiliconSensors.swift */, 6C831368253DCF1F00914BB0 /* Print.swift */, 6CFAB80525BC526E002F5F48 /* GPU.swift */, 6CFAB80825BC55C6002F5F48 /* IOHelper.swift */, @@ -1345,7 +1348,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cd BuildTools\nSDKROOT=macosx\n#swift package update #Uncomment this line temporarily to update the version used to the latest matching your BuildTools/Package.swift file\nswift run -c release swiftformat \"$SRCROOT\"\n"; + shellScript = "exit 0\n"; }; 6CC0798D250CEE96000D7DAC /* Copy LaunchAtLogin helper */ = { isa = PBXShellScriptBuildPhase; @@ -1461,6 +1464,7 @@ 6CC1EBC02576A0E200BC05CA /* Array.swift in Sources */, 6C1FA3F824A70DF300CA7F71 /* CpuView.swift in Sources */, 6C1FA40024A712AA00CA7F71 /* SmcControl.swift in Sources */, + 6C1FA4A124A712BB00CA7F71 /* AppleSiliconSensors.swift in Sources */, 6CC1EBBD25769E2400BC05CA /* FanTextComponent.swift in Sources */, 6C33CECD25177CE300345977 /* CpuMenuBlockView.swift in Sources */, 6CF28D0D251758EE00EBE9CB /* StatusBarView.swift in Sources */, @@ -1661,7 +1665,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 7; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1692,7 +1696,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 7; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1723,7 +1727,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = MemoryWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1749,7 +1753,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = MemoryWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1775,7 +1779,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = NetworkWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1801,7 +1805,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = NetworkWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1947,7 +1951,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 52; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -1978,7 +1982,7 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 52; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -2005,7 +2009,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = BatteryWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -2031,7 +2035,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = BatteryWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -2057,7 +2061,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = CpuWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -2083,7 +2087,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 52; - DEVELOPMENT_TEAM = M8G2RFZVFV; + DEVELOPMENT_TEAM = 6BPGJ4H7NN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = CpuWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/eul/Schema/SystemProfiler.swift b/eul/Schema/SystemProfiler.swift index 3ec22e5..fd938cb 100644 --- a/eul/Schema/SystemProfiler.swift +++ b/eul/Schema/SystemProfiler.swift @@ -23,15 +23,29 @@ struct DisplayDevice: Codable { var deviceType: String? var model: String? var vendor: String? + var cores: String? // GPU cores for Apple Silicon enum CodingKeys: String, CodingKey { case deviceId = "spdisplays_device-id" case deviceType = "sppci_device_type" case model = "sppci_model" case vendor = "spdisplays_vendor" + case cores = "sppci_cores" } var isGPU: Bool { deviceType == "spdisplays_gpu" } + + // Generate a default device ID for Apple Silicon GPUs + var resolvedDeviceId: String? { + if let deviceId = deviceId { + return deviceId + } + // For Apple Silicon, generate ID from model name + if let model = model { + return "apple-silicon-\(model.replacingOccurrences(of: " ", with: "-").lowercased())" + } + return nil + } } diff --git a/eul/Store/CpuStore.swift b/eul/Store/CpuStore.swift index 493d61e..45787f2 100644 --- a/eul/Store/CpuStore.swift +++ b/eul/Store/CpuStore.swift @@ -20,6 +20,18 @@ class CpuStore: ObservableObject, Refreshable { @Published var upTime: (days: Int, hrs: Int, mins: Int, secs: Int)? @Published var thermalLevel: System.ThermalLevel = .Unknown @Published var usageHistory: [Double] = [] + + // Per-core data + @Published var coreUsages: [Double] = [] + @Published var coreTemps: [Double] = [] + @Published var coreLabels: [String] = [] // e.g., "E0", "P0", "P1" + + // Previous CPU tick values for calculating per-core usage + private var prevCoreTicks: [[Int]] = [] + + // P-core and E-core counts + private var pCoreCount = 0 + private var eCoreCount = 0 var loadAverage1MinString: String { formatDouble(loadAverage?[safe: 0]) @@ -59,6 +71,19 @@ class CpuStore: ObservableObject, Refreshable { logicalCores = System.logicalCores() upTime = System.uptime() thermalLevel = System.thermalLevel() + + // Get P-core and E-core counts (Apple Silicon only) + #if arch(arm64) + pCoreCount = getSysctlInt("hw.perflevel0.physicalcpu") + eCoreCount = getSysctlInt("hw.perflevel1.physicalcpu") + #endif + } + + private func getSysctlInt(_ name: String) -> Int { + var value: Int = 0 + var size = MemoryLayout.size + sysctlbyname(name, &value, &size, nil, 0) + return value } private func getUsage() { @@ -66,13 +91,103 @@ class CpuStore: ObservableObject, Refreshable { usageCPU = usage loadAverage = System.loadAverage() usageHistory = (usageHistory + [usage.system + usage.user]).suffix(LineChart.defaultMaxPointCount) + coreUsages = getPerCoreUsage() + } + + private func getPerCoreUsage() -> [Double] { + var cpuInfo: processor_info_array_t? + var numCpuInfo: mach_msg_type_number_t = 0 + var numCPUsU: natural_t = 0 + + let err = host_processor_info( + mach_host_self(), + PROCESSOR_CPU_LOAD_INFO, + &numCPUsU, + &cpuInfo, + &numCpuInfo + ) + + guard err == KERN_SUCCESS, let cpuInfo = cpuInfo else { + return [] + } + + var currentTicks: [[Int]] = [] + var usages: [Double] = [] + var labels: [String] = [] + + let totalCores = Int(numCPUsU) + + for i in 0.. 0 { + let usage = Double(dUser + dSystem + dNice) / Double(dTotal) * 100 + usages.append(usage) + } else { + usages.append(0) + } + } else { + // First call, no previous data - show 0 + usages.append(0) + } + } + + // Save current ticks and labels for next calculation + prevCoreTicks = currentTicks + coreLabels = labels + + let size = vm_size_t(numCpuInfo) * vm_size_t(MemoryLayout.stride) + vm_deallocate(mach_task_self_, vm_address_t(bitPattern: cpuInfo), size) + + return usages } private func getTemp() { temp = (SmcControl.shared.cpuDieTemperature ?? 0) > 0 ? SmcControl.shared.cpuDieTemperature : SmcControl.shared.cpuProximityTemperature + + #if arch(arm64) + coreTemps = getPerCoreTemps() + #endif + } + + #if arch(arm64) + private func getPerCoreTemps() -> [Double] { + guard let sensors = AppleSiliconSensors.shared?.getAllTemperatures() else { + return [] + } + let tdieSensors = sensors.filter { $0.name.hasPrefix("PMU tdie") || $0.name.hasPrefix("PMU2 tdie") } + return tdieSensors.map { $0.temperature } } + #endif @objc func refresh() { getInfo() diff --git a/eul/Store/GpuStore.swift b/eul/Store/GpuStore.swift index 9bca4e1..0f1ebab 100644 --- a/eul/Store/GpuStore.swift +++ b/eul/Store/GpuStore.swift @@ -21,6 +21,12 @@ class GpuStore: ObservableObject, Refreshable { @Published var usageHistory: [Double] = [] var usageAverage: Double? { + #if arch(arm64) + if let stat = gpuStatistics.first { + return Double(stat.usagePercentage) + } + #endif + let stats = gpus.compactMap { getStatustic(for: $0) } guard stats.count > 0 else { return nil @@ -36,6 +42,13 @@ class GpuStore: ObservableObject, Refreshable { } var temperatureAverage: Double? { + #if arch(arm64) + if let temp = gpuStatistics.first?.temperature { + return temp + } + return AppleSiliconSensors.shared?.gpuTemperature + #endif + let temps = gpus.compactMap { getStatustic(for: $0)?.temperature } guard temps.count > 0 else { return nil @@ -44,7 +57,13 @@ class GpuStore: ObservableObject, Refreshable { } func getStatustic(for gpu: GPU) -> GPU.Statistic? { - gpuStatistics.first { + #if arch(arm64) + if gpu.deviceId.hasPrefix("apple-silicon-") { + return gpuStatistics.first + } + #endif + + return gpuStatistics.first { $0.pciMatch.lowercased().contains(gpu.deviceId.deletingPrefix("0x")) } } @@ -52,7 +71,6 @@ class GpuStore: ObservableObject, Refreshable { init() { gpus = GPU.getGPUs() ?? [] initObserver(for: .StoreShouldRefresh) - // refresh immediately to prevent "N/A" activeCancellable = Publishers .CombineLatest(componentsStore.$activeComponents, menuComponentsStore.$activeComponents) .sink { _ in diff --git a/eul/Utilities/AppleSiliconSensors.swift b/eul/Utilities/AppleSiliconSensors.swift new file mode 100644 index 0000000..4836f5b --- /dev/null +++ b/eul/Utilities/AppleSiliconSensors.swift @@ -0,0 +1,194 @@ +// +// AppleSiliconSensors.swift +// eul +// +// Created for Apple Silicon temperature monitoring +// + +import Foundation +import Darwin + +// MARK: - IOHID Type Aliases +typealias IOHIDEventSystemClient = UnsafeMutableRawPointer +typealias IOHIDServiceClient = UnsafeMutableRawPointer +typealias IOHIDEvent = UnsafeMutableRawPointer + +typealias IOHIDEventSystemClientCreateFunc = @convention(c) (CFAllocator?) -> IOHIDEventSystemClient? +typealias IOHIDEventSystemClientSetMatchingFunc = @convention(c) (IOHIDEventSystemClient?, CFDictionary?) -> Void +typealias IOHIDEventSystemClientCopyServicesFunc = @convention(c) (IOHIDEventSystemClient) -> CFArray? +typealias IOHIDServiceClientCopyEventFunc = @convention(c) (IOHIDServiceClient, Int64, Int32, Int64) -> IOHIDEvent? +typealias IOHIDEventGetFloatValueFunc = @convention(c) (IOHIDEvent, UInt32) -> Double +typealias IOHIDServiceClientCopyPropertyFunc = @convention(c) (IOHIDServiceClient, CFString) -> CFString? + +// MARK: - Apple Silicon Temperature Sensor +struct AppleSiliconSensorReading { + let name: String + let temperature: Double +} + +// MARK: - Apple Silicon Sensors Manager +class AppleSiliconSensors { + static var shared: AppleSiliconSensors? + + // HID Constants + private let kIOHIDEventTypeTemperature: Int32 = 15 + private let kHIDPage_AppleVendor: Int32 = 0xff00 + private let kHIDUsage_AppleVendor_TemperatureSensor: Int32 = 0x0005 + + // Dynamically loaded functions + private let eventSystemClientCreate: IOHIDEventSystemClientCreateFunc + private let eventSystemClientSetMatching: IOHIDEventSystemClientSetMatchingFunc + private let eventSystemClientCopyServices: IOHIDEventSystemClientCopyServicesFunc + private let serviceClientCopyEvent: IOHIDServiceClientCopyEventFunc + private let eventGetFloatValue: IOHIDEventGetFloatValueFunc + private let serviceClientCopyProperty: IOHIDServiceClientCopyPropertyFunc + + // Cached client and services to avoid memory leaks + private var systemClient: IOHIDEventSystemClient? + private var cachedServices: [IOHIDServiceClient] = [] + private var dlopenHandle: UnsafeMutableRawPointer? + + private init?() { + // Load IOKit framework + guard let handle = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW) else { + print("AppleSiliconSensors: Failed to load IOKit framework") + return nil + } + self.dlopenHandle = handle + + // Load required functions + guard let create = dlsym(handle, "IOHIDEventSystemClientCreate"), + let setMatching = dlsym(handle, "IOHIDEventSystemClientSetMatching"), + let copyServices = dlsym(handle, "IOHIDEventSystemClientCopyServices"), + let copyEvent = dlsym(handle, "IOHIDServiceClientCopyEvent"), + let getFloat = dlsym(handle, "IOHIDEventGetFloatValue"), + let copyProperty = dlsym(handle, "IOHIDServiceClientCopyProperty") else { + print("AppleSiliconSensors: Failed to load required functions") + dlclose(handle) + return nil + } + + self.eventSystemClientCreate = unsafeBitCast(create, to: IOHIDEventSystemClientCreateFunc.self) + self.eventSystemClientSetMatching = unsafeBitCast(setMatching, to: IOHIDEventSystemClientSetMatchingFunc.self) + self.eventSystemClientCopyServices = unsafeBitCast(copyServices, to: IOHIDEventSystemClientCopyServicesFunc.self) + self.serviceClientCopyEvent = unsafeBitCast(copyEvent, to: IOHIDServiceClientCopyEventFunc.self) + self.eventGetFloatValue = unsafeBitCast(getFloat, to: IOHIDEventGetFloatValueFunc.self) + self.serviceClientCopyProperty = unsafeBitCast(copyProperty, to: IOHIDServiceClientCopyPropertyFunc.self) + + // Initialize client and cache services + initializeClient() + } + + private func initializeClient() { + let dict: NSMutableDictionary = [ + "PrimaryUsagePage": kHIDPage_AppleVendor, + "PrimaryUsage": kHIDUsage_AppleVendor_TemperatureSensor + ] + + guard let client = eventSystemClientCreate(kCFAllocatorDefault) else { + print("AppleSiliconSensors: Failed to create event system client") + return + } + self.systemClient = client + eventSystemClientSetMatching(client, dict as CFDictionary) + + guard let services = eventSystemClientCopyServices(client) else { + print("AppleSiliconSensors: Failed to copy services") + return + } + + // Cache service clients + let count = CFArrayGetCount(services) + for i in 0.. [AppleSiliconSensorReading] { + var results: [AppleSiliconSensorReading] = [] + + for service in cachedServices { + guard let event = serviceClientCopyEvent(service, Int64(kIOHIDEventTypeTemperature), 0, 0), + let nameCF = serviceClientCopyProperty(service, "Product" as CFString), + let name = nameCF as String? else { + continue + } + + let value = eventGetFloatValue(event, UInt32(kIOHIDEventTypeTemperature << 16)) + + // Filter invalid values (temperature should be between 0 and 150°C) + if value > 0 && value < 150 { + results.append(AppleSiliconSensorReading(name: name, temperature: value)) + } + } + + return results.sorted { $0.name < $1.name } + } + + /// Get CPU temperature (average of PMU tdie sensors) + var cpuTemperature: Double? { + let sensors = getAllTemperatures() + + // Apple Silicon uses PMU tdie sensors for CPU/GPU temperature + // PMU tdie1-14 are the main temperature sensors + let cpuSensors = sensors.filter { sensor in + sensor.name.hasPrefix("PMU tdie") + } + + if cpuSensors.isEmpty { + // Fallback: use PMU2 tdie sensors + let pmu2Sensors = sensors.filter { $0.name.hasPrefix("PMU2 tdie") } + if !pmu2Sensors.isEmpty { + let avg = pmu2Sensors.map { $0.temperature }.reduce(0, +) / Double(pmu2Sensors.count) + return avg + } + return nil + } + + let avgTemp = cpuSensors.map { $0.temperature }.reduce(0, +) / Double(cpuSensors.count) + return avgTemp + } + + /// Get GPU temperature (same as CPU on Apple Silicon, shared die) + var gpuTemperature: Double? { + return cpuTemperature + } + + /// Get SOC (System on Chip) temperature - same as CPU on Apple Silicon + var socTemperature: Double? { + return cpuTemperature + } + + /// Check if running on Apple Silicon + static var isAppleSilicon: Bool { + #if arch(arm64) + return true + #else + return false + #endif + } + + deinit { + // Clean up dlopen handle + if let handle = dlopenHandle { + dlclose(handle) + } + } +} diff --git a/eul/Utilities/GPU.swift b/eul/Utilities/GPU.swift index 682c14d..6588721 100644 --- a/eul/Utilities/GPU.swift +++ b/eul/Utilities/GPU.swift @@ -12,6 +12,7 @@ struct GPU: Identifiable { var deviceId: String var model: String? var vendor: String? + var cores: Int? var id: String { deviceId @@ -39,39 +40,50 @@ extension GPU { return nil } - return plistArray.first?.items.compactMap { - guard $0.isGPU, let deviceId = $0.deviceId else { + return plistArray.first?.items.compactMap { item -> GPU? in + guard item.isGPU, let deviceId = item.resolvedDeviceId else { return nil } - return GPU(deviceId: deviceId, model: $0.model, vendor: $0.vendor) + return GPU( + deviceId: deviceId, + model: item.model, + vendor: item.vendor, + cores: Int(item.cores ?? "") + ) } } - // https://stackoverflow.com/questions/10110658/programmatically-get-gpu-percent-usage-in-os-x/22440235#22440235 - // https://github.com/exelban/stats/blob/master/Modules/GPU/reader.swift static func getInfo() -> [Statistic]? { guard let propertyList = IOHelper.getPropertyList(for: kIOAcceleratorClassName) else { return nil } - return propertyList.compactMap { - guard - let pciMatch = $0["IOPCIMatch"] as? String ?? $0["IOPCIPrimaryMatch"] as? String, - let statistics = $0["PerformanceStatistics"] as? [String: Any], - let usagePercentage = statistics["Device Utilization %"] as? Int ?? statistics["GPU Activity(%)"] as? Int - else { - return nil + var results: [Statistic] = [] + + for props in propertyList { + guard let statistics = props["PerformanceStatistics"] as? [String: Any] else { + continue } - - Print("📊 statistics", statistics) - - return Statistic( + + let usagePercentage = statistics["Device Utilization %"] as? Int + ?? statistics["GPU Activity(%)"] as? Int + ?? 0 + + let pciMatch = props["IOPCIMatch"] as? String + ?? props["IOPCIPrimaryMatch"] as? String + ?? "apple-silicon-gpu" + + let temp = statistics["Temperature(C)"] as? Double ?? SmcControl.shared.gpuProximityTemperature + + results.append(Statistic( pciMatch: pciMatch, usagePercentage: usagePercentage, - temperature: statistics["Temperature(C)"] as? Double ?? SmcControl.shared.gpuProximityTemperature, + temperature: temp, coreClock: statistics["Core Clock(MHz)"] as? Int, memoryClock: statistics["Memory Clock(MHz)"] as? Int - ) + )) } + + return results.isEmpty ? nil : results } } diff --git a/eul/Utilities/SmcControl.swift b/eul/Utilities/SmcControl.swift index 4db24cc..c6e3e2a 100644 --- a/eul/Utilities/SmcControl.swift +++ b/eul/Utilities/SmcControl.swift @@ -16,20 +16,45 @@ class SmcControl: Refreshable { var sensors: [TemperatureData] = [] var fans: [FanData] = [] var tempUnit: TemperatureUnit = .celius + var cpuDieTemperature: Double? { - sensors.first(where: { $0.sensor.name == "CPU_0_DIE" })?.temp + #if arch(arm64) + // Apple Silicon: Use IOHID sensors + return AppleSiliconSensors.shared?.cpuTemperature + #else + // Intel: Use SMC + return sensors.first(where: { $0.sensor.name == "CPU_0_DIE" })?.temp + #endif } var cpuProximityTemperature: Double? { - sensors.first(where: { $0.sensor.name == "CPU_0_PROXIMITY" })?.temp + #if arch(arm64) + // Apple Silicon: Fallback to CPU temperature + return AppleSiliconSensors.shared?.cpuTemperature + #else + // Intel: Use SMC + return sensors.first(where: { $0.sensor.name == "CPU_0_PROXIMITY" })?.temp + #endif } var gpuProximityTemperature: Double? { - sensors.first(where: { $0.sensor.name == "GPU_0_PROXIMITY" })?.temp + #if arch(arm64) + // Apple Silicon: Use IOHID GPU sensors + return AppleSiliconSensors.shared?.gpuTemperature + #else + // Intel: Use SMC + return sensors.first(where: { $0.sensor.name == "GPU_0_PROXIMITY" })?.temp + #endif } var memoryProximityTemperature: Double? { - sensors.first(where: { $0.sensor.name == "MEM_SLOTS_PROXIMITY" })?.temp + #if arch(arm64) + // Apple Silicon: Use SOC temperature as approximation + return AppleSiliconSensors.shared?.socTemperature + #else + // Intel: Use SMC + return sensors.first(where: { $0.sensor.name == "MEM_SLOTS_PROXIMITY" })?.temp + #endif } var isFanValid: Bool { @@ -41,6 +66,12 @@ class SmcControl: Refreshable { } init() { + #if arch(arm64) + // Apple Silicon: Use IOHID sensors only, no SMC + print("Apple Silicon detected - using IOHID sensors") + AppleSiliconSensors.initialize() + #else + // Intel: Use SMC for everything do { try SMCKit.open() sensors = try SMCKit.allKnownTemperatureSensors().map { .init(sensor: $0) } @@ -52,6 +83,7 @@ class SmcControl: Refreshable { } catch { print("SMC init error", error) } + #endif } deinit { @@ -63,10 +95,18 @@ class SmcControl: Refreshable { } func close() { + #if arch(arm64) + // No SMC to close on Apple Silicon + #else SMCKit.close() + #endif } @objc func refresh() { + #if arch(arm64) + // Apple Silicon: Temperature is fetched on-demand from AppleSiliconSensors + #else + // Intel: Use SMC for sensor in sensors { do { sensor.temp = try SMCKit.temperature(sensor.sensor.code, unit: tempUnit) @@ -83,6 +123,7 @@ class SmcControl: Refreshable { maxSpeed: $0.maxSpeed ) } + #endif NotificationCenter.default.post(name: .StoreShouldRefresh, object: nil) } } diff --git a/eul/Views/Menu/CpuMenuBlockView.swift b/eul/Views/Menu/CpuMenuBlockView.swift index 4964fd6..152678a 100644 --- a/eul/Views/Menu/CpuMenuBlockView.swift +++ b/eul/Views/Menu/CpuMenuBlockView.swift @@ -53,6 +53,55 @@ struct CpuMenuBlockView: View { } } } + + // Per-core display + if !cpuStore.coreUsages.isEmpty { + SeparatorView() + VStack(alignment: .leading, spacing: 4) { + Text("cpu.cores".localized()) + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.secondary) + + ForEach(Array(cpuStore.coreUsages.enumerated()), id: \.offset) { index, usage in + HStack { + // Core label with P/E indicator + Text(cpuStore.coreLabels.indices.contains(index) ? cpuStore.coreLabels[index] : "C\(index)") + .font(.system(size: 11, weight: .medium)) + .frame(width: 30, alignment: .leading) + .foregroundColor(coreLabelColor(index: index)) + + // Usage bar + GeometryReader { geometry in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.secondary.opacity(0.2)) + .frame(height: 8) + + RoundedRectangle(cornerRadius: 2) + .fill(usageColor(usage)) + .frame(width: geometry.size.width * CGFloat(usage / 100), height: 8) + } + } + .frame(height: 8) + + Text(String(format: "%.0f%%", usage)) + .font(.system(size: 10, weight: .medium)) + .frame(width: 40, alignment: .trailing) + + // Temperature for this core (if available) + if index < cpuStore.coreTemps.count { + let temp = cpuStore.coreTemps[index] + Text(String(format: "%.0f°C", temp)) + .font(.system(size: 10)) + .foregroundColor(.secondary) + .frame(width: 50, alignment: .trailing) + } + } + } + } + .frame(minWidth: 280) + } + if preferenceStore.showCPUTopActivities { SeparatorView() VStack(spacing: 8) { @@ -72,4 +121,27 @@ struct CpuMenuBlockView: View { } .menuBlock() } + + private func usageColor(_ usage: Double) -> Color { + if usage > 80 { + return .red + } else if usage > 50 { + return .orange + } else { + return .green + } + } + + private func coreLabelColor(index: Int) -> Color { + guard cpuStore.coreLabels.indices.contains(index) else { + return .primary + } + let label = cpuStore.coreLabels[index] + if label.hasPrefix("P") { + return .blue // P-cores in blue + } else if label.hasPrefix("E") { + return .green // E-cores in green + } + return .primary + } } diff --git a/eul/Views/Menu/GpuMenuBlockView.swift b/eul/Views/Menu/GpuMenuBlockView.swift index 599d472..07b2737 100644 --- a/eul/Views/Menu/GpuMenuBlockView.swift +++ b/eul/Views/Menu/GpuMenuBlockView.swift @@ -25,9 +25,16 @@ struct GpuMenuBlockView: View { } ForEach(gpuStore.gpus) { gpu in HStack { - Text(gpu.model ?? "N/A") - .secondaryDisplayText() - .lineLimit(1) + VStack(alignment: .leading, spacing: 2) { + Text(gpu.model ?? "N/A") + .secondaryDisplayText() + .lineLimit(1) + if let cores = gpu.cores { + Text("\(cores) cores") + .font(.system(size: 9)) + .foregroundColor(.secondary) + } + } Spacer() if let statistic = gpuStore.getStatustic(for: gpu) { if let coreClock = statistic.coreClock {