summaryrefslogtreecommitdiffstats
path: root/tools/rmtop
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-02 10:58:21 -0400
commitf909a5f822c5c4a4e9526b861d4171f8925f5113 (patch)
treeed362021c83569f49b48079a774f05775e84b0bd /tools/rmtop
parent09f241601423b1ae98e97846ee451768ff8a0a9e (diff)
downloadopen-iov-f909a5f822c5c4a4e9526b861d4171f8925f5113.tar.gz
open-iov-f909a5f822c5c4a4e9526b861d4171f8925f5113.zip
debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop
Diffstat (limited to 'tools/rmtop')
-rw-r--r--tools/rmtop/Makefile46
-rw-r--r--tools/rmtop/include/rmtop.h49
-rwxr-xr-xtools/rmtop/rmtopbin0 -> 45408 bytes
-rw-r--r--tools/rmtop/src/main.c323
-rw-r--r--tools/rmtop/src/panel.c232
5 files changed, 650 insertions, 0 deletions
diff --git a/tools/rmtop/Makefile b/tools/rmtop/Makefile
new file mode 100644
index 0000000..3a5cd67
--- /dev/null
+++ b/tools/rmtop/Makefile
@@ -0,0 +1,46 @@
+# rmtop - read-only ncurses GPU dashboard (Open-IOV).
+#
+# Reuses rmtool's RM query layer (../rmtool/src) so there is exactly one
+# ioctl implementation. Requires ncursesw; falls back to -lncursesw when
+# pkg-config is unavailable.
+CC ?= gcc
+CFLAGS ?= -O2 -Wall -Wextra
+RMTOOL = ../rmtool
+CPPFLAGS += -Iinclude -I$(RMTOOL)/include
+NCURSES_CFLAGS := $(shell pkg-config --cflags ncursesw 2>/dev/null)
+NCURSES_LIBS := $(shell pkg-config --libs ncursesw 2>/dev/null || echo -lncursesw)
+CFLAGS += $(NCURSES_CFLAGS)
+
+OBJS = src/main.o src/panel.o
+SHARED = build/rm_api.o build/query.o build/output.o
+PREFIX ?= /usr/local
+
+all: rmtop
+
+rmtop: $(OBJS) $(SHARED)
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(NCURSES_LIBS)
+
+build:
+ @mkdir -p build
+
+build/rm_api.o: $(RMTOOL)/src/rm_api.c $(RMTOOL)/include/rm_api.h | build
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
+
+build/query.o: $(RMTOOL)/src/query.c $(RMTOOL)/include/rm_api.h $(RMTOOL)/include/rmtool.h | build
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
+
+build/output.o: $(RMTOOL)/src/output.c $(RMTOOL)/include/rmtool.h | build
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
+
+src/%.o: src/%.c include/rmtop.h $(RMTOOL)/include/rm_api.h $(RMTOOL)/include/rmtool.h | build
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
+
+install: rmtop
+ install -d $(DESTDIR)$(PREFIX)/bin
+ install -m 0755 rmtop $(DESTDIR)$(PREFIX)/bin/rmtop
+
+clean:
+ rm -f $(OBJS) $(SHARED) rmtop
+ rmdir build 2>/dev/null || true
+
+.PHONY: all install clean
diff --git a/tools/rmtop/include/rmtop.h b/tools/rmtop/include/rmtop.h
new file mode 100644
index 0000000..9335e89
--- /dev/null
+++ b/tools/rmtop/include/rmtop.h
@@ -0,0 +1,49 @@
+/* rmtop.h - state and rendering declarations for the rmtop dashboard. */
+#ifndef RMTOP_H
+#define RMTOP_H
+
+#include <time.h>
+#include "rmtool.h"
+
+#define RMTOP_VERSION "0.1.0"
+#define RMTOP_MAX_ENGINES NV2080_GPU_MAX_ENGINES_LIST_SIZE
+
+struct rmtop_state {
+ int gpu_minor;
+ char model[RM_NAME_LEN];
+ char bdf[RM_BDF_LEN];
+ char uuid[RM_UUID_LEN];
+ char driver[80];
+
+ int rm_rc;
+ int have_rm;
+ char rm_name[RM_NAME_LEN];
+ NvU32 mc_arch;
+ NvU32 mc_impl;
+ NvU32 mc_rev;
+ NvU8 mc_subrev;
+
+ NvU32 vram_total_kib;
+ NvU32 vram_usable_kib;
+ NvU32 heap_size_kib;
+ NvU32 heap_free_kib;
+ NvU32 bar1_size_kib;
+ NvU32 bar1_avail_kib;
+
+ int have_timer;
+ NvU64 timer_nsec;
+
+ NvU32 engine_count;
+ NvU32 engine_list[RMTOP_MAX_ENGINES];
+ char engine_names[RMTOP_MAX_ENGINES][24];
+ int engine_names_ok;
+
+ time_t sampled_at;
+};
+
+int rmtop_collect(struct rm_ctx *ctx, struct rmtop_state *st);
+void rmtop_render_plain(const struct rmtop_state *st, enum rm_format format);
+void rmtop_render_curses(const struct rmtop_state *st, int interval_ms,
+ int paused);
+
+#endif /* RMTOP_H */
diff --git a/tools/rmtop/rmtop b/tools/rmtop/rmtop
new file mode 100755
index 0000000..08b0664
--- /dev/null
+++ b/tools/rmtop/rmtop
Binary files differ
diff --git a/tools/rmtop/src/main.c b/tools/rmtop/src/main.c
new file mode 100644
index 0000000..b7985df
--- /dev/null
+++ b/tools/rmtop/src/main.c
@@ -0,0 +1,323 @@
+/* main.c - rmtop: ncurses real-time read-only GPU dashboard.
+ *
+ * Interactive mode needs a terminal; --once renders the same panels as plain
+ * table/json/csv text for scripting and harness capture. All sampling is
+ * read-only (RM control queries and /proc/sysfs reads); no write path exists.
+ */
+#include <curses.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "rmtop.h"
+
+static void usage(void)
+{
+ printf("rmtop %s - read-only NVIDIA GPU dashboard (Open-IOV)\n\n",
+ RMTOP_VERSION);
+ printf("usage: rmtop [-g N|BDF] [--interval=MS] [--once] "
+ "[--format=...]\n\n"
+ " -g N|BDF select GPU (default: first)\n"
+ " --interval=MS refresh interval in ms (default 500)\n"
+ " --once render one snapshot as plain text and exit\n"
+ " --format=FMT table|json|csv (with --once)\n"
+ " --help, --version\n\n"
+ "keys: q quit r refresh s interval p pause h help\n"
+ " g/G next/previous GPU\n\n"
+ "Live engine utilization, power and thermals are marked n/a in\n"
+ "this read-only minimal build; see docs/17-RM-HARNESS.md.\n");
+}
+
+static int parse_common(struct rm_ctx *ctx, int *interval_ms, int *once,
+ int argc, char **argv)
+{
+ static const struct option long_opts[] = {
+ { "device-spec", required_argument, NULL, 'g' },
+ { "interval", required_argument, NULL, 'i' },
+ { "once", no_argument, NULL, 'o' },
+ { "format", required_argument, NULL, 'f' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 1001 },
+ { NULL, 0, NULL, 0 },
+ };
+ int opt;
+
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc, argv, "g:i:of:h", long_opts,
+ NULL)) != -1) {
+ if (opt == 'g' || opt == 'f') {
+ if (rm_apply_common_opt(ctx, opt == 'g' ? 'd' : 'f',
+ optarg) < 0)
+ return -1;
+ continue;
+ }
+ if (opt == 'i') {
+ *interval_ms = atoi(optarg);
+ if (*interval_ms < 100)
+ *interval_ms = 100;
+ continue;
+ }
+ if (opt == 'o') {
+ *once = 1;
+ continue;
+ }
+ if (opt == 'h') {
+ usage();
+ exit(0);
+ }
+ if (opt == 1000) {
+ ctx->dev_path = optarg;
+ continue;
+ }
+ if (opt == 1001) {
+ printf("rmtop %s (read-only; Open-IOV)\n",
+ RMTOP_VERSION);
+ exit(0);
+ }
+ return -1;
+ }
+ return 0;
+}
+
+static void draw_bar(WINDOW *win, int y, int x, int width, double ratio,
+ const char *label)
+{
+ int filled = (int)(ratio * width);
+ int i;
+
+ if (filled < 0)
+ filled = 0;
+ if (filled > width)
+ filled = width;
+ mvwprintw(win, y, x, "%-10s [", label);
+ for (i = 0; i < width; i++) {
+ if (i < filled)
+ waddch(win, ACS_CKBOARD);
+ else
+ waddch(win, '-');
+ }
+ wprintw(win, "] %5.1f%%", ratio * 100.0);
+}
+
+void rmtop_render_curses(const struct rmtop_state *st, int interval_ms,
+ int paused)
+{
+ char when[32];
+ struct tm tm;
+ struct timespec ts;
+ int row = 1;
+
+ clock_gettime(CLOCK_REALTIME, &ts);
+ localtime_r(&ts.tv_sec, &tm);
+ strftime(when, sizeof(when), "%H:%M:%S", &tm);
+
+ erase();
+ attron(A_REVERSE);
+ mvprintw(0, 0, " rmtop %s | GPU %d: %.40s | %.50s | %s%s ",
+ RMTOP_VERSION, st->gpu_minor,
+ st->model[0] ? st->model : "-",
+ st->driver[0] ? st->driver : "driver unknown", when,
+ paused ? " | PAUSED" : "");
+ clrtoeol();
+ attroff(A_REVERSE);
+
+ mvprintw(row++, 1, "Bus %-14s UUID %-40s",
+ st->bdf[0] ? st->bdf : "-",
+ st->uuid[0] ? st->uuid : "-");
+ mvprintw(row++, 1, "RM client: %s%s",
+ st->have_rm ? "ok" : "unavailable",
+ st->have_rm && st->rm_name[0] ? " " : "");
+ if (st->have_rm && st->rm_name[0])
+ printw("%s", st->rm_name);
+ mvprintw(row++, 1, "Arch: %s (0x%x/0x%x rev %u)",
+ st->have_rm ? rm_arch_str(st->mc_arch) : "-",
+ st->mc_arch, st->mc_impl, st->mc_rev);
+ mvprintw(row++, 1, "VRAM: %u MiB total, %u MiB free, %u MiB used",
+ st->vram_total_kib / 1024, st->heap_free_kib / 1024,
+ (st->vram_total_kib - st->heap_free_kib) / 1024);
+ mvprintw(row++, 1, "BAR1: %u MiB size, %u MiB available",
+ st->bar1_size_kib / 1024, st->bar1_avail_kib / 1024);
+ row++;
+ mvprintw(row++, 1, "Engines (%u)%s:", st->engine_count,
+ st->engine_names_ok ? "" : " [FIFO names unavailable]");
+ if (st->engine_count > 0) {
+ unsigned int i;
+
+ move(row, 3);
+ for (i = 0; i < st->engine_count; i++) {
+ const char *name = st->engine_names_ok ?
+ st->engine_names[i] : "-";
+
+ printw("%s ", name);
+ if ((i + 1) % 8 == 0 && i + 1 < st->engine_count)
+ mvprintw(++row, 3, " ");
+ }
+ row++;
+ }
+ row++;
+ if (st->have_timer) {
+ time_t secs = (time_t)(st->timer_nsec / 1000000000ULL);
+
+ gmtime_r(&secs, &tm);
+ strftime(when, sizeof(when), "%Y-%m-%dT%H:%M:%S", &tm);
+ mvprintw(row++, 1, "GPU timer: %llu ns (%sZ)",
+ (unsigned long long)st->timer_nsec, when);
+ }
+ row++;
+ {
+ double total = st->vram_total_kib ? st->vram_total_kib : 1.0;
+
+ draw_bar(stdscr, row++, 1, 40,
+ (double)(st->vram_total_kib - st->heap_free_kib) /
+ total, "VRAM");
+ }
+ draw_bar(stdscr, row++, 1, 40,
+ st->bar1_size_kib ?
+ (double)(st->bar1_size_kib - st->bar1_avail_kib) /
+ st->bar1_size_kib : 0.0, "BAR1");
+ row++;
+ attron(A_DIM);
+ mvprintw(row++, 1, "Live util/power/thermal: n/a in this read-only "
+ "minimal build (see docs/17-RM-HARNESS.md)");
+ attroff(A_DIM);
+
+ attron(A_REVERSE);
+ mvprintw(LINES - 1, 0, " q quit | r refresh | s %dms interval | "
+ "p pause | g/G GPU | h help ", interval_ms);
+ clrtoeol();
+ attroff(A_REVERSE);
+ refresh();
+}
+
+static void help_screen(void)
+{
+ erase();
+ mvprintw(0, 1, "rmtop help");
+ mvprintw(2, 1, "This dashboard samples read-only RM controls and "
+ "/proc/sysfs.");
+ mvprintw(3, 1, "It never allocates, maps, resets or claims the GPU.");
+ mvprintw(5, 1, "q / F10 quit");
+ mvprintw(6, 1, "r refresh now");
+ mvprintw(7, 1, "s cycle sample interval (250/500/1000/2000 ms)");
+ mvprintw(8, 1, "p pause/resume sampling");
+ mvprintw(9, 1, "g / G next / previous GPU");
+ mvprintw(10, 1, "h this help");
+ mvprintw(12, 1, "Press any key to return.");
+ refresh();
+ timeout(-1);
+ getch();
+ timeout(0);
+}
+
+int main(int argc, char **argv)
+{
+ struct rm_ctx ctx;
+ struct rmtop_state st;
+ int interval_ms = 500;
+ int once = 0;
+ int paused = 0;
+ int rc;
+
+ ctx.gpu_minor = -1;
+ ctx.dev_path = NULL;
+ ctx.format = RM_FMT_TABLE;
+ ctx.have_rm = 0;
+ ctx.rm.fd = -1;
+
+ if (parse_common(&ctx, &interval_ms, &once, argc, argv) < 0) {
+ usage();
+ return 2;
+ }
+
+ if (once) {
+ rmtop_collect(&ctx, &st);
+ rmtop_render_plain(&st, ctx.format);
+ if (ctx.have_rm)
+ rm_client_close(&ctx.rm);
+ return 0;
+ }
+
+ initscr();
+ cbreak();
+ noecho();
+ keypad(stdscr, TRUE);
+ curs_set(0);
+ if (has_colors()) {
+ start_color();
+ use_default_colors();
+ init_pair(1, COLOR_GREEN, -1);
+ init_pair(2, COLOR_YELLOW, -1);
+ init_pair(3, COLOR_RED, -1);
+ }
+
+ rc = 0;
+ while (1) {
+ int ch;
+
+ if (!paused) {
+ rmtop_collect(&ctx, &st);
+ rmtop_render_curses(&st, interval_ms, paused);
+ timeout(interval_ms);
+ } else {
+ timeout(-1);
+ }
+ ch = getch();
+ switch (ch) {
+ case 'q':
+ case 'Q':
+ case KEY_F(10):
+ goto done;
+ case 'r':
+ break;
+ case 's':
+ if (interval_ms == 250)
+ interval_ms = 500;
+ else if (interval_ms == 500)
+ interval_ms = 1000;
+ else if (interval_ms == 1000)
+ interval_ms = 2000;
+ else
+ interval_ms = 250;
+ break;
+ case 'p':
+ paused = !paused;
+ break;
+ case 'g':
+ case 'G': {
+ struct rm_gpu_desc gpus[RM_MAX_GPUS];
+ int n = rm_scan_gpus(gpus, RM_MAX_GPUS);
+ int i;
+
+ for (i = 0; i < n; i++) {
+ if (gpus[i].minor == ctx.gpu_minor) {
+ int next = (ch == 'g') ?
+ (i + 1) % n : (i + n - 1) % n;
+
+ rm_client_close(&ctx.rm);
+ ctx.have_rm = 0;
+ ctx.gpu_minor = gpus[next].minor;
+ break;
+ }
+ }
+ break;
+ }
+ case 'h':
+ help_screen();
+ break;
+ case KEY_RESIZE:
+ break;
+ default:
+ break;
+ }
+ }
+done:
+ endwin();
+ if (ctx.have_rm)
+ rm_client_close(&ctx.rm);
+ return rc;
+}
diff --git a/tools/rmtop/src/panel.c b/tools/rmtop/src/panel.c
new file mode 100644
index 0000000..9ad5ac8
--- /dev/null
+++ b/tools/rmtop/src/panel.c
@@ -0,0 +1,232 @@
+/* panel.c - rmtop sampling (read-only RM + /proc) and plain rendering. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "rmtop.h"
+
+static int pick_gpu(struct rm_ctx *ctx, struct rmtop_state *st)
+{
+ struct rm_gpu_desc gpus[RM_MAX_GPUS];
+ const struct rm_gpu_desc *g = NULL;
+ int n, i;
+
+ n = rm_scan_gpus(gpus, RM_MAX_GPUS);
+ if (n <= 0) {
+ st->gpu_minor = ctx->gpu_minor;
+ return n;
+ }
+ for (i = 0; i < n; i++) {
+ if (gpus[i].minor == ctx->gpu_minor)
+ g = &gpus[i];
+ }
+ if (g == NULL)
+ g = &gpus[0];
+ st->gpu_minor = g->minor;
+ snprintf(st->model, sizeof(st->model), "%s", g->model);
+ snprintf(st->bdf, sizeof(st->bdf), "%s", g->bdf);
+ snprintf(st->uuid, sizeof(st->uuid), "%s", g->uuid);
+ return n;
+}
+
+static void read_driver_version(struct rmtop_state *st)
+{
+ FILE *fh = fopen("/proc/driver/nvidia/version", "r");
+
+ st->driver[0] = '\0';
+ if (fh == NULL)
+ return;
+ if (fgets(st->driver, sizeof(st->driver), fh) != NULL)
+ st->driver[strcspn(st->driver, "\r\n")] = '\0';
+ fclose(fh);
+}
+
+int rmtop_collect(struct rm_ctx *ctx, struct rmtop_state *st)
+{
+ NV2080_CTRL_MC_ARCH_INFO_PARAMS arch;
+ NV2080_CTRL_GPU_GET_ENGINES_V2_PARAMS engines;
+ NV2080_CTRL_TIMER_GET_TIME_PARAMS timer;
+ NV2080_FIFO_DEVICE_ENTRY *fifo;
+ NvU32 fb_indices[6] = {
+ NV2080_CTRL_FB_INFO_INDEX_TOTAL_RAM_SIZE,
+ NV2080_CTRL_FB_INFO_INDEX_USABLE_RAM_SIZE,
+ NV2080_CTRL_FB_INFO_INDEX_HEAP_SIZE,
+ NV2080_CTRL_FB_INFO_INDEX_HEAP_FREE,
+ NV2080_CTRL_FB_INFO_INDEX_BAR1_SIZE,
+ NV2080_CTRL_FB_INFO_INDEX_BAR1_AVAIL_SIZE,
+ };
+ NvU32 fb_values[6] = { 0 };
+ NvU32 n_fifo = 0;
+ unsigned int i;
+
+ memset(st, 0, sizeof(*st));
+ st->gpu_minor = ctx->gpu_minor;
+ st->sampled_at = time(NULL);
+ read_driver_version(st);
+ pick_gpu(ctx, st);
+
+ st->rm_rc = rm_ctx_open(ctx);
+ if (st->rm_rc != 0)
+ return 0;
+ st->have_rm = 1;
+ st->gpu_minor = ctx->rm.gpu_minor;
+
+ if (rm_query_name(ctx, st->rm_name, sizeof(st->rm_name)) != 0)
+ snprintf(st->rm_name, sizeof(st->rm_name), "%s", "-");
+
+ memset(&arch, 0, sizeof(arch));
+ if (rm_query_mc_arch(ctx, &arch) == 0) {
+ st->mc_arch = arch.architecture;
+ st->mc_impl = arch.implementation;
+ st->mc_rev = arch.revision;
+ st->mc_subrev = arch.subRevision;
+ }
+
+ if (rm_query_fb(ctx, fb_indices, fb_values, 6) == 0) {
+ st->vram_total_kib = fb_values[0];
+ st->vram_usable_kib = fb_values[1];
+ st->heap_size_kib = fb_values[2];
+ st->heap_free_kib = fb_values[3];
+ st->bar1_size_kib = fb_values[4];
+ st->bar1_avail_kib = fb_values[5];
+ }
+
+ memset(&timer, 0, sizeof(timer));
+ {
+ int ioctl_ret;
+
+ if (rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_TIMER_GET_TIME, &timer,
+ sizeof(timer), &ioctl_ret) == 0 &&
+ ioctl_ret == 0) {
+ st->timer_nsec = timer.time_nsec;
+ st->have_timer = 1;
+ }
+ }
+
+ memset(&engines, 0, sizeof(engines));
+ if (rm_query_engines(ctx, &engines) == 0) {
+ st->engine_count = engines.engineCount;
+ if (st->engine_count > RMTOP_MAX_ENGINES)
+ st->engine_count = RMTOP_MAX_ENGINES;
+ for (i = 0; i < st->engine_count; i++)
+ st->engine_list[i] = engines.engineList[i];
+ }
+ fifo = calloc(1, sizeof(*fifo) * 512);
+ if (fifo != NULL) {
+ rm_query_fifo_table(ctx, fifo, 512, &n_fifo);
+ for (i = 0; i < st->engine_count; i++) {
+ snprintf(st->engine_names[i],
+ sizeof(st->engine_names[i]), "%s",
+ rm_engine_name(fifo, n_fifo,
+ st->engine_list[i]));
+ }
+ st->engine_names_ok = st->engine_count > 0;
+ free(fifo);
+ }
+ return 0;
+}
+
+void rmtop_render_plain(const struct rmtop_state *st, enum rm_format format)
+{
+ struct rm_kv kv[16];
+ char v[16][160];
+ char human[32], human2[32];
+ char when[32];
+ struct tm tm;
+ int n = 0;
+
+ if (format != RM_FMT_TABLE)
+ goto build;
+ printf("rmtop %s - read-only GPU dashboard (one-shot)\n",
+ RMTOP_VERSION);
+ printf("sampled %s", ctime(&st->sampled_at));
+build:
+ kv[n].key = "gpu_minor";
+ snprintf(v[n], sizeof(v[n]), "%d", st->gpu_minor);
+ kv[n].value = v[n];
+ n++;
+ kv[n].key = "model";
+ kv[n].value = st->model[0] ? st->model : "-";
+ n++;
+ kv[n].key = "driver";
+ kv[n].value = st->driver[0] ? st->driver : "-";
+ n++;
+ kv[n].key = "bus";
+ kv[n].value = st->bdf[0] ? st->bdf : "-";
+ n++;
+ kv[n].key = "uuid";
+ kv[n].value = st->uuid[0] ? st->uuid : "-";
+ n++;
+ kv[n].key = "rm_name";
+ kv[n].value = st->have_rm && st->rm_name[0] ? st->rm_name : "-";
+ n++;
+ kv[n].key = "rm_client";
+ snprintf(v[n], sizeof(v[n]), "%s",
+ st->have_rm ? "ok" : "unavailable");
+ kv[n].value = v[n];
+ n++;
+ if (st->have_rm) {
+ snprintf(v[n], sizeof(v[n]), "%s (0x%x/0x%x rev %u)",
+ rm_arch_str(st->mc_arch), st->mc_arch,
+ st->mc_impl, st->mc_rev);
+ kv[n].key = "arch";
+ kv[n].value = v[n];
+ n++;
+ }
+ rm_human_kib(st->vram_total_kib, human, sizeof(human));
+ rm_human_kib(st->vram_usable_kib, human2, sizeof(human2));
+ snprintf(v[n], sizeof(v[n]), "%u KiB (%s) / usable %u KiB (%s)",
+ st->vram_total_kib, human, st->vram_usable_kib, human2);
+ kv[n].key = "vram";
+ kv[n].value = v[n];
+ n++;
+ rm_human_kib(st->heap_free_kib, human, sizeof(human));
+ rm_human_kib(st->heap_size_kib, human2, sizeof(human2));
+ snprintf(v[n], sizeof(v[n]), "free %u KiB (%s) / heap %u KiB (%s)",
+ st->heap_free_kib, human, st->heap_size_kib, human2);
+ kv[n].key = "vram_free";
+ kv[n].value = v[n];
+ n++;
+ rm_human_kib(st->bar1_size_kib, human, sizeof(human));
+ rm_human_kib(st->bar1_avail_kib, human2, sizeof(human2));
+ snprintf(v[n], sizeof(v[n]), "size %u KiB (%s), avail %u KiB (%s)",
+ st->bar1_size_kib, human, st->bar1_avail_kib, human2);
+ kv[n].key = "bar1";
+ kv[n].value = v[n];
+ n++;
+ if (st->have_timer) {
+ time_t secs = (time_t)(st->timer_nsec / 1000000000ULL);
+
+ gmtime_r(&secs, &tm);
+ strftime(when, sizeof(when), "%Y-%m-%dT%H:%M:%S", &tm);
+ snprintf(v[n], sizeof(v[n]), "%llu ns (%s.%09lluZ)",
+ (unsigned long long)st->timer_nsec, when,
+ (unsigned long long)(st->timer_nsec % 1000000000ULL));
+ kv[n].key = "gpu_timer";
+ kv[n].value = v[n];
+ n++;
+ }
+ {
+ size_t off = 0;
+ unsigned int i;
+
+ v[n][0] = '\0';
+ for (i = 0; i < st->engine_count && off < sizeof(v[n]) - 2;
+ i++) {
+ const char *name = st->engine_names_ok ?
+ st->engine_names[i] : "engine";
+
+ off += (size_t)snprintf(v[n] + off,
+ sizeof(v[n]) - off, "%s%s",
+ i ? " " : "", name);
+ }
+ kv[n].key = "engines";
+ kv[n].value = v[n][0] ? v[n] : "-";
+ n++;
+ }
+ kv[n].key = "live_util_power_thermal";
+ kv[n].value = "n/a (read-only minimal build; see docs/17)";
+ n++;
+ rm_print_kv("rmtop", kv, n, format);
+}