From 7b4558fe6f23203f0d38844e837a9b1fe5dcb5d3 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Tue, 17 Mar 2026 09:26:34 +0800 Subject: [PATCH 1/9] Allow guest time to be hidden in "non-detailed" CPU meter When both the options "Add guest time in CPU meter percentage" and "Detailed CPU time" are turned off, the CPU meter used to show the "virtual" CPU time in the bar display as a glitch. Now fix it. Signed-off-by: Kang-Che Sung --- linux/Platform.c | 6 +++++- pcp/Platform.c | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/linux/Platform.c b/linux/Platform.c index eaaef4225..97b0275c5 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -371,8 +371,12 @@ double Platform_setCPUValues(Meter* this, unsigned int cpu) { v[CPU_METER_IOWAIT] = cpuData->ioWaitPeriod / total * 100.0; } else { v[CPU_METER_KERNEL] = cpuData->systemAllPeriod / total * 100.0; + this->curItems = 3; + v[CPU_METER_IRQ] = (cpuData->stealPeriod + cpuData->guestPeriod) / total * 100.0; - this->curItems = 4; + if (settings->accountGuestInCPUMeter) { + this->curItems = 4; + } } percent = sumPositiveValues(v, this->curItems); diff --git a/pcp/Platform.c b/pcp/Platform.c index ad59ddfa1..014f91723 100644 --- a/pcp/Platform.c +++ b/pcp/Platform.c @@ -560,9 +560,13 @@ static double Platform_setOneCPUValues(Meter* this, const Settings* settings, pm v[CPU_METER_IOWAIT] = values[CPU_IOWAIT_PERIOD].ull / total * 100.0; } else { v[CPU_METER_KERNEL] = values[CPU_SYSTEM_ALL_PERIOD].ull / total * 100.0; + this->curItems = 3; + value = values[CPU_STEAL_PERIOD].ull + values[CPU_GUEST_PERIOD].ull; v[CPU_METER_IRQ] = value / total * 100.0; - this->curItems = 4; + if (settings->accountGuestInCPUMeter) { + this->curItems = 4; + } } percent = sumPositiveValues(v, this->curItems); From 0eec9d67ae3436a8cbdb589b5b599f1d15d01645 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Tue, 17 Mar 2026 09:39:14 +0800 Subject: [PATCH 2/9] netbsd & openbsd: Simplify CPU meter value assignment code Signed-off-by: Kang-Che Sung --- netbsd/Platform.c | 10 ++-------- openbsd/Platform.c | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/netbsd/Platform.c b/netbsd/Platform.c index 72f7df8b9..723d23d4c 100644 --- a/netbsd/Platform.c +++ b/netbsd/Platform.c @@ -268,16 +268,10 @@ double Platform_setCPUValues(Meter* this, int cpu) { if (host->settings->detailedCPUTime) { v[CPU_METER_KERNEL] = cpuData->sysPeriod / total * 100.0; v[CPU_METER_IRQ] = cpuData->intrPeriod / total * 100.0; - v[CPU_METER_SOFTIRQ] = 0.0; - v[CPU_METER_STEAL] = 0.0; - v[CPU_METER_GUEST] = 0.0; - v[CPU_METER_IOWAIT] = 0.0; - v[CPU_METER_FREQUENCY] = NAN; - this->curItems = 8; + this->curItems = 4; } else { v[CPU_METER_KERNEL] = cpuData->sysAllPeriod / total * 100.0; - v[CPU_METER_IRQ] = 0.0; // No steal nor guest on NetBSD - this->curItems = 4; + this->curItems = 3; } totalPercent = v[CPU_METER_NICE] + v[CPU_METER_NORMAL] + v[CPU_METER_KERNEL] + v[CPU_METER_IRQ]; totalPercent = CLAMP(totalPercent, 0.0, 100.0); diff --git a/openbsd/Platform.c b/openbsd/Platform.c index 0340e03e1..c40ebc472 100644 --- a/openbsd/Platform.c +++ b/openbsd/Platform.c @@ -219,16 +219,10 @@ double Platform_setCPUValues(Meter* this, unsigned int cpu) { if (host->settings->detailedCPUTime) { v[CPU_METER_KERNEL] = cpuData->sysPeriod / total * 100.0; v[CPU_METER_IRQ] = cpuData->intrPeriod / total * 100.0; - v[CPU_METER_SOFTIRQ] = 0.0; - v[CPU_METER_STEAL] = 0.0; - v[CPU_METER_GUEST] = 0.0; - v[CPU_METER_IOWAIT] = 0.0; - v[CPU_METER_FREQUENCY] = NAN; - this->curItems = 8; + this->curItems = 4; } else { v[CPU_METER_KERNEL] = cpuData->sysAllPeriod / total * 100.0; - v[CPU_METER_IRQ] = 0.0; // No steal nor guest on OpenBSD - this->curItems = 4; + this->curItems = 3; } totalPercent = v[CPU_METER_NICE] + v[CPU_METER_NORMAL] + v[CPU_METER_KERNEL] + v[CPU_METER_IRQ]; From 3a61af78cba37224acc6b5f89be0fa25abc62a01 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Wed, 18 Mar 2026 18:04:54 +0800 Subject: [PATCH 3/9] Revert "Fix CPU virtualization bar color and help text in non-detailed mode" This reverts commit 47aeb0a1f282fe294c6ba1e5dbebe05157b6e88c. --- Action.c | 4 ++-- CPUMeter.c | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Action.c b/Action.c index a6094ce9c..f09ed5393 100644 --- a/Action.c +++ b/Action.c @@ -782,8 +782,8 @@ static Htop_Reaction actionHelp(State* st) { addbartext(CRT_colors[CPU_IOWAIT], "/", "io-wait"); addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); } else { - addbartext(CRT_colors[CPU_GUEST], "/", "virt"); - addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); + addbartext(CRT_colors[CPU_GUEST], "/", "guest"); + addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); } addattrstr(CRT_colors[BAR_BORDER], "]"); diff --git a/CPUMeter.c b/CPUMeter.c index 7bc096dbc..960db5015 100644 --- a/CPUMeter.c +++ b/CPUMeter.c @@ -36,13 +36,6 @@ static const int CPUMeter_attributes[] = { CPU_IOWAIT }; -static const int CPUMeter_attributes_summary[] = { - CPU_NICE, - CPU_NORMAL, - CPU_SYSTEM, - CPU_GUEST -}; - typedef struct CPUMeterData_ { unsigned int cpus; Meter** meters; @@ -89,11 +82,6 @@ static void CPUMeter_updateValues(Meter* this) { const Machine* host = this->host; const Settings* settings = host->settings; - if (settings->detailedCPUTime) { - this->curAttributes = CPUMeter_attributes; - } else { - this->curAttributes = CPUMeter_attributes_summary; - } unsigned int cpu = this->param; if (cpu > host->existingCPUs) { From 7fb4d526ed2ade0da3eeecdf97051a11ee6aa214 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Wed, 18 Mar 2026 18:05:10 +0800 Subject: [PATCH 4/9] Restore "virtualized" word for non-detailed CPU meter in help screen The time displayed is (steal+guest), not just guest CPU time. Regression from 3d8fa0b926a463006773534411df91be2b9d68ed Signed-off-by: Kang-Che Sung --- Action.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Action.c b/Action.c index f09ed5393..5f5f4c1ea 100644 --- a/Action.c +++ b/Action.c @@ -782,8 +782,8 @@ static Htop_Reaction actionHelp(State* st) { addbartext(CRT_colors[CPU_IOWAIT], "/", "io-wait"); addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); } else { - addbartext(CRT_colors[CPU_GUEST], "/", "guest"); - addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); + addbartext(CRT_colors[CPU_GUEST], "/", "virtualized"); + addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); } addattrstr(CRT_colors[BAR_BORDER], "]"); From b7eb6865b80f5c2abd1c5dcf8ee261a9bf4f000f Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Wed, 18 Mar 2026 18:05:48 +0800 Subject: [PATCH 5/9] Use CPU_METER_STEAL for 'virtualized' CPU time in non-detailed mode Linux and PCP platform code used to write the 'virtualized' (steal+guest) CPU time into the CPU_METER_IRQ item when the "detailed CPU time" option is off, which would result in a wrong color painted for virtual CPU time in bar mode. Write the 'virtualized' CPU time to CPU_METER_STEAL instead, which is more appropriate for the job. Also update the "virtualized" CPU meter item in the help screen: (1) The color is now CPU_STEAL for consistency. (2) In monochrome mode, two dummy items are displayed before the "virtualized" word so that the bar meter symbol mapping is correct. Signed-off-by: Kang-Che Sung --- Action.c | 5 ++++- linux/Platform.c | 6 ++++-- pcp/Platform.c | 6 ++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Action.c b/Action.c index 5f5f4c1ea..6f60f85bd 100644 --- a/Action.c +++ b/Action.c @@ -781,8 +781,11 @@ static Htop_Reaction actionHelp(State* st) { addbartext(CRT_colors[CPU_GUEST], "/", "guest"); addbartext(CRT_colors[CPU_IOWAIT], "/", "io-wait"); addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); + } else if (CRT_colorScheme == COLORSCHEME_MONOCHROME) { + addbartext(CRT_colors[CPU_STEAL], "/-/-/", "virtualized"); + addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); } else { - addbartext(CRT_colors[CPU_GUEST], "/", "virtualized"); + addbartext(CRT_colors[CPU_STEAL], "/", "virtualized"); addbartext(CRT_colors[BAR_SHADOW], " ", "used%"); } addattrstr(CRT_colors[BAR_BORDER], "]"); diff --git a/linux/Platform.c b/linux/Platform.c index 97b0275c5..4c9245c03 100644 --- a/linux/Platform.c +++ b/linux/Platform.c @@ -373,9 +373,11 @@ double Platform_setCPUValues(Meter* this, unsigned int cpu) { v[CPU_METER_KERNEL] = cpuData->systemAllPeriod / total * 100.0; this->curItems = 3; - v[CPU_METER_IRQ] = (cpuData->stealPeriod + cpuData->guestPeriod) / total * 100.0; + v[CPU_METER_IRQ] = 0.0; // Accounted in 'kernel' + v[CPU_METER_SOFTIRQ] = 0.0; // Accounted in 'kernel' + v[CPU_METER_STEAL] = (cpuData->stealPeriod + cpuData->guestPeriod) / total * 100.0; if (settings->accountGuestInCPUMeter) { - this->curItems = 4; + this->curItems = 6; } } diff --git a/pcp/Platform.c b/pcp/Platform.c index 014f91723..c8c3e5d05 100644 --- a/pcp/Platform.c +++ b/pcp/Platform.c @@ -562,10 +562,12 @@ static double Platform_setOneCPUValues(Meter* this, const Settings* settings, pm v[CPU_METER_KERNEL] = values[CPU_SYSTEM_ALL_PERIOD].ull / total * 100.0; this->curItems = 3; + v[CPU_METER_IRQ] = 0.0; + v[CPU_METER_SOFTIRQ] = 0.0; value = values[CPU_STEAL_PERIOD].ull + values[CPU_GUEST_PERIOD].ull; - v[CPU_METER_IRQ] = value / total * 100.0; + v[CPU_METER_STEAL] = value / total * 100.0; if (settings->accountGuestInCPUMeter) { - this->curItems = 4; + this->curItems = 6; } } From cca350e694d49230d2482304e5fef7c854835b80 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Mon, 3 Nov 2025 00:09:57 +0800 Subject: [PATCH 6/9] Round graph meter dynamic scale and print graph scale Round the graph meter's dynamic scale to a power of two and print the graph scale. For a percent graph, a "%" character is printed in place of the scale. Signed-off-by: Kang-Che Sung --- Meter.c | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/Meter.c b/Meter.c index f921d0bdf..e9e8998cd 100644 --- a/Meter.c +++ b/Meter.c @@ -217,6 +217,22 @@ static const char* const GraphMeterMode_dotsAscii[] = { /*20*/":", /*21*/":", /*22*/":" }; +static void GraphMeterMode_printScale(int exponent) { + if (exponent < 10) { + // "1" to "512"; the (exponent < 0) case is not implemented. + assert(exponent >= 0); + printw("%3u", 1U << exponent); + } else if (exponent > (int)ARRAYSIZE(unitPrefixes) * 10 + 6) { + addstr("inf"); + } else if (exponent % 10 < 7) { + // "1K" to "64K", "1M" to "64M", "1G" to "64G", etc. + printw("%2u%c", 1U << (exponent % 10), unitPrefixes[exponent / 10 - 1]); + } else { + // "M/8" (=128K), "M/4" (=256K), "M/2" (=512K), "G/8" (=128M), etc. + printw("%c/%u", unitPrefixes[exponent / 10], 1U << (10 - exponent % 10)); + } +} + static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { assert(x >= 0); assert(w <= INT_MAX - x); @@ -274,6 +290,11 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { if (w < 1) { goto end; } + + bool needsScaleDisplay = h >= 2; + if (needsScaleDisplay) { + move(y + 1, x); // Cursor position for printing the scale + } x += captionLen; // Graph drawing style (character set, etc.) @@ -297,15 +318,30 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { } size_t i = nValues - (size_t)w * 2; - // Determine the graph scale + // Determine and print the graph scale double total = 1.0; + int scaleExp = 0; if (!isPercentChart) { + total = 0.0; for (size_t j = i; j < nValues; j++) { total = MAXIMUM(data->values[j], total); } assert(total <= DBL_MAX); + + (void)frexp(total, &scaleExp); + scaleExp = MAXIMUM(0, scaleExp); + + total = ldexp(1.0, scaleExp); + total = MINIMUM(DBL_MAX, total); } assert(total >= 1.0); + if (needsScaleDisplay) { + if (isPercentChart) { + addstr(" %"); + } else { + GraphMeterMode_printScale(scaleExp); + } + } // Draw the actual graph for (int col = 0; i < nValues - 1; i += 2, col++) { From c57819696c57d79d651689ec0f8028eb34181771 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Wed, 3 Dec 2025 00:34:52 +0800 Subject: [PATCH 7/9] Introduce powerOf2Floor() and popCount8() functions This is a prerequisite for the feature "Graph meter coloring (with GraphData structure rework)". powerOf2Floor() will utilize __builtin_clz() or stdc_bit_floor_ui() (__builtin_clz() is preferred) if either is supported. popCount8() will utilize ARM NEON instructions and x86 POPCNT instruction if the machine supports either of them. I am not adopting the C23 standard interface stdc_count_ones_uc() yet, as I am not sure C libraries would implement it as fast as our version. Signed-off-by: Kang-Che Sung --- XUtils.c | 10 ++++++++++ XUtils.h | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 51 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/XUtils.c b/XUtils.c index 59d006391..067cee335 100644 --- a/XUtils.c +++ b/XUtils.c @@ -389,3 +389,13 @@ unsigned int countTrailingZeros(unsigned int x) { return mod37BitPosition[(-x & x) % 37]; } #endif + +#if !defined(HAVE_BUILTIN_CLZ) && !defined(HAVE_STDC_BIT_FLOOR) +/* Returns the nearest power of two that is not greater than x. + If x is 0, returns 0. */ +unsigned int powerOf2Floor(unsigned int x) { + for (unsigned int shift = 1; shift < sizeof(x) * CHAR_BIT; shift <<= 1) + x |= x >> shift; + return x - (x >> 1); +} +#endif diff --git a/XUtils.h b/XUtils.h index d398b4fe1..68d06fdfc 100644 --- a/XUtils.h +++ b/XUtils.h @@ -15,14 +15,26 @@ in the source distribution for its full text. #endif #include +#include // IWYU pragma: keep #include #include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include // IWYU pragma: keep #include // IWYU pragma: keep #include "Macros.h" +#ifdef HAVE_STDBIT_H +#include +#endif + +#if defined(HAVE_ARM_NEON_H) && defined(__ARM_NEON) +// ARM C Language Extensions (ACLE) recommends us to check __ARM_NEON before +// including +#include +#endif + ATTR_NORETURN void fail(void); @@ -156,6 +168,46 @@ static inline unsigned int countTrailingZeros(unsigned int x) { unsigned int countTrailingZeros(unsigned int x); #endif +/* Returns the nearest power of two that is not greater than x. + If x is 0, returns 0. */ +#if defined(HAVE_BUILTIN_CLZ) +static inline unsigned int powerOf2Floor(unsigned int x) { + return !x ? 0 : 1U << ((int)sizeof(x) * CHAR_BIT - 1 - __builtin_clz(x)); +} +#elif defined(HAVE_STDC_BIT_FLOOR) +static inline unsigned int powerOf2Floor(unsigned int x) { + return stdc_bit_floor_ui(x); +} +#else +unsigned int powerOf2Floor(unsigned int x); +#endif + +static inline unsigned int popCount8(uint8_t x) { +#if defined(HAVE_ARM_NEON_H) && defined(__ARM_NEON) + // This is the smallest code possible for 8-bit bit count using ARM NEON. + // Might be smaller than __builtin_popcount. + // + // Initialize the vector register. Set all lanes at once so that the + // compiler will not emit instruction to zero-initialize other lanes. + uint8x8_t v = vdup_n_u8(x); + // Count the number of set bits for each lane (8-bit) in the vector. + v = vcnt_u8(v); + // Get lane 0 and discard lanes 1 to 7. (Return type was uint8_t) + return vget_lane_u8(v, 0); +#elif defined(HAVE_BUILTIN_POPCOUNT) && defined(__POPCNT__) + // x86 POPCNT instruction. __builtin_popcount translates to it when it is + // enabled ("-mpopcnt"). (Return type was int) + return (unsigned int)__builtin_popcount(x); +#else + // This code is optimized for uint8_t input and 32- or 64-bit processors. + // Might be smaller than __builtin_popcount (which is tuned for unsigned int + // input type and not uint8_t). + uint32_t n = (uint32_t)(x * 0x08040201ULL); + n = (uint32_t)(((n >> 3) & 0x11111111U) * 0x11111111ULL) >> 28; + return n; +#endif +} + /* IEC unit prefixes */ static const char unitPrefixes[] = { 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q' }; diff --git a/configure.ac b/configure.ac index 06b94683c..184d67223 100644 --- a/configure.ac +++ b/configure.ac @@ -218,7 +218,10 @@ m4_version_prereq( # Optional Section -AC_CHECK_HEADERS([execinfo.h]) +AC_CHECK_HEADERS([ \ + execinfo.h \ + stdbit.h \ +]) if test "$my_htop_platform" = darwin; then AC_CHECK_HEADERS([mach/mach_time.h]) @@ -352,13 +355,57 @@ AC_MSG_CHECKING(for __builtin_ctz) AC_COMPILE_IFELSE( [AC_LANG_PROGRAM( [], - [[__builtin_ctz(1); /* Supported in GCC 3.4 or later */]] + [[return __builtin_ctz(1U); /* Supported in GCC 3.4 or later */]] )], AC_DEFINE([HAVE_BUILTIN_CTZ], 1, [Define to 1 if the compiler supports '__builtin_ctz' function.]) AC_MSG_RESULT(yes), AC_MSG_RESULT(no) ) +AC_MSG_CHECKING(for __builtin_clz) +AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM( + [], + [[return __builtin_clz(-1U); /* Supported in GCC 3.4 or later */]] + )], + AC_DEFINE([HAVE_BUILTIN_CLZ], 1, [Define to 1 if the compiler supports '__builtin_clz' function.]) + AC_MSG_RESULT(yes), + AC_MSG_RESULT(no) +) + +AC_MSG_CHECKING(for __builtin_popcount) +AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM( + [], + [[return __builtin_popcount(0U); /* Supported in GCC 3.4 or later */]] + )], + AC_DEFINE([HAVE_BUILTIN_POPCOUNT], 1, [Define to 1 if the compiler supports '__builtin_popcount' function.]) + AC_MSG_RESULT(yes), + AC_MSG_RESULT(no) +) + +AC_MSG_CHECKING(for stdc_bit_floor) +AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [[ +#include + ]], [[ + /* Both the type-generic and type-specific versions should exist. + htop uses the type-specific version. */ + return stdc_bit_floor(0U) || stdc_bit_floor_ui(0U); + ]] + )], + AC_DEFINE([HAVE_STDC_BIT_FLOOR], 1, [Define to 1 if stdc_bit_floor functions are supported.]) + AC_MSG_RESULT(yes), + AC_MSG_RESULT(no) +) + +case "$host_cpu" in + arm*|aarch64*) + AC_CHECK_HEADERS([arm_neon.h]) + ;; +esac + # ---------------------------------------------------------------------- From 7a84121b7229563b357c7e87119ca7d9417c7e9e Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Tue, 10 Mar 2026 01:27:53 +0800 Subject: [PATCH 8/9] Graph meter coloring (with GraphData structure rework) Rewrite the entire graph meter drawing code to support color graph drawing in addition to the dynamic scaling (both can work together because of the new GraphData structure design). The colors of a graph are based on the percentages of item values of the meter. The rounding differences of each terminal character are addressed through the different numbers of braille dots. Due to low resolution of the character terminal, the rasterized colors may not look nice, but better than nothing. :) The new GraphData structure design has two technical limitations: * The height of a graph meter now has a limit of 8191 (terminal rows). * If dynamic scaling is used, the "total" value or scale of a graph now has to align to a power of 2. The code is designed with the anticipation that the graph height may change at runtime. No UI or option has been implemented for that yet. Signed-off-by: Kang-Che Sung --- Meter.c | 1035 +++++++++++++++++++++++++++++++++++++++++++++++++++---- Meter.h | 3 +- 2 files changed, 973 insertions(+), 65 deletions(-) diff --git a/Meter.c b/Meter.c index e9e8998cd..fd325ea43 100644 --- a/Meter.c +++ b/Meter.c @@ -26,11 +26,57 @@ in the source distribution for its full text. #include "XUtils.h" +#ifndef UINT16_WIDTH +#define UINT16_WIDTH 16 +#endif + #ifndef UINT32_WIDTH #define UINT32_WIDTH 32 #endif #define DEFAULT_GRAPH_HEIGHT 4 /* Unit: rows (lines) */ +#define MAX_GRAPH_HEIGHT 8191 /* == (int)(UINT16_MAX / 8) */ + +typedef struct GraphColorCell_ { + /* Meter item number for the cell's color. Item numbers [1, 255] correspond + to array indices [0, 254] respectively. 0 means no color for the cell. */ + uint8_t itemNum; + /* Bit field for how the character cell should be drawn. Each bit represents + one eighth of the cell. The LSB (bit 0) is the visual top eighth and the + MSB (bit 7) the visual bottom eighth. Some bit patterns are handled + specially. See the code of the printCellDetails() function. */ + uint8_t details; +} GraphColorCell; + +typedef union GraphDataCell_ { + int16_t scaleExp; + uint16_t numDots; + GraphColorCell c; +} GraphDataCell; + +typedef struct GraphDrawContext_ { + uint8_t maxItems; + bool isPercentChart; + size_t nCellsPerValue; +} GraphDrawContext; + +typedef struct GraphColorAdjStack_ { + double startPoint; + double fractionSum; + double valueSum; + uint8_t nItems; +} GraphColorAdjStack; + +typedef struct GraphColorAdjOffset_ { + uint32_t offsetVal; /* "offsetVal" requires at least 22 bits */ + unsigned int nCells; +} GraphColorAdjOffset; + +typedef struct GraphColorComputeState_ { + double valueSum; + unsigned int nCellsPainted; + uint8_t nItemsPainted; +} GraphColorComputeState; typedef struct MeterMode_ { Meter_Draw draw; @@ -197,6 +243,7 @@ static void BarMeterMode_draw(Meter* this, int x, int y, int w) { /* ---------- GraphMeterMode ---------- */ +#if 0 /* Used in old graph meter drawing code; to be removed */ #ifdef HAVE_LIBNCURSESW #define PIXPERROW_UTF8 4 @@ -216,6 +263,704 @@ static const char* const GraphMeterMode_dotsAscii[] = { /*10*/".", /*11*/".", /*12*/":", /*20*/":", /*21*/":", /*22*/":" }; +#endif + +static void GraphMeterMode_reallocateGraphBuffer(Meter* this, const GraphDrawContext* context, size_t nValues) { + GraphData* data = &this->drawData; + + assert(context->nCellsPerValue <= SIZE_MAX / sizeof(GraphDataCell)); + size_t nCellsPerValue = context->nCellsPerValue; + size_t valueSize = nCellsPerValue * sizeof(GraphDataCell); + + if (!valueSize) + goto bufferInitialized; + + data->buffer = xReallocArray(data->buffer, nValues, valueSize); + + // Move existing records ("values") to correct position + assert(nValues >= data->nValues); + size_t moveOffset = (nValues - data->nValues) * nCellsPerValue; + GraphDataCell* dest = &((GraphDataCell*)data->buffer)[moveOffset]; + memmove(dest, data->buffer, data->nValues * valueSize); + + // Fill new spaces with blank records + memset(data->buffer, 0, moveOffset * sizeof(GraphDataCell)); + +bufferInitialized: + data->nValues = nValues; +} + +static inline size_t GraphMeterMode_valueCellIndex(unsigned int h, bool isPercentChart, int deltaExp, unsigned int y, unsigned int* scaleFactor, unsigned int* stride) { + assert(deltaExp >= 0); + assert(deltaExp < UINT16_WIDTH); + + // This function returns an index from the start of a record to the + // GraphColorCell object. This function may be called for reading the cell + // or writing it. When called for reading, the "scaleFactor" pointer must + // not be NULL. The caller is responsible for checking the index is in + // bounds before accessing the cell object. + + if (scaleFactor) + *scaleFactor = 1; + + // The "stride" value is the distance to the next data cell to write to in + // the given scale ("deltaExp"). + if (stride) + *stride = isPercentChart ? 1 : (2U << deltaExp); + + if (isPercentChart) { + assert(deltaExp == 0); + return y; + } + + // A record may be drawn in different scales depending on the largest + // "scaleExp" value of a record set. The colors are precomputed for + // different scales of the same record. It takes (2 * h - 1) cells of space + // to store all the color information. + // + // An example for h = 6: + // + // scale 1*n 2*n 4*n 8*n 16*n | n = value sum of all items + // --------------------------------- | rounded up to a power of + // deltaExp 0 1 2 3 4 | two. The exponent of n is + // --------------------------------- | stored in index [0]. + // array [11] X X X X | X = empty cell + // indices [9] X X X X | Cells whose array indices + // [7] X X X X | are >= (2 * h) are computed + // [5] [10] X X X | from cells of a lower scale + // [3] [6] (12) X X | ("scaleCellDetails") and not + // [1] [2] [4] [8] (16) | stored in the array. + + // "b" is the "base" offset or the upper bits of offset + unsigned int b = (y * 2) << deltaExp; + unsigned int offset = 1U << deltaExp; + + if (!scaleFactor) { + // This function is called for writing. + return b + offset; + } + + // This function is called for reading. + assert(!stride); + + unsigned int offsetTop = (h * 2 - 1) ^ b; + assert(offsetTop != 0); + + if (!offsetTop || offsetTop >= offset) { + // The (!offsetTop) conditional serves as an optimization hint + return b + offset; + } + + offsetTop = powerOf2Floor(offsetTop); + assert(offsetTop != 0); + if (offsetTop) + *scaleFactor = offset / offsetTop; + + return b + offsetTop; +} + +static uint8_t GraphMeterMode_findTopCellItem(const Meter* this, double scaledTotal, unsigned int topCell) { + unsigned int h = (unsigned int)this->h; + assert(topCell < h); + + double valueSum = 0.0; + double maxValue = 0.0; + uint8_t topCellItem = this->curItems - 1; + for (uint8_t i = 0; i < this->curItems && valueSum < DBL_MAX; i++) { + double value = this->values[i]; + if (!isPositive(value)) + continue; + + double newValueSum = MINIMUM(DBL_MAX, valueSum + value); + + if (value > DBL_MAX - valueSum) { + value = DBL_MAX - valueSum; + // This assumption holds for the new "value" as long as the rounding + // mode is consistent. + assert(newValueSum < DBL_MAX || valueSum + value >= DBL_MAX); + } + + valueSum = newValueSum; + + // Find the item that occupies the largest area of the top cell. + // Favor the item with higher index in case of a tie. + + if (topCell > 0) { + double topPoint = (valueSum / scaledTotal) * (double)(int32_t)h; + assert(topPoint >= 0.0); + + if (!(topPoint > (double)(int32_t)topCell)) + continue; + + // This code assumes the default FP rounding mode (i.e. to nearest), + // which requires "area" to be at least (DBL_EPSILON / 2) to win. + + double area = (value / scaledTotal) * (double)(int32_t)h; + assert(area >= 0.0); + + area = MINIMUM(topPoint - (double)(int32_t)topCell, area); + + value = area; + } else { + // No need to compute "area" in this case. Comparing "value" directly + // will give us more precision. + } + if (value >= maxValue) { + maxValue = value; + topCellItem = i; + } + } + return topCellItem; +} + +static int8_t GraphMeterMode_needsExtraCell(unsigned int h, double scaledTotal, unsigned int y, const GraphColorAdjStack* stack, const GraphColorAdjOffset* adjOffset) { + double areaSum = (stack->fractionSum + stack->valueSum / scaledTotal) * (double)(int32_t)h; + double adjOffsetVal = adjOffset ? (double)(int32_t)adjOffset->offsetVal : 0.0; + double halfPoint = (double)(int32_t)y + 0.5; + + // Calculate the best position for rendering this stack of items. Then, + // determine if, by adding a character cell to the item before the stack + // ("rItem" in the context of "computeColors" function), it could distort + // less when this stack of items is drawn on the terminal screen. + // "True" (1), "false" (0), or "undetermined" (-1). + // + // Given real numbers a, b, c and d (a <= b <= c <= d), then: + // 1. The smallest value for (x - a)^2 + (x - b)^2 + (x - c)^2 + (x - d)^2 + // happens when x == (a + b + c + d) / 4; x is the "arithmetic mean". + // 2. The smallest value for |y - a| + |y - b| + |y - c| + |y - d| + // happens when b <= y <= c; y is the "median". + // Both kinds of averages are acceptable. The arithmetic mean is chosen here + // because it is cheaper to produce. + // + // averagePoint := stack->startPoint + (areaSum / (stack->nItems * 2)) + // adjStartPoint := averagePoint - (adjOffsetVal / (stack->nItems * 2)) + + // Intended to compare this but with greater precision: + // isgreater(adjStartPoint, halfPoint) + if (areaSum - adjOffsetVal > (halfPoint - stack->startPoint) * 2.0 * stack->nItems) + return 1; + if (areaSum - adjOffsetVal < (halfPoint - stack->startPoint) * 2.0 * stack->nItems) + return 0; + + assert(stack->valueSum <= DBL_MAX); + double stackArea = (stack->valueSum / scaledTotal) * (double)(int32_t)h; + double adjNCells = adjOffset ? (double)(int32_t)adjOffset->nCells : 0.0; + + // Intended to compare this but with greater precision: + // (stack->startPoint + (stackArea / 2) > halfPoint + (adjNCells / 2)) + if (stackArea - adjNCells > (halfPoint - stack->startPoint) * 2.0) + return 1; + if (stackArea - adjNCells < (halfPoint - stack->startPoint) * 2.0) + return 0; + + return -1; +} + +static void GraphMeterMode_addItemAdjOffset(GraphColorAdjOffset* adjOffset, unsigned int nCells) { + adjOffset->offsetVal += (uint32_t)adjOffset->nCells * 2 + nCells; + adjOffset->nCells += nCells; +} + +static void GraphMeterMode_addItemAdjStack(GraphColorAdjStack* stack, double scaledTotal, double value) { + assert(scaledTotal <= DBL_MAX); + assert(stack->valueSum < DBL_MAX); + + stack->fractionSum += (stack->valueSum / scaledTotal) * 2.0; + stack->valueSum += value; + + assert(stack->nItems < UINT8_MAX); + stack->nItems++; +} + +static uint16_t GraphMeterMode_makeDetailsMask(const GraphColorComputeState* prev, const GraphColorComputeState* new, double prevTopPoint, double rem, int blanksAtTopCell) { + assert(new->nCellsPainted > prev->nCellsPainted); + assert(rem >= 0.0); + assert(rem < 1.0); + + double numDots = ceil(rem * 8.0); + + uint8_t blanksAtEnd; + int8_t roundDirInAscii = 0; + if (blanksAtTopCell >= 0) { + // Caller indicates this is the "top cell" of the record. + assert(blanksAtTopCell < 8); + blanksAtEnd = (uint8_t)blanksAtTopCell; + roundDirInAscii = 1; + } else if (prev->nCellsPainted == 0 || prevTopPoint <= (double)(int32_t)prev->nCellsPainted || (uint8_t)numDots == 0) { + // Align the dots to the bottom. The "blanksAtStart" will equal to 0. + // The (numDots == 0) case also goes here for code size: It implies + // (rem == 0.0) and will set both "blanksAtEnd" and "blanksAtStart" to 0. + blanksAtEnd = (uint8_t)(8 - (uint8_t)numDots) % 8; + } else { + // While the number of dots to paint for an item is rounded up, the + // positioning of the dots rounds to nearest for a visual reason. + // In case of a tie, round to the lower position of the graph, i.e. MSB + // of the "details" data. + + double distance = (prevTopPoint - (double)(int32_t)prev->nCellsPainted) + rem * 0.5; + + // Tiebreaking direction that may be needed in the ASCII display mode. + if (distance > 0.5) { + roundDirInAscii = 1; + } else if (distance < 0.5) { + roundDirInAscii = -1; + } + + distance *= 8.0; + if ((uint8_t)numDots % 2 == 0) + distance -= 0.5; + distance = ceil(distance); + assert(distance >= 0.0); + assert(distance < INT_MAX); + + unsigned int blanksRem = 8 - (unsigned int)(int)numDots / 2; + blanksRem -= MINIMUM(blanksRem, (unsigned int)(int)distance); + blanksAtEnd = (uint8_t)blanksRem; + } + assert(blanksAtEnd < 8); + + uint8_t blanksAtStart = 0; + if (prev->nCellsPainted > 0) { + blanksAtStart = (uint8_t)(8 - (uint8_t)numDots - blanksAtEnd) % 8; + } else { + // Always zero blanks for the first cell. + // When an item would be painted with all cells (from the first cell to + // the "top cell"), it is expected that the bar would be "stretched" to + // represent the sum of the record. + blanksAtStart = 0; + } + assert(blanksAtStart < 8); + + uint16_t mask = 0xFFFFU >> blanksAtStart; + // See the code and comments of the "printCellDetails" function for how + // special bits are used. + bool needsTiebreak = blanksAtStart >= 2 && blanksAtStart < 4 && blanksAtStart == blanksAtEnd; + + if (new->nCellsPainted - prev->nCellsPainted == 1) { + assert(blanksAtStart + blanksAtEnd < 8); + if (roundDirInAscii > 0 && needsTiebreak) { + // Set to display only the upper half in the ASCII display mode. + assert((mask & 0x0800) != 0); + mask ^= 0x0800; + blanksAtEnd = 2; + } + mask >>= 8; + } else if (roundDirInAscii > 0) { + if (blanksAtStart < 4 && (uint8_t)(blanksAtStart + blanksAtEnd % 4) >= 4) { + // Set to display only the upper half for the bottom cell in the ASCII + // display mode. + assert((mask & 0x0800) != 0); + mask ^= 0x0800; + } + } + + mask = (uint16_t)((mask >> blanksAtEnd) << blanksAtEnd); + + if (roundDirInAscii < 0) { + assert(blanksAtStart <= blanksAtEnd); + if ((mask | 0x4000) == 0x7FF8) { + // This special case is the combination of the 4 conditionals, shown + // as asserts below. + assert(new->nCellsPainted - prev->nCellsPainted > 1); + assert(blanksAtEnd < 4); + assert(blanksAtStart % 4 + blanksAtEnd >= 4); + assert(blanksAtStart < blanksAtEnd); + } + + if (needsTiebreak || (mask | 0x4000) == 0x7FF8) { + // Set to display only the lower half for the top cell in the ASCII + // display mode. + assert((mask & 0x0010) != 0); + mask = (mask & 0xFFEF) | 0x0020; + } + } + + // The following result values are impossible as they lack special bits + // needed for the ASCII display mode. + assert(mask != 0x3FF8); // Should be 0x37F8 or 0x3FE8 + assert(mask != 0x7FF8); // Should be 0x77F8 or 0x7FE8 + assert(mask != 0x1FFC); // Should be 0x17FC + assert(mask != 0x1FFE); // Should be 0x17FE + + return mask; +} + +static void GraphMeterMode_paintCellsForItem(GraphDataCell* cellsStart, unsigned int stride, uint8_t itemIndex, unsigned int nCells, uint16_t mask) { + GraphDataCell* cell = cellsStart; + while (nCells > 0) { + cell->c.itemNum = itemIndex + 1; + if (nCells == 1) { + cell->c.details = (uint8_t)mask; + } else if (cell == cellsStart) { + cell->c.details = mask >> 8; + } else { + cell->c.details = 0xFF; + } + nCells--; + cell += stride; + } +} + +static void GraphMeterMode_computeColors(Meter* this, const GraphDrawContext* context, GraphDataCell* valueStart, int deltaExp, double scaledTotal, unsigned int numDots) { + unsigned int h = (unsigned int)this->h; + bool isPercentChart = context->isPercentChart; + + assert(deltaExp >= 0); + assert(numDots > 0); + assert(numDots <= h * 8); + + unsigned int stride; + size_t firstCellIndex = GraphMeterMode_valueCellIndex(h, isPercentChart, deltaExp, 0, NULL, &stride); + assert(firstCellIndex < context->nCellsPerValue); + + // The top cell of the record in this given scale + const uint8_t dotAlignment = 2; + unsigned int blanksAtTopCell = (8 - 1 - (numDots - 1) % 8) / dotAlignment * dotAlignment; + unsigned int topCell = (numDots - 1) / 8; + + bool hasPartialTopCell = false; + if (blanksAtTopCell > 0) { + hasPartialTopCell = true; + } else if (!isPercentChart && topCell % 2 == 0 && ((topCell + 1) << deltaExp) >= h) { + // This "top cell" is rendered as full in one scale, but partial in the + // next scale. (Only happens when "h" is not a power of two.) + hasPartialTopCell = true; + } + + double topCellArea = 0.0; + assert(this->curItems > 0); + uint8_t topCellItem = this->curItems - 1; + if (hasPartialTopCell) { + // Allocate the "top cell" first. The item that acquires the "top cell" + // will have a smaller "area" for the remainder calculation below. + topCellArea = (8 - (int)blanksAtTopCell) / 8.0; + topCellItem = GraphMeterMode_findTopCellItem(this, scaledTotal, topCell); + } + + GraphColorComputeState restart = { + .valueSum = 0.0, + .nCellsPainted = 0, + .nItemsPainted = 0 + }; + double thresholdHigh = 1.0; + double thresholdLow = 0.0; + double threshold = 0.5; + bool rItemIsDetermined = false; + bool rItemHasExtraCell = true; + unsigned int nCellsToPaint = topCell + 1; + bool isLastTiebreak = false; + unsigned int nCellsPaintedHigh = nCellsToPaint + topCellItem + 1; + unsigned int nCellsPaintedLow = 0; + + while (true) { + GraphColorComputeState prev = restart; + double nextThresholdLow = thresholdHigh; + double nextThresholdHigh = thresholdLow; + bool hasThresholdRange = thresholdLow < thresholdHigh; + GraphColorAdjStack stack = { + .startPoint = 0.0, + .fractionSum = 0.0, + .valueSum = 0.0, + .nItems = 0 + }; + GraphColorAdjOffset adjSmall = { + .offsetVal = 0, + .nCells = 0 + }; + GraphColorAdjOffset adjLarge = adjSmall; + + while (prev.nItemsPainted <= topCellItem && prev.valueSum < DBL_MAX) { + double value = this->values[prev.nItemsPainted]; + if (!isPositive(value)) { + if (restart.nItemsPainted == prev.nItemsPainted) + restart.nItemsPainted++; + prev.nItemsPainted++; + continue; + } + + GraphColorComputeState new; + + new.valueSum = MINIMUM(DBL_MAX, prev.valueSum + value); + + if (value > DBL_MAX - prev.valueSum) { + value = DBL_MAX - prev.valueSum; + // This assumption holds for the new "value" as long as the + // rounding mode is consistent. + assert(new.valueSum < DBL_MAX || prev.valueSum + value >= DBL_MAX); + } + + double area = (value / scaledTotal) * (double)(int32_t)h; + assert(area >= 0.0); // "area" can be 0.0 when the division underflows + double rem = area; + + if (prev.nItemsPainted == topCellItem) + rem = MAXIMUM(area, topCellArea) - topCellArea; + + unsigned int nCells = (unsigned int)(int)rem; + rem -= (int)rem; + + // Whether the item will receive an extra cell or be truncated. + // The main method is known as the "largest remainder method". + + // An item whose remainder reaches the Droop quota may either receive + // an extra cell or need a tiebreak (a tie caused by rounding). + // This is the highest threshold we might need to compare with. + bool reachesDroopQuota = rem * (double)(int32_t)(h + 1) > (double)(int32_t)h; + if (reachesDroopQuota && rem < thresholdHigh) + thresholdHigh = rem; + + bool equalsThreshold = false; + bool isInThresholdRange = rem <= thresholdHigh && rem >= thresholdLow; + + assert(threshold > 0.0); + assert(threshold <= 1.0); + if (rem > threshold) { + nextThresholdLow = MINIMUM(rem, nextThresholdLow); + nCells++; + } else if (rem < threshold) { + nextThresholdHigh = MAXIMUM(rem, nextThresholdHigh); + rem = 0.0; + } else if (hasThresholdRange) { + assert(!rItemIsDetermined); + nCells++; + } else if (restart.nItemsPainted >= prev.nItemsPainted) { + assert(restart.nItemsPainted == prev.nItemsPainted); + + // This item will be nicknamed "rItem". Whether the "rItem" will + // receive an extra cell is determined by the rest of the loop. + if (!rItemIsDetermined) { + stack.startPoint = (new.valueSum / scaledTotal) * (double)(int32_t)h; + rem = 0.0; + } else if (rItemHasExtraCell) { + nCells++; + } else { + rem = 0.0; + } + } else { + equalsThreshold = true; + rem = 0.0; + + unsigned int y = prev.nCellsPainted - adjSmall.nCells; + unsigned int rItemMinCells = y - restart.nCellsPainted; + + // The first cell and last cell are painted with dots aligned to + // the bottom and top respectively. If multiple items' remainders + // equal the threshold and would be painted on the same cell, give + // priority to the first or last of the items respectively. + + if (prev.nCellsPainted == 0) { + assert(adjSmall.nCells == 0); + rItemHasExtraCell = true; + } else if (y + 1 >= nCellsToPaint) { + assert(y + 1 == nCellsToPaint); + assert(adjSmall.nCells == 0); + assert(nCells == 0); + rItemHasExtraCell = false; + } else if (!rItemHasExtraCell) { + assert(adjLarge.nCells > adjSmall.nCells); + + int8_t res = GraphMeterMode_needsExtraCell(h, scaledTotal, y, &stack, &adjLarge); + if (res > 0 || (res < 0 && rItemMinCells <= nCells)) { + rItemHasExtraCell = true; + } + } else { + int8_t res = GraphMeterMode_needsExtraCell(h, scaledTotal, y, &stack, &adjSmall); + if (res == 0 || (res < 0 && (rItemMinCells > nCells || prev.nCellsPainted + 1 >= nCellsToPaint))) { + rItemHasExtraCell = false; + } + } + } + + if (!hasThresholdRange && restart.nItemsPainted < prev.nItemsPainted) { + GraphMeterMode_addItemAdjOffset(&adjSmall, nCells); + GraphMeterMode_addItemAdjOffset(&adjLarge, nCells + equalsThreshold); + GraphMeterMode_addItemAdjStack(&stack, scaledTotal, value); + } + + if (hasPartialTopCell && prev.nItemsPainted == topCellItem) + nCells++; + + new.nCellsPainted = prev.nCellsPainted + nCells; + new.nItemsPainted = prev.nItemsPainted + 1; + + // Update the "restart" state if needed + if (restart.nItemsPainted >= prev.nItemsPainted) { + if (!isInThresholdRange) { + restart = new; + } else if (rItemIsDetermined) { + restart = new; + rItemIsDetermined = isLastTiebreak; + rItemHasExtraCell = true; + } + } + + // Paint cells to the buffer + + if (hasPartialTopCell && prev.nItemsPainted == topCellItem) { + // Re-calculate the remainder with the top cell area included + if (rem > 0.0) { + // Has extra cell won from the largest remainder method + rem = area; + } else { + // Did not win extra cell from the remainder + rem = MINIMUM(area, topCellArea); + } + rem -= (int)rem; + } + + bool isItemOnEdge = (prev.nCellsPainted == 0 || new.nCellsPainted == nCellsToPaint); + if (isItemOnEdge && area < (0.125 * dotAlignment)) + rem = (0.125 * dotAlignment); + + if (nCells > 0 && new.nCellsPainted <= nCellsToPaint) { + double prevTopPoint = (prev.valueSum / scaledTotal) * (double)(int32_t)h; + int blanksAtTopCellArg = (new.nCellsPainted == nCellsToPaint) ? (int)blanksAtTopCell : -1; + uint16_t mask = GraphMeterMode_makeDetailsMask(&prev, &new, prevTopPoint, rem, blanksAtTopCellArg); + + GraphDataCell* cellsStart = &valueStart[firstCellIndex + (size_t)stride * prev.nCellsPainted]; + GraphMeterMode_paintCellsForItem(cellsStart, stride, prev.nItemsPainted, nCells, mask); + } + + prev = new; + } + + if (hasThresholdRange) { + if (prev.nCellsPainted == nCellsToPaint) + break; + + // Set new threshold range + if (prev.nCellsPainted > nCellsToPaint) { + nCellsPaintedHigh = prev.nCellsPainted; + assert(thresholdLow < threshold); + thresholdLow = threshold; + } else { + nCellsPaintedLow = prev.nCellsPainted + 1; + assert(thresholdHigh > nextThresholdHigh); + thresholdHigh = nextThresholdHigh; + nextThresholdLow = thresholdLow; + } + + // Make new threshold value + threshold = thresholdHigh; + hasThresholdRange = thresholdLow < thresholdHigh; + if (hasThresholdRange && nCellsPaintedLow < nCellsPaintedHigh) { + // Linear interpolation + assert(nCellsPaintedLow <= nCellsToPaint); + threshold -= ((thresholdHigh - thresholdLow) * (double)(int32_t)(nCellsToPaint - nCellsPaintedLow) / (double)(int32_t)(nCellsPaintedHigh - nCellsPaintedLow)); + threshold = MAXIMUM(nextThresholdLow, threshold); + } + assert(threshold <= thresholdHigh); + } else if (restart.nItemsPainted <= topCellItem && restart.valueSum < DBL_MAX) { + if (prev.nCellsPainted - adjSmall.nCells + adjLarge.nCells < nCellsToPaint) { + rItemHasExtraCell = true; + isLastTiebreak = true; + } else if (prev.nCellsPainted >= nCellsToPaint) { + assert(prev.nCellsPainted == nCellsToPaint); + break; + } + rItemIsDetermined = true; + } else { + assert(restart.nCellsPainted == nCellsToPaint); + break; + } + } +} + +static void GraphMeterMode_recordNewValue(Meter* this, const GraphDrawContext* context) { + uint8_t maxItems = context->maxItems; + bool isPercentChart = context->isPercentChart; + size_t nCellsPerValue = context->nCellsPerValue; + if (!nCellsPerValue) + return; + + GraphData* data = &this->drawData; + size_t nValues = data->nValues; + unsigned int h = (unsigned int)this->h; + + // Move previous records + size_t valueSize = nCellsPerValue * sizeof(GraphDataCell); + GraphDataCell* valueStart = (GraphDataCell*)data->buffer; + valueStart = &valueStart[1 * nCellsPerValue]; + memmove(data->buffer, valueStart, (nValues - 1) * valueSize); + + valueStart = (GraphDataCell*)data->buffer; + valueStart = &valueStart[(nValues - 1) * nCellsPerValue]; + + // Sum the values of all items + double sum = 0.0; + if (this->curItems > 0) { + sum = Meter_computeSum(this); + assert(sum >= 0.0); + assert(sum <= DBL_MAX); + } + + // "total" refers to the value that we would draw as full in graph + double total = sum; + if (isPercentChart) { + total = MAXIMUM(this->total, total); + } else { + // Dynamic scale. "this->total" is ignored. + // Determine the scale and "total" that we need afterward. The "total" is + // rounded up to a power of 2. + int scaleExp = 0; + (void)frexp(total, &scaleExp); + scaleExp = MAXIMUM(0, scaleExp); + + // It's safe to assume "scaleExp" never overflows when IEEE 754 + // (binary64) floating point is used. IEEE 754 always sets the limit + // DBL_MAX_10_EXP == 308. + assert(DBL_MAX_10_EXP <= 308); + assert(scaleExp <= INT16_MAX); + + valueStart[0].scaleExp = (int16_t)scaleExp; + total = ldexp(1.0, scaleExp); + } + // Prevent overflow from "this->total" or ldexp(). + total = MINIMUM(DBL_MAX, total); + + assert(h <= UINT16_MAX / 8); + double maxDots = (double)(int32_t)(h * 8); + + // The total number of dots that we would draw for this record + unsigned int numDots = 0; + if (total > 0.0 && sum > 0.0) { + numDots = (unsigned int)(int32_t)ceil((sum / total) * maxDots); + numDots = MAXIMUM(1, numDots); // Division of (sum / total) can underflow + } + assert(numDots <= UINT16_MAX - (8 - 1)); + + if (maxItems == 1) { + // Record the number of dots in the graph data buffer. + valueStart[isPercentChart ? 0 : 1].numDots = (uint16_t)numDots; + return; + } + + // This is a meter of multiple items. + // First clear the cells, which might contain data of the previous record. + unsigned int y = (numDots + (8 - 1)) / 8; // Round up + size_t i = GraphMeterMode_valueCellIndex(h, isPercentChart, 0, y, NULL, NULL); + if (i < nCellsPerValue) + memset(&valueStart[i], 0, (nCellsPerValue - i) * sizeof(*valueStart)); + + if (numDots <= 0) { + return; // The record is empty. No colors needed. + } + + // Then precompute and store the colors of the cells in the record. + int deltaExp = 0; + double scaledTotal = total; + assert(scaledTotal > 0.0); + while (true) { + GraphMeterMode_computeColors(this, context, valueStart, deltaExp, scaledTotal, numDots); + + if (isPercentChart || ((h - 1) >> deltaExp) == 0 || !(scaledTotal < DBL_MAX)) + break; + + deltaExp++; + scaledTotal = MINIMUM(DBL_MAX, scaledTotal * 2.0); + numDots = (numDots - 1) / 2 + 1; + } +} static void GraphMeterMode_printScale(int exponent) { if (exponent < 10) { @@ -233,6 +978,193 @@ static void GraphMeterMode_printScale(int exponent) { } } +static uint8_t GraphMeterMode_scaleCellDetails(uint8_t details, unsigned int scaleFactor) { + // This scaling routine is only used on the "top cell" of a record. + // (The "top cell" never uses the special meaning of bit 4.) + + assert(scaleFactor > 0); + if (scaleFactor < 2) + return details; + + // This algorithm assumes the "details" display as a dot matrix with two + // sub-character columns (and four sub-character rows). + if (scaleFactor < 4 && (details & 0x0F) != 0x00) { + // Display the cell in half height (bits 0 to 3 are zero). + // Bits 4 and 5 are set together to avoid a jaggy visual. + uint8_t newDetails = 0x30; + // Bit 6 + if (popCount8(details) > 4) + newDetails |= 0x40; + // Bit 7 (equivalent to (details >= 0x80 || popCount8(details) > 6)) + if (details >= 0x7F) + newDetails |= 0x80; + return newDetails; + } + if (details != 0x00) { + // Display the cell in a quarter height (bits 0 to 5 are zero). + // Bits 6 and 7 are set together to avoid a jaggy visual. + return 0xC0; + } + return 0x00; +} + +static int GraphMeterMode_lookupCell(const Meter* this, const GraphDrawContext* context, int scaleExp, size_t valueIndex, unsigned int y, uint8_t* details) { + unsigned int h = (unsigned int)this->h; + const GraphData* data = &this->drawData; + + uint8_t maxItems = context->maxItems; + bool isPercentChart = context->isPercentChart; + size_t nCellsPerValue = context->nCellsPerValue; + + // Reverse the coordinate + assert(y < h); + y = h - 1 - y; + + uint8_t itemIndex = (uint8_t)-1; + *details = 0x00; // Empty the cell + + assert(valueIndex < data->nValues); + const GraphDataCell* valueStart = (const GraphDataCell*)data->buffer; + valueStart = &valueStart[valueIndex * nCellsPerValue]; + + int deltaExp = 0; + if (!isPercentChart) { + // The "scaleExp" member exists only for "dynamic scale" meters (i.e. + // "isPercentChart" being false). + assert(scaleExp >= valueStart[0].scaleExp); + deltaExp = scaleExp - valueStart[0].scaleExp; + } + + if (maxItems == 1) { + unsigned int numDots = valueStart[isPercentChart ? 0 : 1].numDots; + + if (numDots < 1) + goto cellIsEmpty; + + // Scale according to exponent difference. Round up. + numDots = deltaExp < UINT16_WIDTH ? ((numDots - 1) >> deltaExp) : 0; + numDots++; + + if (y > (numDots - 1) / 8) + goto cellIsEmpty; + + itemIndex = 0; + *details = 0xFF; + if (y == (numDots - 1) / 8) { + const uint8_t dotAlignment = 2; + unsigned int blanksAtTopCell = (8 - 1 - (numDots - 1) % 8) / dotAlignment * dotAlignment; + *details <<= blanksAtTopCell; + } + } else { + int deltaExpArg = MINIMUM(UINT16_WIDTH - 1, deltaExp); + + unsigned int scaleFactor; + size_t i = GraphMeterMode_valueCellIndex(h, isPercentChart, deltaExpArg, y, &scaleFactor, NULL); + if (i >= nCellsPerValue) + goto cellIsEmpty; + + if (deltaExp >= UINT16_WIDTH) { + // Any "scaleFactor" value greater than 8 behaves the same as 8 for + // the "scaleCellDetails" function. + scaleFactor = 8; + } + + const GraphDataCell* cell = &valueStart[i]; + itemIndex = cell->c.itemNum - 1; + *details = GraphMeterMode_scaleCellDetails(cell->c.details, scaleFactor); + } + /* fallthrough */ + +cellIsEmpty: + if (y == 0) + *details |= 0xC0; + + if (itemIndex == (uint8_t)-1) + return BAR_SHADOW; + + assert(itemIndex < maxItems); + return Meter_attributes(this)[itemIndex]; +} + +static void GraphMeterMode_printCellDetails(uint8_t details) { + if (details == 0x00) { + // Use ASCII space instead. A braille blank character may display as a + // substitute block and is less distinguishable from a cell with data. + addch(' '); + return; + } + +#ifdef HAVE_LIBNCURSESW + if (CRT_utf8) { + // Bits 3 and 4 of "details" might carry special meaning. When the whole + // byte contains specific bit patterns, it indicates that only half cell + // should be displayed in the ASCII display mode. The bits are supposed + // to be filled in the Unicode display mode. + if ((details & 0x9C) == 0x14 || (details & 0x39) == 0x28) { + if (details == 0x14 || details == 0x28) { // Special case + details = 0x18; + } else { + details |= 0x18; + } + } + + // Convert GraphDataCell.c.details bit representation to Unicode braille + // dot ordering. + // (Bit0) a b (Bit3) From: h g f e d c b a (binary) + // (Bit1) c d (Bit4) | | | X X | + // (Bit2) e f (Bit5) | | | | \ / | | + // (Bit6) g h (Bit7) | | | | X | | + // To: 0x2800 + h g f d b e c a + // Braille Patterns [U+2800, U+28FF] in UTF-8: [E2 A0 80, E2 A3 BF] + char sequence[] = "\xE2\xA0\x80"; + // Bits 6 and 7 are in the second byte of the UTF-8 sequence. + sequence[1] |= details >> 6; + // Bits 0 to 5 are in the third byte. + // The algorithm is optimized for x86 and ARM. + uint32_t n = details * 0x01010101U; + n = (uint32_t)((n & 0x08211204U) * 0x02110408ULL) >> 26; + sequence[2] |= n; + addstr(sequence); + return; + } +#endif + + // ASCII display mode + const char upperHalf = '`'; + const char lowerHalf = '.'; + const char fullCell = ':'; + char c; + + // Detect special cases where we should print only half of the cell. + if ((details & 0x9C) == 0x14) { + c = upperHalf; + } else if ((details & 0x39) == 0x28) { + c = lowerHalf; + // End of special cases + } else if (popCount8(details) > 4) { + c = fullCell; + } else { + // Determine which half has more dots than the other. + uint8_t inverted = details ^ 0x0F; + int difference = (int)popCount8(inverted) - 4; + if (difference < 0) { + c = upperHalf; + } else if (difference > 0) { + c = lowerHalf; + } else { + // Give weight to dots closer to the top or bottom of the cell (LSB or + // MSB, respectively) as a tiebreaker. + // Reverse bits 0 to 3 and subtract it from bits 4 to 7. + // The algorithm is optimized for x86 and ARM. + uint32_t n = inverted * 0x01010101U; + n = (uint32_t)((n & 0xF20508U) * 0x01441080ULL) >> 27; + difference = (int)n - 0x0F; + c = difference < 0 ? upperHalf : lowerHalf; + } + } + addch(c); +} + static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { assert(x >= 0); assert(w <= INT_MAX - x); @@ -248,21 +1180,33 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { // Prepare parameters for drawing assert(this->h >= 1); - int h = this->h; + assert(this->h <= MAX_GRAPH_HEIGHT); + unsigned int h = (unsigned int)this->h; + + uint8_t maxItems = Meter_maxItems(this); + assert(this->curItems <= maxItems); bool isPercentChart = Meter_isPercentChart(this); + size_t nCellsPerValue = maxItems == 1 ? maxItems : h; + if (!isPercentChart) + nCellsPerValue *= 2; + + GraphDrawContext context = { + .maxItems = maxItems, + .isPercentChart = isPercentChart, + .nCellsPerValue = nCellsPerValue + }; + GraphData* data = &this->drawData; // Expand the graph data buffer if necessary - assert(data->nValues / 2 <= INT_MAX); - if (w > (int)(data->nValues / 2) && MAX_METER_GRAPHDATA_VALUES > data->nValues) { - size_t oldNValues = data->nValues; - data->nValues = MAXIMUM(oldNValues + oldNValues / 2, (size_t)w * 2); - data->nValues = MINIMUM(data->nValues, MAX_METER_GRAPHDATA_VALUES); - data->values = xReallocArray(data->values, data->nValues, sizeof(*data->values)); - memmove(data->values + (data->nValues - oldNValues), data->values, oldNValues * sizeof(*data->values)); - memset(data->values, 0, (data->nValues - oldNValues) * sizeof(*data->values)); + assert(data->nValues <= INT_MAX); + if (w > (int)data->nValues && MAX_METER_GRAPHDATA_VALUES > data->nValues) { + size_t nValues = data->nValues; + nValues = MAXIMUM(nValues + nValues / 2, (size_t)w); + nValues = MINIMUM(nValues, MAX_METER_GRAPHDATA_VALUES); + GraphMeterMode_reallocateGraphBuffer(this, &context, nValues); } const size_t nValues = data->nValues; @@ -276,20 +1220,11 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { struct timeval delay = { .tv_sec = globalDelay / 10, .tv_usec = (globalDelay % 10) * 100000L }; timeradd(&host->realtime, &delay, &(data->time)); - memmove(&data->values[0], &data->values[1], (nValues - 1) * sizeof(*data->values)); - - data->values[nValues - 1] = 0.0; - if (this->curItems > 0) { - data->values[nValues - 1] = Meter_computeSum(this); - if (isPercentChart && this->total > 0.0) { - data->values[nValues - 1] /= this->total; - } - } + GraphMeterMode_recordNewValue(this, &context); } - if (w < 1) { + if (w < 1) goto end; - } bool needsScaleDisplay = h >= 2; if (needsScaleDisplay) { @@ -297,44 +1232,22 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { } x += captionLen; - // Graph drawing style (character set, etc.) - const char* const* GraphMeterMode_dots; - int GraphMeterMode_pixPerRow; -#ifdef HAVE_LIBNCURSESW - if (CRT_utf8) { - GraphMeterMode_dots = GraphMeterMode_dotsUtf8; - GraphMeterMode_pixPerRow = PIXPERROW_UTF8; - } else -#endif - { - GraphMeterMode_dots = GraphMeterMode_dotsAscii; - GraphMeterMode_pixPerRow = PIXPERROW_ASCII; - } - // Starting positions of graph data and terminal column - if ((size_t)w > nValues / 2) { - x += w - nValues / 2; - w = (int)(nValues / 2); + if ((size_t)w > nValues) { + x += w - nValues; + w = (int)nValues; } - size_t i = nValues - (size_t)w * 2; + size_t i = nValues - (size_t)w; // Determine and print the graph scale - double total = 1.0; int scaleExp = 0; if (!isPercentChart) { - total = 0.0; for (size_t j = i; j < nValues; j++) { - total = MAXIMUM(data->values[j], total); + const GraphDataCell* valueStart = (const GraphDataCell*)data->buffer; + valueStart = &valueStart[j * nCellsPerValue]; + scaleExp = MAXIMUM(valueStart[0].scaleExp, scaleExp); } - assert(total <= DBL_MAX); - - (void)frexp(total, &scaleExp); - scaleExp = MAXIMUM(0, scaleExp); - - total = ldexp(1.0, scaleExp); - total = MINIMUM(DBL_MAX, total); } - assert(total >= 1.0); if (needsScaleDisplay) { if (isPercentChart) { addstr(" %"); @@ -344,19 +1257,13 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { } // Draw the actual graph - for (int col = 0; i < nValues - 1; i += 2, col++) { - int pix = GraphMeterMode_pixPerRow * h; - int v1 = (int) lround(CLAMP(data->values[i] / total * pix, 1.0, pix)); - int v2 = (int) lround(CLAMP(data->values[i + 1] / total * pix, 1.0, pix)); - - int colorIdx = GRAPH_1; - for (int line = 0; line < h; line++) { - int line1 = CLAMP(v1 - (GraphMeterMode_pixPerRow * (h - 1 - line)), 0, GraphMeterMode_pixPerRow); - int line2 = CLAMP(v2 - (GraphMeterMode_pixPerRow * (h - 1 - line)), 0, GraphMeterMode_pixPerRow); - + for (unsigned int col = 0; i + col < nValues; col++) { + for (unsigned int line = 0; line < h; line++) { + uint8_t details; + int colorIdx = GraphMeterMode_lookupCell(this, &context, scaleExp, i + col, line, &details); + move(y + (int)line, x + (int)col); attrset(CRT_colors[colorIdx]); - mvaddstr(y + line, x + col, GraphMeterMode_dots[line1 * (GraphMeterMode_pixPerRow + 1) + line2]); - colorIdx = GRAPH_2; + GraphMeterMode_printCellDetails(details); } } @@ -548,7 +1455,7 @@ void Meter_delete(Object* cast) { if (Meter_doneFn(this)) { Meter_done(this); } - free(this->drawData.values); + free(this->drawData.buffer); free(this->caption); free(this->values); free(this); @@ -578,8 +1485,8 @@ void Meter_setMode(Meter* this, MeterModeId modeIndex) { this->draw = Meter_drawFn(this); Meter_updateMode(this, modeIndex); } else { - free(this->drawData.values); - this->drawData.values = NULL; + free(this->drawData.buffer); + this->drawData.buffer = NULL; this->drawData.nValues = 0; const MeterMode* mode = &Meter_modes[modeIndex]; diff --git a/Meter.h b/Meter.h index 030de1259..db1d3d368 100644 --- a/Meter.h +++ b/Meter.h @@ -100,13 +100,14 @@ typedef struct MeterClass_ { #define Meter_attributes(this_) As_Meter(this_)->attributes #define Meter_name(this_) As_Meter(this_)->name #define Meter_uiName(this_) As_Meter(this_)->uiName +#define Meter_maxItems(this_) As_Meter(this_)->maxItems #define Meter_isMultiColumn(this_) As_Meter(this_)->isMultiColumn #define Meter_isPercentChart(this_) As_Meter(this_)->isPercentChart typedef struct GraphData_ { struct timeval time; size_t nValues; - double* values; + void* buffer; } GraphData; struct Meter_ { From 64567749eded01ad9d935c0ca36e21cfcb9f271a Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Tue, 10 Mar 2026 01:27:59 +0800 Subject: [PATCH 9/9] Remove unused constant defines of graph meter code Specifically 'PIXPERROW_*' and 'GraphMeterMode_dots*' constants. They were commented out rather than removed in the previous commit (for ease of code reviewing). Now this commit removes the constant defines for good. --- Meter.c | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Meter.c b/Meter.c index fd325ea43..3b714bbb2 100644 --- a/Meter.c +++ b/Meter.c @@ -243,28 +243,6 @@ static void BarMeterMode_draw(Meter* this, int x, int y, int w) { /* ---------- GraphMeterMode ---------- */ -#if 0 /* Used in old graph meter drawing code; to be removed */ -#ifdef HAVE_LIBNCURSESW - -#define PIXPERROW_UTF8 4 -static const char* const GraphMeterMode_dotsUtf8[] = { - /*00*/" ", /*01*/"⢀", /*02*/"⢠", /*03*/"⢰", /*04*/ "⢸", - /*10*/"⡀", /*11*/"⣀", /*12*/"⣠", /*13*/"⣰", /*14*/ "⣸", - /*20*/"⡄", /*21*/"⣄", /*22*/"⣤", /*23*/"⣴", /*24*/ "⣼", - /*30*/"⡆", /*31*/"⣆", /*32*/"⣦", /*33*/"⣶", /*34*/ "⣾", - /*40*/"⡇", /*41*/"⣇", /*42*/"⣧", /*43*/"⣷", /*44*/ "⣿" -}; - -#endif - -#define PIXPERROW_ASCII 2 -static const char* const GraphMeterMode_dotsAscii[] = { - /*00*/" ", /*01*/".", /*02*/":", - /*10*/".", /*11*/".", /*12*/":", - /*20*/":", /*21*/":", /*22*/":" -}; -#endif - static void GraphMeterMode_reallocateGraphBuffer(Meter* this, const GraphDrawContext* context, size_t nValues) { GraphData* data = &this->drawData;