2 Commits
Author SHA1 Message Date
Mikhail Yevchenko 7c06a8089d 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.
2026-09-07 17:06:32 +00:00
Mikhail Yevchenko 076641c867 Add Debian 13 packaging for g15stats 2026-09-07 16:13:47 +03:00
30 changed files with 1839 additions and 4 deletions

No files matched your search

+13
View File
@@ -11,5 +11,18 @@ LIBS = -lg15daemon_client -lg15render -lpthread -lm -lrt
IMCLUDES = -I
g15stats_LDFLAGS = -avoid-version
g15stats_SOURCES = g15stats.c g15stats.h
EXTRA_DIST = gpu.h tests/gpu-test.c tests/render-test.c tests/runtime-test.c tests/render-layouts.sh tests/render-images.sh
noinst_HEADERS += gpu.h
AUTOMAKE_OPTIONS = subdir-objects
check_PROGRAMS = gpu-test render-test runtime-test
TESTS = $(check_PROGRAMS) tests/render-layouts.sh
TEST_EXTENSIONS = .sh
SH_LOG_COMPILER = $(SHELL)
gpu_test_SOURCES = tests/gpu-test.c
gpu_test_LDADD = -lpthread
render_test_SOURCES = tests/render-test.c
render_test_LDADD = -lg15daemon_client -lg15render -lpthread -lm -lrt $(GTOP_LIBS)
runtime_test_SOURCES = tests/runtime-test.c
runtime_test_LDADD = -lg15render -lpthread -lm -lrt $(GTOP_LIBS)
+107
View File
@@ -2,6 +2,113 @@ G15Stats - a CPU/Memory/Swap/Network/Battery/Temperature/Fan Speed/CPU Frequenci
The G15Stats requires libgtop development packages to be installed before compilation.
GPU summary columns:
The default --gpu auto collects ALL PCI devices (sorted PCI address) bound
to amdgpu or nvidia at startup, including mixed vendors. Use --gpu dddd:bb:ss.f (also -gpu), for example
--gpu 0000:01:00.0, to select a particular card. --gpu off restores the original
layout and disables collection. An explicit missing/unsupported card shows N/A;
auto with no supported card leaves the original layout unchanged. Restart to
rediscover cards after hotplug. Intel and the nouveau/radeon drivers are not
supported. Missing tools, permissions or individual metrics produce N/A, not 0.
With one GPU the 160x43 summary retains its existing 4/5 indicator rows and
labels. Graphs end at x=96 instead of 153; TOTAL moves to x=98. A divider
at x=105 separates the column at x=108..159. With two or more GPUs the left
x=0..50 shows compact CPU percent, R used system RAM, T total system RAM,
SWP percent, and (in five-row mode) GPU count. Four-row mode uses y=1,10,19,28;
five-row mode uses y=1,8,15,22,29. System RAM uses the same explicit units as VRAM.
Two GPU columns at x=54..104 and x=108..159 are visible together, with dividers
at x=51 and x=105 (the first column's glyphs end before its right divider).
Each GPU has five lines at y=1,8,15,22,29: vendor/index plus PCI bus:slot.function,
UTL percent, U used VRAM, T total VRAM, TEM temperature in Celsius (C).
For example NV1 01:00.0 and AMD2 11:00.0 identify different cards. Indices are
one-based positions in the selected PCI-sorted inventory, not NVIDIA ordinals;
startup diagnostics map each index to its full domain:bus:slot.function address.
More than two GPUs cycle through consecutive pairs every five seconds from
startup (1/2, 3/4, ..., then 1/2); the last odd card occupies the first column.
All cards are collected continuously, not just the visible pair. The display
refresh interval can delay a page change: each draw advances at most one pair,
so slow refresh never skips pairs. Inventory is bounded at 64 cards;
overflow/enumeration failure is an explicit startup error, never silent omission.
Use explicit PCI selection or off to recover. Hotplug requires restart.
The bottom info bar at y=37..42 and all non-summary screens retain their geometry.
Used and total are on separate lines so each retains an explicit MiB/GiB unit
(TiB/PiB for very large values), rounded to one decimal. Other screens do not
change. U/T refer to VRAM, not memory-controller utilization or system RAM;
on AMD APUs the driver may report only the reserved VRAM carveout.
AMD reads gpu_busy_percent, mem_info_vram_used/total (bytes), and the selected
PCI device's hwmon/temp1_input (millidegrees C). NVIDIA optionally executes
/usr/bin/nvidia-smi with pci.bus_id,utilization.gpu,memory.used,memory.total,temperature.gpu
and --format=csv,noheader,nounits: one query for all NVIDIA cards, matched by PCI
identity (including NVIDIA's eight-digit domain form), not response order. No CUDA/NVML SDK
or additional build dependency is needed. Run as an ordinary user.
Independent AMD workers and one batched NVIDIA worker wait two seconds between
queries; drawing uses only a mutex-protected snapshot array. A stalled AMD read
does not block another card or NVIDIA. NVIDIA queries share one 750ms monotonic deadline, fixed
arguments/environment, no shell, bounded output, and SIGKILL on timeout or
parent death. An unreaped child prevents further launches. Samples older than
five seconds display N/A per card, including when a sysfs read stalls. Worker
shutdown has one shared one-second join/reap budget; a stuck kernel read can
outlive that budget in static storage until process exit (no restart after a
failed stop). Normal application termination exits the process; no driver waits
are added to signal handling. The -r display
refresh interval still applies independently. No GPU settings are written.
Official interface references:
https://docs.kernel.org/gpu/amdgpu/thermal.html
https://docs.kernel.org/gpu/amdgpu/driver-misc.html
https://docs.nvidia.com/deploy/nvidia-smi/index.html
GPU validation:
After configuring/building, make check runs compiled parsing, units, missing-data,
expiry, selection, subprocess deadline/reaping/parent-death and descriptor checks,
plus real libg15render buffer checks. No daemon, input devices or synthetic GPU
sysfs fixtures are used. ./gpu-test auto additionally checks every real GPU,
its sysfs vendor identity and background cache (requires all four metrics).
Explicit PCI arguments still test one card. ./render-test auto output writes
real live 160x43 buffers to output-0.pgm and output-1.pgm (five/four rows), plus
output (four rows). ./render-test off output renders the original GPU-off layout.
For in-memory cases, ./render-test off output COUNT PAGE [large|unknown] selects
1..64 cards and a zero-based pair page. Omit PAGE to export the last pair.
The default values exercise LLONG_MAX VRAM and -273C/1000C; large uses
100.0/192.0GiB and 1000C, while unknown uses N/A for all GPU metrics.
For example ./render-test off output 2 tests worst-width dual-card values;
./render-test off output 3 1 tests the odd last page. With no arguments,
make check retains the worst-width two-card case. These are rendering inputs,
not simulated hardware or claims about the installed GPUs.
These do not connect to g15daemon and are not physical LCD tests.
make check also runs tests/render-layouts.sh for off, single, dual, large,
unknown, odd-last and 64-card layouts, each in both summary modes. In addition
to isolated-column comparisons, the test compares final GPU text, vertical
TOTAL and time-footer cells directly with the installed font's bitmap bits.
This independent oracle includes blank cells and checks full glyph bounds;
repeating renderString on another canvas alone cannot detect shared clipping.
Canvas initialization is tested starting from poisoned storage.
Startup regression: make check also runs runtime-test through the full main
loop for three frames with real libgtop and bitmap rendering. Only daemon
transport is replaced by a private socketpair; no keyboard events are sent.
./runtime-test auto 21 additionally requires valid live GPU samples after
twenty seconds. This is not a physical LCD or real daemon integration test.
The bitmap-only client initializes its own canvas, avoiding g15r_initCanvas:
a TTF-enabled renderer can otherwise write beyond a non-TTF caller's canvas
and corrupt malloc metadata at startup, even with --gpu off.
Each visual export also writes output-0.bin and output-1.bin: the actual 860
packed framebuffer bytes (one means black). With ImageMagick installed, run
sh tests/render-images.sh /absolute/output/directory after generating frames
there. It compares packed PBM, PGM and native PNG pixels, generates exact
nearest-neighbor 8x images, and checks the 8x-to-native round trip. Per-scenario
contact sheets use 4x tiles with labels outside the LCD and one column, avoiding
very wide sheets that viewers may downscale. No trim or crop is applied.
Inspect the individual native and 8x files as well as the contact sheets.
The supplied default small font has only three ink columns plus a blank column
per character; its N, M and other shapes can look incomplete when enlarged.
Use the bitmap checks to distinguish those font shapes from missing pixels.
Features:
Summary Screen displays 4 or 5 indicators from CPU/Frequency/Memory/NET/Temperature/Fan Speed/Swap,
along with current time.
+34
View File
@@ -30,6 +30,40 @@ Requirements
- libgtop
GPU monitoring
--------------
The summary automatically collects ALL supported PCI addresses (amdgpu or nvidia),
including mixed vendors. ``--gpu dddd:bb:ss.f`` (alias ``-gpu``) selects
a card; ``--gpu off`` disables collection and restores the original layout.
``--gpu auto`` is the default. Intel, nouveau and radeon are not supported.
Missing metrics show N/A. No supported GPU in auto mode leaves the layout intact.
An explicitly selected missing card keeps an N/A column. Selection is at startup.
Each column shows vendor/index and PCI bus:slot.function, UTL %, U used VRAM, T total VRAM, and TEM in C on five
lines. VRAM uses explicit MiB/GiB units (TiB/PiB if needed), with one decimal.
One GPU retains summary labels and shortened graphs. Two GPUs appear together
beside compact CPU/RAM-used/RAM-total/SWP rows (plus GPU count in five-row mode).
RAM also has explicit units. More than two cycle in PCI-sorted pairs every five
seconds from startup; all continue collecting. NV1/AMD2-style indices are one-based
selected-inventory positions, mapped to full PCI addresses in startup diagnostics.
The 64-card bound fails explicitly on overflow. The bottom bar and other screens
are unchanged. AMD APU VRAM may mean its reserved
carveout, not all shared system memory.
AMD uses read-only PCI-device sysfs/hwmon files. NVIDIA uses the optional
``/usr/bin/nvidia-smi`` without a shell or SDK dependency. Independent AMD workers
and one PCI-keyed all-NVIDIA query wait two seconds between samples, with a shared
750ms NVIDIA deadline and per-card five-second cache expiry. Shutdown has one
one-second worker join/reap budget. Display refresh remains controlled by ``-r``. See README for
exact layout, interfaces and subprocess lifecycle details. Run as an ordinary user.
``make check`` covers GPU-off, single-card and multi-card native framebuffers.
Final GPU, vertical TOTAL and time-footer glyphs are checked against font bitmap
bits independently of the text renderer. See README for packed framebuffer
exports and ``tests/render-images.sh``, which generates verified native/nearest-8x
PNGs and narrow contact sheets without cropping the LCD area.
=======
Options
=======
+53
View File
@@ -0,0 +1,53 @@
Debian packaging import and fork provenance
==========================================
Imported on 2026-09-07 from the official Debian archive:
https://archive.debian.org/debian/pool/main/g/g15stats/
SHA-256 (filenames relative to that URL):
79289e9f182e086dc5bfe0b1b594fedc36ad7610ee1daec19b05d44c4494d3fa g15stats_1.9.2-2.dsc
ce724380069ee62563f2898b1bbd0afb80bb632cb6dbf3d1747871f7cdfefbb3 g15stats_1.9.2-2.diff.gz
9dda49a3f09e66ce738ceecb07f2c0fef7ecd5ecbd06e435b340dfca35a37da4 g15stats_1.9.2.orig.tar.gz
The diff (2316 bytes) and orig tarball (25190 bytes) match the SHA-256
checksums in the downloaded .dsc. The .dsc's own checksum was computed
locally. The byte-exact .dsc and diff are retained in debian/provenance/;
the diff preserves ALL original debian files, the manual and linker fix.
The tarball can be downloaded from the URL above and verified independently.
Checksum matching alone does not authenticate the uploader's PGP signature.
Verification was attempted with Debian 13's debian-keyring,
debian-maintainers and debian-nonupload keyrings: gpgv reported
"No public key" for DSA key F9935424B1DF9A57 (signature dated 2011-03-15).
No successful signature verification is claimed here.
Archive package: g15stats 1.9.2-2, source format 1.0, maintained by
Giacomo Catenazzi <cate@debian.org>. Original Debian packaging copyright:
2008 Giacomo A. Catenazzi, GPL-2-or-later. All three historical changelog
entries remain unchanged. The archive adds g15stats.1 outside debian/;
it is now debian/g15stats.1, extended for the fork, retaining Mike Lampard's
author credit. The archive's only upstream source patch adds -lpthread -lm
to Makefile.am, credited to Bhavani Shankar in the changelog (#554382).
Both libraries already appear in the fork: do not apply that patch twice.
All nine archived debian files were imported. control, rules, docs, install,
manpages, watch and copyright were adapted; changelog was prepended;
compat was raised from 5 to 13, with Build-Depends: debhelper (>= 13~).
Obsolete CDBS cleanup and SourceForge watching were replaced, not omitted
without a record. No archived quilt series existed.
Upstream snapshot (unmodified tracked files in the orig archive):
Menelkir master 95de11a526346b3d9fd33df051b330c36b1c8a16
Commit date: 2024-06-09; subject: Update README.rst
Upstream: https://gitlab.com/menelkir/g15stats
Local fork origin: https://gt.uo1.net/mikhail/g15stats
Version rationale: git describe gives 3.0.3-13-g95de11a and the tracked
Arch PKGBUILD declares 3.0.3. configure.ac instead has stale AC_INIT=3.0
and VERSION=3.0.1, including at tag 3.0.3. Therefore this package is
3.0.3+git20240609.95de11a-1~local1, NOT Debian's old 1.9.2 nor a new
upstream release. The quilt patch aligns the embedded version and modernizes
only the build system. Runtime engine files remain untouched.
The new changelog/patch identity is the configured local Git user,
Mikhail Yevchenko <m.ṥῥẚɱ.ѓѐḿởύḙ@uo1.net>; it does not impersonate
the historical Debian maintainer or claim upstream authorship.
+52
View File
@@ -0,0 +1,52 @@
g15stats for Debian 13 (local fork package)
=========================================
This packaging targets Linux and debhelper compatibility 13. See PROVENANCE
for the complete Debian 1.9.2-2 import, licensing and snapshot version choice.
All upstream modifications are in debian/patches; do not build in the checkout
when you want it to remain pristine.
Clean source preparation (outside the checkout)
----------------------------------------------
Export ONLY tracked upstream files at commit
95de11a526346b3d9fd33df051b330c36b1c8a16 using git archive, with directory
prefix g15stats-3.0.3+git20240609.95de11a/. Compress with xz to produce
g15stats_3.0.3+git20240609.95de11a.orig.tar.xz. Extract in a temporary build
directory, then copy this debian/ directory into it. Never tar the working
checkout: .git, quilt state and untracked build outputs must not be included.
In a disposable Debian 13 container, install build-essential, debhelper,
autoconf, automake, libtool, pkgconf, libgtop2-dev, libg15-dev,
libg15render-dev and libg15daemon-client-dev (apt-get build-dep on the staged
source directory also resolves Build-Depends). Install lintian for QA.
If the already-built local G15 libraries are available, install their runtime
and development .deb files instead of the distro G15 libraries. Keep libgtop
and other dependencies from Debian 13. Do not install packages on the host.
Run dpkg-buildpackage -S -us -uc in the staged source directory. In another
empty directory/container, extract the resulting .dsc with dpkg-source -x,
then run dpkg-buildpackage -b -us -uc. Source packages here are unsigned;
dpkg-source --no-check is appropriate only after independently checking the
locally produced source artifact checksums. Lint both source and binary
.changes with lintian --fail-on error. Keep logs and all artifacts outside
the checkout. A second independent extraction/build checks rebuildability;
bit-for-bit reproducibility is a separate claim requiring comparison.
Safe smoke test
---------------
Install the resulting binary in a disposable container with no USB mappings,
no host networking and no privileged mode. Suppress service startup using
policy-rc.d before installing g15daemon. Run debian/tests/help, which compares
timeout-bounded --help and -h output. These exit before any daemon connection,
sensor access or keyboard thread. Do not invoke the normal client or -d.
The same test is declared as a superficial autopkgtest.
Scope and limitations
---------------------
No service/init, udev rule, keyboard macro or automatic startup is shipped.
The existing runtime code has not been safety-certified or rewritten: legacy
sensor paths, argument parsing, array bounds, string handling and threading
need separate runtime review. Build/CLI tests do not validate hardware, LCD
rendering, live daemon interaction, battery support or other architectures.
Use --info-rotate, not the incorrectly documented short form -ir; see the
manual for the existing parser discrepancy.
+48
View File
@@ -0,0 +1,48 @@
g15stats (3.0.3+git20240609.95de11a-1~local1) UNRELEASED; urgency=medium
* Add cached multi-vendor GPU summary columns: independent read-only amdgpu
workers and optional, deadline-bounded batched shell-free nvidia-smi
queries.
Collect all supported cards by default, display two columns concurrently
beside compact CPU/RAM data, and cycle larger inventories in sorted pairs.
Document PCI selection, bounds, units, missing metrics and unchanged no-GPU
layouts; add multi-adapter isolation, live collector and framebuffer tests.
Check complete GPU/TOTAL/footer glyphs against font bits independently;
exercise off/single/paged layouts and verify lossless visual exports.
* Initialize the bitmap canvas locally, avoiding the optional FreeType
canvas ABI mismatch that corrupts the heap during application startup.
Add a full-main regression with isolated daemon transport.
* Import Debian 1.9.2-2 packaging and manual, retaining its changelog,
attribution and checksum-recorded archive provenance.
* Package Menelkir master 95de11a526346b3d9fd33df051b330c36b1c8a16,
a post-3.0.3 snapshot, rather than mislabelling this fork as 1.9.2.
* Use debhelper compat 13, 3.0 (quilt), direct build dependencies,
rootless builds and all Debian hardening flags on Debian 13.
* Quilt build fixes preserve compiler flags, link libgtop correctly,
remove the bare include flag and align the embedded snapshot version.
* Extend the archived manual and add isolated CLI smoke tests.
Do not install init scripts or automatically start the client.
-- Mikhail Yevchenko <m.ṥῥẚɱ.ѓѐḿởύḙ@uo1.net> Mon, 07 Sep 2026 15:47:05 +0300
g15stats (1.9.2-2) unstable; urgency=low
* Supports binutils-gold linker, writing explicitly our library
dependencies. Patch from Bhavani Shankar (Closes: #554382).
-- Giacomo Catenazzi <cate@debian.org> Tue, 15 Mar 2011 08:04:59 +0100
g15stats (1.9.2-1) unstable; urgency=low
* New upstream version (Closes: #530548).
* Add support for automake and autoconf
-- Giacomo Catenazzi <cate@debian.org> Tue, 04 Aug 2009 13:15:38 +0200
g15stats (1.0-1) unstable; urgency=low
* Initial release. (Closes: #439792).
-- Giacomo Catenazzi <cate@debian.org> Tue, 18 Mar 2008 08:09:06 +0100
+1
View File
@@ -0,0 +1 @@
13
+38
View File
@@ -0,0 +1,38 @@
Source: g15stats
Section: utils
Priority: optional
Maintainer: Mikhail Yevchenko <m.ṥῥẚɱ.ѓѐḿởύḙ@uo1.net>
Build-Depends: debhelper (>= 13~),
autoconf,
automake,
libg15-dev,
libg15daemon-client-dev,
libg15render-dev,
libgtop2-dev,
libtool,
pkgconf
Standards-Version: 4.7.2
Rules-Requires-Root: no
Homepage: https://gitlab.com/menelkir/g15stats
Package: g15stats
Architecture: linux-any
Depends: g15daemon, ${shlibs:Depends}, ${misc:Depends}
Description: system statistics display client for G15Daemon
Displays system statistics on the LCD of supported Logitech keyboards
through g15daemon.
.
CPU Screen displays graphs of User/System/Nice and Idle time, along with
LoadAVG and Uptime.
.
Memory Screen displays Memory Total & Free, and graph of Used vs
Buffered+Cached Memory.
.
Swap Screen displays Used, Free and Total swap space, along with the number
of pages currently paged in/out.
.
Network Screen displays Total bytes In/Out, history graph, Peak speed.
.
Additional screens show CPU frequency, battery charge, temperature and
fan speed where supported by the system's sensors.
+40
View File
@@ -0,0 +1,40 @@
This package was debianized by Giacomo A. Catenazzi <cate@debian.org> on
Sat, 15 Sep 2007 08:32:51 +0200.
It was downloaded from http://g15daemon.sourceforge.net/
Upstream Authors (also see AUTHORS):
Mike Lampard <mlampard@users.sourceforge.net>
Antonio Bartolini <robynhub@gmail.com>
Piotr Czarnecki <czarnyckm@users.sf.net>
Daniel Menelkir <dmenelkir@gmail.com>
Copyright:
Copyright (c) 2006-2007 Mike Lampard
Copyright (c) 2008-2010 Mike Lampard (g15stats.c)
License:
g15daemon is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
g15daemon is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
License can be found in the file `/usr/share/common-licenses/GPL-2'.
The Debian packaging is (C) 2008, Giacomo A. Catenazzi <cate@debian.org> and
is licensed under the GPL v2 (or later), see `/usr/share/common-licenses/GPL-2'.
Local packaging additions are Copyright (c) 2026 Mikhail Yevchenko
<m.ṥῥẚɱ.ѓѐḿởύḙ@uo1.net>, licensed under GPL v2 or later.
The imported manual names Mike Lampard as author; its Debian archive origin
and subsequent local updates are documented in debian/PROVENANCE.
The original Debian diff, including the original copyright notice, is retained
unchanged in debian/provenance/. No authorship of the fork is claimed here.
Vendored
+4
View File
@@ -0,0 +1,4 @@
README
AUTHORS
debian/README.Debian
debian/PROVENANCE
+119
View File
@@ -0,0 +1,119 @@
.\" Imported from Debian 1.9.2-2; see debian/PROVENANCE for attribution.
.TH "g15stats" "1" "7 September 2026" "G15Daemon - g15stats" "G15Daemon User Manual"
.SH "NAME"
g15stats \- system statistics display client for G15Daemon
.SH "SYNOPSIS"
.B g15stats
.RI [ options ]
.SH "DESCRIPTION"
The program provides usage meters for the LCD on
supported Logitech keyboards, using g15daemon:
.PP
CPU Screen displays graphs of User/System/Nice and Idle time, along with
LoadAVG and Uptime.
.PP
Memory Screen displays Memory Total & Free, and graph of Used vs
Buffered+Cached Memory.
.PP
Swap Screen displays Used, Free and Total swap space, along with the number
of pages currently paged in/out.
.PP
Network Screen displays Total bytes In/Out, history graph, Peak speed.
.PP
The summary, CPU frequency, battery, temperature and fan screens display
additional statistics where the relevant system sensors are available.
.SH "OPTIONS"
g15stats understands the following options:
.TP
.BI "\-gpu, --gpu " "auto|off|dddd:bb:ss.f"
Select the GPU summary columns. Default auto collects all PCI addresses
bound to amdgpu or nvidia at startup, including mixed vendors; off disables collection and retains
the original layout. A PCI address selects one card explicitly.
No supported card in auto mode leaves the original layout intact; an explicit
missing card shows N/A. Restart after hotplug to rediscover devices.
Intel, nouveau and radeon are not supported.
.br
.P
.HP
\-h,--help show usage information.
.P
.HP
\-d,--daemon run in background.
.P
.HP
\-i,--interface [interface] monitor network interface [interface] ie -i eth0.
.P
.HP
\-nsa,--net-scale-absolute scale net graphs against maximum speed seen.
.TP
.BR \-u , " --unicore"
Display unicore graphs on the CPU screen.
.TP
.BI "\-r, --refresh " seconds
Set the refresh interval from 1 to 300 seconds (default: 1).
.TP
.BI "\-t, --temperature " id
Select the temperature hwmon sensor (0 to 3).
.TP
.BI "\-f, --fan " id
Select the fan hwmon sensor (0 to 3).
.TP
.BI "\-gt, --global-temp " id
Show this temperature probe instead of the maximum on the summary screen.
.TP
.BR \-df , " --disable-freq"
Disable CPU frequency monitoring.
.TP
.B --info-rotate
Rotate the bottom information bar. Use the long option: the fork documents
\-ir in its help, but its parser checks \-rc and also treats that prefix as
\-r. The documented short form \-ir is not supported correctly.
.TP
.BR \-vc , " --variable-cpu"
Recalculate the CPU count for systems supporting CPU hotplug.
.SH "KEYS"
L2 selects the previous screen; L3 selects the next screen.
L4 selects an alternative view where available; L5 changes the bottom bar.
.SH "NOTES"
Each GPU column shows vendor/index and PCI bus:slot.function, UTL percent,
U used VRAM, T total VRAM, and TEM in C.
Used and total occupy separate rows with explicit MiB/GiB units (TiB/PiB
if needed), rounded to one decimal. On AMD APUs VRAM can mean only the reserved
carveout. With one GPU the summary retains its 4/5 indicators and shortened
graphs. With two GPUs both columns appear together beside compact CPU percent,
R used system RAM, T total system RAM and SWP percent (plus GPU count in five-row
mode). RAM has explicit units too. More than two cycle in PCI-sorted pairs every
five seconds from startup; all cards continue collecting. NV1/AMD2-style indices
are one-based selected-inventory positions, not NVIDIA ordinals; startup diagnostics
map them to full PCI addresses. The 64-card bound fails explicitly on overflow.
The bottom bar and other screens are unchanged. See README for pixel coordinates.
.PP
AMD reads device-local sysfs/hwmon metrics. NVIDIA optionally uses
/usr/bin/nvidia-smi with no shell or SDK dependency. Independent AMD workers and
one batched NVIDIA worker wait two seconds between queries. All NVIDIA cards
share one PCI-keyed query with a 750ms deadline;
timeouts kill the child, and an unreaped child prevents additional launches.
The child is also killed on parent death. Data older than five seconds and
unavailable metrics show N/A, never fabricated zero. Display refresh remains
controlled by the refresh option. Worker shutdown has one shared one-second
join/reap budget; a stuck kernel read can outlive it until process exit.
No GPU settings are written.
.PP
Normal operation requires a running g15daemon. This package does not install
or enable a g15stats service. Run the client as an ordinary user.
The help options exit before connecting to g15daemon.
Sensor availability depends on the kernel and hardware; legacy battery
paths may be unavailable on modern systems.
.SH "AUTHOR"
Mike Lampard
.SH "SEE ALSO"
.BR g15daemon (1),
.BR libg15 (3)
+1
View File
@@ -0,0 +1 @@
usr/bin/g15stats
+1
View File
@@ -0,0 +1 @@
debian/g15stats.1
+47
View File
@@ -0,0 +1,47 @@
Description: Preserve Debian flags and use conventional automake linking
Remove the bare -I (which consumes the next compiler argument), pass GTOP
libraries after objects, and let debhelper install selected documentation.
The historical Bhavani Shankar explicit pthread/math linkage fix is already
present in this fork and remains intact. Align stale configure versions with
the pinned post-3.0.3 snapshot (see debian/PROVENANCE).
Author: Mikhail Yevchenko <m.ṥῥẚɱ.ѓѐḿởύḙ@uo1.net>
Forwarded: not-needed
Last-Update: 2026-09-07
--- a/configure.ac
+++ b/configure.ac
@@ -1,7 +1,5 @@
-AC_INIT([g15stats],[3.0],[dmenelkir@gmail.com])
+AC_INIT([g15stats],[3.0.3+git20240609.95de11a],[dmenelkir@gmail.com])
-PACKAGE=g15stats
-VERSION=3.0.1
AC_PREFIX_DEFAULT(/usr)
AC_CONFIG_AUX_DIR(config)
-AM_INIT_AUTOMAKE($PACKAGE, $VERSION)
+AM_INIT_AUTOMAKE([foreign])
@@ -26,6 +24,4 @@
CPPFLAGS="$CPPFLAGS $GTOP_CFLAGS"
-CFLAGS="$CPPFLAGS $GTOP_CFLAGS"
-LDFLAGS="$LDFLAGS $GTOP_LIBS"
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,12 +2,8 @@
-AM_CFLAGS = -Wall @CFLAGS@ -I$(top_builddir) -I$(top_srcdir)
+AM_CFLAGS = -Wall
+AM_CPPFLAGS = -I$(top_builddir) -I$(top_srcdir)
METASOURCES = AUTO
bin_PROGRAMS = g15stats
noinst_HEADERS = g15stats.h
-docdir = @docdir@
-doc_DATA = README ChangeLog AUTHORS NEWS COPYING
-
-LIBS = -lg15daemon_client -lg15render -lpthread -lm -lrt
-IMCLUDES = -I
-g15stats_LDFLAGS = -avoid-version
+g15stats_LDADD = -lg15daemon_client -lg15render -lpthread -lm -lrt $(GTOP_LIBS)
g15stats_SOURCES = g15stats.c g15stats.h
+1
View File
@@ -0,0 +1 @@
modern-build.patch
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Format: 1.0
Source: g15stats
Binary: g15stats
Architecture: any
Version: 1.9.2-2
Maintainer: Giacomo Catenazzi <cate@debian.org>
Homepage: http://www.g15tools.com
Standards-Version: 3.8.2
Build-Depends: cdbs, debhelper (>= 5), autotools-dev, libxtst-dev, libfreetype6-dev, libusb-dev, libgtop2-dev, libg15-dev, libg15render-dev (>= 1.2-3), libg15daemon-client-dev, automake, libtool, autoconf
Checksums-Sha1:
b53ea7414bd47b68a73aaa8f548d36f50b47aa09 25190 g15stats_1.9.2.orig.tar.gz
245c96ab8267a31c5e1ebebe91307e8ff99bb976 2316 g15stats_1.9.2-2.diff.gz
Checksums-Sha256:
9dda49a3f09e66ce738ceecb07f2c0fef7ecd5ecbd06e435b340dfca35a37da4 25190 g15stats_1.9.2.orig.tar.gz
ce724380069ee62563f2898b1bbd0afb80bb632cb6dbf3d1747871f7cdfefbb3 2316 g15stats_1.9.2-2.diff.gz
Files:
beb285f739127e91b97af465e6b53c4a 25190 g15stats_1.9.2.orig.tar.gz
bec0a80e18f8905d4d70a1c9fc575043 2316 g15stats_1.9.2-2.diff.gz
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
iEYEARECAAYFAk1/EkEACgkQ+ZNUJLHfmlclzQCeMMSD3P7ix2G7M8JJnlfTbqBS
4U4AnjhLSSPFun2rWQirQ5w9D7ubM3gC
=ounU
-----END PGP SIGNATURE-----
Vendored Executable
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/make -f
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
%:
dh $@
override_dh_auto_install:
dh_auto_install --destdir=debian/tmp
+1
View File
@@ -0,0 +1 @@
3.0 (quilt)
+1
View File
@@ -0,0 +1 @@
debian/provenance/g15stats_1.9.2-2.diff.gz
+3
View File
@@ -0,0 +1,3 @@
Tests: help
Depends: @
Restrictions: superficial
Vendored Executable
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
# Help returns before daemon connection, sensor polling or keyboard threads.
# Run only in an isolated testbed without USB device passthrough.
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT HUP INT TERM
timeout 5 g15stats --help > "$work/long"
timeout 5 g15stats -h > "$work/short"
cmp "$work/long" "$work/short"
grep -F 'g15stats 3.0.3+git20240609.95de11a' "$work/long"
grep -F -- '--interface' "$work/long"
grep -F -- '--refresh' "$work/long"
+5
View File
@@ -0,0 +1,5 @@
version=4
# Local post-3.0.3 snapshot: see PROVENANCE; no upstream signatures available.
opts="mode=git, pgpmode=none, dversionmangle=s/\+git.*//" \
https://gitlab.com/menelkir/g15stats.git refs/tags/@ANY_VERSION@
+119 -4
View File
@@ -55,6 +55,53 @@ This is a simple stats client showing graphs for CPU, MEM & Swap usage, Network
#include <glibtop/uptime.h>
#include <glibtop/sysinfo.h>
#include "g15stats.h"
#include "gpu.h"
/* Main-thread-only summary geometry; other screens retain their full width. */
static int summary_gpu;
static void init_canvas(g15canvas *canvas)
{
/* We use bitmap fonts only. libg15render's g15r_initCanvas can write a
* FreeType handle beyond sizeof(g15canvas) when the library was built
* with TTF_SUPPORT but its consumer was not. Initialize our own storage
* instead; the bitmap API uses only the common buffer/mode prefix. */
memset(canvas, 0, sizeof(*canvas));
}
static void gpu_lines(size_t index, gpu_sample sample, char lines[5][32])
{
char memory[24];
snprintf(lines[0], 32, "%s%zu %.7s",
gpu_devices[index].backend == 1 ? "AMD" : gpu_devices[index].backend == 2 ? "NV" : "GPU",
index + 1, gpu_devices[index].bus + 5);
snprintf(lines[1], sizeof(lines[1]), "UTL N/A");
if (sample.busy >= 0)
snprintf(lines[1], sizeof(lines[1]), "UTL %lld%%", sample.busy);
gpu_memory(memory, sizeof(memory), sample.used);
snprintf(lines[2], sizeof(lines[2]), "U %s", memory);
gpu_memory(memory, sizeof(memory), sample.total);
snprintf(lines[3], sizeof(lines[3]), "T %s", memory);
snprintf(lines[4], sizeof(lines[4]), "TEM N/A");
if (sample.temp != LLONG_MIN)
snprintf(lines[4], sizeof(lines[4]), "TEM %lldC", sample.temp);
}
static void draw_gpu_column(g15canvas *canvas)
{
gpu_frame frame = gpu_snapshot();
size_t first = gpu_display_first(frame.count, gpu_milliseconds());
size_t i;
for (i = first; i < frame.count && i < first + 2; i++) {
char lines[5][32];
int row, x = frame.count == 1 ? 108 : 54 + (int)(i - first) * 54;
gpu_lines(i, frame.samples[i], lines);
g15r_drawLine(canvas, x - 3, 0, x - 3, 34, G15_COLOR_BLACK);
for (row = 0; row < 5; row++)
g15r_renderString(canvas, (unsigned char *)lines[row], 0,
G15_TEXT_SMALL, x, 1 + row * 7);
}
}
int g15screen_fd;
@@ -213,7 +260,7 @@ void print_vert_label_logic(g15canvas *canvas, char *label, unsigned int sx){
}
void print_vert_label(g15canvas *canvas, char *label){
print_vert_label_logic(canvas, label, TEXT_RIGHT);
print_vert_label_logic(canvas, label, summary_gpu ? 98 : TEXT_RIGHT);
}
void drawBar_reversed (g15canvas * canvas, int x1, int y1, int x2, int y2, int color,
@@ -264,8 +311,9 @@ void drawBar_reversed (g15canvas * canvas, int x1, int y1, int x2, int y2, int c
}
void drawBar_both(g15canvas *canvas, int y1, int y2, int bar_current, int bar_total, int rbar_current, int rbar_total) {
g15r_drawBar(canvas, BAR_START, y1, BAR_END, y2, G15_COLOR_BLACK, bar_current, bar_total, 4);
drawBar_reversed(canvas, BAR_START, y1, BAR_END, y2, G15_COLOR_BLACK, rbar_current, rbar_total, 5);
int end = summary_gpu ? 96 : BAR_END;
g15r_drawBar(canvas, BAR_START, y1, end, y2, G15_COLOR_BLACK, bar_current, bar_total, 4);
drawBar_reversed(canvas, BAR_START, y1, end, y2, G15_COLOR_BLACK, rbar_current, rbar_total, 5);
}
void drawLine_both(g15canvas *canvas, int y1, int y2) {
@@ -826,6 +874,42 @@ void draw_cpu_screen_unicore(g15canvas *canvas, char *tmpstr, int drawgraph, int
draw_cpu_screen_unicore_logic(canvas, cpu, tmpstr, drawgraph, printlabels, 0);
}
/* Two 52px GPU columns leave 51px for a compact CPU/system-RAM summary.
* Keep this separate from the legacy geometry and generic sensor probing. */
static void compact_ram_lines(char lines[5][32], long long used, long long total)
{
char value[24];
gpu_memory(value, sizeof(value), used);
snprintf(lines[1], 32, "R %s", value);
gpu_memory(value, sizeof(value), total);
snprintf(lines[2], 32, "T %s", value);
}
static void draw_compact_summary(g15canvas *canvas)
{
glibtop_cpu cpu;
glibtop_mem mem;
glibtop_swap swap;
static guint64 last_total, last_idle;
char lines[5][32];
int row, rows = mode[SCREEN_SUMMARY] ? 4 : 5;
glibtop_get_cpu(&cpu);
glibtop_get_mem(&mem);
glibtop_get_swap(&swap);
snprintf(lines[0], 32, "CPU N/A");
if (cpu.total > last_total && cpu.idle >= last_idle && cpu.idle - last_idle <= cpu.total - last_total)
snprintf(lines[0], 32, "CPU %.0f%%", 100.0 * (1.0 - (double)(cpu.idle - last_idle) / (cpu.total - last_total)));
last_total = cpu.total;
last_idle = cpu.idle;
compact_ram_lines(lines, mem.total - mem.free, mem.total);
snprintf(lines[3], 32, "SWP %.0f%%", swap.total ? 100.0 * swap.used / swap.total : 0.0);
snprintf(lines[4], 32, "GPUs %zu", gpu_count);
g15r_clearScreen(canvas, G15_COLOR_WHITE);
for (row = 0; row < rows; row++)
g15r_renderString(canvas, (unsigned char *)lines[row], 0,
G15_TEXT_SMALL, 1, 1 + row * (rows == 5 ? 7 : 9));
}
void draw_cpu_screen_multicore(g15canvas *canvas, char *tmpstr, int unicore) {
glibtop_cpu cpu;
int core,ncpumax;
@@ -839,6 +923,10 @@ void draw_cpu_screen_multicore(g15canvas *canvas, char *tmpstr, int unicore) {
b_nice[GLIBTOP_NCPU],b_sys[GLIBTOP_NCPU],b_idle[GLIBTOP_NCPU],
b_irq[GLIBTOP_NCPU],b_iowait[GLIBTOP_NCPU];
if (summary_gpu && gpu_count > 1 && cycle == SCREEN_SUMMARY) {
draw_compact_summary(canvas);
return;
}
init_cpu_count();
ncpumax = ncpu;
@@ -1694,8 +1782,21 @@ int main(int argc, char *argv[]){
unsigned char interface[128];
static char tmpstr[MAX_LINES];
int unicore = 0;
const char *gpu_selection = "auto";
for (i=0;i<argc;i++) {
if (!strcmp(argv[i], "--gpu") || !strcmp(argv[i], "-gpu")) {
if (++i >= argc) {
fprintf(stderr, "--gpu requires auto, off or a PCI address\n");
return 1;
}
gpu_selection = argv[i];
if (!gpu_selection_valid(gpu_selection)) {
fprintf(stderr, "Invalid GPU selection; use auto, off or dddd:bb:ss.f\n");
return 1;
}
continue;
}
if(0==strncmp(argv[i],"-d",2)||0==strncmp(argv[i],"--daemon",8)) {
go_daemon=1;
}
@@ -1722,6 +1823,8 @@ int main(int argc, char *argv[]){
printf("%s %s - (c) 2008-2010 Mike Lampard, Piotr Czarnecki\n",PACKAGE_NAME,VERSION);
printf("Usage: %s [Options]\n", PACKAGE_NAME);
printf("Options:\n");
printf("--gpu (-gpu) [auto|off|dddd:bb:ss.f] GPU columns (default: auto, all supported GPUs).\n");
printf(" Two GPUs side by side; more cycle in PCI-sorted pairs every 5 seconds.\n");
printf("--daemon (-d) run in background\n");
printf("--unicore (-u) display unicore graphs only on the CPU screen\n");
printf("--help (-h) this help text.\n");
@@ -1785,6 +1888,13 @@ int main(int argc, char *argv[]){
}
}
}
if (gpu_detect(gpu_selection)) {
fprintf(stderr, "GPU enumeration failed or exceeds %d adapters; use --gpu PCI or off\n", GPU_MAX);
return 1;
}
for (size_t gpu = 0; gpu < gpu_count; gpu++)
fprintf(stderr, "GPU %zu: %s (%s)\n", gpu + 1, gpu_devices[gpu].bus,
gpu_devices[gpu].backend == 1 ? "AMD" : gpu_devices[gpu].backend == 2 ? "NV" : "unsupported/missing");
if((g15screen_fd = new_g15_screen(G15_G15RBUF))<0){
printf("Sorry, cant connect to the G15daemon\n");
return -1;
@@ -1795,11 +1905,12 @@ int main(int argc, char *argv[]){
daemonise(0,0);
if(canvas != NULL)
g15r_initCanvas(canvas);
init_canvas(canvas);
else
return -1;
glibtop_init();
if (gpu_start()) fprintf(stderr, "GPU collector unavailable; affected cards display N/A\n");
pthread_create(&keys_thread,NULL,(void*)keyboard_watch,NULL);
if(have_nic==1)
@@ -1813,6 +1924,7 @@ int main(int argc, char *argv[]){
int cycle_old = cycle;
while(1) {
calc_info_cycle();
summary_gpu = cycle == SCREEN_SUMMARY && gpu_count;
switch (cycle) {
case SCREEN_SUMMARY:
case SCREEN_CPU:
@@ -1919,6 +2031,8 @@ int main(int argc, char *argv[]){
break;
}
cycle_old = cycle;
if (summary_gpu) draw_gpu_column(canvas);
summary_gpu = 0;
print_info_label(canvas, tmpstr);
canvas->mode_xor = 0;
@@ -1926,6 +2040,7 @@ int main(int argc, char *argv[]){
g15_send(g15screen_fd,(char *)canvas->buffer,G15_BUFFER_LEN);
g15stats_wait(wait_seconds);
}
gpu_stop();
glibtop_close();
close(g15screen_fd);
+443
View File
@@ -0,0 +1,443 @@
/* SPDX-License-Identifier: GPL-2.0-or-later
* Read-only Linux GPU telemetry. Included privately by g15stats.c.
*/
#ifndef G15STATS_GPU_H
#define G15STATS_GPU_H
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
typedef struct {
long long busy, used, total, temp; /* percent, bytes, bytes, Celsius */
} gpu_sample;
static const gpu_sample gpu_unknown = {-1, -1, -1, LLONG_MIN};
#define GPU_MAX 64
typedef struct {
int backend; /* 0 missing, 1 amdgpu, 2 nvidia */
char device[PATH_MAX], bus[32];
} gpu_adapter;
typedef struct {
size_t count;
gpu_sample samples[GPU_MAX];
} gpu_frame;
static gpu_adapter gpu_devices[GPU_MAX]; /* immutable while workers run */
static size_t gpu_count;
static gpu_sample gpu_cache[GPU_MAX];
static long long gpu_stamp[GPU_MAX];
static pthread_mutex_t gpu_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_t gpu_threads[GPU_MAX];
static size_t gpu_thread_count;
static atomic_int gpu_stopping;
static pid_t gpu_pending = -1; /* NVIDIA worker owns until joined. */
static long long gpu_epoch;
static size_t gpu_page;
static long long gpu_page_due;
static int gpu_selection_valid(const char *selection)
{
size_t i;
if (!strcmp(selection, "auto") || !strcmp(selection, "off")) return 1;
if (strlen(selection) != 12) return 0;
for (i = 0; i < 12; i++) {
if (i == 4 || i == 7) { if (selection[i] != ':') return 0; }
else if (i == 10) { if (selection[i] != '.') return 0; }
else if (!isxdigit((unsigned char)selection[i])) return 0;
}
return selection[8] <= '1' && selection[11] <= '7';
}
static long long gpu_milliseconds(void)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now))
return 0;
return (long long)now.tv_sec * 1000 + now.tv_nsec / 1000000;
}
/* Reject partial values, overflow and unsupported textual fields. */
static long long gpu_number(const char *text, long long min, long long max,
long long missing)
{
char *end;
long long value;
while (isspace((unsigned char)*text)) text++;
if (!isdigit((unsigned char)*text) && *text != '-') return missing;
errno = 0;
value = strtoll(text, &end, 10);
if (end == text || errno) return missing;
while (isspace((unsigned char)*end)) end++;
return *end || value < min || value > max ? missing : value;
}
static long long gpu_file(const char *path, long long min, long long max,
long long missing)
{
char text[96];
FILE *file = fopen(path, "re");
long long value = missing;
if (!file) return missing;
if (fgets(text, sizeof(text), file) && !ferror(file)) {
/* sysfs values fit in one short line; do not accept truncated input. */
if (fgetc(file) == EOF && !ferror(file))
value = gpu_number(text, min, max, missing);
}
fclose(file);
return value;
}
/* Select once, in sorted PCI-address order, not unstable DRM card order.
* Explicit PCI selection keeps an N/A column even for missing devices.
*/
static int gpu_detect(const char *selection)
{
glob_t paths = {0};
size_t i;
gpu_count = 0;
gpu_epoch = gpu_milliseconds();
gpu_page = 0;
gpu_page_due = gpu_epoch + 5000;
memset(gpu_stamp, 0, sizeof(gpu_stamp));
for (i = 0; i < GPU_MAX; i++) gpu_cache[i] = gpu_unknown;
if (!strcmp(selection, "off")) return 0;
int error = glob("/sys/bus/pci/devices/*", 0, NULL, &paths);
if (error && error != GLOB_NOMATCH) {
globfree(&paths);
return -1;
}
for (i = 0; i < paths.gl_pathc; i++) {
char path[PATH_MAX], driver[PATH_MAX];
const char *bus = strrchr(paths.gl_pathv[i], '/') + 1;
ssize_t len;
int backend;
if (strcmp(selection, "auto") && strcasecmp(selection, bus)) continue;
snprintf(path, sizeof(path), "%s/driver", paths.gl_pathv[i]);
len = readlink(path, driver, sizeof(driver) - 1);
if (len < 0) continue;
driver[len] = 0;
if (!strcmp(strrchr(driver, '/') + 1, "amdgpu"))
backend = 1;
else if (!strcmp(strrchr(driver, '/') + 1, "nvidia"))
backend = 2;
else continue;
if (gpu_count == GPU_MAX) {
globfree(&paths);
gpu_count = 0;
return -1; /* Never silently truncate the inventory. */
}
gpu_devices[gpu_count].backend = backend;
snprintf(gpu_devices[gpu_count].device, PATH_MAX, "%s", paths.gl_pathv[i]);
snprintf(gpu_devices[gpu_count].bus, 32, "%s", bus);
gpu_count++;
}
globfree(&paths);
if (!gpu_count && strcmp(selection, "auto")) {
memset(&gpu_devices[0], 0, sizeof(gpu_devices[0]));
snprintf(gpu_devices[0].bus, 32, "%s", selection);
gpu_count = 1;
}
return 0;
}
static gpu_sample gpu_amd(const gpu_adapter *adapter)
{
gpu_sample sample = gpu_unknown;
char path[PATH_MAX + 64];
glob_t paths = {0};
snprintf(path, sizeof(path), "%s/gpu_busy_percent", adapter->device);
sample.busy = gpu_file(path, 0, 100, -1);
snprintf(path, sizeof(path), "%s/mem_info_vram_used", adapter->device);
sample.used = gpu_file(path, 0, LLONG_MAX, -1);
snprintf(path, sizeof(path), "%s/mem_info_vram_total", adapter->device);
sample.total = gpu_file(path, 1, LLONG_MAX, -1);
snprintf(path, sizeof(path), "%s/hwmon/hwmon*/temp1_input", adapter->device);
if (!glob(path, 0, NULL, &paths) && paths.gl_pathc) {
long long temp = gpu_file(paths.gl_pathv[0], -273000, 1000000, LLONG_MIN);
if (temp != LLONG_MIN) sample.temp = temp / 1000;
}
globfree(&paths);
if (sample.total >= 0 && sample.used > sample.total) sample.used = -1;
return sample;
}
static gpu_sample gpu_csv(char *text)
{
gpu_sample sample = gpu_unknown;
char *fields[4], *cursor = text;
int i;
for (i = 0; i < 4; i++) {
fields[i] = strsep(&cursor, ",");
if (!fields[i]) return sample;
}
if (cursor) return sample;
sample.busy = gpu_number(fields[0], 0, 100, -1);
sample.used = gpu_number(fields[1], 0, LLONG_MAX / 1048576, -1);
sample.total = gpu_number(fields[2], 1, LLONG_MAX / 1048576, -1);
sample.temp = gpu_number(fields[3], -273, 1000, LLONG_MIN);
if (sample.used >= 0) sample.used *= 1048576;
if (sample.total >= 0) sample.total *= 1048576;
if (sample.total >= 0 && sample.used > sample.total) sample.used = -1;
return sample;
}
/* Only the collector calls this. Never wait for a driver in the draw thread.
* A killed but uninterruptible child remains owned, preventing process buildup.
*/
static int gpu_above_stdio(int fd)
{
int copy;
if (fd < 0 || fd >= 3) return fd;
copy = fcntl(fd, F_DUPFD_CLOEXEC, 3);
close(fd);
return copy;
}
static int gpu_command(const char *executable, char *const args[], char *text, size_t capacity)
{
int pipes[2], status, nullfd;
pid_t pid, parent = getpid(), result;
size_t used = 0;
long long deadline;
long maxfd = sysconf(_SC_OPEN_MAX);
char *const env[] = {"LC_ALL=C", NULL};
if (gpu_pending > 0) {
result = waitpid(gpu_pending, &status, WNOHANG);
if (!result || (result < 0 && errno == EINTR)) return 0;
gpu_pending = -1;
}
if (pipe2(pipes, O_CLOEXEC | O_NONBLOCK)) return 0;
pipes[0] = gpu_above_stdio(pipes[0]);
pipes[1] = gpu_above_stdio(pipes[1]);
if (pipes[0] < 0 || pipes[1] < 0) {
if (pipes[0] >= 0) close(pipes[0]);
if (pipes[1] >= 0) close(pipes[1]);
return 0;
}
/* Only the reader is nonblocking. A large all-card response must not
* make nvidia-smi fail with EAGAIN when the pipe temporarily fills. */
if (fcntl(pipes[1], F_SETFL, 0) < 0) {
close(pipes[0]);
close(pipes[1]);
return 0;
}
nullfd = gpu_above_stdio(open("/dev/null", O_RDWR | O_CLOEXEC));
if (nullfd < 0) { close(pipes[0]); close(pipes[1]); return 0; }
pid = fork();
if (!pid) {
/* Async-signal-safe child path: no stdio, allocation or shell. */
if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent) _exit(127);
if (dup2(nullfd, 0) < 0 || dup2(pipes[1], 1) < 0 || dup2(nullfd, 2) < 0)
_exit(127);
#ifdef SYS_close_range
if (syscall(SYS_close_range, 3U, ~0U, 0) < 0)
#endif
{
long fd;
if (maxfd < 0) _exit(127);
for (fd = 3; fd < maxfd; fd++) close((int)fd);
}
execve(executable, args, env);
_exit(127);
}
close(nullfd);
close(pipes[1]);
if (pid < 0) { close(pipes[0]); return 0; }
deadline = gpu_milliseconds() + 750;
for (;;) {
ssize_t n = read(pipes[0], text + used, capacity - 1 - used);
if (n > 0) used += (size_t)n;
result = waitpid(pid, &status, WNOHANG);
if (result == pid) {
/* Drain the final bytes after exit before parsing. */
while (used < capacity - 1 &&
(n = read(pipes[0], text + used, capacity - 1 - used)) > 0)
used += (size_t)n;
close(pipes[0]);
text[used] = 0;
return used < capacity - 1 && WIFEXITED(status) && !WEXITSTATUS(status);
}
if (result < 0 && errno != EINTR) { close(pipes[0]); return 0; }
if (atomic_load(&gpu_stopping) || gpu_milliseconds() >= deadline || used == capacity - 1) break;
/* Avoid spinning on POLLHUP if stdout closes before the process exits. */
poll(NULL, 0, 20);
}
kill(pid, SIGKILL);
gpu_pending = pid;
close(pipes[0]);
return 0;
}
/* Match numeric PCI identity: NVIDIA commonly prints an eight-digit domain.
* Malformed/duplicate rows must never contaminate another adapter's sample. */
static int gpu_bus_equal(const char *a, const char *b)
{
unsigned ad, ab, as, af, bd, bb, bs, bf;
int n = 0, m = 0;
if (sscanf(a, "%8x:%2x:%2x.%1x %n", &ad, &ab, &as, &af, &n) != 4 || a[n]) return 0;
if (sscanf(b, "%8x:%2x:%2x.%1x %n", &bd, &bb, &bs, &bf, &m) != 4 || b[m]) return 0;
return ad == bd && ab == bb && as == bs && af == bf;
}
static void gpu_nvidia_parse(char *text, gpu_frame *frame)
{
char *line;
unsigned char seen[GPU_MAX] = {0};
size_t i;
while ((line = strsep(&text, "\n"))) {
char *bus = strsep(&line, ",");
if (!line) continue;
for (i = 0; i < gpu_count; i++) {
if (gpu_devices[i].backend != 2 || !gpu_bus_equal(bus, gpu_devices[i].bus)) continue;
frame->samples[i] = seen[i] ? gpu_unknown : gpu_csv(line);
seen[i] = 1;
break;
}
}
}
static void gpu_nvidia(gpu_frame *frame)
{
char text[GPU_MAX * 256];
char *const args[] = {"nvidia-smi",
"--query-gpu=pci.bus_id,utilization.gpu,memory.used,memory.total,temperature.gpu",
"--format=csv,noheader,nounits", NULL};
if (gpu_command("/usr/bin/nvidia-smi", args, text, sizeof(text)))
gpu_nvidia_parse(text, frame);
}
static void gpu_publish(size_t i, gpu_sample sample)
{
pthread_mutex_lock(&gpu_lock);
gpu_cache[i] = sample;
gpu_stamp[i] = gpu_milliseconds();
pthread_mutex_unlock(&gpu_lock);
}
/* Each AMD adapter has an independent worker; one NVIDIA worker queries all
* NVIDIA cards within one deadline. A stalled sysfs driver cannot stall peers. */
static void *gpu_watch(void *arg)
{
size_t index = (gpu_adapter *)arg - gpu_devices, i;
while (!atomic_load(&gpu_stopping)) {
if (gpu_devices[index].backend == 1) {
gpu_publish(index, gpu_amd(&gpu_devices[index]));
} else {
gpu_frame frame = {.count = gpu_count};
for (i = 0; i < gpu_count; i++) frame.samples[i] = gpu_unknown;
gpu_nvidia(&frame);
pthread_mutex_lock(&gpu_lock);
for (i = 0; i < gpu_count; i++) if (gpu_devices[i].backend == 2) {
gpu_cache[i] = frame.samples[i];
gpu_stamp[i] = gpu_milliseconds();
}
pthread_mutex_unlock(&gpu_lock);
}
for (i = 0; i < 100 && !atomic_load(&gpu_stopping); i++) poll(NULL, 0, 20);
}
return NULL;
}
static int gpu_start(void)
{
size_t i;
int nvidia = 0, failed = 0;
atomic_store(&gpu_stopping, 0);
for (i = 0; i < gpu_count; i++) {
if (!gpu_devices[i].backend || (gpu_devices[i].backend == 2 && nvidia++)) continue;
if (pthread_create(&gpu_threads[gpu_thread_count], NULL, gpu_watch, &gpu_devices[i])) failed = 1;
else gpu_thread_count++;
}
return failed;
}
/* One shared shutdown deadline, not one per adapter. A stuck kernel read may
* outlive this call; its static storage remains valid until process exit.
* Never redetect/restart after a failed stop. */
static int gpu_stop(void)
{
struct timespec deadline;
long long end = gpu_milliseconds() + 1000;
size_t i;
int failed = 0;
atomic_store(&gpu_stopping, 1);
clock_gettime(CLOCK_REALTIME, &deadline);
deadline.tv_sec++;
for (i = 0; i < gpu_thread_count; i++) {
if (pthread_timedjoin_np(gpu_threads[i], NULL, &deadline)) {
pthread_detach(gpu_threads[i]);
failed = 1;
}
}
gpu_thread_count = 0;
if (!failed && gpu_pending > 0) {
int status;
pid_t result;
do {
result = waitpid(gpu_pending, &status, WNOHANG);
if (result == gpu_pending || (result < 0 && errno == ECHILD)) {
gpu_pending = -1;
break;
}
poll(NULL, 0, 5);
} while (gpu_milliseconds() < end);
if (gpu_pending > 0) failed = 1;
}
return failed;
}
static gpu_frame gpu_snapshot(void)
{
gpu_frame frame = {.count = gpu_count};
size_t i;
pthread_mutex_lock(&gpu_lock);
for (i = 0; i < gpu_count; i++)
frame.samples[i] = !gpu_stamp[i] || gpu_milliseconds() - gpu_stamp[i] > 5000 ? gpu_unknown : gpu_cache[i];
pthread_mutex_unlock(&gpu_lock);
return frame;
}
static size_t gpu_page_first(size_t count, unsigned long long elapsed)
{
return count > 2 ? (elapsed / 5000 % ((count + 1) / 2)) * 2 : 0;
}
/* Main thread only. Advance at most one pair per actual draw, so -r 10 (or
* leaving the summary screen) cannot alias the cycle and hide half the GPUs. */
static size_t gpu_display_first(size_t count, long long now)
{
if (count > 2 && now >= gpu_page_due) {
gpu_page = (gpu_page + 1) % ((count + 1) / 2);
gpu_page_due = now + 5000;
}
return gpu_page_first(count, gpu_page * 5000ULL);
}
/* Each line fits the 52-pixel column (13 small-font character cells).
* Separate used/total rows retain useful precision, even on large cards.
*/
static void gpu_memory(char *text, size_t size, long long bytes)
{
const char *unit = "MiB";
double value = (double)bytes / 1048576;
if (bytes < 0) { snprintf(text, size, "N/A"); return; }
if (value >= 1024) { value /= 1024; unit = "GiB"; }
if (value >= 1024) { value /= 1024; unit = "TiB"; }
if (value >= 1024) { value /= 1024; unit = "PiB"; }
snprintf(text, size, "%.1f%s", value, unit);
}
#endif
+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);
}