From a2dd583b4cfcd4bb20307063506b3b5f69dee38e Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Sat, 24 Aug 2024 05:08:45 +0800 Subject: [PATCH 01/14] Make downcast explicit in GraphMeterMode_draw() This suppresses a "-Wshorten-64-to-32" warning in Clang 19. --- Meter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meter.c b/Meter.c index 4463a90a0..48d9d43f3 100644 --- a/Meter.c +++ b/Meter.c @@ -251,7 +251,7 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { // Starting positions of graph data and terminal column if ((size_t)w > nValues / 2) { x += w - nValues / 2; - w = nValues / 2; + w = (int)(nValues / 2); } size_t i = nValues - (size_t)w * 2; From 2d8877ed5361b7903a6f57edda2cddbb22b26430 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Thu, 25 Jul 2024 02:46:34 +0800 Subject: [PATCH 02/14] Introduce isPercentChart member in MeterClass This property distinguishes meters that have a (relatively) fixed "total" value and meters that do not have a definite maximum value. The former meters would be drawn as a "100% stacked bar" or "graph", and the latter could have their "total" values updated automatically in bar meter mode. This commit is a prerequisite for the feature "Graph meter dynamic scaling and percent graph drawing". Signed-off-by: Kang-Che Sung --- BatteryMeter.c | 1 + CPUMeter.c | 1 + DiskIOMeter.c | 1 + FileDescriptorMeter.c | 1 + LoadAverageMeter.c | 2 ++ MemoryMeter.c | 1 + Meter.h | 7 +++++++ NetworkIOMeter.c | 1 + SwapMeter.c | 1 + TasksMeter.c | 1 + linux/GPUMeter.c | 1 + linux/HugePageMeter.c | 1 + linux/PressureStallMeter.c | 6 ++++++ linux/ZramMeter.c | 1 + zfs/ZfsArcMeter.c | 1 + zfs/ZfsCompressedArcMeter.c | 1 + 16 files changed, 28 insertions(+) diff --git a/BatteryMeter.c b/BatteryMeter.c index 001249d4c..7ab1bac97 100644 --- a/BatteryMeter.c +++ b/BatteryMeter.c @@ -64,6 +64,7 @@ const MeterClass BatteryMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, + .isPercentChart = true, .total = 100.0, .attributes = BatteryMeter_attributes, .name = "Battery", diff --git a/CPUMeter.c b/CPUMeter.c index 1451ed77a..9345b96b2 100644 --- a/CPUMeter.c +++ b/CPUMeter.c @@ -348,6 +348,7 @@ const MeterClass CPUMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = CPU_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = CPUMeter_attributes, .name = "CPU", diff --git a/DiskIOMeter.c b/DiskIOMeter.c index 8af7d9563..f0698692d 100644 --- a/DiskIOMeter.c +++ b/DiskIOMeter.c @@ -154,6 +154,7 @@ const MeterClass DiskIOMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, + .isPercentChart = true, .total = 100.0, .attributes = DiskIOMeter_attributes, .name = "DiskIO", diff --git a/FileDescriptorMeter.c b/FileDescriptorMeter.c index cd3baf58c..bd7585cfd 100644 --- a/FileDescriptorMeter.c +++ b/FileDescriptorMeter.c @@ -110,6 +110,7 @@ const MeterClass FileDescriptorMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 2, + .isPercentChart = false, .total = 65536.0, .attributes = FileDescriptorMeter_attributes, .name = "FileDescriptors", diff --git a/LoadAverageMeter.c b/LoadAverageMeter.c index 6bf13a03e..f70f884b3 100644 --- a/LoadAverageMeter.c +++ b/LoadAverageMeter.c @@ -111,6 +111,7 @@ const MeterClass LoadAverageMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = false, .total = 100.0, .attributes = LoadAverageMeter_attributes, .name = "LoadAverage", @@ -129,6 +130,7 @@ const MeterClass LoadMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, + .isPercentChart = false, .total = 100.0, .attributes = LoadMeter_attributes, .name = "Load", diff --git a/MemoryMeter.c b/MemoryMeter.c index 3b7835256..83788b7ee 100644 --- a/MemoryMeter.c +++ b/MemoryMeter.c @@ -111,6 +111,7 @@ const MeterClass MemoryMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = MEMORY_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = MemoryMeter_attributes, .name = "Memory", diff --git a/Meter.h b/Meter.h index b53a82324..d81e8d8bc 100644 --- a/Meter.h +++ b/Meter.h @@ -75,6 +75,12 @@ typedef struct MeterClass_ { const char* const description; /* optional meter description in header setup menu */ const uint8_t maxItems; const bool isMultiColumn; /* whether the meter draws multiple sub-columns (defaults to false) */ + + /* Specifies how the meter is rendered in bar or graph mode: + true: a percent bar or graph with 'total' representing 100% or maximum. + false: the meter has no definite maximum; 'total' repesents initial + maximum value while actual maximum is updated automatically. */ + const bool isPercentChart; } MeterClass; #define As_Meter(this_) ((const MeterClass*)((this_)->super.klass)) @@ -95,6 +101,7 @@ typedef struct MeterClass_ { #define Meter_name(this_) As_Meter(this_)->name #define Meter_uiName(this_) As_Meter(this_)->uiName #define Meter_isMultiColumn(this_) As_Meter(this_)->isMultiColumn +#define Meter_isPercentChart(this_) As_Meter(this_)->isPercentChart typedef struct GraphData_ { struct timeval time; diff --git a/NetworkIOMeter.c b/NetworkIOMeter.c index da3ce71da..5dbce37fc 100644 --- a/NetworkIOMeter.c +++ b/NetworkIOMeter.c @@ -171,6 +171,7 @@ const MeterClass NetworkIOMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 2, + .isPercentChart = false, .total = 100.0, .attributes = NetworkIOMeter_attributes, .name = "NetworkIO", diff --git a/SwapMeter.c b/SwapMeter.c index 29c295d32..faa194556 100644 --- a/SwapMeter.c +++ b/SwapMeter.c @@ -75,6 +75,7 @@ const MeterClass SwapMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = SWAP_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = SwapMeter_attributes, .name = "Swap", diff --git a/TasksMeter.c b/TasksMeter.c index fc1e4b0ed..e87813353 100644 --- a/TasksMeter.c +++ b/TasksMeter.c @@ -74,6 +74,7 @@ const MeterClass TasksMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 4, + .isPercentChart = false, .total = 100.0, .attributes = TasksMeter_attributes, .name = "Tasks", diff --git a/linux/GPUMeter.c b/linux/GPUMeter.c index 628d4c71c..64ca5f9d3 100644 --- a/linux/GPUMeter.c +++ b/linux/GPUMeter.c @@ -172,6 +172,7 @@ const MeterClass GPUMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = ARRAYSIZE(GPUMeter_engineData) + 1, + .isPercentChart = true, .total = 100.0, .attributes = GPUMeter_attributes, .name = "GPU", diff --git a/linux/HugePageMeter.c b/linux/HugePageMeter.c index bd16f5dc2..f34bff8df 100644 --- a/linux/HugePageMeter.c +++ b/linux/HugePageMeter.c @@ -101,6 +101,7 @@ const MeterClass HugePageMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = ARRAYSIZE(HugePageMeter_active_labels), + .isPercentChart = true, .total = 100.0, .attributes = HugePageMeter_attributes, .name = "HugePages", diff --git a/linux/PressureStallMeter.c b/linux/PressureStallMeter.c index 5010c11d2..942213ea5 100644 --- a/linux/PressureStallMeter.c +++ b/linux/PressureStallMeter.c @@ -77,6 +77,7 @@ const MeterClass PressureStallCPUSomeMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallCPUSome", @@ -95,6 +96,7 @@ const MeterClass PressureStallIOSomeMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallIOSome", @@ -113,6 +115,7 @@ const MeterClass PressureStallIOFullMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallIOFull", @@ -131,6 +134,7 @@ const MeterClass PressureStallIRQFullMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallIRQFull", @@ -149,6 +153,7 @@ const MeterClass PressureStallMemorySomeMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallMemorySome", @@ -167,6 +172,7 @@ const MeterClass PressureStallMemoryFullMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, + .isPercentChart = true, .total = 100.0, .attributes = PressureStallMeter_attributes, .name = "PressureStallMemoryFull", diff --git a/linux/ZramMeter.c b/linux/ZramMeter.c index 2a1c7715c..fe10c3baa 100644 --- a/linux/ZramMeter.c +++ b/linux/ZramMeter.c @@ -77,6 +77,7 @@ const MeterClass ZramMeter_class = { .defaultMode = BAR_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = ZRAM_METER_ITEMCOUNT, + .isPercentChart = true, .total = 100.0, .attributes = ZramMeter_attributes, .name = "Zram", diff --git a/zfs/ZfsArcMeter.c b/zfs/ZfsArcMeter.c index 87b7e19ce..8177ff2a5 100644 --- a/zfs/ZfsArcMeter.c +++ b/zfs/ZfsArcMeter.c @@ -93,6 +93,7 @@ const MeterClass ZfsArcMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 6, + .isPercentChart = true, .total = 100.0, .attributes = ZfsArcMeter_attributes, .name = "ZFSARC", diff --git a/zfs/ZfsCompressedArcMeter.c b/zfs/ZfsCompressedArcMeter.c index 35ab8b379..cd3bf43f6 100644 --- a/zfs/ZfsCompressedArcMeter.c +++ b/zfs/ZfsCompressedArcMeter.c @@ -81,6 +81,7 @@ const MeterClass ZfsCompressedArcMeter_class = { .defaultMode = TEXT_METERMODE, .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, + .isPercentChart = true, .total = 100.0, .attributes = ZfsCompressedArcMeter_attributes, .name = "ZFSCARC", From 9607813577144785a6e846ffd7996f3d1005bece Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Sun, 25 Aug 2024 02:23:43 +0800 Subject: [PATCH 03/14] Update "total" value for non-percent bar meters If "isPercentChart" of a meter is false, update its "total" value automatically in the bar meter mode. The "total" value is capped to DBL_MAX in order to ensure the division never produces NaN. The newly introduced Meter_computeSum() function will be reused by the feature "Graph meter dynamic scaling and percent graph drawing". Signed-off-by: Kang-Che Sung --- Meter.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Meter.c b/Meter.c index 48d9d43f3..a3d3596eb 100644 --- a/Meter.c +++ b/Meter.c @@ -10,6 +10,7 @@ in the source distribution for its full text. #include "Meter.h" #include +#include #include #include #include @@ -46,6 +47,14 @@ static inline void Meter_displayBuffer(const Meter* this, RichString* out) { } } +static double Meter_computeSum(const Meter* this) { + assert(this->curItems > 0); + assert(this->values); + double sum = sumPositiveValues(this->values, this->curItems); + // Prevent rounding to infinity in IEEE 754 + return MINIMUM(DBL_MAX, sum); +} + /* ---------- TextMeterMode ---------- */ static void TextMeterMode_draw(Meter* this, int x, int y, int w) { @@ -120,6 +129,14 @@ static void BarMeterMode_draw(Meter* this, int x, int y, int w) { assert(startPos <= w); assert(startPos + w <= RichString_sizeVal(bar)); + // Update the "total" if necessary + if (!Meter_isPercentChart(this) && this->curItems > 0) { + double sum = Meter_computeSum(this); + if (this->total < sum) { + this->total = sum; + } + } + int blockSizes[10]; // First draw in the bar[] buffer... From 7776f0ac7f64475af2ed619b3e3234637d542b20 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Sun, 25 Aug 2024 17:32:25 +0800 Subject: [PATCH 04/14] Graph meter dynamic scaling and percent graph drawing Implement dynamic scaling for Graph meter mode, and separate it from "100% graph" drawing. This is controlled by the "isPercentChart" property of a MeterClass. If "isPercentChart" is true, the meter would be drawn as a "100% graph". Graph meters now expect the "total" value may change, and the newly changed "total" value no longer affects the percent graph drawings of earlier meter values. If "isPercentChart" is false, the meter would be drawn with a dynamic scale. Signed-off-by: Kang-Che Sung --- Meter.c | 23 ++++++++++++++++++++--- Meter.h | 1 + 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Meter.c b/Meter.c index a3d3596eb..1b97afa7f 100644 --- a/Meter.c +++ b/Meter.c @@ -212,6 +212,10 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { attrset(CRT_colors[METER_TEXT]); const int captionLen = 3; mvaddnstr(y, x, caption, captionLen); + + // Prepare parameters for drawing + uint8_t maxItems = Meter_maxItems(this); + bool isPercentChart = Meter_isPercentChart(this); x += captionLen; w -= captionLen; @@ -243,8 +247,10 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { data->values[nValues - 1] = 0.0; if (this->curItems > 0) { - assert(this->values); - data->values[nValues - 1] = sumPositiveValues(this->values, this->curItems); + data->values[nValues - 1] = Meter_computeSum(this); + if (isPercentChart && this->total > 0.0) { + data->values[nValues - 1] /= this->total; + } } } @@ -272,10 +278,21 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { } size_t i = nValues - (size_t)w * 2; + // Determine the graph scale + double total = 1.0; + if (maxItems > 0 && !isPercentChart) { + for (size_t j = i; j < nValues; j++) { + if (total < data->values[j]) { + total = data->values[j]; + } + } + assert(total <= DBL_MAX); + } + assert(total >= 1.0); + // Draw the actual graph for (int col = 0; i < nValues - 1; i += 2, col++) { int pix = GraphMeterMode_pixPerRow * GRAPH_HEIGHT; - double total = MAXIMUM(this->total, 1); 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)); diff --git a/Meter.h b/Meter.h index d81e8d8bc..813f3776a 100644 --- a/Meter.h +++ b/Meter.h @@ -100,6 +100,7 @@ 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 From 4395374719d554071fc190b6621b54b035e62743 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Sun, 25 Aug 2024 17:33:34 +0800 Subject: [PATCH 05/14] 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 | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/Meter.c b/Meter.c index 1b97afa7f..c086c948c 100644 --- a/Meter.c +++ b/Meter.c @@ -206,6 +206,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) { // Draw the caption const char* caption = Meter_getCaption(this); @@ -216,6 +232,10 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { // Prepare parameters for drawing uint8_t maxItems = Meter_maxItems(this); bool isPercentChart = Meter_isPercentChart(this); + bool needsScaleDisplay = maxItems > 0 && GRAPH_HEIGHT >= 2; + if (needsScaleDisplay) { + move(y + 1, x); // Cursor position for printing the scale + } x += captionLen; w -= captionLen; @@ -278,17 +298,34 @@ 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 (maxItems > 0 && !isPercentChart) { + total = 0.0; for (size_t j = i; j < nValues; j++) { if (total < data->values[j]) { total = data->values[j]; } } assert(total <= DBL_MAX); + (void)frexp(total, &scaleExp); + if (scaleExp < 0) { + scaleExp = 0; + } + total = ldexp(1.0, scaleExp); + if (total > DBL_MAX) { + total = DBL_MAX; + } } 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 98ce9370e6f44cc07a2f24aaf89e0d3b05fbb4f5 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Mon, 13 Nov 2023 02:28:55 +0800 Subject: [PATCH 06/14] Adjust LoadAverageMeter "total" assignment --- LoadAverageMeter.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/LoadAverageMeter.c b/LoadAverageMeter.c index f70f884b3..a0b05f2be 100644 --- a/LoadAverageMeter.c +++ b/LoadAverageMeter.c @@ -46,15 +46,15 @@ static void LoadAverageMeter_updateValues(Meter* this) { this->curItems = 1; // change bar color and total based on value + if (this->total < this->host->activeCPUs) { + this->total = this->host->activeCPUs; + } if (this->values[0] < 1.0) { this->curAttributes = OK_attributes; - this->total = 1.0; } else if (this->values[0] < this->host->activeCPUs) { this->curAttributes = Medium_attributes; - this->total = this->host->activeCPUs; } else { this->curAttributes = High_attributes; - this->total = 2 * this->host->activeCPUs; } xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%.2f/%.2f/%.2f", this->values[0], this->values[1], this->values[2]); @@ -78,15 +78,15 @@ static void LoadMeter_updateValues(Meter* this) { Platform_getLoadAverage(&this->values[0], &five, &fifteen); // change bar color and total based on value + if (this->total < this->host->activeCPUs) { + this->total = this->host->activeCPUs; + } if (this->values[0] < 1.0) { this->curAttributes = OK_attributes; - this->total = 1.0; } else if (this->values[0] < this->host->activeCPUs) { this->curAttributes = Medium_attributes; - this->total = this->host->activeCPUs; } else { this->curAttributes = High_attributes; - this->total = 2 * this->host->activeCPUs; } xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%.2f", this->values[0]); @@ -112,7 +112,7 @@ const MeterClass LoadAverageMeter_class = { .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 3, .isPercentChart = false, - .total = 100.0, + .total = 1.0, .attributes = LoadAverageMeter_attributes, .name = "LoadAverage", .uiName = "Load average", @@ -131,7 +131,7 @@ const MeterClass LoadMeter_class = { .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 1, .isPercentChart = false, - .total = 100.0, + .total = 1.0, .attributes = LoadMeter_attributes, .name = "Load", .uiName = "Load", From a8b319b2554200c8fea56f08f6a8fa328d0f4ca3 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Mon, 13 Nov 2023 02:41:38 +0800 Subject: [PATCH 07/14] TasksMeter: remove code for auto-updating "total" --- TasksMeter.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/TasksMeter.c b/TasksMeter.c index e87813353..f522b753b 100644 --- a/TasksMeter.c +++ b/TasksMeter.c @@ -34,7 +34,6 @@ static void TasksMeter_updateValues(Meter* this) { this->values[1] = pt->userlandThreads; this->values[2] = pt->totalTasks - pt->kernelThreads - pt->userlandThreads; this->values[3] = MINIMUM(pt->runningTasks, host->activeCPUs); - this->total = pt->totalTasks; xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "%u/%u", MINIMUM(pt->runningTasks, host->activeCPUs), pt->totalTasks); } @@ -75,7 +74,7 @@ const MeterClass TasksMeter_class = { .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 4, .isPercentChart = false, - .total = 100.0, + .total = 1.0, .attributes = TasksMeter_attributes, .name = "Tasks", .uiName = "Task counter", From f1d2b634613831b05ed03088d5507ce41214d382 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Mon, 13 Nov 2023 02:42:56 +0800 Subject: [PATCH 08/14] NetworkIOMeter: remove code for auto-updating "total" --- NetworkIOMeter.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/NetworkIOMeter.c b/NetworkIOMeter.c index 5dbce37fc..3f4acdea5 100644 --- a/NetworkIOMeter.c +++ b/NetworkIOMeter.c @@ -111,9 +111,6 @@ static void NetworkIOMeter_updateValues(Meter* this) { this->values[0] = cached_rxb_diff; this->values[1] = cached_txb_diff; - if (cached_rxb_diff + cached_txb_diff > this->total) { - this->total = cached_rxb_diff + cached_txb_diff; - } if (status == RATESTATUS_NODATA) { xSnprintf(this->txtBuffer, sizeof(this->txtBuffer), "no data"); @@ -172,7 +169,7 @@ const MeterClass NetworkIOMeter_class = { .supportedModes = METERMODE_DEFAULT_SUPPORTED, .maxItems = 2, .isPercentChart = false, - .total = 100.0, + .total = 1.0, .attributes = NetworkIOMeter_attributes, .name = "NetworkIO", .uiName = "Network IO", From b4d430f597f62e7c02ef7d8324d2b7df05179638 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Sun, 25 Aug 2024 08:29:30 +0800 Subject: [PATCH 09/14] Use "h" property as height when drawing Graph meter This is a code quality change that avoids dependency on the hard-coded GRAPH_HEIGHT in GraphMeterMode_draw(). This doesn't enable variable graph heights per meter, but it makes room for implementing such feature. Signed-off-by: Kang-Che Sung --- Meter.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Meter.c b/Meter.c index c086c948c..185be4de6 100644 --- a/Meter.c +++ b/Meter.c @@ -29,7 +29,7 @@ in the source distribution for its full text. #define UINT32_WIDTH 32 #endif -#define GRAPH_HEIGHT 4 /* Unit: rows (lines) */ +#define DEFAULT_GRAPH_HEIGHT 4 /* Unit: rows (lines) */ typedef struct MeterMode_ { Meter_Draw draw; @@ -230,9 +230,12 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { mvaddnstr(y, x, caption, captionLen); // Prepare parameters for drawing + assert(this->h >= 1); + int graphHeight = this->h; + uint8_t maxItems = Meter_maxItems(this); bool isPercentChart = Meter_isPercentChart(this); - bool needsScaleDisplay = maxItems > 0 && GRAPH_HEIGHT >= 2; + bool needsScaleDisplay = maxItems > 0 && graphHeight >= 2; if (needsScaleDisplay) { move(y + 1, x); // Cursor position for printing the scale } @@ -329,14 +332,14 @@ 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 * GRAPH_HEIGHT; + int pix = GraphMeterMode_pixPerRow * graphHeight; 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 < GRAPH_HEIGHT; line++) { - int line1 = CLAMP(v1 - (GraphMeterMode_pixPerRow * (GRAPH_HEIGHT - 1 - line)), 0, GraphMeterMode_pixPerRow); - int line2 = CLAMP(v2 - (GraphMeterMode_pixPerRow * (GRAPH_HEIGHT - 1 - line)), 0, GraphMeterMode_pixPerRow); + for (int line = 0; line < graphHeight; line++) { + int line1 = CLAMP(v1 - (GraphMeterMode_pixPerRow * (graphHeight - 1 - line)), 0, GraphMeterMode_pixPerRow); + int line2 = CLAMP(v2 - (GraphMeterMode_pixPerRow * (graphHeight - 1 - line)), 0, GraphMeterMode_pixPerRow); attrset(CRT_colors[colorIdx]); mvaddstr(y + line, x + col, GraphMeterMode_dots[line1 * (GraphMeterMode_pixPerRow + 1) + line2]); @@ -434,7 +437,7 @@ static const MeterMode Meter_modes[] = { }, [GRAPH_METERMODE] = { .uiName = "Graph", - .h = GRAPH_HEIGHT, + .h = DEFAULT_GRAPH_HEIGHT, .draw = GraphMeterMode_draw, }, [LED_METERMODE] = { From 0e44068eb567f400858ca3827852a2acc9e27bc1 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Sun, 25 Aug 2024 06:23:30 +0800 Subject: [PATCH 10/14] 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 | 12 ++++++++++++ XUtils.h | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ configure.ac | 43 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/XUtils.c b/XUtils.c index 94a14df64..248c572a6 100644 --- a/XUtils.c +++ b/XUtils.c @@ -12,6 +12,7 @@ in the source distribution for its full text. #include #include #include +#include // IWYU pragma: keep #include #include #include @@ -387,3 +388,14 @@ 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 73335a611..f80d2d5a1 100644 --- a/XUtils.h +++ b/XUtils.h @@ -14,8 +14,10 @@ in the source distribution for its full text. #error "Must have #include \"config.h\" line at the top of the file that includes these XUtils helper functions" #endif +#include // IWYU pragma: keep #include #include // IWYU pragma: keep +#include // IWYU pragma: keep #include #include // IWYU pragma: keep #include // IWYU pragma: keep @@ -23,6 +25,16 @@ in the source distribution for its full text. #include "Compat.h" #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); @@ -149,6 +161,49 @@ 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) { + if (x == 0) + return 0; + + return 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) + // With ARM Advanced SIMD extension (NEON), this generates smaller code 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 smaller than the subroutine + // call of the compiler __builtin_popcount (which is tuned for + // unsigned int input type and not uint8_t). + uint32_t n = (uint32_t)(x * 0x08040201U); + n = (uint32_t)(((n >> 3) & 0x11111111U) * 0x11111111U) >> 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 3bbaa7159..3eac8974c 100644 --- a/configure.ac +++ b/configure.ac @@ -166,7 +166,10 @@ fi]) # 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]) @@ -292,11 +295,47 @@ AC_LINK_IFELSE([ AC_MSG_CHECKING(for __builtin_ctz) AC_COMPILE_IFELSE([ - AC_LANG_PROGRAM([], [[__builtin_ctz(1); /* Supported in GCC 3.4 or later */]])], + AC_LANG_PROGRAM([], [[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*) + dnl ARM NEON intrinsics + AC_CHECK_HEADERS([arm_neon.h]) + ;; +esac + # ---------------------------------------------------------------------- From 18fa4d8e60025838ae1a1570dc5f4cc530f9bcb2 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Mon, 26 Aug 2024 05:49:08 +0800 Subject: [PATCH 11/14] 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 | 1023 +++++++++++++++++++++++++++++++++++++++++++++++++++---- Meter.h | 2 +- 2 files changed, 959 insertions(+), 66 deletions(-) diff --git a/Meter.c b/Meter.c index 185be4de6..4e31de8ee 100644 --- a/Meter.c +++ b/Meter.c @@ -12,6 +12,7 @@ in the source distribution for its full text. #include #include #include +#include #include #include @@ -25,11 +26,51 @@ 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_ { + uint8_t itemNum; + 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; @@ -186,6 +227,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 @@ -205,6 +247,706 @@ 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; + + 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 size_t GraphMeterMode_valueCellIndex(unsigned int graphHeight, bool isPercentChart, int deltaExp, unsigned int y, unsigned int* scaleFactor, unsigned int* increment) { + assert(deltaExp >= 0); + assert(deltaExp < UINT16_WIDTH); + + if (scaleFactor) + *scaleFactor = 1; + if (increment) + *increment = isPercentChart ? 1 : (2U << deltaExp); + + unsigned int yTop = (graphHeight - 1) >> deltaExp; + if (y > yTop) { + return (size_t)-1; + } + + if (isPercentChart) { + assert(deltaExp == 0); + return y; + } + + // A record may be rendered 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 * graphHeight - 1) cells + // of space to store all the color information. + // + // An example for graphHeight = 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 * graphHeight) are + // [5] [10] X X X | computed from cells of a + // [3] [6] (12) X X | lower scale and not stored in + // [1] [2] [4] [8] (16) | 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. The "increment" argument is + // optional, but the caller should assert the index is in bounds. + if (b + offset > 2 * graphHeight - 1) { + return (size_t)-1; + } + return b + offset; + } + + // This function is called for reading. + assert(!increment); + + if (y < yTop) { + return b + offset; + } + + assert(((2 * graphHeight - 1) & b) == b); + + unsigned int offsetTop = powerOf2Floor(2 * graphHeight - 1 - b); + if (offsetTop) { + *scaleFactor = offset / offsetTop; + } + + return b + offsetTop; +} + +static uint8_t GraphMeterMode_findTopCellItem(const Meter* this, double scaledTotal, unsigned int topCell) { + unsigned int graphHeight = (unsigned int)this->h; + assert(topCell < graphHeight); + + 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 = valueSum + value; + if (newValueSum > DBL_MAX) + newValueSum = DBL_MAX; + + 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)(int)graphHeight; + assert(topPoint >= 0.0); + + if (!(topPoint > (double)(int)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)(int)graphHeight; + assert(area >= 0.0); + + if (area > topPoint - (double)(int)topCell) + area = topPoint - (double)(int)topCell; + + if (area >= maxValue) { + maxValue = area; + topCellItem = i; + } + } else { + // Compare "value" directly. It is possible for an "area" to + // underflow here and still wins as the largest area. + if (value >= maxValue) { + maxValue = value; + topCellItem = i; + } + } + } + return topCellItem; +} + +static int8_t GraphMeterMode_needsExtraCell(unsigned int graphHeight, double scaledTotal, unsigned int y, const GraphColorAdjStack* stack, const GraphColorAdjOffset* adjOffset) { + double areaSum = (stack->fractionSum + stack->valueSum / scaledTotal) * (double)(int)graphHeight; + double adjOffsetVal = adjOffset ? (double)(int32_t)adjOffset->offsetVal : 0.0; + double halfPoint = (double)(int)y + 0.5; + + // Calculate the best position for rendering this stack of items. + // 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)(int)graphHeight; + double adjNCells = adjOffset ? (double)(int)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) { + assert(blanksAtTopCell < 8); + blanksAtEnd = (uint8_t)blanksAtTopCell; + roundDirInAscii = 1; + } else if (prev->nCellsPainted == 0 || prevTopPoint <= (double)(int)prev->nCellsPainted || (uint8_t)numDots == 0) { + blanksAtEnd = (uint8_t)(8 - (uint8_t)numDots) % 8; + } else { + // Unlike other conditions, this one rounds to nearest for visual reason. + // In case of a tie, display the dot at lower position of the graph, + // i.e. MSB of the "details" data. + + double distance = prevTopPoint - (double)(int)prev->nCellsPainted; + distance = distance + 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; + 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) { + assert((mask & 0x0800) != 0); + mask ^= 0x0800; + } + mask >>= 8; + } else if (roundDirInAscii > 0) { + if (blanksAtStart < 4 && (uint8_t)(blanksAtStart + blanksAtEnd % 4) >= 4) { + assert((mask & 0x0800) != 0); + mask ^= 0x0800; + } + } + + mask = (uint16_t)((mask >> blanksAtEnd) << blanksAtEnd); + + if (needsTiebreak) { + if (roundDirInAscii > 0) { + mask |= 0x0004; + } else if (roundDirInAscii < 0) { + assert((mask & 0x0010) != 0); + mask = (mask & 0xFFEF) | 0x0020; + } + } else 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); + + 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 increment, 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 += increment; + } +} + +static void GraphMeterMode_computeColors(Meter* this, const GraphDrawContext* context, GraphDataCell* valueStart, int deltaExp, double scaledTotal, unsigned int numDots) { + unsigned int graphHeight = (unsigned int)this->h; + bool isPercentChart = context->isPercentChart; + + assert(deltaExp >= 0); + assert(numDots > 0); + assert(numDots <= graphHeight * 8); + + unsigned int increment; + size_t firstCellIndex = GraphMeterMode_valueCellIndex(graphHeight, isPercentChart, deltaExp, 0, NULL, &increment); + assert(firstCellIndex < context->nCellsPerValue); + + unsigned int topCell = (numDots - 1) / 8; + const uint8_t dotAlignment = 2; + unsigned int blanksAtTopCell = ((topCell + 1) * 8 - numDots) / dotAlignment * dotAlignment; + + bool hasPartialTopCell = false; + if (blanksAtTopCell > 0) { + hasPartialTopCell = true; + } else if (!isPercentChart && topCell % 2 == 0 && topCell == ((graphHeight - 1) >> deltaExp)) { + // This "top cell" is rendered as full in one scale, but partial in the + // next scale. (Only happens when graphHeight 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 = prev.valueSum + value; + if (new.valueSum > DBL_MAX) + new.valueSum = DBL_MAX; + + 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)(int)graphHeight; + 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)(int)(graphHeight + 1) > (double)(int)graphHeight; + 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) { + if (rem < nextThresholdLow) { + nextThresholdLow = rem; + } + nCells++; + } else if (rem < threshold) { + if (rem > nextThresholdHigh) { + nextThresholdHigh = rem; + } + 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)(int)graphHeight; + 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 whose 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(graphHeight, scaledTotal, y, &stack, &adjLarge); + if (res > 0 || (res < 0 && rItemMinCells <= nCells)) { + rItemHasExtraCell = true; + } + } else { + int8_t res = GraphMeterMode_needsExtraCell(graphHeight, 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)(int)graphHeight; + int blanksAtTopCellArg = (new.nCellsPainted == nCellsToPaint) ? (int)blanksAtTopCell : -1; + uint16_t mask = GraphMeterMode_makeDetailsMask(&prev, &new, prevTopPoint, rem, blanksAtTopCellArg); + + GraphDataCell* cellsStart = &valueStart[firstCellIndex + (size_t)increment * prev.nCellsPainted]; + GraphMeterMode_paintCellsForItem(cellsStart, increment, 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)(int)(nCellsToPaint - nCellsPaintedLow) / (double)(int)(nCellsPaintedHigh - nCellsPaintedLow)); + if (threshold < nextThresholdLow) { + threshold = nextThresholdLow; + } + } + 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 graphHeight = (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]; + + // Compute "sum" and "total" + double sum = 0.0; + if (this->curItems > 0) { + sum = Meter_computeSum(this); + assert(sum >= 0.0); + assert(sum <= DBL_MAX); + } + double total; + if (isPercentChart) { + total = MAXIMUM(this->total, sum); + } else { + int scaleExp = 0; + (void)frexp(sum, &scaleExp); + if (scaleExp < 0) { + scaleExp = 0; + } + // In IEEE 754 binary64 (DBL_MAX_EXP == 1024, DBL_MAX_10_EXP == 308), + // "scaleExp" never overflows. + assert(DBL_MAX_10_EXP < 9864); + assert(scaleExp <= INT16_MAX); + valueStart[0].scaleExp = (int16_t)scaleExp; + total = ldexp(1.0, scaleExp); + } + if (total > DBL_MAX) + total = DBL_MAX; + + assert(graphHeight <= UINT16_MAX / 8); + double maxDots = (double)(int32_t)(graphHeight * 8); + int numDots = 0; + if (total > 0.0) { + numDots = (int)ceil((sum / total) * maxDots); + assert(numDots >= 0); + if (sum > 0.0 && numDots <= 0) { + numDots = 1; // Division of (sum / total) underflows + } + } + + if (maxItems == 1) { + assert(numDots <= UINT16_MAX); + valueStart[isPercentChart ? 0 : 1].numDots = (uint16_t)numDots; + return; + } + + // Clear cells + unsigned int y = ((unsigned int)numDots + 8 - 1) / 8; // Round up + size_t i = GraphMeterMode_valueCellIndex(graphHeight, isPercentChart, 0, y, NULL, NULL); + if (i < nCellsPerValue) { + memset(&valueStart[i], 0, (nCellsPerValue - i) * sizeof(*valueStart)); + } + + if (sum <= 0.0) + return; + + int deltaExp = 0; + double scaledTotal = total; + assert(scaledTotal > 0.0); + while (true) { + numDots = (int)ceil((sum / scaledTotal) * maxDots); + if (numDots <= 0) { + numDots = 1; // Division of (sum / scaledTotal) underflows + } + + GraphMeterMode_computeColors(this, context, valueStart, deltaExp, scaledTotal, (unsigned int)numDots); + + if (isPercentChart || !(scaledTotal < DBL_MAX) || (1U << deltaExp) >= graphHeight) { + break; + } + + deltaExp++; + scaledTotal *= 2.0; + if (scaledTotal > DBL_MAX) { + scaledTotal = DBL_MAX; + } + } +} static void GraphMeterMode_printScale(int exponent) { if (exponent < 10) { @@ -222,6 +964,186 @@ static void GraphMeterMode_printScale(int exponent) { } } +static uint8_t GraphMeterMode_scaleCellDetails(uint8_t details, unsigned int scaleFactor) { + // Only the "top cell" of a record may need scaling like this; the cell does + // not use the special meaning of bit 4. + // This algorithm assumes the "details" be printed in braille characters. + assert(scaleFactor > 0); + if (scaleFactor < 2) { + return details; + } + if (scaleFactor < 4 && (details & 0x0F) != 0x00) { + // Display the cell in half height (bits 0 to 3 are zero). + // Bits 4 and 5 are set simultaneously 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 simultaneously. + 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 graphHeight = (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 < graphHeight); + y = graphHeight - 1 - y; + + uint8_t itemIndex = (uint8_t)-1; + *details = 0x00; // Empty the cell + + if (maxItems < 1) + goto cellIsEmpty; + + assert(valueIndex < data->nValues); + const GraphDataCell* valueStart = (const GraphDataCell*)data->buffer; + valueStart = &valueStart[valueIndex * nCellsPerValue]; + + int deltaExp = isPercentChart ? 0 : scaleExp - valueStart[0].scaleExp; + assert(deltaExp >= 0); + + 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 = deltaExp >= UINT16_WIDTH ? UINT16_WIDTH - 1 : deltaExp; + + unsigned int scaleFactor; + size_t i = GraphMeterMode_valueCellIndex(graphHeight, 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[4] = "\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) * 0x02110408U) >> 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) * 0x01441080U) >> 27; + difference = (int)n - 0x0F; + c = difference < 0 ? upperHalf : lowerHalf; + } + } + addch(c); +} + static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { // Draw the caption const char* caption = Meter_getCaption(this); @@ -231,10 +1153,21 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { // Prepare parameters for drawing assert(this->h >= 1); - int graphHeight = this->h; + assert(this->h <= MAX_GRAPH_HEIGHT); + unsigned int graphHeight = (unsigned int)this->h; uint8_t maxItems = Meter_maxItems(this); bool isPercentChart = Meter_isPercentChart(this); + size_t nCellsPerValue = maxItems <= 1 ? maxItems : graphHeight; + if (!isPercentChart) + nCellsPerValue *= 2; + + GraphDrawContext context = { + .maxItems = maxItems, + .isPercentChart = isPercentChart, + .nCellsPerValue = nCellsPerValue + }; + bool needsScaleDisplay = maxItems > 0 && graphHeight >= 2; if (needsScaleDisplay) { move(y + 1, x); // Cursor position for printing the scale @@ -245,14 +1178,12 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { 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; @@ -266,62 +1197,30 @@ 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 <= 0) return; - // 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 (maxItems > 0 && !isPercentChart) { - total = 0.0; for (size_t j = i; j < nValues; j++) { - if (total < data->values[j]) { - total = data->values[j]; + const GraphDataCell* valueStart = (const GraphDataCell*)data->buffer; + valueStart = &valueStart[j * nCellsPerValue]; + if (scaleExp < valueStart[0].scaleExp) { + scaleExp = valueStart[0].scaleExp; } } - assert(total <= DBL_MAX); - (void)frexp(total, &scaleExp); - if (scaleExp < 0) { - scaleExp = 0; - } - total = ldexp(1.0, scaleExp); - if (total > DBL_MAX) { - total = DBL_MAX; - } } - assert(total >= 1.0); if (needsScaleDisplay) { if (isPercentChart) { addstr(" %"); @@ -331,19 +1230,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 * graphHeight; - 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 < graphHeight; line++) { - int line1 = CLAMP(v1 - (GraphMeterMode_pixPerRow * (graphHeight - 1 - line)), 0, GraphMeterMode_pixPerRow); - int line2 = CLAMP(v2 - (GraphMeterMode_pixPerRow * (graphHeight - 1 - line)), 0, GraphMeterMode_pixPerRow); - + for (unsigned int line = 0; line < graphHeight; line++) { + for (unsigned int col = 0; i + col < nValues; col++) { + 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); } } attrset(CRT_colors[RESET_COLOR]); @@ -519,7 +1412,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); @@ -549,8 +1442,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 813f3776a..0471b5b1b 100644 --- a/Meter.h +++ b/Meter.h @@ -107,7 +107,7 @@ typedef struct MeterClass_ { typedef struct GraphData_ { struct timeval time; size_t nValues; - double* values; + void* buffer; } GraphData; struct Meter_ { From cee45a7d50cbeed9f6edb7db4ce5dc15b4c82e25 Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Mon, 26 Aug 2024 05:49:10 +0800 Subject: [PATCH 12/14] 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 4e31de8ee..6f5672f16 100644 --- a/Meter.c +++ b/Meter.c @@ -227,28 +227,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; From d47f88c04338388c22100d096c5900da28f8a5ac Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Mon, 26 Aug 2024 07:09:46 +0800 Subject: [PATCH 13/14] (draft) "Positive/negative" Graph meter style Signed-off-by: Kang-Che Sung --- Meter.c | 163 ++++++++++++++++++++++++++------------------------------ 1 file changed, 76 insertions(+), 87 deletions(-) diff --git a/Meter.c b/Meter.c index 6f5672f16..6c73845ac 100644 --- a/Meter.c +++ b/Meter.c @@ -251,6 +251,7 @@ static void GraphMeterMode_reallocateGraphBuffer(Meter* this, const GraphDrawCon data->nValues = nValues; } +#if 0 /* Unused code */ static size_t GraphMeterMode_valueCellIndex(unsigned int graphHeight, bool isPercentChart, int deltaExp, unsigned int y, unsigned int* scaleFactor, unsigned int* increment) { assert(deltaExp >= 0); assert(deltaExp < UINT16_WIDTH); @@ -829,9 +830,9 @@ static void GraphMeterMode_computeColors(Meter* this, const GraphDrawContext* co } } } +#endif /* End of unused code */ 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) @@ -851,18 +852,23 @@ static void GraphMeterMode_recordNewValue(Meter* this, const GraphDrawContext* c valueStart = &valueStart[(nValues - 1) * nCellsPerValue]; // Compute "sum" and "total" - double sum = 0.0; - if (this->curItems > 0) { - sum = Meter_computeSum(this); - assert(sum >= 0.0); - assert(sum <= DBL_MAX); - } - double total; + double total = 0.0; if (isPercentChart) { - total = MAXIMUM(this->total, sum); + assert(this->total >= 0.0); + total = this->total; + valueStart[0].scaleExp = 0; } else { + if (isPositive(this->values[0])) + total = this->values[0]; + + if (this->curItems > 1 && isgreater(this->values[1], total)) + total = this->values[1]; + + if (total > DBL_MAX) + total = DBL_MAX; + int scaleExp = 0; - (void)frexp(sum, &scaleExp); + (void)frexp(total, &scaleExp); if (scaleExp < 0) { scaleExp = 0; } @@ -878,51 +884,25 @@ static void GraphMeterMode_recordNewValue(Meter* this, const GraphDrawContext* c assert(graphHeight <= UINT16_MAX / 8); double maxDots = (double)(int32_t)(graphHeight * 8); - int numDots = 0; - if (total > 0.0) { - numDots = (int)ceil((sum / total) * maxDots); - assert(numDots >= 0); - if (sum > 0.0 && numDots <= 0) { - numDots = 1; // Division of (sum / total) underflows + for (uint8_t i = 0; i < 2; i++) { + double value = 0.0; + if (i < this->curItems && isPositive(this->values[i])) + value = this->values[i]; + + if (value > total) + value = total; + + int numDots = 0; + if (total > 0.0) { + numDots = (int)ceil((value / total) * maxDots); + assert(numDots >= 0); + if (value > 0.0 && numDots <= 0) { + numDots = 1; // Division of (value / total) underflows + } } - } - if (maxItems == 1) { assert(numDots <= UINT16_MAX); - valueStart[isPercentChart ? 0 : 1].numDots = (uint16_t)numDots; - return; - } - - // Clear cells - unsigned int y = ((unsigned int)numDots + 8 - 1) / 8; // Round up - size_t i = GraphMeterMode_valueCellIndex(graphHeight, isPercentChart, 0, y, NULL, NULL); - if (i < nCellsPerValue) { - memset(&valueStart[i], 0, (nCellsPerValue - i) * sizeof(*valueStart)); - } - - if (sum <= 0.0) - return; - - int deltaExp = 0; - double scaledTotal = total; - assert(scaledTotal > 0.0); - while (true) { - numDots = (int)ceil((sum / scaledTotal) * maxDots); - if (numDots <= 0) { - numDots = 1; // Division of (sum / scaledTotal) underflows - } - - GraphMeterMode_computeColors(this, context, valueStart, deltaExp, scaledTotal, (unsigned int)numDots); - - if (isPercentChart || !(scaledTotal < DBL_MAX) || (1U << deltaExp) >= graphHeight) { - break; - } - - deltaExp++; - scaledTotal *= 2.0; - if (scaledTotal > DBL_MAX) { - scaledTotal = DBL_MAX; - } + valueStart[1 + i].numDots = (uint16_t)numDots; } } @@ -942,6 +922,7 @@ static void GraphMeterMode_printScale(int exponent) { } } +#if 0 /* Unused code */ static uint8_t GraphMeterMode_scaleCellDetails(uint8_t details, unsigned int scaleFactor) { // Only the "top cell" of a record may need scaling like this; the cell does // not use the special meaning of bit 4. @@ -969,6 +950,7 @@ static uint8_t GraphMeterMode_scaleCellDetails(uint8_t details, unsigned int sca } return 0x00; } +#endif /* End of unused code */ static int GraphMeterMode_lookupCell(const Meter* this, const GraphDrawContext* context, int scaleExp, size_t valueIndex, unsigned int y, uint8_t* details) { unsigned int graphHeight = (unsigned int)this->h; @@ -995,50 +977,59 @@ static int GraphMeterMode_lookupCell(const Meter* this, const GraphDrawContext* int deltaExp = isPercentChart ? 0 : scaleExp - valueStart[0].scaleExp; assert(deltaExp >= 0); - if (maxItems == 1) { - unsigned int numDots = valueStart[isPercentChart ? 0 : 1].numDots; + unsigned int numDots = valueStart[1].numDots; + if (numDots >= 1) { + if (deltaExp + 1 < UINT16_WIDTH) { + numDots = ((numDots - 1) >> (deltaExp + 1)) + 1; + } else { + numDots = 1; + } + } + unsigned int blanksAtEnd = graphHeight * 4 - numDots; - if (numDots < 1) - goto cellIsEmpty; + numDots = valueStart[2].numDots; + if (numDots >= 1) { + if (deltaExp + 1 < UINT16_WIDTH) { + numDots = ((numDots - 1) >> (deltaExp + 1)) + 1; + } else { + numDots = 1; + } + } + unsigned int blanksAtStart = graphHeight * 4 - numDots; - // Scale according to exponent difference. Round up. - numDots = deltaExp < UINT16_WIDTH ? ((numDots - 1) >> deltaExp) : 0; - numDots++; + if (graphHeight - 1 - y < blanksAtEnd / 8) + goto cellIsEmpty; + if (y < blanksAtStart / 8) + goto cellIsEmpty; - if (y > (numDots - 1) / 8) - goto cellIsEmpty; + if (y * 2 == graphHeight - 1 && !(valueStart[1].numDots || valueStart[2].numDots)) + goto cellIsEmpty; + if (maxItems <= 1 || y * 2 > graphHeight - 1) { 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 if (y * 2 < graphHeight - 1) { + itemIndex = 1; } else { - int deltaExpArg = deltaExp >= UINT16_WIDTH ? UINT16_WIDTH - 1 : deltaExp; - - unsigned int scaleFactor; - size_t i = GraphMeterMode_valueCellIndex(graphHeight, isPercentChart, deltaExpArg, y, &scaleFactor, NULL); - if (i >= nCellsPerValue) - goto cellIsEmpty; + itemIndex = valueStart[1].numDots >= valueStart[2].numDots ? 0 : 1; + } - if (deltaExp >= UINT16_WIDTH) { - // Any "scaleFactor" value greater than 8 behaves the same as 8 for the - // "scaleCellDetails" function. - scaleFactor = 8; + if (y * 2 == graphHeight - 1 && valueStart[1].numDots > 8 && valueStart[2].numDots > 8) { + *details = valueStart[1].numDots >= valueStart[2].numDots ? 0x0F : 0xF0; + } else { + *details = 0xFF; + const uint8_t dotAlignment = 2; + if (y == blanksAtStart / 8) { + blanksAtStart = (blanksAtStart % 8) / dotAlignment * dotAlignment; + *details >>= blanksAtStart; + } + if ((graphHeight - 1 - y) == blanksAtEnd / 8) { + blanksAtEnd = (blanksAtEnd % 8) / dotAlignment * dotAlignment; + *details = (uint8_t)((*details >> blanksAtEnd) << blanksAtEnd); } - - 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; @@ -1136,9 +1127,7 @@ static void GraphMeterMode_draw(Meter* this, int x, int y, int w) { uint8_t maxItems = Meter_maxItems(this); bool isPercentChart = Meter_isPercentChart(this); - size_t nCellsPerValue = maxItems <= 1 ? maxItems : graphHeight; - if (!isPercentChart) - nCellsPerValue *= 2; + size_t nCellsPerValue = maxItems < 1 ? maxItems : 3; GraphDrawContext context = { .maxItems = maxItems, From 5e3eeb13900d069142ceede8fe879936acfb66ea Mon Sep 17 00:00:00 2001 From: Explorer09 Date: Thu, 28 Mar 2024 20:58:58 +0800 Subject: [PATCH 14/14] (draft) Add a special case for cell "details" This special case of "details" value would print as ':' in the ASCII display mode. It is used in "positive/negative" graph display only. Signed-off-by: Kang-Che Sung --- Meter.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Meter.c b/Meter.c index 6c73845ac..946d58803 100644 --- a/Meter.c +++ b/Meter.c @@ -1026,6 +1026,9 @@ static int GraphMeterMode_lookupCell(const Meter* this, const GraphDrawContext* blanksAtEnd = (blanksAtEnd % 8) / dotAlignment * dotAlignment; *details = (uint8_t)((*details >> blanksAtEnd) << blanksAtEnd); } + if (*details == 0x3C) { + *details = 0x24; + } } /* fallthrough */ @@ -1050,7 +1053,7 @@ static void GraphMeterMode_printCellDetails(uint8_t details) { // 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 & 0x9C) == 0x14 || (details & 0x39) == 0x28 || details == 0x24) { if (details == 0x14 || details == 0x28) { // Special case details = 0x18; } else { @@ -1088,6 +1091,8 @@ static void GraphMeterMode_printCellDetails(uint8_t details) { c = upperHalf; } else if ((details & 0x39) == 0x28) { c = lowerHalf; + } else if (details == 0x24) { + c = fullCell; // End of special cases } else if (popCount8(details) > 4) { c = fullCell;