Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ if(BTOP_LTO)
endif()

if(APPLE)
target_sources(libbtop PRIVATE src/osx/btop_collect.cpp src/osx/sensors.cpp src/osx/smc.cpp)
target_sources(libbtop PRIVATE src/osx/btop_collect.cpp src/osx/sensors.cpp src/osx/smc.cpp src/osx/ioreport.cpp)
elseif(CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR CMAKE_SYSTEM_NAME STREQUAL "MidnightBSD")
target_sources(libbtop PRIVATE src/freebsd/btop_collect.cpp)
elseif(CMAKE_SYSTEM_NAME STREQUAL "OpenBSD")
Expand Down
178 changes: 172 additions & 6 deletions src/btop_draw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,8 @@ namespace Cpu {
vector<Draw::Graph> graphs_upper;
vector<Draw::Graph> graphs_lower;
Draw::Meter cpu_meter;
#ifdef __APPLE__
#endif
vector<Draw::Meter> gpu_meters;
vector<Draw::Graph> core_graphs;
vector<Draw::Graph> temp_graphs;
Expand Down Expand Up @@ -698,7 +700,6 @@ namespace Cpu {
}

cpu_meter = Draw::Meter{cpu_meter_width, "cpu"};

if (mid_line) {
out += Mv::to(y + graph_up_height + 1, x) + Fx::ub + Theme::c("cpu_box") + Symbols::div_left + Theme::c("div_line")
+ Symbols::h_line * (width - b_width - 2) + Symbols::div_right
Expand Down Expand Up @@ -834,6 +835,12 @@ namespace Cpu {
+ Symbols::h_line * ((freq_range ? 17 : 7) - cpuHz.size())
+ Symbols::title_left + Fx::b + Theme::c("title") + cpuHz + Fx::ub + Theme::c("div_line") + Symbols::title_right;

#ifdef __APPLE__
//? Skip default CPU bar if we have P/E cores - we'll draw P-CPU and E-CPU bars in the core section
const bool skip_cpu_bar = (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0);
if (not skip_cpu_bar)
#endif
{
out += Mv::to(b_y + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + "CPU " + cpu_meter(safeVal(cpu.cpu_percent, "total"s).back())
+ Theme::g("cpu").at(clamp(safeVal(cpu.cpu_percent, "total"s).back(), 0ll, 100ll)) + rjust(to_string(safeVal(cpu.cpu_percent, "total"s).back()), 4) + Theme::c("main_fg") + '%';
if (show_temps) {
Expand All @@ -850,10 +857,11 @@ namespace Cpu {
string cwatts_post = "W";

max_observed_pwr = max(max_observed_pwr, cpu.usage_watts);
out += Theme::g("cached").at(clamp(cpu.usage_watts / max_observed_pwr * 100.0f, 0.0f, 100.0f)) + cwatts + Theme::c("main_fg") + cwatts_post;
out += Theme::g("cached").at(clamp(cpu.usage_watts / max_observed_pwr * 100.0f, 0.0f, 100.0f)) + cwatts + Theme::c("main_fg") + cwatts_post;
}

out += Theme::c("div_line") + Symbols::v_line;
}
} catch (const std::exception& e) {
throw std::runtime_error("graphs, clock, meter : " + string{e.what()});
}
Expand All @@ -870,12 +878,41 @@ namespace Cpu {
};

//? Core text and graphs
#ifdef __APPLE__
//? On Apple Silicon, start at row 0 since we skip the default CPU bar
const bool pe_layout = (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0);
int cx = 0, cy = (pe_layout ? 0 : 1), cc = 0, core_width = (b_column_size == 0 ? 2 : 3);
#else
int cx = 0, cy = 1, cc = 0, core_width = (b_column_size == 0 ? 2 : 3);
#endif
if (Shared::coreCount >= 100) core_width++;
for (const auto& n : iota(0, Shared::coreCount)) {

//? Helper lambda to draw a single core line
#ifdef __APPLE__
const int p_cores_count = Cpu::cpu_core_info.p_cores;
const int e_cores_count = Cpu::cpu_core_info.e_cores;
const bool use_pe_labels = (p_cores_count > 0 and e_cores_count > 0);
#endif

auto draw_core = [&](int n) {
auto enabled = is_cpu_enabled(n);
#ifdef __APPLE__
string core_label;
if (use_pe_labels) {
// On Apple Silicon: E-cores are indices 0 to e_cores-1, P-cores are e_cores to end
if (n < e_cores_count) {
core_label = "E" + to_string(n);
} else {
core_label = "P" + to_string(n - e_cores_count);
}
} else {
core_label = (Shared::coreCount < 100 ? "C" : "") + to_string(n);
}
out += Mv::to(b_y + cy + 1, b_x + cx + 1) + Theme::c(enabled ? "main_fg" : "inactive_fg") + Fx::b + ljust(core_label, core_width + 1) + Fx::ub;
#else
out += Mv::to(b_y + cy + 1, b_x + cx + 1) + Theme::c(enabled ? "main_fg" : "inactive_fg") + (Shared::coreCount < 100 ? Fx::b + 'C' + Fx::ub : "")
+ ljust(to_string(n), core_width);
#endif
if ((b_column_size > 0 or extra_width > 0) and cmp_less(n, core_graphs.size()))
out += Theme::c("inactive_fg") + graph_bg * (5 * b_column_size + extra_width) + Mv::l(5 * b_column_size + extra_width)
+ core_graphs.at(n)(safeVal(cpu.core_percent, n), data_same or redraw);
Expand All @@ -893,10 +930,107 @@ namespace Cpu {
}

out += Theme::c("div_line") + Symbols::v_line;
};

if ((++cy > ceil((double)Shared::coreCount / b_columns) or cy == max_row) and n != Shared::coreCount - 1) {
if (++cc >= b_columns) break;
cy = 1; cx = (b_width / b_columns) * cc;
#ifdef __APPLE__
//? ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
//? Apple Silicon P/E Core Layout
//?
//? Apple Silicon chips have Performance (P) and Efficiency (E) cores.
//? We display them in separate sections with their own frequency bars:
//? ┌─────────────────────────┐
//? │ P-CPU ■■■■■■■■■ 3.2 GHz │ <- P-core average + frequency
//? │ P0 ▃▅▇ 45% │ P1 ▂▄▆ 32% │ <- Individual P-cores
//? │ E-CPU ■■■■■■■■■ 2.1 GHz │ <- E-core average + frequency
//? │ E0 ▁▃▅ 12% │ E1 ▂▄▆ 18% │ <- Individual E-cores
//? └─────────────────────────┘
//?
//? Core indices: E-cores are 0 to e_cores-1, P-cores are e_cores to end.
//? Frequency is obtained via IOReport framework (sudoless).
//? ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const int p_cores_total = Cpu::cpu_core_info.p_cores;
const int e_cores_total = Cpu::cpu_core_info.e_cores;
const bool has_pe_cores = (p_cores_total > 0 and e_cores_total > 0);

if (has_pe_cores) {
const int col_width = b_width / b_columns;

//? On Apple Silicon: E-cores are indices 0 to e_cores-1, P-cores are e_cores to end
//? Calculate P-core and E-core average percentages
long long p_sum = 0, e_sum = 0;
for (int i = 0; i < e_cores_total and i < (int)cpu.core_percent.size(); ++i) {
e_sum += safeVal(cpu.core_percent, i).back();
}
for (int i = e_cores_total; i < e_cores_total + p_cores_total and i < (int)cpu.core_percent.size(); ++i) {
p_sum += safeVal(cpu.core_percent, i).back();
}
const long long p_avg = p_cores_total > 0 ? p_sum / p_cores_total : 0;
const long long e_avg = e_cores_total > 0 ? e_sum / e_cores_total : 0;

//? P-CPU bar (spans full width of all columns)
string p_label = "P-CPU";
const int p_freq = Cpu::cpu_core_info.p_freq_mhz;
const string p_freq_str = p_freq > 0 ? " " + format_freq(p_freq) : "";
//? Total row width = b_width - 1 (content area, box border is separate)
//? Content = "P-CPU " (6) + meter + freq_str + border (1) = b_width - 1
const int p_freq_width = static_cast<int>(p_freq_str.size());
const int p_meter = max(1, b_width - 7 - p_freq_width - 1);
out += Mv::to(b_y + cy + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + p_label + Fx::ub + " "
+ Draw::Meter{p_meter, "cpu"}(p_avg)
+ Theme::g("cpu").at(clamp(p_avg, 0ll, 100ll)) + p_freq_str
+ Theme::c("div_line") + Symbols::v_line;
cy++;

//? Draw P-cores (indices e_cores_total to e_cores_total+p_cores_total-1)
int p_drawn = 0;
const int p_rows = (p_cores_total + b_columns - 1) / b_columns;
for (int row = 0; row < p_rows and p_drawn < p_cores_total and cy < max_row; ++row) {
for (int col = 0; col < b_columns and p_drawn < p_cores_total; ++col) {
cx = col * col_width;
draw_core(e_cores_total + p_drawn);
p_drawn++;
}
cy++;
}

//? E-CPU section header bar (spans full width of all columns)
if (cy < max_row) {
cx = 0;
string e_label = "E-CPU";
const int e_freq = Cpu::cpu_core_info.e_freq_mhz;
const string e_freq_str = e_freq > 0 ? " " + format_freq(e_freq) : "";
const int e_freq_width = static_cast<int>(e_freq_str.size());
const int e_meter = max(1, b_width - 7 - e_freq_width - 1);
out += Mv::to(b_y + cy + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + e_label + Fx::ub + " "
+ Draw::Meter{e_meter, "cpu"}(e_avg)
+ Theme::g("cpu").at(clamp(e_avg, 0ll, 100ll)) + e_freq_str
+ Theme::c("div_line") + Symbols::v_line;
cy++;
}

//? Draw E-cores (indices 0 to e_cores_total-1)
const int e_rows = (e_cores_total + b_columns - 1) / b_columns;
int e_drawn = 0;
for (int row = 0; row < e_rows and e_drawn < e_cores_total and cy < max_row; ++row) {
for (int col = 0; col < b_columns and e_drawn < e_cores_total; ++col) {
cx = col * col_width;
draw_core(e_drawn);
e_drawn++;
}
cy++;
}
cx = 0;
} else
#endif
{
//? Standard layout: all cores in columns
for (const auto& n : iota(0, Shared::coreCount)) {
draw_core(n);

if ((++cy > ceil((double)Shared::coreCount / b_columns) or cy == max_row) and n != Shared::coreCount - 1) {
if (++cc >= b_columns) break;
cy = 1; cx = (b_width / b_columns) * cc;
}
}
}

Expand Down Expand Up @@ -2277,6 +2411,22 @@ namespace Draw {
b_columns = max(2, (int)ceil((double)(Shared::coreCount + 1) / (height - gpus_extra_height - 5)));
#else
b_columns = max(1, (int)ceil((double)(Shared::coreCount + 1) / (height - 5)));
#endif
#ifdef __APPLE__
//? Apple Silicon P/E layout needs extra rows for section headers.
//? Adjust columns so both P-core and E-core sections fit vertically.
//? Iterative solution is O(max_cores) ≈ O(16), negligible cost.
if (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0) {
const int p_cores = Cpu::cpu_core_info.p_cores;
const int e_cores = Cpu::cpu_core_info.e_cores;
const int available_rows = height - 5 - 2; // -2 for P-CPU and E-CPU header bars
while (b_columns < Shared::coreCount) {
int p_rows = (p_cores + b_columns - 1) / b_columns;
int e_rows = (e_cores + b_columns - 1) / b_columns;
if (p_rows + e_rows <= available_rows) break;
b_columns++;
}
}
#endif
if (b_columns * (21 + 12 * show_temp) < width - (width / 3)) {
b_column_size = 2;
Expand All @@ -2301,6 +2451,22 @@ namespace Draw {
#else
b_height = min(height - 2, (int)ceil((double)Shared::coreCount / b_columns) + 4);
#endif
#ifdef __APPLE__
//? On Apple Silicon with P/E cores, recalculate b_height based on actual P/E row needs
//? P/E layout needs: P-CPU header + P rows + E-CPU header + E rows + base (4)
if (Cpu::cpu_core_info.p_cores > 0 and Cpu::cpu_core_info.e_cores > 0) {
const int p_cores = Cpu::cpu_core_info.p_cores;
const int e_cores = Cpu::cpu_core_info.e_cores;
int p_rows = (p_cores + b_columns - 1) / b_columns;
int e_rows = (e_cores + b_columns - 1) / b_columns;
//? +4 base, +2 for P-CPU and E-CPU headers (we skip default CPU bar, so net +1 from original)
int pe_height = p_rows + e_rows + 4 + 1;
#ifdef GPU_SUPPORT
pe_height += gpus_extra_height;
#endif
b_height = min(height - 2, pe_height);
}
#endif

b_x = x + width - b_width - 1;
b_y = y + ceil((double)(height - 2) / 2) - ceil((double)b_height / 2) + 1;
Expand Down
13 changes: 13 additions & 0 deletions src/btop_shared.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,19 @@ namespace Cpu {

auto get_cpuHz() -> string;

#ifdef __APPLE__
//* Apple Silicon P-core and E-core information
struct core_info {
int p_cores = 0; // Performance cores (perflevel0) - indices e_cores to e_cores+p_cores-1
int e_cores = 0; // Efficiency cores (perflevel1) - indices 0 to e_cores-1
int p_freq_mhz = 0; // Current P-cluster frequency in MHz
int e_freq_mhz = 0; // Current E-cluster frequency in MHz
};
extern core_info cpu_core_info;
auto get_core_info() -> core_info;
void update_core_frequencies(); // Update current P/E frequencies via IOReport
#endif

//* Get battery info from /sys
auto get_battery() -> tuple<int, float, long, string>;

Expand Down
15 changes: 15 additions & 0 deletions src/btop_tools.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,21 @@ namespace Tools {
return str;
}

//* Format CPU frequency: 3 digits max, e.g., "1.9 GHz" or "600 MHz"
inline string format_freq(int mhz) {
if (mhz <= 0) return "";
string str;
if (mhz > 999) {
str = fmt::format("{:.1f}", mhz / 1000.0);
if (str.size() > 3) str.resize(3);
if (str.back() == '.') str.pop_back();
str += " GHz";
} else {
str = fmt::format("{} MHz", mhz);
}
return str;
}

//* Check if vector <vec> contains value <find_val>
template <typename T, typename T2>
inline bool v_contains(const vector<T>& vec, const T2& find_val) {
Expand Down
42 changes: 42 additions & 0 deletions src/osx/btop_collect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ tab-size = 4
#include "sensors.hpp"
#endif
#include "smc.hpp"
#include "ioreport.hpp"

#if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
#define kIOMainPortDefault kIOMasterPortDefault
Expand Down Expand Up @@ -109,6 +110,7 @@ namespace Cpu {
string cpu_sensor;
vector<string> core_sensors;
std::unordered_map<int, int> core_mapping;
core_info cpu_core_info;
} // namespace Cpu

namespace Mem {
Expand Down Expand Up @@ -187,6 +189,7 @@ namespace Shared {
Cpu::cpuName = Cpu::get_cpuName();
Cpu::got_sensors = Cpu::get_sensors();
Cpu::core_mapping = Cpu::get_core_mapping();
Cpu::cpu_core_info = Cpu::get_core_info();

//? Init for namespace Mem
Mem::old_uptime = system_uptime();
Expand Down Expand Up @@ -367,6 +370,42 @@ namespace Cpu {
return core_map;
}

auto get_core_info() -> core_info {
core_info info;
size_t size = sizeof(int);

// On Apple Silicon:
// - perflevel0 = Performance cores (P-cores)
// - perflevel1 = Efficiency cores (E-cores)
// Note: E-cores are enumerated FIRST (indices 0 to e_cores-1), P-cores SECOND
if (sysctlbyname("hw.perflevel0.physicalcpu", &info.p_cores, &size, nullptr, 0) != 0) {
info.p_cores = 0;
}

size = sizeof(int);
if (sysctlbyname("hw.perflevel1.physicalcpu", &info.e_cores, &size, nullptr, 0) != 0) {
info.e_cores = 0;
}

// Fallback: if detection fails (Intel Mac or error), treat all as P-cores
if (info.p_cores == 0 and info.e_cores == 0) {
info.p_cores = Shared::coreCount;
}

return info;
}

void update_core_frequencies() {
//? Initialize IOReport (handles double-init internally)
IOReport::init();

if (IOReport::is_available()) {
auto [e_freq, p_freq] = IOReport::get_cpu_frequencies();
cpu_core_info.e_freq_mhz = static_cast<int>(e_freq);
cpu_core_info.p_freq_mhz = static_cast<int>(p_freq);
}
}

class IOPSInfo_Wrap {
CFTypeRef data;
public:
Expand Down Expand Up @@ -431,6 +470,9 @@ namespace Cpu {
return current_cpu;
auto &cpu = current_cpu;

//? Update P/E core frequencies
update_core_frequencies();

if (getloadavg(cpu.load_avg.data(), cpu.load_avg.size()) < 0) {
Logger::error("failed to get load averages");
}
Expand Down
Loading
Loading