summaryrefslogtreecommitdiffstats
path: root/tools
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
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')
-rw-r--r--tools/rmtool/Makefile35
-rw-r--r--tools/rmtool/include/rm_api.h297
-rw-r--r--tools/rmtool/include/rmtool.h71
-rwxr-xr-xtools/rmtool/rmtoolbin0 -> 57040 bytes
-rw-r--r--tools/rmtool/src/engine.c252
-rw-r--r--tools/rmtool/src/gpu.c283
-rw-r--r--tools/rmtool/src/main.c95
-rw-r--r--tools/rmtool/src/mem.c146
-rw-r--r--tools/rmtool/src/nv2080.c398
-rw-r--r--tools/rmtool/src/output.c251
-rw-r--r--tools/rmtool/src/query.c256
-rw-r--r--tools/rmtool/src/rm_api.c291
-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
17 files changed, 3025 insertions, 0 deletions
diff --git a/tools/rmtool/Makefile b/tools/rmtool/Makefile
new file mode 100644
index 0000000..a66b7de
--- /dev/null
+++ b/tools/rmtool/Makefile
@@ -0,0 +1,35 @@
+# rmtool - read-only NVIDIA RM host oracle (Open-IOV).
+#
+# Builds src/*.c into ./rmtool and a reusable lib/librmapi.a that rmtop links
+# against. No external SDK or library dependency.
+CC ?= gcc
+CFLAGS ?= -O2 -Wall -Wextra
+CPPFLAGS += -Iinclude
+PREFIX ?= /usr/local
+
+OBJS = src/main.o src/rm_api.o src/query.o src/output.o src/gpu.o \
+ src/engine.o src/mem.o src/nv2080.o
+LIBOBJS = src/rm_api.o src/query.o src/output.o
+LIB = lib/librmapi.a
+
+all: rmtool $(LIB)
+
+rmtool: $(OBJS)
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS)
+
+$(LIB): $(LIBOBJS)
+ @mkdir -p lib
+ ar rcs $@ $(LIBOBJS)
+
+src/%.o: src/%.c include/rm_api.h include/rmtool.h
+ $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
+
+install: rmtool
+ install -d $(DESTDIR)$(PREFIX)/bin
+ install -m 0755 rmtool $(DESTDIR)$(PREFIX)/bin/rmtool
+
+clean:
+ rm -f $(OBJS) rmtool $(LIB)
+ rmdir lib 2>/dev/null || true
+
+.PHONY: all install clean
diff --git a/tools/rmtool/include/rm_api.h b/tools/rmtool/include/rm_api.h
new file mode 100644
index 0000000..5d7f8cf
--- /dev/null
+++ b/tools/rmtool/include/rm_api.h
@@ -0,0 +1,297 @@
+/* rm_api.h - minimal self-contained NVIDIA RM userspace client.
+ *
+ * ABI and control IDs mirror the NV570 SDK (nvos.h, ctrl2080*.h) but the
+ * definitions live here so the tools build with no external SDK dependency.
+ * This is the shared layer used by rmtool and rmtop; it is read-only by
+ * design: it only allocates an RM client/device/subdevice and issues
+ * read-only NV2080/NV0080 control queries.
+ */
+#ifndef RM_API_H
+#define RM_API_H
+
+#include <stdint.h>
+
+typedef uint8_t NvU8;
+typedef uint16_t NvU16;
+typedef uint32_t NvU32;
+typedef uint64_t NvU64;
+typedef uint32_t NvHandle;
+typedef uint64_t NvP64;
+typedef uint32_t NvV32;
+
+/* NVIDIA escape encoding: _IOWR('F', nr, type) */
+#define NV_IOCTL_MAGIC 'F'
+#define NV_IOCTL_BASE 200
+#define NV_ESC_RM_ALLOC 0x2B
+#define NV_ESC_RM_CONTROL 0x2A
+#define NV_ESC_CHECK_VERSION_STR (NV_IOCTL_BASE + 10)
+#define NV_ESC_WAIT_OPEN_COMPLETE (NV_IOCTL_BASE + 18)
+
+/* RM classes used by the client ladder */
+#define NV01_ROOT 0x00000000U
+#define NV01_ROOT_CLIENT 0x00000041U
+#define NV01_DEVICE_0 0x00000080U
+#define NV20_SUBDEVICE_0 0x00002080U
+
+/* NVOS21/NVOS64 match the running driver ABI (NV570 kernel nvos.h): both
+ * carry paramsSize. The userspace SDK snapshot in-tree has an older layout;
+ * using it results in the kernel rejecting the ioctl with EINVAL.
+ */
+typedef struct {
+ NvHandle hRoot;
+ NvHandle hObjectParent;
+ NvHandle hObjectNew;
+ NvV32 hClass;
+ NvP64 pAllocParms __attribute__((aligned(8)));
+ NvU32 paramsSize;
+ NvV32 status;
+} NVOS21_PARAMETERS;
+
+#define NVOS64_FLAGS_NONE 0x00000000U
+#define NVOS64_FLAGS_FINN_SERIALIZED 0x00000001U
+
+typedef struct {
+ NvHandle hRoot;
+ NvHandle hObjectParent;
+ NvHandle hObjectNew;
+ NvV32 hClass;
+ NvP64 pAllocParms __attribute__((aligned(8)));
+ NvP64 pRightsRequested __attribute__((aligned(8)));
+ NvU32 paramsSize;
+ NvU32 flags;
+ NvV32 status;
+} NVOS64_PARAMETERS;
+
+/* NVOS54 (NV04_CONTROL), standard nvos.h layout: sizeof = 32 */
+typedef struct {
+ NvHandle hClient;
+ NvHandle hObject;
+ NvV32 cmd;
+ NvU32 flags;
+ NvP64 params __attribute__((aligned(8)));
+ NvU32 paramsSize;
+ NvV32 status;
+} NVOS54_PARAMETERS;
+
+#define NV_RM_API_VERSION_STRING_LENGTH 64
+#define NV_RM_API_VERSION_CMD_QUERY '2'
+#define NV_RM_API_VERSION_REPLY_RECOGNIZED 1U
+
+typedef struct {
+ NvU32 cmd;
+ NvU32 reply;
+ char versionString[NV_RM_API_VERSION_STRING_LENGTH];
+} nv_ioctl_rm_api_version_t;
+
+typedef struct {
+ int rc;
+ NvU32 adapterStatus;
+} nv_ioctl_wait_open_complete_t;
+
+typedef struct {
+ NvU32 deviceId;
+ NvHandle hClientShare;
+ NvHandle hTargetClient;
+ NvHandle hTargetDevice;
+ NvV32 flags;
+ NvU64 vaSpaceSize __attribute__((aligned(8)));
+ NvU64 vaStartInternal __attribute__((aligned(8)));
+ NvU64 vaLimitInternal __attribute__((aligned(8)));
+ NvV32 vaMode;
+} NV0080_ALLOC_PARAMETERS;
+
+typedef struct {
+ NvU32 subDeviceId;
+} NV2080_ALLOC_PARAMETERS;
+
+#define NV_IOCTL_RM_ALLOC21 \
+ _IOWR(NV_IOCTL_MAGIC, NV_ESC_RM_ALLOC, NVOS21_PARAMETERS)
+#define NV_IOCTL_RM_ALLOC64 \
+ _IOWR(NV_IOCTL_MAGIC, NV_ESC_RM_ALLOC, NVOS64_PARAMETERS)
+#define NV_IOCTL_RM_CONTROL \
+ _IOWR(NV_IOCTL_MAGIC, NV_ESC_RM_CONTROL, NVOS54_PARAMETERS)
+#define NV_IOCTL_CHECK_VERSION \
+ _IOWR(NV_IOCTL_MAGIC, NV_ESC_CHECK_VERSION_STR, \
+ nv_ioctl_rm_api_version_t)
+#define NV_IOCTL_WAIT_OPEN_COMPLETE \
+ _IOWR(NV_IOCTL_MAGIC, NV_ESC_WAIT_OPEN_COMPLETE, \
+ nv_ioctl_wait_open_complete_t)
+
+/* ------------------------------------------------------------------ */
+/* Read-only NV2080/NV0080 controls used by the tools. */
+/* ------------------------------------------------------------------ */
+
+/* NV2080_CTRL_CMD_GPU_GET_INFO_V2 (subdevice, no embedded pointers) */
+#define NV2080_CTRL_CMD_GPU_GET_INFO_V2 0x20800102U
+#define NV2080_CTRL_CMD_GPU_GET_NAME_STRING 0x20800110U
+#define NV2080_CTRL_CMD_GPU_QUERY_MODE 0x20800128U
+#define NV2080_CTRL_CMD_GPU_GET_ID 0x20800142U
+#define NV2080_CTRL_CMD_GPU_GET_ENGINES_V2 0x20800170U
+#define NV2080_CTRL_CMD_MC_GET_ARCH_INFO 0x20801701U
+#define NV2080_CTRL_CMD_FB_GET_INFO_V2 0x20801303U
+#define NV2080_CTRL_CMD_BUS_GET_PCI_INFO 0x20801801U
+#define NV2080_CTRL_CMD_TIMER_GET_TIME 0x20800403U
+#define NV2080_CTRL_CMD_FIFO_GET_DEVICE_INFO_TABLE 0x20801112U
+#define NV0080_CTRL_CMD_GPU_GET_CLASSLIST_V2 0x00000292U
+
+#define NV2080_CTRL_GPU_INFO_MAX_LIST_SIZE 0x41U
+#define NV2080_CTRL_GPU_INFO_INDEX_SYSMEM_ACCESS 0x1fU
+#define NV2080_CTRL_GPU_INFO_INDEX_GPU_SM_VERSION 0x2cU
+#define NV2080_CTRL_GPU_INFO_INDEX_GPU_FLA_CAPABILITY 0x2dU
+#define NV2080_CTRL_GPU_INFO_INDEX_4K_PAGE_ISOLATION_REQUIRED 0x33U
+#define NV2080_CTRL_GPU_INFO_INDEX_DISPLAY_ENABLED 0x34U
+#define NV2080_CTRL_GPU_INFO_INDEX_GPU_PROFILING_CAPABILITY 0x36U
+#define NV2080_CTRL_GPU_INFO_INDEX_GPU_DEBUGGING_CAPABILITY 0x37U
+#define NV2080_CTRL_GPU_INFO_INDEX_CMP_SKU 0x3cU
+#define NV2080_CTRL_GPU_INFO_INDEX_DMABUF_CAPABILITY 0x3dU
+
+typedef struct {
+ NvU32 index;
+ NvU32 data;
+} NV2080_CTRL_GPU_INFO;
+
+typedef struct {
+ NvU32 gpuInfoListSize;
+ NV2080_CTRL_GPU_INFO gpuInfoList[NV2080_CTRL_GPU_INFO_MAX_LIST_SIZE];
+} NV2080_CTRL_GPU_GET_INFO_V2_PARAMS;
+
+#define NV2080_GPU_MAX_NAME_STRING_LENGTH 0x40U
+
+typedef struct {
+ NvU32 gpuNameStringFlags;
+ union {
+ NvU8 ascii[NV2080_GPU_MAX_NAME_STRING_LENGTH];
+ NvU16 unicode[NV2080_GPU_MAX_NAME_STRING_LENGTH];
+ } gpuNameString;
+} NV2080_CTRL_GPU_GET_NAME_STRING_PARAMS;
+
+typedef struct {
+ NvU32 gpuId;
+} NV2080_CTRL_GPU_GET_ID_PARAMS;
+
+typedef struct {
+ NvU32 mode;
+} NV2080_CTRL_GPU_QUERY_MODE_PARAMS;
+
+#define NV2080_GPU_MAX_ENGINES_LIST_SIZE 0x54U
+
+typedef struct {
+ NvU32 engineCount;
+ NvU32 engineList[NV2080_GPU_MAX_ENGINES_LIST_SIZE];
+} NV2080_CTRL_GPU_GET_ENGINES_V2_PARAMS;
+
+typedef struct {
+ NvU32 architecture;
+ NvU32 implementation;
+ NvU32 revision;
+ NvU8 subRevision;
+} NV2080_CTRL_MC_ARCH_INFO_PARAMS;
+
+#define NV2080_CTRL_FB_INFO_MAX_LIST_SIZE 0x39U
+#define NV2080_CTRL_FB_INFO_INDEX_TOTAL_RAM_SIZE 0x08U
+#define NV2080_CTRL_FB_INFO_INDEX_HEAP_SIZE 0x09U
+#define NV2080_CTRL_FB_INFO_INDEX_LARGEST_FREE_REGION_SIZE_KB 0x12U
+#define NV2080_CTRL_FB_INFO_INDEX_HEAP_FREE 0x16U
+#define NV2080_CTRL_FB_INFO_INDEX_BAR1_SIZE 0x05U
+#define NV2080_CTRL_FB_INFO_INDEX_BAR1_AVAIL_SIZE 0x1dU
+#define NV2080_CTRL_FB_INFO_INDEX_USABLE_RAM_SIZE 0x20U
+
+typedef struct {
+ NvU32 index;
+ NvU32 data;
+} NV2080_CTRL_FB_INFO;
+
+typedef struct {
+ NvU32 fbInfoListSize;
+ NV2080_CTRL_FB_INFO fbInfoList[NV2080_CTRL_FB_INFO_MAX_LIST_SIZE];
+} NV2080_CTRL_FB_GET_INFO_V2_PARAMS;
+
+typedef struct {
+ NvU32 pciDeviceId;
+ NvU32 pciSubSystemId;
+ NvU32 pciRevisionId;
+ NvU32 pciExtDeviceId;
+} NV2080_CTRL_BUS_GET_PCI_INFO_PARAMS;
+
+typedef struct {
+ NvU64 time_nsec __attribute__((aligned(8)));
+} NV2080_CTRL_TIMER_GET_TIME_PARAMS;
+
+/* NV2080_CTRL_FIFO_GET_DEVICE_INFO_TABLE: static engine table with names.
+ * Sizes match tools/rmq/rmq.c, which has produced ground-truth on TU104.
+ */
+typedef struct {
+ NvU32 engineData[16];
+ NvU32 pbdmaIds[2];
+ NvU32 pbdmaFaultIds[2];
+ NvU32 numPbdmas;
+ char engineName[16];
+} NV2080_FIFO_DEVICE_ENTRY;
+
+typedef struct {
+ NvU32 baseIndex;
+ NvU32 numEntries;
+ NvU32 bMore;
+ NV2080_FIFO_DEVICE_ENTRY entries[32];
+} NV2080_CTRL_FIFO_GET_DEVICE_INFO_TABLE_PARAMS;
+
+#define NV0080_CTRL_GPU_CLASSLIST_MAX_SIZE 100U
+
+typedef struct {
+ NvU32 numClasses;
+ NvU32 classList[NV0080_CTRL_GPU_CLASSLIST_MAX_SIZE];
+} NV0080_CTRL_GPU_GET_CLASSLIST_V2_PARAMS;
+
+/* ------------------------------------------------------------------ */
+/* Client + GPU discovery */
+/* ------------------------------------------------------------------ */
+
+struct rm_client {
+ int fd; /* /dev/nvidiactl control fd */
+ int gpu_fd; /* /dev/nvidiaN device fd (open-complete) */
+ NvU32 hClient;
+ NvU32 hDevice;
+ NvU32 hSubdevice;
+ int gpu_minor;
+};
+
+#define RM_MAX_GPUS 32
+#define RM_BDF_LEN 32
+#define RM_NAME_LEN 128
+#define RM_UUID_LEN 96
+#define RM_VBIOS_LEN 64
+
+struct rm_gpu_desc {
+ int minor;
+ char bdf[RM_BDF_LEN];
+ char model[RM_NAME_LEN];
+ char uuid[RM_UUID_LEN];
+ char vbios[RM_VBIOS_LEN];
+ int irq;
+};
+
+/* Open /dev/nvidiactl, bind a GPU (gpu_minor; <0 means 0), alloc the
+ * root-client -> device -> subdevice ladder, and keep the matching
+ * /dev/nvidiaN device fd open for the lifetime of the client.
+ * dev_path (may be NULL) overrides the GPU device node.
+ * Returns 0 on success, negative errno, or an RM status.
+ */
+int rm_client_open(struct rm_client *c, int gpu_minor, const char *dev_path);
+void rm_client_close(struct rm_client *c);
+
+/* Issue a control on hObject; returns the raw NV_STATUS (0 = NV_OK) and
+ * stores -1 in *ioctl_ret on transport failure (NULL allowed). */
+NvU32 rm_control(struct rm_client *c, NvU32 hObject, NvU32 cmd,
+ void *params, NvU32 size, int *ioctl_ret);
+
+/* Scan the per-GPU information files under /proc/driver/nvidia (read-only,
+ * no device access).
+ */
+int rm_scan_gpus(struct rm_gpu_desc *out, int max);
+
+/* True if cmd is on the read-only allowlist (see nv2080.c). */
+int rm_control_is_readonly(NvU32 cmd);
+
+const char *rm_status_str(NvU32 status);
+
+#endif /* RM_API_H */
diff --git a/tools/rmtool/include/rmtool.h b/tools/rmtool/include/rmtool.h
new file mode 100644
index 0000000..55166f8
--- /dev/null
+++ b/tools/rmtool/include/rmtool.h
@@ -0,0 +1,71 @@
+/* rmtool.h - internal shared declarations for rmtool commands and output. */
+#ifndef RMTOOL_H
+#define RMTOOL_H
+
+#include <stdio.h>
+#include <stddef.h>
+#include "rm_api.h"
+
+#define RMTOOL_VERSION "0.1.0"
+
+enum rm_format {
+ RM_FMT_TABLE = 0,
+ RM_FMT_JSON,
+ RM_FMT_CSV,
+};
+
+struct rm_kv {
+ const char *key;
+ const char *value;
+};
+
+struct rm_ctx {
+ int gpu_minor; /* -1 = first/default GPU */
+ const char *dev_path; /* NULL = /dev/nvidiactl */
+ enum rm_format format;
+ struct rm_client rm;
+ int have_rm;
+};
+
+/* output.c */
+enum rm_format rm_format_parse(const char *name);
+const char *rm_human_kib(NvU64 kib, char *buf, size_t len);
+void rm_print_kv(const char *title, const struct rm_kv *kv, int n,
+ enum rm_format format);
+void rm_print_table(const char *title, const char *const *headers,
+ const char *cells, size_t stride, int rows, int cols,
+ enum rm_format format);
+void rm_hexdump(const void *data, size_t len);
+
+/* query.c */
+int rm_ctx_open(struct rm_ctx *ctx);
+int rm_apply_common_opt(struct rm_ctx *ctx, int opt, const char *arg);
+NvU32 rm_query_name(struct rm_ctx *ctx, char *buf, size_t len);
+NvU32 rm_query_mc_arch(struct rm_ctx *ctx, NV2080_CTRL_MC_ARCH_INFO_PARAMS *p);
+NvU32 rm_query_pci(struct rm_ctx *ctx, NV2080_CTRL_BUS_GET_PCI_INFO_PARAMS *p);
+NvU32 rm_query_fb(struct rm_ctx *ctx, const NvU32 *indices, NvU32 *values,
+ NvU32 n);
+NvU32 rm_query_engines(struct rm_ctx *ctx,
+ NV2080_CTRL_GPU_GET_ENGINES_V2_PARAMS *p);
+NvU32 rm_query_fifo_table(struct rm_ctx *ctx,
+ NV2080_FIFO_DEVICE_ENTRY *entries,
+ NvU32 max_entries, NvU32 *n_entries);
+const char *rm_arch_str(NvU32 architecture);
+const char *rm_engine_type_name(NvU32 engine_type);
+const char *rm_engine_name(const NV2080_FIFO_DEVICE_ENTRY *entries,
+ NvU32 n_entries, NvU32 engine_type);
+
+/* gpu.c */
+int cmd_gpu(int argc, char **argv, struct rm_ctx *ctx);
+
+/* engine.c */
+int cmd_engine(int argc, char **argv, struct rm_ctx *ctx);
+
+/* mem.c */
+int cmd_mem(int argc, char **argv, struct rm_ctx *ctx);
+
+/* nv2080.c (includes sample) */
+int cmd_nv2080(int argc, char **argv, struct rm_ctx *ctx);
+int cmd_sample(int argc, char **argv, struct rm_ctx *ctx);
+
+#endif /* RMTOOL_H */
diff --git a/tools/rmtool/rmtool b/tools/rmtool/rmtool
new file mode 100755
index 0000000..a0c6bea
--- /dev/null
+++ b/tools/rmtool/rmtool
Binary files differ
diff --git a/tools/rmtool/src/engine.c b/tools/rmtool/src/engine.c
new file mode 100644
index 0000000..c247e47
--- /dev/null
+++ b/tools/rmtool/src/engine.c
@@ -0,0 +1,252 @@
+/* engine.c - rmtool engine list/info via read-only GPU/engine controls. */
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+
+#include "rmtool.h"
+
+static void usage(void)
+{
+ printf("usage: rmtool engine list [-d N|BDF] [--format=...]\n"
+ " rmtool engine info [-d N|BDF] -e ENGINE\n"
+ "ENGINE is a FIFO table name (gr, ce0, nvdec0, ...).\n");
+}
+
+static int parse_list(int argc, char **argv, struct rm_ctx *ctx)
+{
+ static const struct option long_opts[] = {
+ { "device-spec", required_argument, NULL, 'd' },
+ { "format", required_argument, NULL, 'f' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 },
+ };
+ int opt;
+
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc, argv, "d:f:h", long_opts, NULL)) != -1) {
+ int rc;
+
+ if (opt == 'h')
+ return 1;
+ rc = rm_apply_common_opt(ctx, opt, optarg);
+ if (rc < 0)
+ return -1;
+ }
+ if (optind < argc) {
+ fprintf(stderr, "rmtool: unexpected argument '%s'\n",
+ argv[optind]);
+ return -1;
+ }
+ return 0;
+}
+
+static int engine_list(struct rm_ctx *ctx)
+{
+ NV2080_CTRL_GPU_GET_ENGINES_V2_PARAMS engines;
+ NV2080_FIFO_DEVICE_ENTRY *fifo;
+ const char *headers[] = { "IDX", "TYPE", "NAME", "PBDMAS",
+ "PBDMA IDS" };
+ char (*cells)[64];
+ int rc;
+ NvU32 status, n_fifo = 0;
+ unsigned int i;
+
+ fifo = calloc(1, sizeof(*fifo) * 512);
+ cells = calloc(1, sizeof(*cells) * (NV2080_GPU_MAX_ENGINES_LIST_SIZE *
+ 5));
+ if (fifo == NULL || cells == NULL) {
+ free(fifo);
+ free(cells);
+ return 2;
+ }
+ rc = rm_ctx_open(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: cannot open RM client: rc=%d\n", rc);
+ free(fifo);
+ free(cells);
+ return 2;
+ }
+ status = rm_query_engines(ctx, &engines);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: GPU_GET_ENGINES_V2: %s\n",
+ rm_status_str(status));
+ free(fifo);
+ free(cells);
+ return 2;
+ }
+ status = rm_query_fifo_table(ctx, fifo, 512, &n_fifo);
+
+ for (i = 0; i < engines.engineCount; i++) {
+ char type[24];
+
+ snprintf(type, sizeof(type), "0x%x", engines.engineList[i]);
+ snprintf(cells[i * 5 + 0], sizeof(cells[0]), "%u", i);
+ snprintf(cells[i * 5 + 1], sizeof(cells[0]), "%s", type);
+ snprintf(cells[i * 5 + 2], sizeof(cells[0]), "%s",
+ rm_engine_name(fifo, n_fifo, engines.engineList[i]));
+ snprintf(cells[i * 5 + 3], sizeof(cells[0]), "-");
+ snprintf(cells[i * 5 + 4], sizeof(cells[0]), "-");
+ }
+ if (status == 0) {
+ NvU32 j;
+
+ for (j = 0; j < n_fifo && j < engines.engineCount; j++) {
+ snprintf(cells[j * 5 + 2], sizeof(cells[0]), "%s",
+ fifo[j].engineName);
+ snprintf(cells[j * 5 + 3], sizeof(cells[0]), "%u",
+ fifo[j].numPbdmas);
+ snprintf(cells[j * 5 + 4], sizeof(cells[0]),
+ "%u,%u", fifo[j].pbdmaIds[0],
+ fifo[j].pbdmaIds[1]);
+ }
+ }
+ if (ctx->format == RM_FMT_TABLE)
+ printf("%u engine(s); FIFO table %s\n", engines.engineCount,
+ status == 0 ? "read" : "unavailable");
+ rm_print_table("engines", headers, &cells[0][0], sizeof(cells[0]),
+ (int)engines.engineCount, 5, ctx->format);
+ free(fifo);
+ free(cells);
+ return 0;
+}
+
+static int engine_info(struct rm_ctx *ctx, const char *want)
+{
+ NV2080_FIFO_DEVICE_ENTRY *fifo;
+ struct rm_kv kv[8];
+ char v[8][256];
+ NvU32 n_fifo = 0, i;
+ int rc, n = 0;
+ NvU32 status;
+
+ fifo = calloc(1, sizeof(*fifo) * 512);
+ if (fifo == NULL)
+ return 2;
+ rc = rm_ctx_open(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: cannot open RM client: rc=%d\n", rc);
+ free(fifo);
+ return 2;
+ }
+ status = rm_query_fifo_table(ctx, fifo, 512, &n_fifo);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: FIFO_GET_DEVICE_INFO_TABLE: %s\n",
+ rm_status_str(status));
+ free(fifo);
+ return 2;
+ }
+ for (i = 0; i < n_fifo; i++) {
+ if (strcasecmp(fifo[i].engineName, want) != 0)
+ continue;
+ snprintf(v[0], sizeof(v[0]), "%s", fifo[i].engineName);
+ kv[n].key = "Engine";
+ kv[n++].value = v[0];
+ snprintf(v[1], sizeof(v[1]), "%u", i);
+ kv[n].key = "Table index";
+ kv[n++].value = v[1];
+ snprintf(v[2], sizeof(v[2]), "%u", fifo[i].numPbdmas);
+ kv[n].key = "PBDMA count";
+ kv[n++].value = v[2];
+ snprintf(v[3], sizeof(v[3]), "%u,%u", fifo[i].pbdmaIds[0],
+ fifo[i].pbdmaIds[1]);
+ kv[n].key = "PBDMA ids";
+ kv[n++].value = v[3];
+ snprintf(v[4], sizeof(v[4]), "%u,%u",
+ fifo[i].pbdmaFaultIds[0], fifo[i].pbdmaFaultIds[1]);
+ kv[n].key = "PBDMA fault ids";
+ kv[n++].value = v[4];
+ snprintf(v[5], sizeof(v[5]),"%u", fifo[i].engineData[0]);
+ kv[n].key = "engineData[0] (type)";
+ kv[n++].value = v[5];
+ {
+ size_t off = 0;
+ unsigned int j;
+
+ for (j = 0; j < 16 && off < sizeof(v[6]); j++)
+ off += (size_t)snprintf(v[6] + off,
+ sizeof(v[6]) - off,
+ "%s%u", j ? "," : "",
+ fifo[i].engineData[j]);
+ }
+ kv[n].key = "engineData[0..15]";
+ kv[n++].value = v[6];
+ rm_print_kv("engine info (FIFO device table)", kv, n,
+ ctx->format);
+ free(fifo);
+ return 0;
+ }
+ fprintf(stderr, "rmtool: engine '%s' not found in FIFO table "
+ "(%u entries)\n", want, n_fifo);
+ free(fifo);
+ return 2;
+}
+
+int cmd_engine(int argc, char **argv, struct rm_ctx *ctx)
+{
+ const char *want = NULL;
+ int rc;
+
+ if (argc < 2) {
+ usage();
+ return 2;
+ }
+ if (strcmp(argv[1], "list") == 0) {
+ rc = parse_list(argc - 1, argv + 1, ctx);
+ if (rc < 0)
+ return 2;
+ if (rc == 1) {
+ usage();
+ return 0;
+ }
+ return engine_list(ctx);
+ }
+ if (strcmp(argv[1], "info") == 0) {
+ int opt;
+ static const struct option long_opts[] = {
+ { "device-spec", required_argument, NULL, 'd' },
+ { "format", required_argument, NULL, 'f' },
+ { "engine", required_argument, NULL, 'e' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 },
+ };
+
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc - 1, argv + 1, "d:f:e:h",
+ long_opts, NULL)) != -1) {
+ if (opt == 'h') {
+ usage();
+ return 0;
+ }
+ if (opt == 'e') {
+ want = optarg;
+ continue;
+ }
+ if (rm_apply_common_opt(ctx, opt, optarg) < 0)
+ return 2;
+ }
+ if (want == NULL) {
+ usage();
+ return 2;
+ }
+ return engine_info(ctx, want);
+ }
+ if (strcmp(argv[1], "allocate") == 0 ||
+ strcmp(argv[1], "start") == 0 ||
+ strcmp(argv[1], "stop") == 0 ||
+ strcmp(argv[1], "config") == 0) {
+ fprintf(stderr,
+ "rmtool: 'engine %s' is a write/ownership operation "
+ "and is refused by policy (read-only host oracle). "
+ "See debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md.\n",
+ argv[1]);
+ return 4;
+ }
+ usage();
+ return 2;
+}
diff --git a/tools/rmtool/src/gpu.c b/tools/rmtool/src/gpu.c
new file mode 100644
index 0000000..ff9b26c
--- /dev/null
+++ b/tools/rmtool/src/gpu.c
@@ -0,0 +1,283 @@
+/* gpu.c - rmtool gpu list/topology/info: enumeration and identity queries. */
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "rmtool.h"
+
+static int read_sysfs(const char *bdf, const char *file, char *buf, size_t len)
+{
+ char path[512];
+ FILE *fh;
+
+ snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/%s", bdf, file);
+ fh = fopen(path, "r");
+ if (fh == NULL) {
+ buf[0] = '\0';
+ return -1;
+ }
+ if (fgets(buf, (int)len, fh) == NULL)
+ buf[0] = '\0';
+ fclose(fh);
+ buf[strcspn(buf, "\r\n")] = '\0';
+ return 0;
+}
+
+static int parse_common(int argc, char **argv, struct rm_ctx *ctx)
+{
+ static const struct option long_opts[] = {
+ { "device-spec", required_argument, NULL, 'd' },
+ { "format", required_argument, NULL, 'f' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 },
+ };
+ int opt;
+
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc, argv, "d:f:h", long_opts, NULL)) != -1) {
+ int rc;
+
+ if (opt == 'h')
+ return 1;
+ rc = rm_apply_common_opt(ctx, opt, optarg);
+ if (rc < 0)
+ return -1;
+ if (rc == 0) {
+ fprintf(stderr, "rmtool: unknown option\n");
+ return -1;
+ }
+ }
+ if (optind < argc) {
+ fprintf(stderr, "rmtool: unexpected argument '%s'\n",
+ argv[optind]);
+ return -1;
+ }
+ return 0;
+}
+
+static void list_usage(void)
+{
+ printf("usage: rmtool gpu list [--format=table|json|csv]\n"
+ " rmtool gpu topology [--format=...]\n"
+ " rmtool gpu info [-d N|BDF] [--format=...]\n");
+}
+
+static int gpu_list(struct rm_ctx *ctx)
+{
+ struct rm_gpu_desc gpus[RM_MAX_GPUS];
+ const char *headers[] = { "MINOR", "BUS", "MODEL", "UUID", "VBIOS" };
+ char cells[RM_MAX_GPUS * 5][RM_UUID_LEN + 8];
+ int n;
+ int i;
+
+ n = rm_scan_gpus(gpus, RM_MAX_GPUS);
+ if (n < 0) {
+ fprintf(stderr, "rmtool: cannot read /proc/driver/nvidia/gpus: "
+ "%s\n", strerror(-n));
+ return 2;
+ }
+ for (i = 0; i < n; i++) {
+ snprintf(cells[i * 5 + 0], sizeof(cells[0]), "%d",
+ gpus[i].minor);
+ snprintf(cells[i * 5 + 1], sizeof(cells[0]), "%s",
+ gpus[i].bdf);
+ snprintf(cells[i * 5 + 2], sizeof(cells[0]), "%s",
+ gpus[i].model);
+ snprintf(cells[i * 5 + 3], sizeof(cells[0]), "%s",
+ gpus[i].uuid);
+ snprintf(cells[i * 5 + 4], sizeof(cells[0]), "%s",
+ gpus[i].vbios);
+ }
+ if (ctx->format == RM_FMT_TABLE)
+ printf("%d GPU(s) on this host\n", n);
+ rm_print_table("gpus", headers, &cells[0][0], sizeof(cells[0]), n, 5,
+ ctx->format);
+ return 0;
+}
+
+static int gpu_topology(struct rm_ctx *ctx)
+{
+ struct rm_gpu_desc gpus[RM_MAX_GPUS];
+ const char *headers[] = { "MINOR", "BUS", "LINK", "MAX LINK", "NUMA",
+ "VENDOR:DEVICE" };
+ char cells[RM_MAX_GPUS * 6][RM_UUID_LEN];
+ int n;
+ int i;
+
+ n = rm_scan_gpus(gpus, RM_MAX_GPUS);
+ if (n < 0) {
+ fprintf(stderr, "rmtool: cannot read /proc/driver/nvidia/gpus: "
+ "%s\n", strerror(-n));
+ return 2;
+ }
+ for (i = 0; i < n; i++) {
+ char speed[32], width[32], max_speed[32], max_width[32];
+ char numa[16], vendor[32], device[32];
+
+ read_sysfs(gpus[i].bdf, "current_link_speed", speed,
+ sizeof(speed));
+ read_sysfs(gpus[i].bdf, "current_link_width", width,
+ sizeof(width));
+ read_sysfs(gpus[i].bdf, "max_link_speed", max_speed,
+ sizeof(max_speed));
+ read_sysfs(gpus[i].bdf, "max_link_width", max_width,
+ sizeof(max_width));
+ read_sysfs(gpus[i].bdf, "numa_node", numa, sizeof(numa));
+ read_sysfs(gpus[i].bdf, "vendor", vendor, sizeof(vendor));
+ read_sysfs(gpus[i].bdf, "device", device, sizeof(device));
+ snprintf(cells[i * 6 + 0], sizeof(cells[0]), "%d",
+ gpus[i].minor);
+ snprintf(cells[i * 6 + 1], sizeof(cells[0]), "%s",
+ gpus[i].bdf);
+ snprintf(cells[i * 6 + 2], sizeof(cells[0]), "%s x%s",
+ speed, width);
+ snprintf(cells[i * 6 + 3], sizeof(cells[0]), "%s x%s",
+ max_speed, max_width);
+ snprintf(cells[i * 6 + 4], sizeof(cells[0]), "%s", numa);
+ snprintf(cells[i * 6 + 5], sizeof(cells[0]), "%s:%s", vendor,
+ device);
+ }
+ rm_print_table("gpu topology (sysfs, read-only)", headers,
+ &cells[0][0], sizeof(cells[0]), n, 6, ctx->format);
+ return 0;
+}
+
+static int gpu_info(struct rm_ctx *ctx)
+{
+ struct rm_gpu_desc gpus[RM_MAX_GPUS];
+ NV2080_CTRL_MC_ARCH_INFO_PARAMS arch;
+ NV2080_CTRL_BUS_GET_PCI_INFO_PARAMS pci;
+ NV2080_CTRL_GPU_GET_ENGINES_V2_PARAMS engines;
+ struct rm_kv kv[20];
+ char v[20][96];
+ int n = 0;
+ int i, n_gpus, rc;
+ NvU32 status;
+
+ n_gpus = rm_scan_gpus(gpus, RM_MAX_GPUS);
+ rc = rm_ctx_open(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: cannot open RM client "
+ "(need the driver bound and read access): rc=%d\n", rc);
+ return 2;
+ }
+
+ status = rm_query_name(ctx, v[0], sizeof(v[0]));
+ kv[n].key = "Name";
+ kv[n++].value = status == 0 ? v[0] : "query failed";
+
+ memset(v[1], 0, sizeof(v[1]));
+ snprintf(v[1], sizeof(v[1]), "%d", ctx->rm.gpu_minor);
+ kv[n].key = "GPU minor";
+ kv[n++].value = v[1];
+
+ if (n_gpus > 0) {
+ const struct rm_gpu_desc *g = NULL;
+
+ for (i = 0; i < n_gpus; i++) {
+ if (gpus[i].minor == ctx->rm.gpu_minor)
+ g = &gpus[i];
+ }
+ if (g == NULL)
+ g = &gpus[0];
+ kv[n].key = "Model (proc)";
+ kv[n++].value = g->model;
+ kv[n].key = "Bus location";
+ kv[n++].value = g->bdf;
+ kv[n].key = "GPU UUID";
+ kv[n++].value = g->uuid;
+ kv[n].key = "Video BIOS";
+ kv[n++].value = g->vbios;
+ }
+
+ status = rm_query_mc_arch(ctx, &arch);
+ if (status == 0) {
+ snprintf(v[2], sizeof(v[2]), "%s (0x%x/0x%x rev %u)",
+ rm_arch_str(arch.architecture), arch.architecture,
+ arch.implementation, arch.revision);
+ kv[n].key = "Architecture";
+ kv[n++].value = v[2];
+ } else {
+ snprintf(v[2], sizeof(v[2]), "MC_GET_ARCH_INFO: %s",
+ rm_status_str(status));
+ kv[n].key = "Architecture";
+ kv[n++].value = v[2];
+ }
+
+ status = rm_query_pci(ctx, &pci);
+ if (status == 0) {
+ snprintf(v[3], sizeof(v[3]), "%04x:%04x (subsystem %04x)",
+ pci.pciDeviceId & 0xffff,
+ (pci.pciDeviceId >> 16) & 0xffff,
+ pci.pciSubSystemId);
+ kv[n].key = "PCI device";
+ kv[n++].value = v[3];
+ snprintf(v[4], sizeof(v[4]), "%u", pci.pciRevisionId);
+ kv[n].key = "PCI revision";
+ kv[n++].value = v[4];
+ }
+
+ status = rm_query_engines(ctx, &engines);
+ snprintf(v[5], sizeof(v[5]), "%s",
+ status == 0 ? "ok" : rm_status_str(status));
+ kv[n].key = "Engine inventory";
+ kv[n++].value = v[5];
+ if (status == 0) {
+ snprintf(v[6], sizeof(v[6]), "%u", engines.engineCount);
+ kv[n].key = "Engine count";
+ kv[n++].value = v[6];
+ }
+
+ rm_print_kv("rmtool gpu info (read-only RM queries)", kv, n,
+ ctx->format);
+ return 0;
+}
+
+int cmd_gpu(int argc, char **argv, struct rm_ctx *ctx)
+{
+ const char *sub;
+
+ if (argc < 2) {
+ list_usage();
+ return 2;
+ }
+ sub = argv[1];
+ if (strcmp(sub, "list") == 0) {
+ int rc = parse_common(argc - 1, argv + 1, ctx);
+
+ if (rc < 0)
+ return 2;
+ if (rc == 1) {
+ list_usage();
+ return 0;
+ }
+ return gpu_list(ctx);
+ }
+ if (strcmp(sub, "topology") == 0) {
+ int rc = parse_common(argc - 1, argv + 1, ctx);
+
+ if (rc < 0)
+ return 2;
+ if (rc == 1) {
+ list_usage();
+ return 0;
+ }
+ return gpu_topology(ctx);
+ }
+ if (strcmp(sub, "info") == 0) {
+ int rc = parse_common(argc - 1, argv + 1, ctx);
+
+ if (rc < 0)
+ return 2;
+ if (rc == 1) {
+ list_usage();
+ return 0;
+ }
+ return gpu_info(ctx);
+ }
+ list_usage();
+ return 2;
+}
diff --git a/tools/rmtool/src/main.c b/tools/rmtool/src/main.c
new file mode 100644
index 0000000..4e4b440
--- /dev/null
+++ b/tools/rmtool/src/main.c
@@ -0,0 +1,95 @@
+/* main.c - rmtool entry point: command dispatch and global help/version.
+ *
+ * rmtool is a read-only host RM oracle for the Open-IOV accuracy loop. It
+ * never allocates engine/channel/memory objects and never touches VFIO,
+ * mdevs or kernel modules; write commands are refused by policy.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "rmtool.h"
+
+static void usage(void)
+{
+ printf("rmtool %s - read-only NVIDIA RM host oracle\n\n",
+ RMTOOL_VERSION);
+ printf("usage: rmtool <command> [subcommand] [options]\n\n"
+ "commands:\n"
+ " gpu list|topology|info enumeration/identity\n"
+ " engine list|info engine inventory (read-only)\n"
+ " mem stats VRAM/heap/BAR1 totals\n"
+ " nv2080 class list device class list\n"
+ " nv2080 method call allowlisted read-only controls\n"
+ " sample timer GPU timer sampling\n"
+ " version print version\n\n"
+ "options:\n"
+ " -d N|BDF select GPU (minor or bus id)\n"
+ " -f FMT output format table|json|csv\n"
+ " --device PATH use an explicit GPU device node\n\n"
+ "safety: read-only queries only. alloc/map/pin, engine/channel\n"
+ "control and any write control are refused. Never touches\n"
+ "modules, VFIO, mdevs, sysfs bindings or /sys/fs/pstore.\n\n"
+ "Not implemented (documented commands): channel, context,\n"
+ "resserve, cudasw, resman, gsp. See docs/17-RM-HARNESS.md.\n");
+}
+
+int main(int argc, char **argv)
+{
+ struct rm_ctx ctx;
+ const char *command;
+ int rc;
+
+ ctx.gpu_minor = -1;
+ ctx.dev_path = NULL;
+ ctx.format = RM_FMT_TABLE;
+ ctx.have_rm = 0;
+ ctx.rm.fd = -1;
+
+ if (argc < 2) {
+ usage();
+ return 2;
+ }
+ command = argv[1];
+ if (strcmp(command, "--help") == 0 || strcmp(command, "-h") == 0 ||
+ strcmp(command, "help") == 0) {
+ usage();
+ return 0;
+ }
+ if (strcmp(command, "--version") == 0 ||
+ strcmp(command, "version") == 0) {
+ printf("rmtool %s (read-only; Open-IOV RM oracle)\n",
+ RMTOOL_VERSION);
+ return 0;
+ }
+ if (strcmp(command, "gpu") == 0) {
+ rc = cmd_gpu(argc - 1, argv + 1, &ctx);
+ } else if (strcmp(command, "engine") == 0) {
+ rc = cmd_engine(argc - 1, argv + 1, &ctx);
+ } else if (strcmp(command, "mem") == 0) {
+ rc = cmd_mem(argc - 1, argv + 1, &ctx);
+ } else if (strcmp(command, "nv2080") == 0) {
+ rc = cmd_nv2080(argc - 1, argv + 1, &ctx);
+ } else if (strcmp(command, "sample") == 0) {
+ rc = cmd_sample(argc - 1, argv + 1, &ctx);
+ } else if (strcmp(command, "channel") == 0 ||
+ strcmp(command, "context") == 0 ||
+ strcmp(command, "resserve") == 0 ||
+ strcmp(command, "cudasw") == 0 ||
+ strcmp(command, "resman") == 0 ||
+ strcmp(command, "gsp") == 0) {
+ fprintf(stderr, "rmtool: '%s' is a documented but not-yet-"
+ "implemented surface in this minimal build; it is a "
+ "write/ownership path and is refused. See "
+ "docs/17-RM-HARNESS.md.\n", command);
+ rc = 3;
+ } else {
+ fprintf(stderr, "rmtool: unknown command '%s'\n", command);
+ usage();
+ rc = 2;
+ }
+
+ if (ctx.have_rm)
+ rm_client_close(&ctx.rm);
+ return rc;
+}
diff --git a/tools/rmtool/src/mem.c b/tools/rmtool/src/mem.c
new file mode 100644
index 0000000..a85bd70
--- /dev/null
+++ b/tools/rmtool/src/mem.c
@@ -0,0 +1,146 @@
+/* mem.c - rmtool mem stats: read-only FB/heap/BAR1 information.
+ *
+ * Allocation, mapping, pinning and arbitrary-handle inspection are
+ * deliberately not implemented: they are write operations against the live
+ * driver and are out of scope for this read-only oracle.
+ */
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "rmtool.h"
+
+static const NvU32 fb_indices[] = {
+ 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_LARGEST_FREE_REGION_SIZE_KB,
+ NV2080_CTRL_FB_INFO_INDEX_BAR1_SIZE,
+ NV2080_CTRL_FB_INFO_INDEX_BAR1_AVAIL_SIZE,
+};
+
+static const char *fb_names[] = {
+ "vram_total_kib",
+ "vram_usable_kib",
+ "heap_size_kib",
+ "heap_free_kib",
+ "largest_free_region_kib",
+ "bar1_size_kib",
+ "bar1_avail_kib",
+};
+
+#define FB_N (sizeof(fb_indices) / sizeof(fb_indices[0]))
+
+static void usage(void)
+{
+ printf("usage: rmtool mem stats [-d N|BDF] [--format=...]\n"
+ " rmtool mem info -h HANDLE (not implemented: write "
+ "path)\n"
+ "rmtool is read-only; alloc/map/unmap/pin are refused.\n");
+}
+
+static int mem_stats(struct rm_ctx *ctx)
+{
+ NvU32 values[FB_N];
+ struct rm_kv kv[FB_N + 4];
+ char v[FB_N + 4][128];
+ NvU32 status;
+ int rc, n = 0;
+ unsigned int i;
+
+ rc = rm_ctx_open(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: cannot open RM client: rc=%d\n", rc);
+ return 2;
+ }
+ memset(values, 0, sizeof(values));
+ status = rm_query_fb(ctx, fb_indices, values, FB_N);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: FB_GET_INFO_V2: %s\n",
+ rm_status_str(status));
+ return 2;
+ }
+ for (i = 0; i < FB_N; i++) {
+ char human[32];
+
+ rm_human_kib(values[i], human, sizeof(human));
+ snprintf(v[n], sizeof(v[n]), "%u (%s)", values[i], human);
+ kv[n].key = fb_names[i];
+ kv[n].value = v[n];
+ n++;
+ }
+ if (values[2] >= values[3]) {
+ unsigned int used = values[2] - values[3];
+ char human[32];
+
+ rm_human_kib(used, human, sizeof(human));
+ snprintf(v[n], sizeof(v[n]), "%u (%s)", used, human);
+ kv[n].key = "heap_used_kib (derived)";
+ kv[n].value = v[n];
+ n++;
+ }
+ rm_print_kv("VRAM / heap (FB_GET_INFO_V2, KiB units)", kv, n,
+ ctx->format);
+ return 0;
+}
+
+static int parse_common(int argc, char **argv, struct rm_ctx *ctx)
+{
+ static const struct option long_opts[] = {
+ { "device-spec", required_argument, NULL, 'd' },
+ { "format", required_argument, NULL, 'f' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 },
+ };
+ int opt;
+
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc, argv, "d:f:h", long_opts, NULL)) != -1) {
+ int rc;
+
+ if (opt == 'h')
+ return 1;
+ rc = rm_apply_common_opt(ctx, opt, optarg);
+ if (rc < 0)
+ return -1;
+ }
+ return 0;
+}
+
+int cmd_mem(int argc, char **argv, struct rm_ctx *ctx)
+{
+ int rc;
+
+ if (argc < 2) {
+ usage();
+ return 2;
+ }
+ if (strcmp(argv[1], "stats") == 0) {
+ rc = parse_common(argc - 1, argv + 1, ctx);
+ if (rc < 0)
+ return 2;
+ if (rc == 1) {
+ usage();
+ return 0;
+ }
+ return mem_stats(ctx);
+ }
+ if (strcmp(argv[1], "alloc") == 0 ||
+ strcmp(argv[1], "map") == 0 ||
+ strcmp(argv[1], "unmap") == 0 ||
+ strcmp(argv[1], "pin") == 0 ||
+ strcmp(argv[1], "info") == 0) {
+ fprintf(stderr,
+ "rmtool: 'mem %s' is a write/ownership operation and "
+ "is refused by policy (read-only host oracle). "
+ "See debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md.\n",
+ argv[1]);
+ return 4;
+ }
+ usage();
+ return 2;
+}
diff --git a/tools/rmtool/src/nv2080.c b/tools/rmtool/src/nv2080.c
new file mode 100644
index 0000000..5dd808e
--- /dev/null
+++ b/tools/rmtool/src/nv2080.c
@@ -0,0 +1,398 @@
+/* nv2080.c - rmtool nv2080 class list / method call and sample.
+ *
+ * The generic control entry point is restricted to a read-only allowlist so
+ * the tools can never mutate the live GPU or driver state. Anything else is
+ * refused before the ioctl is made.
+ */
+#include <getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "rmtool.h"
+
+static const NvU32 readonly_cmds[] = {
+ NV2080_CTRL_CMD_GPU_GET_INFO_V2,
+ NV2080_CTRL_CMD_GPU_GET_NAME_STRING,
+ NV2080_CTRL_CMD_GPU_QUERY_MODE,
+ NV2080_CTRL_CMD_GPU_GET_ID,
+ NV2080_CTRL_CMD_GPU_GET_ENGINES_V2,
+ NV2080_CTRL_CMD_MC_GET_ARCH_INFO,
+ NV2080_CTRL_CMD_FB_GET_INFO_V2,
+ NV2080_CTRL_CMD_BUS_GET_PCI_INFO,
+ NV2080_CTRL_CMD_TIMER_GET_TIME,
+ NV2080_CTRL_CMD_FIFO_GET_DEVICE_INFO_TABLE,
+ NV0080_CTRL_CMD_GPU_GET_CLASSLIST_V2,
+};
+
+int rm_control_is_readonly(NvU32 cmd)
+{
+ unsigned int i;
+
+ for (i = 0; i < sizeof(readonly_cmds) / sizeof(readonly_cmds[0]); i++) {
+ if (readonly_cmds[i] == cmd)
+ return 1;
+ }
+ return 0;
+}
+
+static const char *class_name(NvU32 klass)
+{
+ switch (klass) {
+ case NV01_DEVICE_0:
+ return "NV01_DEVICE_0";
+ case NV20_SUBDEVICE_0:
+ return "NV20_SUBDEVICE_0";
+ default:
+ return "-";
+ }
+}
+
+static void usage(void)
+{
+ printf("usage: rmtool nv2080 class list [-d N|BDF] [--format=...]\n"
+ " rmtool nv2080 method call -m CMD [--object=OBJ] "
+ "[--params=HEX]\n"
+ "OBJ is subdevice (default), device or client. CMD must be on "
+ "the\nread-only allowlist; write controls are refused.\n"
+ " rmtool sample timer [-d N|BDF] [--interval=100ms] "
+ "[--count=N]\n");
+}
+
+static int class_list(struct rm_ctx *ctx)
+{
+ NV0080_CTRL_GPU_GET_CLASSLIST_V2_PARAMS classes;
+ const char *headers[] = { "IDX", "CLASS", "NAME" };
+ char cells[NV0080_CTRL_GPU_CLASSLIST_MAX_SIZE * 3][32];
+ unsigned int i;
+ int rc, ioctl_ret;
+ NvU32 status;
+
+ rc = rm_ctx_open(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: cannot open RM client: rc=%d\n", rc);
+ return 2;
+ }
+ memset(&classes, 0, sizeof(classes));
+ status = rm_control(&ctx->rm, ctx->rm.hDevice,
+ NV0080_CTRL_CMD_GPU_GET_CLASSLIST_V2, &classes,
+ sizeof(classes), &ioctl_ret);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: GPU_GET_CLASSLIST_V2: %s\n",
+ rm_status_str(status));
+ return 2;
+ }
+ if (classes.numClasses > NV0080_CTRL_GPU_CLASSLIST_MAX_SIZE)
+ classes.numClasses = NV0080_CTRL_GPU_CLASSLIST_MAX_SIZE;
+ for (i = 0; i < classes.numClasses; i++) {
+ snprintf(cells[i * 3 + 0], sizeof(cells[0]), "%u", i);
+ snprintf(cells[i * 3 + 1], sizeof(cells[0]), "0x%x",
+ classes.classList[i]);
+ snprintf(cells[i * 3 + 2], sizeof(cells[0]), "%s",
+ class_name(classes.classList[i]));
+ }
+ rm_print_table("device classes (NV0080_CTRL_GPU_GET_CLASSLIST_V2)",
+ headers, &cells[0][0], sizeof(cells[0]),
+ (int)classes.numClasses, 3, ctx->format);
+ return 0;
+}
+
+static int parse_hex(const char *hex, unsigned char **out, size_t *len)
+{
+ size_t n, i;
+ unsigned char *buf;
+
+ if (hex == NULL) {
+ *out = NULL;
+ *len = 0;
+ return 0;
+ }
+ if (hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X'))
+ hex += 2;
+ n = strlen(hex);
+ if (n % 2 != 0) {
+ fprintf(stderr, "rmtool: --params must be whole hex bytes\n");
+ return -1;
+ }
+ buf = malloc(n / 2 + 1);
+ if (buf == NULL)
+ return -1;
+ for (i = 0; i < n / 2; i++) {
+ unsigned int byte;
+
+ if (sscanf(hex + i * 2, "%2x", &byte) != 1) {
+ fprintf(stderr, "rmtool: bad hex byte '%c%c'\n",
+ hex[i * 2], hex[i * 2 + 1]);
+ free(buf);
+ return -1;
+ }
+ buf[i] = (unsigned char)byte;
+ }
+ *out = buf;
+ *len = n / 2;
+ return 0;
+}
+
+static int method_call(struct rm_ctx *ctx, NvU32 cmd, const char *object,
+ const char *hex)
+{
+ unsigned char *params;
+ size_t len;
+ NvU32 hObject;
+ int rc, ioctl_ret;
+ NvU32 status;
+
+ if (!rm_control_is_readonly(cmd)) {
+ fprintf(stderr, "rmtool: control 0x%x is not on the read-only "
+ "allowlist; refused (see docs/17-RM-HARNESS.md)\n",
+ cmd);
+ return 4;
+ }
+ if (parse_hex(hex, &params, &len) < 0)
+ return 2;
+ rc = rm_ctx_open(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: cannot open RM client: rc=%d\n", rc);
+ free(params);
+ return 2;
+ }
+ if (object == NULL || strcmp(object, "subdevice") == 0)
+ hObject = ctx->rm.hSubdevice;
+ else if (strcmp(object, "device") == 0)
+ hObject = ctx->rm.hDevice;
+ else if (strcmp(object, "client") == 0)
+ hObject = ctx->rm.hClient;
+ else {
+ fprintf(stderr, "rmtool: unknown --object '%s'\n", object);
+ free(params);
+ return 2;
+ }
+ status = rm_control(&ctx->rm, hObject, cmd, params, (NvU32)len,
+ &ioctl_ret);
+ printf("cmd=0x%x object=0x%x params=%zu bytes\n", cmd, hObject, len);
+ if (ioctl_ret != 0) {
+ fprintf(stderr, "rmtool: control ioctl failed: %s\n",
+ strerror(-ioctl_ret));
+ free(params);
+ return 2;
+ }
+ printf("status=%s\n", rm_status_str(status));
+ if (status == 0 && len > 0)
+ rm_hexdump(params, len);
+ free(params);
+ return status == 0 ? 0 : 2;
+}
+
+static double parse_interval_ms(const char *s)
+{
+ char *end;
+ double v = strtod(s, &end);
+
+ if (end != NULL && strcmp(end, "ms") == 0)
+ return v;
+ if (end != NULL && strcmp(end, "s") == 0)
+ return v * 1000.0;
+ if (end != NULL && strcmp(end, "us") == 0)
+ return v / 1000.0;
+ return v;
+}
+
+static int sample_timer(struct rm_ctx *ctx, double interval_ms, int count)
+{
+ int rc, i;
+
+ rc = rm_ctx_open(ctx);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: cannot open RM client: rc=%d\n", rc);
+ return 2;
+ }
+ for (i = 0; i < count; i++) {
+ NV2080_CTRL_TIMER_GET_TIME_PARAMS p;
+ struct tm tm;
+ char when[32];
+ time_t secs;
+ int ioctl_ret;
+ NvU32 status;
+
+ memset(&p, 0, sizeof(p));
+ status = rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_TIMER_GET_TIME, &p,
+ sizeof(p), &ioctl_ret);
+ if (status != 0 || ioctl_ret != 0) {
+ fprintf(stderr, "rmtool: TIMER_GET_TIME: %s\n",
+ rm_status_str(status));
+ return 2;
+ }
+ secs = (time_t)(p.time_nsec / 1000000000ULL);
+ gmtime_r(&secs, &tm);
+ strftime(when, sizeof(when), "%Y-%m-%dT%H:%M:%S", &tm);
+ printf("gpu_time_ns=%llu (%s.%09lluZ)\n",
+ (unsigned long long)p.time_nsec, when,
+ (unsigned long long)(p.time_nsec % 1000000000ULL));
+ fflush(stdout);
+ if (i + 1 < count) {
+ struct timespec req;
+
+ req.tv_sec = (time_t)(interval_ms / 1000.0);
+ req.tv_nsec = (long)((interval_ms -
+ (double)req.tv_sec * 1000.0) *
+ 1000000.0);
+ nanosleep(&req, NULL);
+ }
+ }
+ return 0;
+}
+
+static int unsupported_sample(const char *kind)
+{
+ fprintf(stderr,
+ "rmtool: 'sample %s' is not implemented in this minimal build: "
+ "it needs NVML-class perfmon/thermal/power controls whose "
+ "structs are not exposed in the in-tree SDK. Use nvidia-smi "
+ "for now; see debug/risc-v-gsp-rm-vm/docs/17-RM-HARNESS.md.\n",
+ kind);
+ return 3;
+}
+
+int cmd_nv2080(int argc, char **argv, struct rm_ctx *ctx)
+{
+ int opt;
+
+ if (argc < 3) {
+ usage();
+ return 2;
+ }
+ if (strcmp(argv[1], "class") == 0 && strcmp(argv[2], "list") == 0) {
+ static const struct option long_opts[] = {
+ { "device-spec", required_argument, NULL, 'd' },
+ { "format", required_argument, NULL, 'f' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 },
+ };
+
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc - 2, argv + 2, "d:f:h",
+ long_opts, NULL)) != -1) {
+ if (opt == 'h') {
+ usage();
+ return 0;
+ }
+ if (rm_apply_common_opt(ctx, opt, optarg) < 0)
+ return 2;
+ }
+ return class_list(ctx);
+ }
+ if (strcmp(argv[1], "method") == 0 && strcmp(argv[2], "call") == 0) {
+ static const struct option long_opts[] = {
+ { "cmd", required_argument, NULL, 'm' },
+ { "method", required_argument, NULL, 'm' },
+ { "class", required_argument, NULL, 'c' },
+ { "object", required_argument, NULL, 'o' },
+ { "params", required_argument, NULL, 'p' },
+ { "format", required_argument, NULL, 'f' },
+ { "device-spec", required_argument, NULL, 'd' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 },
+ };
+ const char *object = NULL;
+ const char *params = NULL;
+ const char *klass = NULL;
+ NvU32 cmd = 0;
+ int have_cmd = 0;
+
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc - 2, argv + 2, "m:c:o:p:f:d:h",
+ long_opts, NULL)) != -1) {
+ if (opt == 'h') {
+ usage();
+ return 0;
+ }
+ if (opt == 'm') {
+ cmd = (NvU32)strtoul(optarg, NULL, 0);
+ have_cmd = 1;
+ continue;
+ }
+ if (opt == 'o') {
+ object = optarg;
+ continue;
+ }
+ if (opt == 'p') {
+ params = optarg;
+ continue;
+ }
+ if (opt == 'c') {
+ klass = optarg;
+ continue;
+ }
+ if (rm_apply_common_opt(ctx, opt, optarg) < 0)
+ return 2;
+ }
+ if (!have_cmd) {
+ usage();
+ return 2;
+ }
+ if (klass != NULL)
+ fprintf(stderr, "rmtool: note: --class %s is accepted "
+ "for documentation parity; the RM control "
+ "command is sent as given with -m.\n", klass);
+ return method_call(ctx, cmd, object, params);
+ }
+ if (strcmp(argv[1], "event") == 0) {
+ fprintf(stderr, "rmtool: nv2080 event subscribe/wait is not "
+ "implemented (event handling is a write path).\n");
+ return 3;
+ }
+ usage();
+ return 2;
+}
+
+int cmd_sample(int argc, char **argv, struct rm_ctx *ctx)
+{
+ static const struct option long_opts[] = {
+ { "device-spec", required_argument, NULL, 'd' },
+ { "format", required_argument, NULL, 'f' },
+ { "interval", required_argument, NULL, 'i' },
+ { "count", required_argument, NULL, 'n' },
+ { "device", required_argument, NULL, 1000 },
+ { "help", no_argument, NULL, 'h' },
+ { NULL, 0, NULL, 0 },
+ };
+ double interval_ms = 1000.0;
+ int count = 1;
+ int opt;
+
+ if (argc < 2) {
+ usage();
+ return 2;
+ }
+ if (strcmp(argv[1], "timer") != 0)
+ return unsupported_sample(argv[1]);
+ optind = 1;
+ opterr = 0;
+ while ((opt = getopt_long(argc - 1, argv + 1, "d:f:i:n:h",
+ long_opts, NULL)) != -1) {
+ if (opt == 'h') {
+ usage();
+ return 0;
+ }
+ if (opt == 'i') {
+ interval_ms = parse_interval_ms(optarg);
+ continue;
+ }
+ if (opt == 'n') {
+ count = atoi(optarg);
+ continue;
+ }
+ if (rm_apply_common_opt(ctx, opt, optarg) < 0)
+ return 2;
+ }
+ if (count <= 0)
+ count = 1;
+ return sample_timer(ctx, interval_ms, count);
+}
diff --git a/tools/rmtool/src/output.c b/tools/rmtool/src/output.c
new file mode 100644
index 0000000..6de5d02
--- /dev/null
+++ b/tools/rmtool/src/output.c
@@ -0,0 +1,251 @@
+/* output.c - table/JSON/CSV formatting helpers shared by rmtool commands. */
+#include <ctype.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "rmtool.h"
+
+enum rm_format rm_format_parse(const char *name)
+{
+ if (name == NULL || strcmp(name, "table") == 0)
+ return RM_FMT_TABLE;
+ if (strcmp(name, "json") == 0)
+ return RM_FMT_JSON;
+ if (strcmp(name, "csv") == 0)
+ return RM_FMT_CSV;
+ return (enum rm_format)-1;
+}
+
+const char *rm_human_kib(NvU64 kib, char *buf, size_t len)
+{
+ if (kib >= 1024ULL * 1024ULL)
+ snprintf(buf, len, "%.2f GiB", (double)kib / (1024.0 * 1024.0));
+ else if (kib >= 1024ULL)
+ snprintf(buf, len, "%.1f MiB", (double)kib / 1024.0);
+ else
+ snprintf(buf, len, "%llu KiB", (unsigned long long)kib);
+ return buf;
+}
+
+static int json_plain_number(const char *value)
+{
+ char *end;
+ double d;
+
+ if (value == NULL || *value == '\0')
+ return 0;
+ d = strtod(value, &end);
+ if (end == value || *end != '\0')
+ return 0;
+ (void)d;
+ return 1;
+}
+
+static void json_string(FILE *out, const char *value)
+{
+ const unsigned char *p = (const unsigned char *)value;
+
+ fputc('"', out);
+ for (; p != NULL && *p != '\0'; p++) {
+ switch (*p) {
+ case '"':
+ fputs("\\\"", out);
+ break;
+ case '\\':
+ fputs("\\\\", out);
+ break;
+ case '\n':
+ fputs("\\n", out);
+ break;
+ case '\r':
+ fputs("\\r", out);
+ break;
+ case '\t':
+ fputs("\\t", out);
+ break;
+ default:
+ if (*p < 0x20)
+ fprintf(out, "\\u%04x", *p);
+ else
+ fputc(*p, out);
+ break;
+ }
+ }
+ fputc('"', out);
+}
+
+static void json_value(FILE *out, const char *value)
+{
+ if (value == NULL) {
+ fputs("null", out);
+ } else if (json_plain_number(value)) {
+ fputs(value, out);
+ } else {
+ json_string(out, value);
+ }
+}
+
+static void csv_value(FILE *out, const char *value)
+{
+ const char *p;
+ int needs_quote = 0;
+
+ if (value == NULL)
+ value = "";
+ for (p = value; *p != '\0'; p++) {
+ if (*p == ',' || *p == '"' || *p == '\n' || *p == '\r') {
+ needs_quote = 1;
+ break;
+ }
+ }
+ if (!needs_quote) {
+ fputs(value, out);
+ return;
+ }
+ fputc('"', out);
+ for (p = value; *p != '\0'; p++) {
+ if (*p == '"')
+ fputc('"', out);
+ fputc(*p, out);
+ }
+ fputc('"', out);
+}
+
+void rm_print_kv(const char *title, const struct rm_kv *kv, int n,
+ enum rm_format format)
+{
+ int i;
+
+ if (format == RM_FMT_JSON) {
+ printf("{\"title\": ");
+ json_string(stdout, title);
+ printf(", \"values\": {");
+ for (i = 0; i < n; i++) {
+ if (i)
+ fputs(", ", stdout);
+ json_string(stdout, kv[i].key);
+ fputs(": ", stdout);
+ json_value(stdout, kv[i].value);
+ }
+ printf("}}\n");
+ return;
+ }
+ if (format == RM_FMT_CSV) {
+ for (i = 0; i < n; i++) {
+ csv_value(stdout, kv[i].key);
+ fputc(',', stdout);
+ csv_value(stdout, kv[i].value);
+ fputc('\n', stdout);
+ }
+ return;
+ }
+ printf("%s\n", title);
+ for (i = 0; i < n; i++)
+ printf(" %-28s %s\n", kv[i].key,
+ kv[i].value ? kv[i].value : "-");
+}
+
+static const char *cell_at(const char *cells, size_t stride, int row,
+ int col, int cols)
+{
+ return cells + (size_t)(row * cols + col) * stride;
+}
+
+void rm_print_table(const char *title, const char *const *headers,
+ const char *cells, size_t stride, int rows, int cols,
+ enum rm_format format)
+{
+ int *width;
+ int r, c;
+
+ if (format == RM_FMT_JSON) {
+ printf("[");
+ for (r = 0; r < rows; r++) {
+ if (r)
+ fputs(", ", stdout);
+ fputc('{', stdout);
+ for (c = 0; c < cols; c++) {
+ if (c)
+ fputs(", ", stdout);
+ json_string(stdout, headers[c]);
+ fputs(": ", stdout);
+ json_value(stdout, cell_at(cells, stride, r,
+ c, cols));
+ }
+ fputc('}', stdout);
+ }
+ printf("]\n");
+ return;
+ }
+ if (format == RM_FMT_CSV) {
+ for (c = 0; c < cols; c++) {
+ if (c)
+ fputc(',', stdout);
+ csv_value(stdout, headers[c]);
+ }
+ fputc('\n', stdout);
+ for (r = 0; r < rows; r++) {
+ for (c = 0; c < cols; c++) {
+ if (c)
+ fputc(',', stdout);
+ csv_value(stdout, cell_at(cells, stride, r,
+ c, cols));
+ }
+ fputc('\n', stdout);
+ }
+ return;
+ }
+ width = calloc((size_t)cols, sizeof(*width));
+ if (width == NULL)
+ return;
+ for (c = 0; c < cols; c++)
+ width[c] = (int)strlen(headers[c]);
+ for (r = 0; r < rows; r++) {
+ for (c = 0; c < cols; c++) {
+ const char *v = cell_at(cells, stride, r, c, cols);
+ int len = v ? (int)strlen(v) : 0;
+
+ if (len > width[c])
+ width[c] = len;
+ }
+ }
+ printf("%s\n", title);
+ for (c = 0; c < cols; c++)
+ printf("%-*s%s", width[c], headers[c],
+ c + 1 < cols ? " " : "\n");
+ for (r = 0; r < rows; r++) {
+ for (c = 0; c < cols; c++)
+ printf("%-*s%s", width[c],
+ cell_at(cells, stride, r, c, cols),
+ c + 1 < cols ? " " : "\n");
+ }
+ free(width);
+}
+
+void rm_hexdump(const void *data, size_t len)
+{
+ const unsigned char *p = data;
+ size_t off;
+
+ for (off = 0; off < len; off += 16) {
+ size_t i;
+
+ printf(" %04zx ", off);
+ for (i = 0; i < 16; i++) {
+ if (off + i < len)
+ printf("%02x ", p[off + i]);
+ else
+ fputs(" ", stdout);
+ if (i == 7)
+ fputc(' ', stdout);
+ }
+ fputs(" |", stdout);
+ for (i = 0; i < 16 && off + i < len; i++) {
+ unsigned char ch = p[off + i];
+
+ fputc(isprint(ch) ? ch : '.', stdout);
+ }
+ puts("|");
+ }
+}
diff --git a/tools/rmtool/src/query.c b/tools/rmtool/src/query.c
new file mode 100644
index 0000000..c9a7460
--- /dev/null
+++ b/tools/rmtool/src/query.c
@@ -0,0 +1,256 @@
+/* query.c - shared read-only RM queries used by rmtool and rmtop. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "rmtool.h"
+
+int rm_ctx_open(struct rm_ctx *ctx)
+{
+ int rc;
+
+ if (ctx->have_rm)
+ return 0;
+ rc = rm_client_open(&ctx->rm, ctx->gpu_minor, ctx->dev_path);
+ if (rc != 0)
+ return rc;
+ ctx->have_rm = 1;
+ return 0;
+}
+
+NvU32 rm_query_name(struct rm_ctx *ctx, char *buf, size_t len)
+{
+ NV2080_CTRL_GPU_GET_NAME_STRING_PARAMS p;
+ NvU32 status;
+ int ioctl_ret;
+
+ memset(&p, 0, sizeof(p));
+ p.gpuNameStringFlags = 0;
+ status = rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_GPU_GET_NAME_STRING, &p,
+ sizeof(p), &ioctl_ret);
+ if (status != 0)
+ return status;
+ p.gpuNameString.ascii[sizeof(p.gpuNameString.ascii) - 1] = '\0';
+ snprintf(buf, len, "%s", p.gpuNameString.ascii);
+ return 0;
+}
+
+NvU32 rm_query_mc_arch(struct rm_ctx *ctx, NV2080_CTRL_MC_ARCH_INFO_PARAMS *p)
+{
+ int ioctl_ret;
+
+ memset(p, 0, sizeof(*p));
+ return rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_MC_GET_ARCH_INFO, p, sizeof(*p),
+ &ioctl_ret);
+}
+
+NvU32 rm_query_pci(struct rm_ctx *ctx, NV2080_CTRL_BUS_GET_PCI_INFO_PARAMS *p)
+{
+ int ioctl_ret;
+
+ memset(p, 0, sizeof(*p));
+ return rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_BUS_GET_PCI_INFO, p, sizeof(*p),
+ &ioctl_ret);
+}
+
+NvU32 rm_query_fb(struct rm_ctx *ctx, const NvU32 *indices, NvU32 *values,
+ NvU32 n)
+{
+ NV2080_CTRL_FB_GET_INFO_V2_PARAMS p;
+ NvU32 status;
+ NvU32 i;
+ int ioctl_ret;
+
+ if (n > NV2080_CTRL_FB_INFO_MAX_LIST_SIZE)
+ return (NvU32)-1;
+ memset(&p, 0, sizeof(p));
+ p.fbInfoListSize = n;
+ for (i = 0; i < n; i++) {
+ p.fbInfoList[i].index = indices[i];
+ values[i] = 0;
+ }
+ status = rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_FB_GET_INFO_V2, &p, sizeof(p),
+ &ioctl_ret);
+ if (status != 0)
+ return status;
+ for (i = 0; i < n; i++) {
+ NvU32 j;
+
+ for (j = 0; j < p.fbInfoListSize && j < n; j++) {
+ if (p.fbInfoList[j].index == indices[i]) {
+ values[i] = p.fbInfoList[j].data;
+ break;
+ }
+ }
+ }
+ return 0;
+}
+
+NvU32 rm_query_engines(struct rm_ctx *ctx,
+ NV2080_CTRL_GPU_GET_ENGINES_V2_PARAMS *p)
+{
+ int ioctl_ret;
+
+ memset(p, 0, sizeof(*p));
+ return rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_GPU_GET_ENGINES_V2, p, sizeof(*p),
+ &ioctl_ret);
+}
+
+NvU32 rm_query_fifo_table(struct rm_ctx *ctx,
+ NV2080_FIFO_DEVICE_ENTRY *entries,
+ NvU32 max_entries, NvU32 *n_entries)
+{
+ NV2080_CTRL_FIFO_GET_DEVICE_INFO_TABLE_PARAMS t;
+ NvU32 base = 0;
+ NvU32 total = 0;
+
+ *n_entries = 0;
+ do {
+ NvU32 status;
+ NvU32 i;
+ int ioctl_ret;
+
+ memset(&t, 0, sizeof(t));
+ t.baseIndex = base;
+ status = rm_control(&ctx->rm, ctx->rm.hSubdevice,
+ NV2080_CTRL_CMD_FIFO_GET_DEVICE_INFO_TABLE,
+ &t, sizeof(t), &ioctl_ret);
+ if (status != 0)
+ return status;
+ for (i = 0; i < t.numEntries && total < max_entries; i++)
+ entries[total++] = t.entries[i];
+ base += t.numEntries;
+ if (!t.bMore || t.numEntries == 0)
+ break;
+ } while (1);
+ *n_entries = total;
+ return 0;
+}
+
+const char *rm_arch_str(NvU32 architecture)
+{
+ switch (architecture) {
+ case 0xE0000023U:
+ return "T23X";
+ case 0x00000160U:
+ return "TU100";
+ case 0x00000170U:
+ return "GA100";
+ case 0x00000180U:
+ return "GH100";
+ case 0x00000190U:
+ return "AD100";
+ case 0x000001A0U:
+ return "GB100";
+ case 0x000001B0U:
+ return "GB200";
+ default:
+ return "unknown";
+ }
+}
+
+const char *rm_engine_type_name(NvU32 engine_type)
+{
+ static const struct {
+ NvU32 type;
+ const char *name;
+ } table[] = {
+ { 0x01, "gr" }, { 0x09, "ce0" }, { 0x0a, "ce1" },
+ { 0x0b, "ce2" }, { 0x0c, "ce3" }, { 0x0d, "ce4" },
+ { 0x0e, "ce5" }, { 0x0f, "ce6" }, { 0x10, "ce7" },
+ { 0x11, "ce8" }, { 0x12, "ce9" }, { 0x34, "ce10" },
+ { 0x35, "ce11" }, { 0x36, "ce12" }, { 0x37, "ce13" },
+ { 0x38, "ce14" }, { 0x39, "ce15" },
+ { 0x13, "nvdec0" }, { 0x14, "nvdec1" }, { 0x15, "nvdec2" },
+ { 0x16, "nvdec3" }, { 0x17, "nvdec4" }, { 0x18, "nvdec5" },
+ { 0x19, "nvdec6" }, { 0x1a, "nvdec7" },
+ { 0x1b, "nvenc0" }, { 0x1c, "nvenc1" }, { 0x1d, "nvenc2" },
+ { 0x3f, "nvenc3" },
+ { 0x2b, "nvjpg0" }, { 0x2c, "nvjpg1" }, { 0x2d, "nvjpg2" },
+ { 0x2e, "nvjpg3" }, { 0x2f, "nvjpg4" }, { 0x30, "nvjpg5" },
+ { 0x31, "nvjpg6" }, { 0x32, "nvjpg7" },
+ { 0x33, "ofa0" }, { 0x3e, "ofa1" },
+ { 0x24, "vic" }, { 0x26, "sec2" }, { 0x22, "sw" },
+ { 0x27, "host" }, { 0x29, "pmu" }, { 0x2a, "fbflcn" },
+ { 0x1e, "vp" }, { 0x1f, "me" }, { 0x20, "ppp" },
+ { 0x21, "mpeg" }, { 0x23, "cipher" }, { 0x25, "mp" },
+ { 0x28, "dpu" },
+ };
+ unsigned int i;
+
+ for (i = 0; i < sizeof(table) / sizeof(table[0]); i++) {
+ if (table[i].type == engine_type)
+ return table[i].name;
+ }
+ return NULL;
+}
+
+const char *rm_engine_name(const NV2080_FIFO_DEVICE_ENTRY *entries,
+ NvU32 n_entries, NvU32 engine_type)
+{
+ static char fallback[24];
+ const char *name;
+ NvU32 i;
+
+ for (i = 0; i < n_entries; i++) {
+ if (entries[i].engineName[0] == '\0')
+ continue;
+ if (entries[i].engineData[0] == engine_type)
+ return entries[i].engineName;
+ }
+ name = rm_engine_type_name(engine_type);
+ if (name != NULL)
+ return name;
+ snprintf(fallback, sizeof(fallback), "engine[0x%x]", engine_type);
+ return fallback;
+}
+
+int rm_apply_common_opt(struct rm_ctx *ctx, int opt, const char *arg)
+{
+ if (opt == 'd') {
+ char *end;
+ long minor = strtol(arg, &end, 0);
+
+ if (arg[0] != '\0' && *end == '\0') {
+ ctx->gpu_minor = (int)minor;
+ return 1;
+ }
+ if (strchr(arg, ':') != NULL) {
+ 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 (strcmp(gpus[i].bdf, arg) == 0) {
+ ctx->gpu_minor = gpus[i].minor;
+ return 1;
+ }
+ }
+ fprintf(stderr, "rmtool: no GPU with bus id %s\n", arg);
+ return -1;
+ }
+ fprintf(stderr, "rmtool: bad GPU selector '%s'\n", arg);
+ return -1;
+ }
+ if (opt == 'f') {
+ enum rm_format fmt = rm_format_parse(arg);
+
+ if ((int)fmt < 0) {
+ fprintf(stderr, "rmtool: bad format '%s' "
+ "(table|json|csv)\n", arg);
+ return -1;
+ }
+ ctx->format = fmt;
+ return 1;
+ }
+ if (opt == 1000) {
+ ctx->dev_path = arg;
+ return 1;
+ }
+ return 0;
+}
diff --git a/tools/rmtool/src/rm_api.c b/tools/rmtool/src/rm_api.c
new file mode 100644
index 0000000..17fb48b
--- /dev/null
+++ b/tools/rmtool/src/rm_api.c
@@ -0,0 +1,291 @@
+/* rm_api.c - RM ioctl client, GPU discovery and control plumbing.
+ *
+ * The ioctl ladder (root -> device -> subdevice) and NV_ESC_RM_CONTROL
+ * pattern follow tools/rmq/rmq.c and tools/guest-diag/guest_engines.c.
+ * All operations here are control-path only; no kernel module, VFIO or
+ * device state is modified.
+ */
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include "rm_api.h"
+
+#define CONTROL_NODE "/dev/nvidiactl"
+
+static NvU32 rm_alloc_root(int fd, NvHandle *out)
+{
+ NVOS21_PARAMETERS p = { 0 };
+
+ p.hRoot = 0;
+ p.hObjectParent = 0;
+ p.hClass = NV01_ROOT_CLIENT;
+ if (ioctl(fd, NV_IOCTL_RM_ALLOC21, &p) < 0)
+ return (NvU32)-1;
+ if (p.status != 0)
+ return p.status;
+ *out = p.hObjectNew;
+ return 0;
+}
+
+static NvU32 rm_alloc64(int fd, NvHandle hRoot, NvHandle hParent,
+ NvU32 hClass, void *params, NvU32 params_size,
+ NvHandle *out)
+{
+ NVOS64_PARAMETERS p = { 0 };
+
+ p.hRoot = hRoot;
+ p.hObjectParent = hParent;
+ p.hClass = hClass;
+ p.pAllocParms = (NvP64)params;
+ p.paramsSize = params_size;
+ p.flags = NVOS64_FLAGS_NONE;
+ if (ioctl(fd, NV_IOCTL_RM_ALLOC64, &p) < 0)
+ return (NvU32)-1;
+ if (p.status != 0)
+ return p.status;
+ *out = p.hObjectNew;
+ return 0;
+}
+
+static NvU32 rm_version_check(int fd)
+{
+ nv_ioctl_rm_api_version_t v = { 0 };
+
+ v.cmd = NV_RM_API_VERSION_CMD_QUERY;
+ if (ioctl(fd, NV_IOCTL_CHECK_VERSION, &v) < 0)
+ return (NvU32)-1;
+ if (v.reply != NV_RM_API_VERSION_REPLY_RECOGNIZED)
+ return (NvU32)-2;
+ return 0;
+}
+
+/* The open module registers a GPU once its device node open completes; a
+ * NV01_DEVICE_0 alloc issued before that fails with NV_ERR_INVALID_STATE.
+ */
+static int rm_wait_gpu_open(int gpu_minor, const char *dev_path, int *gpu_fd)
+{
+ nv_ioctl_wait_open_complete_t w = { 0 };
+ char path[64];
+ int fd;
+
+ if (dev_path != NULL) {
+ snprintf(path, sizeof(path), "%s", dev_path);
+ } else if (gpu_minor >= 0) {
+ snprintf(path, sizeof(path), "/dev/nvidia%d", gpu_minor);
+ } else {
+ snprintf(path, sizeof(path), "/dev/nvidia0");
+ }
+ fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+ if (ioctl(fd, NV_IOCTL_WAIT_OPEN_COMPLETE, &w) < 0) {
+ int err = -errno;
+
+ close(fd);
+ return err;
+ }
+ if (w.rc != 0) {
+ close(fd);
+ return w.rc;
+ }
+ *gpu_fd = fd;
+ return 0;
+}
+
+int rm_client_open(struct rm_client *c, int gpu_minor, const char *dev_path)
+{
+ NV0080_ALLOC_PARAMETERS dev_params;
+ NV2080_ALLOC_PARAMETERS sub_params;
+ NvU32 status;
+ int rc;
+
+ memset(c, 0, sizeof(*c));
+ c->fd = -1;
+ c->gpu_fd = -1;
+ c->gpu_minor = gpu_minor >= 0 ? gpu_minor : 0;
+ c->fd = open(CONTROL_NODE, O_RDWR | O_CLOEXEC);
+ if (c->fd < 0)
+ return -errno;
+
+ status = rm_version_check(c->fd);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: RM version check failed: %s\n",
+ rm_status_str(status));
+ rm_client_close(c);
+ return (int)status;
+ }
+
+ rc = rm_wait_gpu_open(gpu_minor, dev_path, &c->gpu_fd);
+ if (rc != 0) {
+ fprintf(stderr, "rmtool: GPU open/complete failed: %s\n",
+ strerror(-rc));
+ rm_client_close(c);
+ return rc;
+ }
+
+ status = rm_alloc_root(c->fd, &c->hClient);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: NV01_ROOT_CLIENT alloc failed: "
+ "0x%x\n", status);
+ rm_client_close(c);
+ return (int)status;
+ }
+
+ memset(&dev_params, 0, sizeof(dev_params));
+ dev_params.deviceId = (NvU32)c->gpu_minor;
+ status = rm_alloc64(c->fd, c->hClient, c->hClient, NV01_DEVICE_0,
+ &dev_params, sizeof(dev_params), &c->hDevice);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: NV01_DEVICE_0 alloc failed: 0x%x\n",
+ status);
+ rm_client_close(c);
+ return (int)status;
+ }
+
+ memset(&sub_params, 0, sizeof(sub_params));
+ sub_params.subDeviceId = 0;
+ status = rm_alloc64(c->fd, c->hClient, c->hDevice, NV20_SUBDEVICE_0,
+ &sub_params, sizeof(sub_params), &c->hSubdevice);
+ if (status != 0) {
+ fprintf(stderr, "rmtool: NV20_SUBDEVICE_0 alloc failed: "
+ "0x%x\n", status);
+ rm_client_close(c);
+ return (int)status;
+ }
+ return 0;
+}
+
+void rm_client_close(struct rm_client *c)
+{
+ if (c->gpu_fd >= 0)
+ close(c->gpu_fd);
+ if (c->fd >= 0)
+ close(c->fd);
+ c->fd = -1;
+ c->gpu_fd = -1;
+ c->hClient = 0;
+ c->hDevice = 0;
+ c->hSubdevice = 0;
+}
+
+NvU32 rm_control(struct rm_client *c, NvU32 hObject, NvU32 cmd,
+ void *params, NvU32 size, int *ioctl_ret)
+{
+ NVOS54_PARAMETERS p = { 0 };
+
+ p.hClient = c->hClient;
+ p.hObject = hObject;
+ p.cmd = cmd;
+ p.params = (NvP64)params;
+ p.paramsSize = size;
+ if (ioctl(c->fd, NV_IOCTL_RM_CONTROL, &p) < 0) {
+ if (ioctl_ret)
+ *ioctl_ret = -errno;
+ return (NvU32)-1;
+ }
+ if (ioctl_ret)
+ *ioctl_ret = 0;
+ return p.status;
+}
+
+const char *rm_status_str(NvU32 status)
+{
+ static char buf[32];
+
+ if (status == 0)
+ return "NV_OK";
+ snprintf(buf, sizeof(buf), "NV_STATUS(0x%x)", status);
+ return buf;
+}
+
+/* ------------------------------------------------------------------ */
+/* /proc/driver/nvidia/gpus discovery */
+/* ------------------------------------------------------------------ */
+
+static void copy_trim(char *dst, size_t len, const char *src)
+{
+ size_t n;
+
+ while (*src == ' ' || *src == '\t')
+ src++;
+ n = strcspn(src, "\r\n");
+ if (n >= len)
+ n = len - 1;
+ memcpy(dst, src, n);
+ dst[n] = '\0';
+ while (n > 0 && (dst[n - 1] == ' ' || dst[n - 1] == '\t'))
+ dst[--n] = '\0';
+}
+
+static void parse_information(const char *path, struct rm_gpu_desc *gpu)
+{
+ FILE *fh;
+ char line[512];
+
+ fh = fopen(path, "r");
+ if (fh == NULL)
+ return;
+ while (fgets(line, sizeof(line), fh) != NULL) {
+ char *colon = strchr(line, ':');
+
+ if (colon == NULL)
+ continue;
+ *colon = '\0';
+ if (strcmp(line, "Model") == 0)
+ copy_trim(gpu->model, sizeof(gpu->model), colon + 1);
+ else if (strcmp(line, "GPU UUID") == 0)
+ copy_trim(gpu->uuid, sizeof(gpu->uuid), colon + 1);
+ else if (strcmp(line, "Video BIOS") == 0)
+ copy_trim(gpu->vbios, sizeof(gpu->vbios), colon + 1);
+ else if (strcmp(line, "Bus Location") == 0)
+ copy_trim(gpu->bdf, sizeof(gpu->bdf), colon + 1);
+ else if (strcmp(line, "IRQ") == 0)
+ gpu->irq = atoi(colon + 1);
+ else if (strcmp(line, "Device Minor") == 0)
+ gpu->minor = atoi(colon + 1);
+ }
+ fclose(fh);
+}
+
+static int gpu_compare(const void *a, const void *b)
+{
+ const struct rm_gpu_desc *ga = a;
+ const struct rm_gpu_desc *gb = b;
+
+ return ga->minor - gb->minor;
+}
+
+int rm_scan_gpus(struct rm_gpu_desc *out, int max)
+{
+ const char *root = "/proc/driver/nvidia/gpus";
+ DIR *dir;
+ struct dirent *ent;
+ int n = 0;
+
+ dir = opendir(root);
+ if (dir == NULL)
+ return -errno;
+ while ((ent = readdir(dir)) != NULL && n < max) {
+ char info[512];
+ struct rm_gpu_desc *gpu = &out[n];
+
+ if (ent->d_name[0] == '.')
+ continue;
+ memset(gpu, 0, sizeof(*gpu));
+ gpu->minor = -1;
+ copy_trim(gpu->bdf, sizeof(gpu->bdf), ent->d_name);
+ snprintf(info, sizeof(info), "%s/%s/information", root,
+ ent->d_name);
+ parse_information(info, gpu);
+ n++;
+ }
+ closedir(dir);
+ qsort(out, n, sizeof(*out), gpu_compare);
+ return n;
+}
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);
+}