diff --git a/Modules/GPU/bridge.h b/Modules/GPU/bridge.h new file mode 100644 index 00000000..9f5176ef --- /dev/null +++ b/Modules/GPU/bridge.h @@ -0,0 +1,30 @@ +// +// bridge.h +// GPU +// +// Created by Serhiy Mytrovtsiy on 02/04/2026 +// Using Swift 6.0 +// Running on macOS 15.1 +// +// Copyright © 2026 Serhiy Mytrovtsiy. All rights reserved. +// + +#ifndef bridge_h +#define bridge_h + +#include + +typedef struct IOReportSubscriptionRef* IOReportSubscriptionRef; + +CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef a, CFStringRef b, uint64_t c, uint64_t d, uint64_t e); +void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef null); +IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b, CFMutableDictionaryRef* c, uint64_t d, CFTypeRef e); +CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef a, CFMutableDictionaryRef b, CFTypeRef c); +CFStringRef IOReportChannelGetGroup(CFDictionaryRef a); +CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef a); +CFStringRef IOReportChannelGetChannelName(CFDictionaryRef a); +int32_t IOReportStateGetCount(CFDictionaryRef a); +CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef a, int32_t b); +int64_t IOReportStateGetResidency(CFDictionaryRef a, int32_t b); + +#endif /* bridge_h */ diff --git a/Modules/GPU/main.swift b/Modules/GPU/main.swift index c8a079df..c60b2e30 100644 --- a/Modules/GPU/main.swift +++ b/Modules/GPU/main.swift @@ -40,6 +40,7 @@ public struct GPU_Info: Codable { public var utilization: Double? = nil public var renderUtilization: Double? = nil public var tilerUtilization: Double? = nil + public var aneUtilization: Double? = nil init(id: String, type: GPU_type, IOClass: String, vendor: String? = nil, model: String, cores: Int?, utilization: Double? = nil, render: Double? = nil, tiler: Double? = nil) { self.id = id diff --git a/Modules/GPU/popup.swift b/Modules/GPU/popup.swift index 43c5af42..35516d23 100644 --- a/Modules/GPU/popup.swift +++ b/Modules/GPU/popup.swift @@ -82,6 +82,7 @@ private class GPUView: NSStackView { private var utilizationChart: LineChartView? = nil private var renderUtilizationChart: LineChartView? = nil private var tilerUtilizationChart: LineChartView? = nil + private var aneUtilizationChart: LineChartView? = nil public var sizeCallback: (() -> Void) @@ -180,6 +181,7 @@ private class GPUView: NSStackView { self.addStats(id: "GPU utilization", self.value.utilization) self.addStats(id: "Render utilization", self.value.renderUtilization) self.addStats(id: "Tiler utilization", self.value.tilerUtilization) + self.addStats(id: "ANE utilization", self.value.aneUtilization) container.addArrangedSubview(circles) container.addArrangedSubview(charts) @@ -272,6 +274,14 @@ private class GPUView: NSStackView { if self.tilerUtilizationChart == nil { self.tilerUtilizationChart = chart } + } else if id == "ANE utilization" { + circle.setValue(value) + circle.setText("\(Int(value*100))%") + circle.toolTip = "\(localizedString(id)): \(Int(value*100))%" + + if self.aneUtilizationChart == nil { + self.aneUtilizationChart = chart + } } } @@ -286,6 +296,7 @@ private class GPUView: NSStackView { self.addStats(id: "GPU utilization", gpu.utilization) self.addStats(id: "Render utilization", gpu.renderUtilization) self.addStats(id: "Tiler utilization", gpu.tilerUtilization) + self.addStats(id: "ANE utilization", gpu.aneUtilization) } if let value = gpu.temperature { @@ -304,6 +315,9 @@ private class GPUView: NSStackView { if let value = gpu.tilerUtilization { self.tilerUtilizationChart?.addValue(value) } + if let value = gpu.aneUtilization { + self.aneUtilizationChart?.addValue(value) + } } @objc private func showDetails() { @@ -330,6 +344,7 @@ private class GPUDetails: NSView { private var utilization: NSTextField? = nil private var renderUtilization: NSTextField? = nil private var tilerUtilization: NSTextField? = nil + private var aneUtilization: NSTextField? = nil open override var intrinsicContentSize: CGSize { return CGSize(width: self.bounds.width, height: self.bounds.height) @@ -410,6 +425,12 @@ private class GPUDetails: NSView { grid.addRow(with: arr) num += 1 } + if let value = value.aneUtilization { + let arr = keyValueRow("\(localizedString("ANE utilization")):", "\(Int(value*100))%") + self.aneUtilization = arr.last + grid.addRow(with: arr) + num += 1 + } self.setFrameSize(NSSize(width: self.frame.width, height: (16 * num) + Constants.Popup.margins)) grid.setFrameSize(NSSize(width: grid.frame.width, height: self.frame.height - Constants.Popup.margins)) @@ -452,5 +473,8 @@ private class GPUDetails: NSView { if let value = gpu.tilerUtilization { self.tilerUtilization?.stringValue = "\(Int(value*100))%" } + if let value = gpu.aneUtilization { + self.aneUtilization?.stringValue = "\(Int(value*100))%" + } } } diff --git a/Modules/GPU/portal.swift b/Modules/GPU/portal.swift index ec0dc2d8..ef1e77c5 100644 --- a/Modules/GPU/portal.swift +++ b/Modules/GPU/portal.swift @@ -13,11 +13,12 @@ import Cocoa import Kit public class Portal: PortalWrapper { - private var circle: HalfCircleGraphView? = nil + private var circle: PieChartView? = nil private var usageField: NSTextField? = nil private var renderField: NSTextField? = nil private var tilerField: NSTextField? = nil + private var aneField: NSTextField? = nil private var initialized: Bool = false @@ -56,7 +57,7 @@ public class Portal: PortalWrapper { right: Constants.Popup.spacing*4 ) - let chart = HalfCircleGraphView() + let chart = PieChartView(openCircle: true) chart.toolTip = localizedString("GPU usage") view.addArrangedSubview(chart) self.circle = chart @@ -73,6 +74,7 @@ public class Portal: PortalWrapper { self.usageField = portalRow(view, title: "\(localizedString("Usage")):").1 self.renderField = portalRow(view, title: "\(localizedString("Render")):").1 self.tilerField = portalRow(view, title: "\(localizedString("Tiler")):").1 + self.aneField = portalRow(view, title: "\(localizedString("ANE")):").1 return view } @@ -89,6 +91,9 @@ public class Portal: PortalWrapper { if let value = value.tilerUtilization { self.tilerField?.stringValue = "\(Int(value*100))%" } + if let value = value.aneUtilization { + self.aneField?.stringValue = "\(Int(value*100))%" + } self.circle?.toolTip = "\(localizedString("GPU usage")): \(Int(value.utilization!*100))%" self.circle?.setValue(value.utilization!) diff --git a/Modules/GPU/reader.swift b/Modules/GPU/reader.swift index ac2a7774..9064ea30 100644 --- a/Modules/GPU/reader.swift +++ b/Modules/GPU/reader.swift @@ -28,6 +28,10 @@ internal class InfoReader: Reader { private var gpus: GPUs = GPUs() private var displays: [gpu_s] = [] private var devices: [device] = [] + + private var aneChannels: CFMutableDictionary? = nil + private var aneSubscription: IOReportSubscriptionRef? = nil + private var previousANEResidencies: [(on: Int64, total: Int64)] = [] public override func setup() { if let list = SystemKit.shared.device.info.gpu { @@ -39,6 +43,10 @@ internal class InfoReader: Reader { } let devices = PCIdevices.filter{ $0.object(forKey: "IOName") as? String == "display" } + #if arch(arm64) + self.setupANE() + #endif + devices.forEach { (dict: NSDictionary) in guard let deviceID = dict["device-id"] as? Data, let vendorID = dict["vendor-id"] as? Data else { error("device-id or vendor-id not found", log: self.log) @@ -205,7 +213,83 @@ internal class InfoReader: Reader { } } + #if arch(arm64) + let aneValue = self.readANEUtilization() + for i in self.gpus.list.indices where self.gpus.list[i].IOClass.lowercased().contains("agx") { + self.gpus.list[i].aneUtilization = aneValue ?? 0 + } + #endif + self.gpus.list.sort{ !$0.state && $1.state } self.callback(self.gpus) } + + // MARK: - ANE utilization + + private func setupANE() { + guard let channel = IOReportCopyChannelsInGroup("SoC Stats" as CFString, "Cluster Power States" as CFString, 0, 0, 0)?.takeRetainedValue() else { return } + + let size = CFDictionaryGetCount(channel) + guard let mutable = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, channel), + let dict = mutable as? [String: Any], dict["IOReportChannels"] != nil else { return } + + self.aneChannels = mutable + var sub: Unmanaged? + self.aneSubscription = IOReportCreateSubscription(nil, mutable, &sub, 0, nil) + sub?.release() + } + + private func readANEUtilization() -> Double? { + guard let subscription = self.aneSubscription, + let channels = self.aneChannels, + let reportSample = IOReportCreateSamples(subscription, channels, nil)?.takeRetainedValue(), + let dict = reportSample as? [String: Any] else { + return nil + } + let items = dict["IOReportChannels"] as! CFArray + + var currentResidencies: [(on: Int64, total: Int64)] = [] + + for i in 0.. 0 else { return 0 } + return Double(totalDeltaOn) / Double(totalDeltaAll) + } } diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index b664067a..ea2f65fd 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 5CAA50722C8E417700B13E13 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAA50712C8E417700B13E13 /* Text.swift */; }; 5CB3878A2C35A7110030459D /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB387892C35A7110030459D /* widget.swift */; }; 5CC3B4E52F5A033000775E2C /* reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC3B4E42F5A032E00775E2C /* reader.swift */; }; + 5CC8042A2F7EDECA00B78DC7 /* bridge.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CC804292F7EDECA00B78DC7 /* bridge.h */; }; 5CCA5CD52D4E8DB3002917F0 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; }; 5CD342F42B2F2FB700225631 /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD342F32B2F2FB700225631 /* notifications.swift */; }; 5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7E78B2C318512006BC92C /* WidgetKit.framework */; }; @@ -204,6 +205,7 @@ 9AF9EE0A24648751005D2270 /* Disk.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9AF9EE0F2464875F005D2270 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE0E2464875F005D2270 /* main.swift */; }; 9AF9EE1124648ADC005D2270 /* readers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AF9EE1024648ADC005D2270 /* readers.swift */; }; + AA00000000000000000000A1 /* libIOReport.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C1E45552D11D66200525864 /* libIOReport.tbd */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -552,6 +554,7 @@ 5CAA50712C8E417700B13E13 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; 5CB387892C35A7110030459D /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; }; 5CC3B4E42F5A032E00775E2C /* reader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = reader.swift; sourceTree = ""; }; + 5CC804292F7EDECA00B78DC7 /* bridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bridge.h; sourceTree = ""; }; 5CD342F32B2F2FB700225631 /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; 5CE7E78A2C318512006BC92C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 5CE7E78B2C318512006BC92C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; @@ -811,6 +814,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + AA00000000000000000000A1 /* libIOReport.tbd in Frameworks */, 9A2847C72666AA8C00EC1F6D /* Kit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1160,6 +1164,7 @@ 5C0A9CA32C467F7A00EE6A89 /* widget.swift */, 9A90E18C24EAD2BB00471E9A /* Info.plist */, 9A90E19724EAD3B000471E9A /* config.plist */, + 5CC804292F7EDECA00B78DC7 /* bridge.h */, ); path = GPU; sourceTree = ""; @@ -1355,6 +1360,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 5CC8042A2F7EDECA00B78DC7 /* bridge.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3215,6 +3221,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = Modules/GPU/bridge.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -3250,6 +3257,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = Modules/GPU/bridge.h; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = "";