Add mixed-vendor GPU summary and fix startup heap corruption

Collect AMD amdgpu sysfs and optional NVIDIA nvidia-smi stats in the
background, auto-detecting supported adapters in PCI order and rotating
additional pairs alongside CPU/RAM. Explicit PCI selection and off mode
remain supported; missing metrics render as N/A.

Initialize the bitmap-only canvas locally instead of the ABI-dependent
libg15render initializer: the installed TTF-enabled renderer wrote an
8-byte FreeType pointer past the smaller bitmap-only allocation, corrupting
the heap before GPU threads started. Add collector, rendering and full-main
regression tests.
This commit is contained in:
Mikhail Yevchenko committed 2026-09-07 17:06:32 +00:00
1 parent 076641c867
commit 7c06a8089d
12 files changed
+1417 -4

No files matched your search

+284
View File
@@ -0,0 +1,284 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#define _GNU_SOURCE 1
#include "../gpu.h"
#include <assert.h>
#include <dirent.h>
static int descriptors(void)
{
DIR *dir = opendir("/proc/self/fd");
struct dirent *entry;
int count = 0;
assert(dir);
while ((entry = readdir(dir)))
if (entry->d_name[0] != '.') count++;
closedir(dir);
return count;
}
static void unknown(gpu_sample s)
{
assert(s.busy == -1 && s.used == -1 && s.total == -1 && s.temp == LLONG_MIN);
}
static gpu_sample test_command(const char *path, char *const args[])
{
char text[256];
return gpu_command(path, args, text, sizeof(text)) ? gpu_csv(text) : gpu_unknown;
}
static void *cache_writer(void *arg)
{
size_t index = *(size_t *)arg;
int i;
for (i = 0; i < 20000; i++)
gpu_publish(index, (gpu_sample){i % 101, i, i + 1, (long long)index});
return NULL;
}
static void cache_concurrency(void)
{
pthread_t writers[2];
size_t indices[2] = {0, 1}, i;
int repeat;
gpu_count = 2;
for (i = 0; i < 2; i++) {
gpu_publish(i, (gpu_sample){0, 0, 1, (long long)i});
assert(!pthread_create(&writers[i], NULL, cache_writer, &indices[i]));
}
for (repeat = 0; repeat < 20000; repeat++) {
gpu_frame frame = gpu_snapshot();
assert(frame.count == 2);
for (i = 0; i < 2; i++) {
assert(frame.samples[i].total == frame.samples[i].used + 1);
assert(frame.samples[i].busy == frame.samples[i].used % 101);
assert(frame.samples[i].temp == (long long)i);
}
}
for (i = 0; i < 2; i++) assert(!pthread_join(writers[i], NULL));
}
static void parent_death(void)
{
pid_t child, grandchild = 0, result;
long long deadline;
char path[96];
int status;
assert(!prctl(PR_SET_CHILD_SUBREAPER, 1));
child = fork();
assert(child >= 0);
if (!child) {
char *const args[] = {"sleep", "10", NULL};
test_command("/usr/bin/sleep", args);
_exit(0);
}
snprintf(path, sizeof(path), "/proc/%ld/task/%ld/children", (long)child, (long)child);
deadline = gpu_milliseconds() + 500;
do {
FILE *file = fopen(path, "r");
long id;
if (file) {
if (fscanf(file, "%ld", &id) == 1) grandchild = (pid_t)id;
fclose(file);
}
if (!grandchild) poll(NULL, 0, 5);
} while (!grandchild && gpu_milliseconds() < deadline);
assert(grandchild > 0);
assert(!kill(child, SIGKILL));
assert(waitpid(child, &status, 0) == child);
deadline = gpu_milliseconds() + 1000;
do {
result = waitpid(grandchild, &status, WNOHANG);
if (!result) poll(NULL, 0, 5);
} while (!result && gpu_milliseconds() < deadline);
assert(result == grandchild && WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL);
assert(!prctl(PR_SET_CHILD_SUBREAPER, 0));
}
int main(int argc, char **argv)
{
gpu_sample s;
char text[256], memory[32];
char *const sleeper[] = {"sleep", "10", NULL};
char *const failure[] = {"false", NULL};
long long start;
int status;
pid_t result;
int fd_count, repeat;
assert(gpu_page_first(0, 5000) == 0);
assert(gpu_page_first(2, 5000) == 0);
assert(gpu_page_first(3, 5000) == 2);
assert(gpu_page_first(3, 10000) == 0);
for (repeat = 0; repeat < GPU_MAX; repeat += 2)
assert(gpu_page_first(GPU_MAX, (unsigned long long)repeat / 2 * 5000) == (size_t)repeat);
gpu_page = 0;
gpu_page_due = 5000;
assert(gpu_display_first(5, 0) == 0);
assert(gpu_display_first(5, 10000) == 2);
assert(gpu_display_first(5, 20000) == 4);
assert(gpu_display_first(5, 30000) == 0);
assert(gpu_display_first(5, 30001) == 0);
assert(gpu_selection_valid("auto") && gpu_selection_valid("off"));
assert(gpu_selection_valid("0000:AB:1f.7"));
assert(!gpu_selection_valid("0000:01:20.0"));
assert(!gpu_selection_valid("0000:01:00.8"));
assert(!gpu_selection_valid("-001:01:00.0"));
assert(!gpu_selection_valid("../../passwd"));
assert(!gpu_selection_valid("0000:01:00.0;id"));
assert(gpu_number(" 0\n", 0, 100, -1) == 0);
assert(gpu_number("100", 0, 100, -1) == 100);
assert(gpu_number("101", 0, 100, -1) == -1);
assert(gpu_number("12oops", 0, 100, -1) == -1);
assert(gpu_number("", 0, 100, -1) == -1);
assert(gpu_number("N/A", 0, 100, -1) == -1);
assert(gpu_number("999999999999999999999999", 0, LLONG_MAX, -1) == -1);
strcpy(text, "100, 12288, 16384, -5\n");
s = gpu_csv(text);
assert(s.busy == 100 && s.used == 12884901888LL &&
s.total == 17179869184LL && s.temp == -5);
strcpy(text, "N/A, [Not Supported], 16384, 0\n");
s = gpu_csv(text);
assert(s.busy == -1 && s.used == -1 && s.total == 17179869184LL && s.temp == 0);
strcpy(text, "0, 0, 0, N/A\n");
s = gpu_csv(text);
assert(s.busy == 0 && s.used == 0 && s.total == -1 && s.temp == LLONG_MIN);
strcpy(text, "0, 20, 10, 35\n");
assert(gpu_csv(text).used == -1);
strcpy(text, "0, 1, 2"); unknown(gpu_csv(text));
strcpy(text, "0, 1, 2, 3, 4"); unknown(gpu_csv(text));
strcpy(text, "N/A, N/A, N/A, N/A"); unknown(gpu_csv(text));
gpu_memory(memory, sizeof(memory), -1); assert(!strcmp(memory, "N/A"));
gpu_memory(memory, sizeof(memory), 19 * 1048576); assert(!strcmp(memory, "19.0MiB"));
gpu_memory(memory, sizeof(memory), 17179869184LL); assert(!strcmp(memory, "16.0GiB"));
gpu_memory(memory, sizeof(memory), LLONG_MAX); assert(strlen(memory) <= 11);
assert(!gpu_detect("off") && !gpu_count);
assert(!gpu_detect("ffff:ff:1f.7") && gpu_count == 1 && !gpu_devices[0].backend);
gpu_cache[0] = s;
gpu_stamp[0] = gpu_milliseconds() - 6000;
unknown(gpu_snapshot().samples[0]);
{
gpu_frame frame = {.count = 3};
gpu_count = 3;
for (repeat = 0; repeat < 3; repeat++) {
snprintf(gpu_devices[repeat].bus, 32, "0000:%02x:00.0", repeat + 1);
gpu_devices[repeat].backend = repeat == 1 ? 1 : 2;
frame.samples[repeat] = gpu_unknown;
}
frame.samples[1] = (gpu_sample){77, 1, 2, 42};
strcpy(text, "00000000:03:00.0, 90, 4, 8, 55\n00000000:01:00.0, N/A, 1, 2, N/A\n");
gpu_nvidia_parse(text, &frame);
assert(frame.samples[0].busy == -1 && frame.samples[0].used == 1048576);
assert(frame.samples[1].busy == 77 && frame.samples[1].used == 1);
assert(frame.samples[2].busy == 90 && frame.samples[2].temp == 55);
strcpy(text, "00000000:03:00.0, 1, 4, 8, 55\n0000:03:00.0, 2, 4, 8, 55\n");
gpu_nvidia_parse(text, &frame);
unknown(frame.samples[2]);
assert(!gpu_bus_equal("0000:01:00.0junk", "0000:01:00.0"));
assert(gpu_bus_equal("00000000:AB:00.0 ", "0000:ab:00.0"));
assert(!gpu_bus_equal("0001:01:00.0", "0000:01:00.0"));
for (repeat = 0; repeat < 3; repeat++) gpu_publish(repeat, frame.samples[repeat]);
gpu_stamp[0] = gpu_milliseconds() - 6000;
frame = gpu_snapshot();
unknown(frame.samples[0]);
assert(frame.samples[1].busy == 77);
unknown(frame.samples[2]);
/* Missing AMD path is independent of the populated neighboring cache. */
snprintf(gpu_devices[0].device, PATH_MAX, "/nonexistent/g15stats-test");
gpu_publish(0, gpu_amd(&gpu_devices[0]));
assert(gpu_snapshot().samples[1].busy == 77);
}
/* Real processes exercise the deadline and reaping, not fake GPU files. */
start = gpu_milliseconds();
unknown(test_command("/usr/bin/sleep", sleeper));
assert(gpu_milliseconds() - start >= 750);
assert(gpu_milliseconds() - start < 1500);
poll(NULL, 0, 50);
unknown(test_command("/usr/bin/false", failure));
result = waitpid(-1, &status, WNOHANG);
assert(result == -1 && errno == ECHILD);
unknown(test_command("/nonexistent/g15stats-test", failure));
fd_count = descriptors();
for (repeat = 0; repeat < 20; repeat++)
unknown(test_command("/usr/bin/false", failure));
assert(descriptors() == fd_count);
{
char payload[12001], output[GPU_MAX * 256];
char *const args[] = {"printf", "%s", payload, NULL};
memset(payload, 'x', sizeof(payload) - 1);
payload[sizeof(payload) - 1] = 0;
assert(gpu_command("/usr/bin/printf", args, output, sizeof(output)));
assert(!strcmp(payload, output));
assert(!gpu_command("/usr/bin/printf", args, output, 256));
poll(NULL, 0, 50);
unknown(test_command("/usr/bin/false", failure));
}
parent_death();
cache_concurrency();
if (argc == 2) {
gpu_frame frame;
size_t i;
assert(!gpu_detect(argv[1]) && gpu_count);
assert(!gpu_start());
start = gpu_milliseconds();
do {
frame = gpu_snapshot();
for (i = 0; i < frame.count; i++) if (frame.samples[i].total <= 0) break;
if (i == frame.count) break;
poll(NULL, 0, 10);
} while (gpu_milliseconds() - start < 1800);
for (i = 0; i < frame.count; i++) {
char path[PATH_MAX + 32];
s = frame.samples[i];
if (i) assert(strcmp(gpu_devices[i-1].bus, gpu_devices[i].bus) < 0);
snprintf(path, sizeof(path), "%s/vendor", gpu_devices[i].device);
FILE *vendor = fopen(path, "r");
unsigned id;
assert(vendor && fscanf(vendor, "%x", &id) == 1);
fclose(vendor);
assert(id == (gpu_devices[i].backend == 1 ? 0x1002U : 0x10deU));
printf("LIVE %zu %s vendor=%04x backend=%d utilization=%lld used=%lld total=%lld temp=%lld\n",
i+1, gpu_devices[i].bus, id, gpu_devices[i].backend, s.busy, s.used, s.total, s.temp);
assert(s.busy >= 0 && s.used >= 0 && s.total > 0 && s.temp != LLONG_MIN);
}
for (repeat = 0; repeat < 100000; repeat++) {
frame = gpu_snapshot();
assert(frame.count == gpu_count);
for (i = 0; i < frame.count; i++) assert(frame.samples[i].total > 0);
}
start = gpu_milliseconds();
assert(!gpu_stop());
assert(gpu_milliseconds() - start < 1200);
puts("PASS all live adapters, sysfs vendor identities, concurrent snapshots and bounded shutdown");
/* Read-only failure injection: point just the stopped AMD collector at
* a nonexistent path, then prove the actual NVIDIA peer remains live. */
if (gpu_count > 1) {
size_t amd = GPU_MAX, nv = GPU_MAX;
char saved[PATH_MAX];
for (i = 0; i < gpu_count; i++) {
if (gpu_devices[i].backend == 1) amd = i;
if (gpu_devices[i].backend == 2) nv = i;
}
if (amd < GPU_MAX && nv < GPU_MAX) {
strcpy(saved, gpu_devices[amd].device);
strcpy(gpu_devices[amd].device, "/nonexistent/g15stats-test");
for (i = 0; i < gpu_count; i++) gpu_publish(i, gpu_unknown);
assert(!gpu_start());
start = gpu_milliseconds();
do {
frame = gpu_snapshot();
if (frame.samples[nv].total > 0) break;
poll(NULL, 0, 10);
} while (gpu_milliseconds() - start < 1800);
assert(frame.samples[nv].total > 0 && frame.samples[nv].busy >= 0);
unknown(frame.samples[amd]);
assert(!gpu_stop());
strcpy(gpu_devices[amd].device, saved);
puts("PASS failed AMD collector isolated from real live NVIDIA peer");
}
}
}
puts("PASS GPU parsing, units, missing data, expiry, selection, deadline, reap and parent death");
return 0;
}
+53
View File
@@ -0,0 +1,53 @@
#!/bin/sh
# Convert existing render-test exports without trimming, resampling or labels
# inside the framebuffer. ImageMagick is optional, not a make check dependency.
set -eu
if [ "$#" -ne 1 ]; then
echo "Usage: sh tests/render-images.sh ABSOLUTE_OUTPUT_DIRECTORY" >&2
exit 2
fi
cd "$1"
for pgm in ./*-[01].pgm; do
[ -f "$pgm" ] || continue
base=${pgm%.pgm}
[ "$(wc -c < "$base.bin")" -eq 860 ]
{ printf 'P4\n160 43\n'; cat "$base.bin"; } > "$base.pbm"
# Compare the packed 1-bit buffer independently with the PGM export.
compare -metric AE "$base.pbm" "$pgm" null: 2> "$base-pixels.log"
convert "$pgm" +repage -type TrueColor "PNG24:$base-native.png"
convert "$base-native.png" -sample 800% +repage "PNG24:$base-8x.png"
convert "$base-8x.png" -sample 160x43! +repage "PNG24:$base-roundtrip.png"
compare -metric AE "$pgm" "$base-native.png" null: 2>> "$base-pixels.log"
compare -metric AE "$pgm" "$base-roundtrip.png" null: 2>> "$base-pixels.log"
rm "$base-roundtrip.png"
# Smaller contact tiles avoid shrinking wide sheets in image viewers.
# Keep the eightfold originals available for individual pixel inspection.
convert "$base-native.png" -sample 400% +repage "PNG24:$base-4x.png"
done
for group in off nvidia amd single dual large unknown odd maximum; do
[ -f "$group-0-4x.png" ] || continue
montage -font DejaVu-Sans -pointsize 14 -background '#dddddd' \
-label "$group: five-row summary (4x)" "$group-0-4x.png" \
-label "$group: four-row summary (4x)" "$group-1-4x.png" \
-tile 1x2 -geometry +8+8 "PNG24:contact-$group.png"
height=$(identify -format '%h' "contact-$group.png")
for variant in 0 1; do
top=$((8 + variant * height / 2))
convert "contact-$group.png" -crop "640x172+8+$top" +repage \
"PNG24:$group-tile-check.png"
compare -metric AE "$group-$variant-4x.png" "$group-tile-check.png" \
null: 2>> "$group-contact-pixels.log"
done
rm "$group-tile-check.png"
done
if [ -f contact-off.png ] && [ -f contact-nvidia.png ] && [ -f contact-amd.png ]; then
convert contact-off.png contact-nvidia.png contact-amd.png -append +repage \
PNG24:contact-legacy.png
fi
if [ -f contact-dual.png ] && [ -f contact-large.png ] && [ -f contact-unknown.png ]; then
convert contact-dual.png contact-large.png contact-unknown.png -append +repage \
PNG24:contact-metrics.png
fi
if [ -f contact-odd.png ] && [ -f contact-maximum.png ]; then
convert contact-odd.png contact-maximum.png -append +repage PNG24:contact-paging.png
fi
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# Native-pixel checks only: no image conversion, daemon or input-device access.
set -eu
renderer=${RENDER_TEST:-./render-test}
out=$(mktemp -d "${TMPDIR:-/tmp}/g15stats-layouts-XXXXXX")
trap 'rm -rf "$out"' EXIT HUP INT TERM
"$renderer" off "$out/off"
"$renderer" off "$out/single" 1
"$renderer" off "$out/dual" 2
"$renderer" off "$out/large" 2 0 large
"$renderer" off "$out/unknown" 2 0 unknown
"$renderer" off "$out/odd" 3 1
"$renderer" off "$out/maximum" 64 31
+230
View File
@@ -0,0 +1,230 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Exercise the real renderer without any daemon connection or input thread.
*/
#define main g15stats_application_main
#include "../g15stats.c"
#undef main
#include <assert.h>
/* Independent font-bitmap oracle: do not repeat renderString on a second
* canvas, since that also repeats any clipping/erasure in the renderer.
* The default fonts have zero gap and one blank trailing pixel column.
* Compare every cell, including blank pixels, against the font file's bits. */
static void assert_text(g15canvas *canvas, const char *text, int size, int x, int y, int right)
{
g15font *font = g15r_requestG15DefaultFont(size);
assert(font && font->default_gap == 0);
assert(font->font_height == font->ascender_height);
for (; *text; text++) {
unsigned char c = (unsigned char)*text;
int width = font->glyph[c].width;
int stride = (width + 7) / 8;
assert(font->active[c] && font->glyph[c].buffer);
assert(x >= 0 && x + width <= 160);
assert(y >= 0 && y + font->font_height <= 43);
for (int dy = 0; dy < font->font_height; dy++)
for (int dx = 0; dx < width; dx++) {
int bit = !!(font->glyph[c].buffer[dy * stride + dx / 8] &
(0x80 >> (dx % 8)));
/* A 13-character first-column header's final blank advance
* pixel shares x=105 with the next divider; no ink may do so. */
if (x + dx >= right) {
assert(!text[1] && dx == width - 1 && !bit);
continue;
}
if (g15r_getPixel(canvas, x + dx, y + dy) != bit) {
fprintf(stderr, "Glyph %c pixel (%d,%d): expected %d, got %d\n",
c, x + dx, y + dy, bit,
g15r_getPixel(canvas, x + dx, y + dy));
assert(0);
}
}
x += width;
}
}
int main(int argc, char **argv)
{
g15canvas canvas, before;
char tmp[MAX_LINES];
int x, y, variant;
size_t i;
g15font *font = g15r_requestG15DefaultFont(G15_TEXT_SMALL);
assert(font);
assert(g15r_testG15FontWidth(font, "T 8192.0PiB") <= 52);
assert(g15r_testG15FontWidth(font, "UTL 100%") <= 52);
{
char lines[5][32];
compact_ram_lines(lines, LLONG_MAX, LLONG_MAX);
assert(strstr(lines[1], "PiB") && strstr(lines[2], "PiB"));
assert(g15r_testG15FontWidth(font, lines[1]) <= 50);
assert(g15r_testG15FontWidth(font, lines[2]) <= 50);
compact_ram_lines(lines, 19 * 1048576, 17179869184LL);
assert(!strcmp(lines[1], "R 19.0MiB"));
assert(!strcmp(lines[2], "T 16.0GiB"));
}
memset(&canvas, 0xa5, sizeof(canvas));
init_canvas(&canvas);
assert(!canvas.mode_xor && !canvas.mode_reverse && !canvas.mode_cache);
for (y = 0; y < 43; y++) for (x = 0; x < 160; x++)
assert(!g15r_getPixel(&canvas, x, y));
/* Original full-width geometry must remain exact with the column off. */
init_canvas(&before);
summary_gpu = 0;
drawBar_both(&canvas, 1, 8, 37, 100, 63, 100);
g15r_drawBar(&before, BAR_START, 1, BAR_END, 8, G15_COLOR_BLACK, 37, 100, 4);
drawBar_reversed(&before, BAR_START, 1, BAR_END, 8, G15_COLOR_BLACK, 63, 100, 5);
assert(!memcmp(canvas.buffer, before.buffer, sizeof(canvas.buffer)));
glibtop_init();
/* No device fixtures: sample the actual selected GPU when requested. */
assert(!gpu_detect(argc > 1 ? argv[1] : "off"));
if (gpu_count && gpu_devices[0].backend) {
long long start = gpu_milliseconds();
assert(!gpu_start());
do {
gpu_frame frame = gpu_snapshot();
for (i = 0; i < frame.count; i++) if (frame.samples[i].total < 0) break;
if (i == frame.count) break;
poll(NULL, 0, 10);
} while (gpu_milliseconds() - start < 1800);
assert(i == gpu_count);
assert(!gpu_stop());
}
/* No-argument make check retains the worst-width fixture. Explicit off
* must exercise the application's actual zero-GPU/full-width layout.
* Optional count/page/profile arguments are in-memory visual cases only. */
if (!gpu_count && (argc == 1 || argc > 3)) {
gpu_count = argc > 3 ? (size_t)atoi(argv[3]) : 2;
assert(gpu_count >= 1 && gpu_count <= GPU_MAX);
gpu_devices[0].backend = 2;
gpu_devices[1].backend = 1;
strcpy(gpu_devices[0].bus, "0000:01:00.0");
strcpy(gpu_devices[1].bus, "0000:11:00.0");
gpu_publish(0, (gpu_sample){100, LLONG_MAX, LLONG_MAX, -273});
gpu_publish(1, (gpu_sample){100, LLONG_MAX, LLONG_MAX, 1000});
for (i = 2; i < gpu_count; i++) {
gpu_devices[i].backend = 1;
snprintf(gpu_devices[i].bus, 32, "0001:%02x:1f.7", (unsigned)i);
gpu_publish(i, i % 2 ? gpu_unknown : (gpu_sample){100, LLONG_MAX, LLONG_MAX, -273});
}
if (gpu_count > 2) {
/* Exercise every pair at the maximum inventory, including the
* 13-character AMD64 header and an odd final card. */
for (i = 0; i < (gpu_count + 1) / 2; i++) {
gpu_page = i;
gpu_page_due = LLONG_MAX;
g15r_clearScreen(&canvas, G15_COLOR_WHITE);
draw_gpu_column(&canvas);
for (y = 35; y < 43; y++) for (x = 0; x < 160; x++)
assert(!g15r_getPixel(&canvas, x, y));
}
/* Keep the final pair for the exported preview and pixel checks. */
}
}
if (argc > 4) {
gpu_page = (size_t)atoi(argv[4]);
assert(gpu_page < (gpu_count + 1) / 2);
gpu_page_due = LLONG_MAX;
}
if (argc > 5) {
assert(!strcmp(argv[5], "large") || !strcmp(argv[5], "unknown"));
for (i = 0; i < gpu_count; i++)
gpu_publish(i, !strcmp(argv[5], "unknown") ? gpu_unknown :
(gpu_sample){100, 100LL << 30, 192LL << 30, 1000});
}
for (i = 0; i < gpu_count; i++) {
char lines[5][32];
int row;
gpu_lines(i, gpu_cache[i], lines);
for (row = 0; row < 5; row++) {
assert(g15r_testG15FontWidth(font, lines[row]) <= 52);
printf("GPU %zu row %d: %s\n", i + 1, row, lines[row]);
}
}
for (variant = 0; variant < 2; variant++) {
cycle = SCREEN_SUMMARY;
mode[SCREEN_SUMMARY] = variant;
have_temp = have_fan = 0; /* Keep legacy generic hwmon probing out of QA. */
summary_gpu = gpu_count != 0;
draw_cpu_screen_multicore(&canvas, tmp, 0);
before = canvas;
draw_gpu_column(&canvas);
for (y = 0; y < 43; y++)
for (x = 0; x < 160; x++)
if (x < (gpu_count > 1 ? 51 : 105) || y >= 35)
assert(g15r_getPixel(&canvas, x, y) == g15r_getPixel(&before, x, y));
/* Re-render each column on an empty canvas: exact pixels prove neither
* neighboring column nor CPU text overlaps it, including edge glyphs. */
{
gpu_frame frame = gpu_snapshot();
size_t first = gpu_page_first(frame.count, gpu_page * 5000ULL);
for (i = first; i < frame.count && i < first + 2; i++) {
g15canvas isolated;
char lines[5][32];
int row, left = frame.count == 1 ? 108 : 54 + (int)(i-first) * 54;
init_canvas(&isolated);
gpu_lines(i, frame.samples[i], lines);
for (row = 0; row < 5; row++)
g15r_renderString(&isolated, (unsigned char *)lines[row], 0, G15_TEXT_SMALL, left, 1+row*7);
for (y = 0; y < 35; y++) for (x = left; x < (left == 54 ? 105 : 160); x++)
assert(g15r_getPixel(&canvas, x, y) == g15r_getPixel(&isolated, x, y));
}
}
summary_gpu = 0;
print_time_info(&canvas, tmp);
assert(g15r_testG15FontWidth(font, tmp) <= 160);
assert_text(&canvas, tmp, G15_TEXT_SMALL,
80 - (int)strlen(tmp) * 4 / 2, INFO_ROW, 160);
if (gpu_count < 2) {
const char *label = "TOTAL";
for (int row = 0; row < 5; row++) {
char letter[2] = {label[row], 0};
assert_text(&canvas, letter, G15_TEXT_MED,
gpu_count ? 98 : TEXT_RIGHT, row * 7, 160);
}
}
{
gpu_frame frame = gpu_snapshot();
size_t first = gpu_page_first(frame.count, gpu_page * 5000ULL);
for (i = first; i < frame.count && i < first + 2; i++) {
char lines[5][32];
int left = frame.count == 1 ? 108 : 54 + (int)(i-first) * 54;
gpu_lines(i, frame.samples[i], lines);
for (int row = 0; row < 5; row++)
assert_text(&canvas, lines[row], G15_TEXT_SMALL, left, 1+row*7,
left == 54 ? 105 : 160);
}
}
if (argc > 2) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s-%d.bin", argv[2], variant);
FILE *raw = fopen(path, "wb");
assert(raw);
assert(fwrite(canvas.buffer, 1, 160 * 43 / 8, raw) == 160 * 43 / 8);
assert(!fclose(raw));
snprintf(path, sizeof(path), "%s-%d.pgm", argv[2], variant);
FILE *out = fopen(path, "w");
assert(out);
fprintf(out, "P2\n160 43\n255\n");
for (y = 0; y < 43; y++) {
for (x = 0; x < 160; x++) fprintf(out, "%d ", g15r_getPixel(&canvas, x, y) ? 0 : 255);
fputc('\n', out);
}
assert(!fclose(out));
}
}
if (argc > 2) {
FILE *out = fopen(argv[2], "w");
assert(out);
fprintf(out, "P2\n160 43\n255\n");
for (y = 0; y < 43; y++) {
for (x = 0; x < 160; x++)
fprintf(out, "%d ", g15r_getPixel(&canvas, x, y) ? 0 : 255);
fputc('\n', out);
}
assert(!fclose(out));
}
glibtop_close();
puts("PASS real LCD buffer: initialization, original geometry, both summary variants, independent glyph bits, complete TOTAL/footer, column bounds");
return 0;
}
+77
View File
@@ -0,0 +1,77 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Full application startup/draw loop, real libgtop and bitmap renderer.
* Only daemon transport is replaced. No TCP, USB, or input-device access.
* Default: GPU off, three frames. Optional: runtime-test auto 21 verifies
* real read-only GPU samples over twenty seconds (not a physical LCD test).
*/
#define main g15stats_application_main
#include "../g15stats.c"
#undef main
#include <assert.h>
static unsigned frames, target = 3;
static int peer = -1;
/* Catch the ABI-unsafe initializer even with a non-TTF renderer in CI.
* Do not stub any bitmap drawing or host statistics functions. */
void g15r_initCanvas(g15canvas *canvas)
{
(void)canvas;
fputs("FAIL: bitmap client called ABI-dependent g15r_initCanvas\n", stderr);
abort();
}
int new_g15_screen(int type)
{
int pair[2];
assert(type == G15_G15RBUF);
assert(!socketpair(AF_UNIX, SOCK_STREAM, 0, pair));
peer = pair[1]; /* No key events: actual keyboard thread blocks in recv. */
return pair[0];
}
int g15_send(int fd, char *buffer, unsigned int length)
{
unsigned ink = 0;
gpu_frame frame = gpu_snapshot();
(void)fd;
assert(length == G15_BUFFER_LEN);
for (unsigned i = 0; i < 160 * 43 / 8; i++) ink += !!buffer[i];
assert(ink);
fprintf(stderr, "runtime frame %u: %u ink bytes, %zu GPUs\n",
++frames, ink, frame.count);
if (frames == target) {
glibtop_cpu cpu;
glibtop_mem mem;
glibtop_get_cpu(&cpu);
glibtop_get_mem(&mem);
assert(cpu.total > 0 && mem.total > 0 && mem.free <= mem.total);
fprintf(stderr, "host stats: CPU ticks=%llu RAM total=%llu free=%llu\n",
(unsigned long long)cpu.total, (unsigned long long)mem.total,
(unsigned long long)mem.free);
for (size_t i = 0; i < frame.count; i++) {
gpu_sample s = frame.samples[i];
fprintf(stderr, "%s: busy=%lld used=%lld total=%lld temp=%lld\n",
gpu_devices[i].bus, s.busy, s.used, s.total, s.temp);
assert(s.busy >= 0 && s.used >= 0 && s.total > 0 && s.temp != LLONG_MIN);
}
assert(!gpu_stop());
/* Full main is intentionally endless. Terminate without closing the
* peer first (which would wake the legacy keyboard loop with EOF). */
fputs("PASS full main, isolated transport, real renderer/collectors\n", stderr);
_Exit(0);
}
return length;
}
int main(int argc, char **argv)
{
char *args[] = {"g15stats", "--gpu", argc > 1 ? argv[1] : "off", NULL};
if (argc > 2) {
long n = gpu_number(argv[2], 3, 30, -1);
assert(n >= 3);
target = (unsigned)n;
}
alarm(target + 10); /* Failure bound, not a passing timeout. */
return g15stats_application_main(3, args);
}