/* SPDX-License-Identifier: BSD-3-Clause */ /* Copyright (c) 2026 Open-IOV contributors */ /* 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 #include #include #include #include #include #include #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; }