diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c5bdce3..33981964 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,11 +198,22 @@ if(BTOP_STATIC) target_link_options(libbtop PUBLIC -static LINKER:--fatal-warnings) endif() +# Enable GPU support on Apple Silicon +if(APPLE AND BTOP_GPU) + target_compile_definitions(libbtop PUBLIC GPU_SUPPORT) +endif() + # Other platform dependent flags if(APPLE) target_link_libraries(libbtop $ $ ) + if(BTOP_GPU) + find_library(IOREPORT_LIB IOReport) + if(IOREPORT_LIB) + target_link_libraries(libbtop ${IOREPORT_LIB}) + endif() + endif() elseif(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR CMAKE_SYSTEM_NAME STREQUAL "MidnightBSD") # Avoid version mismatch for libstdc++ when a specific version of GCC is installed and not the # default one since all use the default ones RPATH diff --git a/Makefile b/Makefile index d08a4ac7..0bb9cbd5 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,9 @@ ifeq ($(PLATFORM_LC)$(ARCH),linuxx86_64) INTEL_GPU_SUPPORT := true endif endif +ifeq ($(PLATFORM_LC),macos) + GPU_SUPPORT := true +endif ifneq ($(GPU_SUPPORT),true) GPU_SUPPORT := false endif @@ -115,7 +118,7 @@ else ifeq ($(PLATFORM_LC),$(filter $(PLATFORM_LC),freebsd midnightbsd)) else ifeq ($(PLATFORM_LC),macos) PLATFORM_DIR := osx THREADS := $(shell sysctl -n hw.ncpu || echo 1) - override ADDFLAGS += -framework IOKit -framework CoreFoundation -Wno-format-truncation + override ADDFLAGS += -framework IOKit -framework CoreFoundation -lIOReport -Wno-format-truncation SU_GROUP := wheel else ifeq ($(PLATFORM_LC),openbsd) PLATFORM_DIR := openbsd diff --git a/src/btop.cpp b/src/btop.cpp index ffc4e9e6..a754e6d0 100644 --- a/src/btop.cpp +++ b/src/btop.cpp @@ -231,6 +231,9 @@ void clean_quit(int sig) { #ifdef GPU_SUPPORT Gpu::Nvml::shutdown(); Gpu::Rsmi::shutdown(); + #ifdef __APPLE__ + Gpu::AppleSilicon::shutdown(); + #endif #endif diff --git a/src/btop_config.cpp b/src/btop_config.cpp index 36555e3f..c9211771 100644 --- a/src/btop_config.cpp +++ b/src/btop_config.cpp @@ -239,7 +239,7 @@ namespace Config { {"rsmi_measure_pcie_speeds", "#* Measure PCIe throughput on AMD cards, may impact performance on certain cards."}, {"gpu_mirror_graph", "#* Horizontally mirror the GPU graph."}, - {"shown_gpus", "#* Set which GPU vendors to show. Available values are \"nvidia amd intel\""}, + {"shown_gpus", "#* Set which GPU vendors to show. Available values are \"nvidia amd intel apple\""}, {"custom_gpu_name0", "#* Custom gpu0 model name, empty string to disable."}, {"custom_gpu_name1", "#* Custom gpu1 model name, empty string to disable."}, {"custom_gpu_name2", "#* Custom gpu2 model name, empty string to disable."}, @@ -288,7 +288,7 @@ namespace Config { {"custom_gpu_name4", ""}, {"custom_gpu_name5", ""}, {"show_gpu_info", "Auto"}, - {"shown_gpus", "nvidia amd intel"} + {"shown_gpus", "nvidia amd intel apple"} #endif }; std::unordered_map stringsTmp; diff --git a/src/btop_menu.cpp b/src/btop_menu.cpp index 491220b8..b9cb1882 100644 --- a/src/btop_menu.cpp +++ b/src/btop_menu.cpp @@ -598,7 +598,8 @@ namespace Menu { "Manually set which gpu vendors to show.", "", "Available values are", - "\"nvidia\", \"amd\", and \"intel\".", + "\"nvidia\", \"amd\", \"intel\",", + "and \"apple\".", "Separate values with whitespace.", "", "A restart is required to apply changes."}, diff --git a/src/btop_shared.hpp b/src/btop_shared.hpp index eaa72ca0..96f4de32 100644 --- a/src/btop_shared.hpp +++ b/src/btop_shared.hpp @@ -183,6 +183,11 @@ namespace Gpu { namespace Rsmi { extern bool shutdown(); } + #ifdef __APPLE__ + namespace AppleSilicon { + extern bool shutdown(); + } + #endif //* Collect gpu stats and temperatures auto collect(bool no_update = false) -> vector&; diff --git a/src/osx/btop_collect.cpp b/src/osx/btop_collect.cpp index 197cec55..b720bb2d 100644 --- a/src/osx/btop_collect.cpp +++ b/src/osx/btop_collect.cpp @@ -68,6 +68,49 @@ tab-size = 4 #endif #include "smc.hpp" +#if defined(GPU_SUPPORT) +#include +#include + +//? IOReport C function declarations for Apple Silicon GPU metrics +extern "C" { + typedef struct IOReportSubscription* IOReportSubscriptionRef; + + CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef group, CFStringRef subgroup, + uint64_t a, uint64_t b, uint64_t c); + void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef cfnull); + IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b, + CFMutableDictionaryRef* c, uint64_t d, CFTypeRef cfnull); + CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef sub, + CFMutableDictionaryRef chan, CFTypeRef cfnull); + CFDictionaryRef IOReportCreateSamplesDelta(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef cfnull); + CFStringRef IOReportChannelGetGroup(CFDictionaryRef item); + CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef item); + CFStringRef IOReportChannelGetChannelName(CFDictionaryRef item); + int64_t IOReportSimpleGetIntegerValue(CFDictionaryRef item, int32_t idx); + CFStringRef IOReportChannelGetUnitLabel(CFDictionaryRef item); + int32_t IOReportStateGetCount(CFDictionaryRef item); + CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef item, int32_t idx); + int64_t IOReportStateGetResidency(CFDictionaryRef item, int32_t idx); + + //? IOHIDEvent declarations for GPU temperature + typedef struct __IOHIDEvent* IOHIDEventRef; + typedef struct __IOHIDServiceClient* IOHIDServiceClientRef; + typedef struct __IOHIDEventSystemClient* IOHIDEventSystemClientRef; + #ifdef __LP64__ + typedef double IOHIDFloat; + #else + typedef float IOHIDFloat; + #endif + IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator); + int IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match); + CFArrayRef IOHIDEventSystemClientCopyServices(IOHIDEventSystemClientRef client); + IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef sc, int64_t type, int32_t a, int64_t b); + CFStringRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef property); + IOHIDFloat IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field); +} +#endif // GPU_SUPPORT + #if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000 #define kIOMainPortDefault kIOMasterPortDefault #endif @@ -78,6 +121,43 @@ namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; +//? RAII wrapper for CoreFoundation types — releases via CFRelease() on destruction +template +struct CFRef { + T ref; + CFRef() : ref(nullptr) {} + CFRef(T ref) : ref(ref) {} + ~CFRef() { if (ref) CFRelease((CFTypeRef)ref); } + CFRef(const CFRef&) = delete; + CFRef& operator=(const CFRef&) = delete; + CFRef(CFRef&& other) noexcept : ref(other.ref) { other.ref = nullptr; } + CFRef& operator=(CFRef&& other) noexcept { + if (this != &other) { reset(); ref = other.ref; other.ref = nullptr; } + return *this; + } + operator T() const { return ref; } + T get() const { return ref; } + T* ptr() { return &ref; } + void reset(T new_ref = nullptr) { + if (ref) CFRelease((CFTypeRef)ref); + ref = new_ref; + } + T release() { T r = ref; ref = nullptr; return r; } +}; + +//? RAII wrapper for IOKit object types — releases via IOObjectRelease() on destruction +struct IORef { + io_object_t ref; + IORef() : ref(0) {} + IORef(io_object_t ref) : ref(ref) {} + ~IORef() { if (ref) IOObjectRelease(ref); } + IORef(const IORef&) = delete; + IORef& operator=(const IORef&) = delete; + operator io_object_t() const { return ref; } + io_object_t get() const { return ref; } + io_object_t* ptr() { return &ref; } +}; + //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- namespace Cpu { @@ -115,6 +195,442 @@ namespace Mem { double old_uptime; } +#if defined(GPU_SUPPORT) +namespace Gpu { + vector gpus; + + //? Stub shutdown for backends not available on macOS + namespace Nvml { bool shutdown() { return false; } } + namespace Rsmi { bool shutdown() { return false; } } + + //? Apple Silicon GPU data collection via IOReport + namespace AppleSilicon { + bool initialized = false; + unsigned int device_count = 0; + + //? Forward declaration + template + bool collect(gpu_info* gpus_slice); + + //? IOReport subscription state + IOReportSubscriptionRef ior_sub = nullptr; + CFMutableDictionaryRef ior_chan = nullptr; + CFDictionaryRef prev_sample = nullptr; + uint64_t prev_sample_time = 0; + + //? GPU frequency table from DVFS + vector gpu_freqs; + + static string cfstring_to_string(CFStringRef cfstr) { + if (not cfstr) return ""; + char buf[256]; + if (CFStringGetCString(cfstr, buf, sizeof(buf), kCFStringEncodingUTF8)) + return string(buf); + return ""; + } + + static string get_chip_name() { + char buf[256]; + size_t size = sizeof(buf); + if (sysctlbyname("machdep.cpu.brand_string", buf, &size, nullptr, 0) == 0) + return string(buf); + return "Apple Silicon GPU"; + } + + static uint64_t get_mach_time_ms() { + static mach_timebase_info_data_t timebase = {0, 0}; + if (timebase.denom == 0) mach_timebase_info(&timebase); + return (mach_absolute_time() * timebase.numer / timebase.denom) / 1000000; + } + + //? Read GPU DVFS frequency table from IORegistry pmgr node + static void get_gpu_freqs_from_pmgr() { + io_iterator_t iter_raw; + //? matchDict ownership is consumed by IOServiceGetMatchingServices + CFMutableDictionaryRef matchDict = IOServiceMatching("AppleARMIODevice"); + if (IOServiceGetMatchingServices(kIOMainPortDefault, matchDict, &iter_raw) != kIOReturnSuccess) + return; + IORef iter(iter_raw); + + io_object_t entry_raw; + while ((entry_raw = IOIteratorNext(iter)) != 0) { + IORef entry(entry_raw); + char name[128]; + if (IORegistryEntryGetName(entry, name) == kIOReturnSuccess and string(name) == "pmgr") { + CFMutableDictionaryRef props_raw = nullptr; + if (IORegistryEntryCreateCFProperties(entry, &props_raw, kCFAllocatorDefault, 0) == kIOReturnSuccess and props_raw) { + CFRef props(props_raw); + CFDataRef dvfs_data = (CFDataRef)CFDictionaryGetValue(props, CFSTR("voltage-states9")); + if (dvfs_data) { + auto len = CFDataGetLength(dvfs_data); + auto ptr = CFDataGetBytePtr(dvfs_data); + //? Pairs of (freq, voltage), 4 bytes each + for (CFIndex i = 0; i + 7 < len; i += 8) { + uint32_t freq = 0; + memcpy(&freq, ptr + i, 4); + if (freq > 0) gpu_freqs.push_back(freq / (1000 * 1000)); // Hz -> MHz + } + } + } + } + } + } + + bool init() { + if (initialized) return false; + + //? Get GPU frequency table + get_gpu_freqs_from_pmgr(); + + //? Set up IOReport channels for GPU Stats and Energy Model + CFRef gpu_stats_group(CFStringCreateWithCString(kCFAllocatorDefault, "GPU Stats", kCFStringEncodingUTF8)); + CFRef gpu_perf_subgroup(CFStringCreateWithCString(kCFAllocatorDefault, "GPU Performance States", kCFStringEncodingUTF8)); + CFRef energy_group(CFStringCreateWithCString(kCFAllocatorDefault, "Energy Model", kCFStringEncodingUTF8)); + + CFRef gpu_chan(IOReportCopyChannelsInGroup(gpu_stats_group, gpu_perf_subgroup, 0, 0, 0)); + CFRef energy_chan(IOReportCopyChannelsInGroup(energy_group, nullptr, 0, 0, 0)); + + if (not gpu_chan.get() and not energy_chan.get()) { + Logger::info("Apple Silicon GPU: No IOReport channels found, GPU monitoring unavailable"); + return false; + } + + //? Merge channels into a single subscription + if (gpu_chan.get() and energy_chan.get()) { + IOReportMergeChannels(gpu_chan, energy_chan, nullptr); + } + CFDictionaryRef base_chan = gpu_chan.get() ? gpu_chan.get() : energy_chan.get(); + + auto size = CFDictionaryGetCount(base_chan); + ior_chan = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, size, base_chan); + + //? Create IOReport subscription + CFMutableDictionaryRef sub_dict = nullptr; + ior_sub = IOReportCreateSubscription(nullptr, ior_chan, &sub_dict, 0, nullptr); + if (not ior_sub) { + Logger::warning("Apple Silicon GPU: Failed to create IOReport subscription"); + CFRelease(ior_chan); + ior_chan = nullptr; + return false; + } + + device_count = 1; //? Apple Silicon has one integrated GPU + gpus.resize(gpus.size() + device_count); + gpu_names.resize(gpu_names.size() + device_count); + + initialized = true; + + //? Take initial sample for delta computation + prev_sample = IOReportCreateSamples(ior_sub, ior_chan, nullptr); + prev_sample_time = get_mach_time_ms(); + + //? Run init collect to populate names and supported functions + collect<1>(gpus.data()); + + return true; + } + + bool shutdown() { + if (not initialized) return false; + if (prev_sample) { CFRelease(prev_sample); prev_sample = nullptr; } + if (ior_chan) { CFRelease(ior_chan); ior_chan = nullptr; } + if (ior_sub) { CFRelease((CFTypeRef)ior_sub); ior_sub = nullptr; } + initialized = false; + return true; + } + + //? Read GPU temperature via IOHIDEventSystem thermal sensors + static long long get_gpu_temp_iohid() { + #if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 + constexpr int kHIDPage_AppleVendor = 0xff00; + constexpr int kHIDUsage_TemperatureSensor = 5; + constexpr int64_t kIOHIDEventTypeTemperature = 15; + + CFStringRef keys[2] = { CFSTR("PrimaryUsagePage"), CFSTR("PrimaryUsage") }; + int page = kHIDPage_AppleVendor, usage = kHIDUsage_TemperatureSensor; + CFRef num0(CFNumberCreate(nullptr, kCFNumberSInt32Type, &page)); + CFRef num1(CFNumberCreate(nullptr, kCFNumberSInt32Type, &usage)); + const void* values[] = { num0.get(), num1.get() }; + CFRef match(CFDictionaryCreate(nullptr, + (const void**)keys, values, 2, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + + CFRef system(IOHIDEventSystemClientCreate(kCFAllocatorDefault)); + if (not system.get()) return -1; + IOHIDEventSystemClientSetMatching(system, match); + CFRef services(IOHIDEventSystemClientCopyServices(system)); + + if (not services.get()) return -1; + + double gpu_temp_sum = 0; + int gpu_temp_count = 0; + long count = CFArrayGetCount(services); + for (long i = 0; i < count; i++) { + auto sc = (IOHIDServiceClientRef)CFArrayGetValueAtIndex(services, i); + if (not sc) continue; + CFRef name(IOHIDServiceClientCopyProperty(sc, CFSTR("Product"))); + if (not name.get()) continue; + char buf[200]; + CFStringGetCString(name, buf, 200, kCFStringEncodingASCII); + string n(buf); + //? "GPU MTR Temp Sensor" is the standard Apple Silicon GPU temp sensor name + if (n.find("GPU") != string::npos) { + CFRef event(IOHIDServiceClientCopyEvent(sc, kIOHIDEventTypeTemperature, 0, 0)); + if (event.get()) { + double temp = IOHIDEventGetFloatValue(event, kIOHIDEventTypeTemperature << 16); + if (temp > 0 and temp < 150) { + gpu_temp_sum += temp; + gpu_temp_count++; + } + } + } + } + + if (gpu_temp_count > 0) + return static_cast(round(gpu_temp_sum / gpu_temp_count)); + #endif + return -1; + } + + template + bool collect(gpu_info* gpus_slice) { + if (not initialized) return false; + + if constexpr (is_init) { + //? Device name + string chip = get_chip_name(); + gpu_names[0] = chip + " GPU"; + + //? Power max (typical Apple Silicon GPU TDP ~15-20W) + gpus_slice[0].pwr_max_usage = 20000; // 20W in mW + gpu_pwr_total_max += gpus_slice[0].pwr_max_usage; + + //? Temperature max + gpus_slice[0].temp_max = 110; + + //? Memory total (unified memory architecture — GPU shares system RAM) + int64_t memsize = 0; + size_t size = sizeof(memsize); + if (sysctlbyname("hw.memsize", &memsize, &size, nullptr, 0) == 0) + gpus_slice[0].mem_total = memsize; + + //? Supported functions + gpus_slice[0].supported_functions = { + .gpu_utilization = true, + .mem_utilization = true, + .gpu_clock = not gpu_freqs.empty(), + .mem_clock = false, + .pwr_usage = true, + .pwr_state = false, + .temp_info = true, + .mem_total = true, + .mem_used = true, + .pcie_txrx = false, + .encoder_utilization = false, + .decoder_utilization = false + }; + } + + //? Take new IOReport sample and compute delta + CFDictionaryRef cur_sample = IOReportCreateSamples(ior_sub, ior_chan, nullptr); + if (not cur_sample) return false; + + uint64_t cur_time = get_mach_time_ms(); + uint64_t dt = cur_time - prev_sample_time; + if (dt == 0) dt = 1; + + CFRef delta; + if (prev_sample) { + delta.reset(IOReportCreateSamplesDelta(prev_sample, cur_sample, nullptr)); + CFRelease(prev_sample); + } + prev_sample = cur_sample; + prev_sample_time = cur_time; + + if (not delta.get()) return false; + + //? Parse delta samples + CFArrayRef channels = (CFArrayRef)CFDictionaryGetValue(delta, CFSTR("IOReportChannels")); + if (not channels) return false; + + long long gpu_utilization = 0; + bool got_gpu_util = false; + double gpu_power_watts = 0; + bool got_gpu_power = false; + + long chan_count = CFArrayGetCount(channels); + for (long i = 0; i < chan_count; i++) { + CFDictionaryRef item = (CFDictionaryRef)CFArrayGetValueAtIndex(channels, i); + if (not item) continue; + + string group = cfstring_to_string(IOReportChannelGetGroup(item)); + string subgroup = cfstring_to_string(IOReportChannelGetSubGroup(item)); + string channel = cfstring_to_string(IOReportChannelGetChannelName(item)); + + //? GPU utilization from residency states + if (group == "GPU Stats" and subgroup == "GPU Performance States" and channel == "GPUPH") { + int32_t state_count = IOReportStateGetCount(item); + if (state_count <= 0) continue; + + int64_t total_residency = 0; + int64_t active_residency = 0; + double weighted_freq = 0; + + //? Find offset past IDLE/OFF/DOWN states + int offset = 0; + for (int32_t s = 0; s < state_count; s++) { + string name = cfstring_to_string(IOReportStateGetNameForIndex(item, s)); + if (name == "IDLE" or name == "OFF" or name == "DOWN") + offset = s + 1; + total_residency += IOReportStateGetResidency(item, s); + } + + int freq_count = static_cast(gpu_freqs.size()); + for (int32_t s = offset; s < state_count; s++) { + int64_t res = IOReportStateGetResidency(item, s); + active_residency += res; + int freq_idx = s - offset; + if (freq_idx < freq_count and active_residency > 0) + weighted_freq += static_cast(res) * gpu_freqs[freq_idx]; + } + + if (total_residency > 0) { + double usage_ratio = static_cast(active_residency) / static_cast(total_residency); + gpu_utilization = clamp(static_cast(round(usage_ratio * 100.0)), 0ll, 100ll); + got_gpu_util = true; + + //? Calculate average frequency + if (active_residency > 0 and not gpu_freqs.empty()) { + double avg_freq = weighted_freq / static_cast(active_residency); + gpus_slice[0].gpu_clock_speed = static_cast(round(avg_freq)); + } + } + } + + //? GPU power from Energy Model + if (group == "Energy Model" and channel == "GPU Energy") { + string unit = cfstring_to_string(IOReportChannelGetUnitLabel(item)); + int64_t val = IOReportSimpleGetIntegerValue(item, 0); + double energy = static_cast(val); + double divisor = static_cast(dt) / 1000.0; // dt is in ms + + if (unit.find("nJ") != string::npos) energy /= 1e9; + else if (unit.find("uJ") != string::npos or unit.find("\xc2\xb5J") != string::npos) energy /= 1e6; + else if (unit.find("mJ") != string::npos) energy /= 1e3; + //? energy is now in Joules + + if (divisor > 0) { + gpu_power_watts = energy / divisor; + got_gpu_power = true; + } + } + } + + //? Store GPU utilization + if (got_gpu_util) { + gpus_slice[0].gpu_percent.at("gpu-totals").push_back(gpu_utilization); + gpus_slice[0].mem_utilization_percent.push_back(gpu_utilization); + } + + //? Store power usage (convert W to mW) + if (got_gpu_power) { + gpus_slice[0].pwr_usage = static_cast(round(gpu_power_watts * 1000.0)); + if (gpus_slice[0].pwr_usage > gpus_slice[0].pwr_max_usage) + gpus_slice[0].pwr_max_usage = gpus_slice[0].pwr_usage; + gpus_slice[0].gpu_percent.at("gpu-pwr-totals").push_back( + clamp(static_cast(round(static_cast(gpus_slice[0].pwr_usage) * 100.0 / static_cast(gpus_slice[0].pwr_max_usage))), 0ll, 100ll)); + } + + //? GPU temperature + if (gpus_slice[0].supported_functions.temp_info and Config::getB("check_temp")) { + long long temp = get_gpu_temp_iohid(); + if (temp > 0) + gpus_slice[0].temp.push_back(temp); + } + + //? Memory usage (unified memory — report system memory usage) + if (gpus_slice[0].supported_functions.mem_total) { + vm_size_t page_size; + mach_port_t mach_port = mach_host_self(); + vm_statistics64_data_t vm_stats; + mach_msg_type_number_t count = sizeof(vm_stats) / sizeof(natural_t); + host_page_size(mach_port, &page_size); + + if (host_statistics64(mach_port, HOST_VM_INFO64, (host_info64_t)&vm_stats, &count) == KERN_SUCCESS) { + long long used = (static_cast(vm_stats.active_count) + + static_cast(vm_stats.inactive_count) + + static_cast(vm_stats.wire_count) + + static_cast(vm_stats.speculative_count) + + static_cast(vm_stats.compressor_page_count) + - static_cast(vm_stats.purgeable_count) + - static_cast(vm_stats.external_page_count)) * static_cast(page_size); + if (used < 0) used = 0; + gpus_slice[0].mem_used = used; + if (gpus_slice[0].mem_total > 0) { + auto used_pct = static_cast(round(static_cast(used) * 100.0 / static_cast(gpus_slice[0].mem_total))); + gpus_slice[0].gpu_percent.at("gpu-vram-totals").push_back(clamp(used_pct, 0ll, 100ll)); + } + } + } + + return true; + } + + //? Explicit template instantiations + template bool collect(gpu_info*); + template bool collect(gpu_info*); + } // namespace AppleSilicon + + //? Collect data from Apple Silicon GPU + auto collect(bool no_update) -> vector& { + if (Runner::stopping or (no_update and not gpus.empty())) return gpus; + + AppleSilicon::collect<0>(gpus.data()); + + //* Calculate averages + long long avg = 0; + long long mem_usage_total = 0; + long long mem_total = 0; + long long pwr_total = 0; + for (auto& gpu : gpus) { + if (gpu.supported_functions.gpu_utilization and not gpu.gpu_percent.at("gpu-totals").empty()) + avg += gpu.gpu_percent.at("gpu-totals").back(); + if (gpu.supported_functions.mem_used) + mem_usage_total += gpu.mem_used; + if (gpu.supported_functions.mem_total) + mem_total += gpu.mem_total; + if (gpu.supported_functions.pwr_usage) + pwr_total += gpu.pwr_usage; + + //* Trim vectors if there are more values than needed for graphs + if (width != 0) { + while (cmp_greater(gpu.gpu_percent.at("gpu-totals").size(), width * 2)) gpu.gpu_percent.at("gpu-totals").pop_front(); + while (cmp_greater(gpu.mem_utilization_percent.size(), width)) gpu.mem_utilization_percent.pop_front(); + while (cmp_greater(gpu.gpu_percent.at("gpu-pwr-totals").size(), width)) gpu.gpu_percent.at("gpu-pwr-totals").pop_front(); + while (cmp_greater(gpu.temp.size(), 18)) gpu.temp.pop_front(); + while (cmp_greater(gpu.gpu_percent.at("gpu-vram-totals").size(), width/2)) gpu.gpu_percent.at("gpu-vram-totals").pop_front(); + } + } + + if (not gpus.empty()) { + shared_gpu_percent.at("gpu-average").push_back(avg / static_cast(gpus.size())); + if (mem_total != 0) + shared_gpu_percent.at("gpu-vram-total").push_back(mem_usage_total * 100 / mem_total); + if (gpu_pwr_total_max != 0) + shared_gpu_percent.at("gpu-pwr-total").push_back(pwr_total * 100 / gpu_pwr_total_max); + } + + if (width != 0) { + while (cmp_greater(shared_gpu_percent.at("gpu-average").size(), width * 2)) shared_gpu_percent.at("gpu-average").pop_front(); + while (cmp_greater(shared_gpu_percent.at("gpu-vram-total").size(), width)) shared_gpu_percent.at("gpu-vram-total").pop_front(); + while (cmp_greater(shared_gpu_percent.at("gpu-pwr-total").size(), width)) shared_gpu_percent.at("gpu-pwr-total").pop_front(); + } + + return gpus; + } +} // namespace Gpu +#endif // GPU_SUPPORT + class MachProcessorInfo { public: processor_info_array_t info_array; @@ -188,6 +704,31 @@ namespace Shared { Cpu::got_sensors = Cpu::get_sensors(); Cpu::core_mapping = Cpu::get_core_mapping(); + //? Init for namespace Gpu + #ifdef GPU_SUPPORT + auto shown_gpus = Config::getS("shown_gpus"); + if (shown_gpus.contains("apple")) { + Gpu::AppleSilicon::init(); + } + + if (not Gpu::gpu_names.empty()) { + for (auto const& [key, _] : Gpu::gpus[0].gpu_percent) + Cpu::available_fields.push_back(key); + for (auto const& [key, _] : Gpu::shared_gpu_percent) + Cpu::available_fields.push_back(key); + + using namespace Gpu; + count = gpus.size(); + gpu_b_height_offsets.resize(gpus.size()); + for (size_t i = 0; i < gpu_b_height_offsets.size(); ++i) + gpu_b_height_offsets[i] = gpus[i].supported_functions.gpu_utilization + + gpus[i].supported_functions.pwr_usage + + (gpus[i].supported_functions.encoder_utilization or gpus[i].supported_functions.decoder_utilization) + + (gpus[i].supported_functions.mem_total or gpus[i].supported_functions.mem_used) + * (1 + 2*(gpus[i].supported_functions.mem_total and gpus[i].supported_functions.mem_used) + 2*gpus[i].supported_functions.mem_utilization); + } + #endif + //? Init for namespace Mem Mem::old_uptime = system_uptime(); Mem::collect();