This is just a proof of concept utility that takes a PCI device address and a monitor ID and finds the xrandr output associated with that monitor id. --- Changes in v2: - used different format for specifying the PCI address (pci/$domain/$dev.$fn) - used err()/errx() to report errors (from err.h) - Added some debug output (export DEBUG=1) - read PCI address from sysfs by getting the link for /sys/class/drm/card0 instead of /sys/class/drm/card0/device - handle the full PCI heirarchy (including bridges) - handle different vendor/device types by customizing the expected xrandr names - Query X outputs outside the loop so they don't get looked up for every different device - handle Nvidia devices that don't provide outputs via the DRM subsystem by assuming that it's the only device being used by X and simply looking up the Nth Xrandr output. - added tests - various other fixes Makefile.am | 18 + configure.ac | 1 + src/vdagent/get-xrandr-output.c | 806 ++++++++++++++++++++++++++++++++ 3 files changed, 825 insertions(+) create mode 100644 src/vdagent/get-xrandr-output.c diff --git a/Makefile.am b/Makefile.am index 3e405bc..b159650 100644 --- a/Makefile.am +++ b/Makefile.am @@ -3,6 +3,7 @@ NULL = bin_PROGRAMS = src/spice-vdagent sbin_PROGRAMS = src/spice-vdagentd +noinst_PROGRAMS = src/get-xrandr-output common_sources = \ src/udscs.c \ @@ -77,6 +78,23 @@ src_spice_vdagentd_SOURCES = \ src/vdagentd/virtio-port.h \ $(NULL) +src_get_xrandr_output_SOURCES = \ + src/vdagent/get-xrandr-output.c \ + $(NULL) + +src_get_xrandr_output_CFLAGS = \ + $(AM_CPPFLAGS) \ + $(DRM_CFLAGS) \ + $(X_CFLAGS) \ + $(GLIB2_CFLAGS) \ + $(NULL) + +src_get_xrandr_output_LDADD = \ + $(DRM_LIBS) \ + $(X_LIBS) \ + $(GLIB2_LIBS) \ + $(NULL) + if HAVE_CONSOLE_KIT src_spice_vdagentd_SOURCES += src/vdagentd/console-kit.c else diff --git a/configure.ac b/configure.ac index 7cb44db..55b031e 100644 --- a/configure.ac +++ b/configure.ac @@ -105,6 +105,7 @@ PKG_CHECK_MODULES(X, [xfixes xrandr >= 1.3 xinerama x11]) PKG_CHECK_MODULES(SPICE, [spice-protocol >= 0.12.13]) PKG_CHECK_MODULES(ALSA, [alsa >= 1.0.22]) PKG_CHECK_MODULES([DBUS], [dbus-1]) +PKG_CHECK_MODULES([DRM], [libdrm]) if test "$with_session_info" = "auto" || test "$with_session_info" = "systemd"; then PKG_CHECK_MODULES([LIBSYSTEMD_LOGIN], diff --git a/src/vdagent/get-xrandr-output.c b/src/vdagent/get-xrandr-output.c new file mode 100644 index 0000000..c2116d2 --- /dev/null +++ b/src/vdagent/get-xrandr-output.c @@ -0,0 +1,806 @@ +/* get-xrandr-output.c proof of concept for converting PCI address and device + * display id to an xrandr output in the guest. + * + * Copyright 2018 Red Hat, Inc. + * + * Red Hat Authors: + * Jonathon Jongsma <jjongsma@xxxxxxxxxx> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <string.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <xf86drm.h> +#include <xf86drmMode.h> +#include <unistd.h> +#include <X11/extensions/Xrandr.h> +#include <glib.h> + +static bool debug = false; +#define DEBUG(...) \ + if (G_UNLIKELY(debug)) printf(__VA_ARGS__); + +typedef struct PciDevice { + int domain; + uint8_t bus; + uint8_t slot; + uint8_t function; +} PciDevice; + +typedef struct PciAddress { + int domain; + GList *devices; /* PciDevice */ +} PciAddress; + +PciAddress* pci_address_new() +{ + return g_new0(PciAddress, 1); +} + +void pci_address_free(PciAddress *addr) +{ + g_list_free_full(addr->devices, g_free); + g_free(addr); +} + + +int read_next_hex_number(const char *input, char delim, char **endptr) +{ + assert(input != NULL); + assert(endptr != NULL); + + const char *pos = strchr(input, delim); + int n; + if (!pos) { + *endptr = NULL; + return 0; + } + + char *endpos; + n = strtol(input, &endpos, 16); + + // check if we read all characters until the delimiter + if (endpos != pos) + endpos = NULL; + + *endptr = endpos; + return n; +} + +// the device should be specified in BDF notation (e.g. 0000:00:02.0) +// see https://wiki.xen.org/wiki/Bus:Device.Function_(BDF)_Notation +bool parse_pci_device(const char *bdf, const char *end, PciDevice *device) +{ + if (!device) + return false; + + char *pos; + device->domain = read_next_hex_number(bdf, ':', &pos); + if (!pos) + return false; + + device->bus = read_next_hex_number(pos + 1, ':', &pos); + if (!pos) + return false; + + device->slot = read_next_hex_number(pos + 1, '.', &pos); + if (!pos) + return false; + + device->function = strtol(pos + 1, &pos, 16); + if (!pos || (end != NULL && end != pos)) + return false; + + return true; +} + +// We need to extract the pci address of the device from the sysfs entry for the device like so: +// $ readlink /sys/class/drm/card0 +// This should give you a path such as this for cards on the root bus: +// /sys/devices/pci0000:00/0000:00:02.0/drm/card0 +// or something like this if there is a pci bridge: +// /sys/devices/pci0000:00/0000:00:03.0/0000:01:01.0/0000:02:03.0/virtio2/drm/card0 +PciAddress* parse_pci_address_from_sysfs_path(const char* addr) +{ + char *pos = strstr(addr, "/pci"); + if (!pos) + return NULL; + + // advance to the numbers in pci0000:00 + pos += 4; + int domain = read_next_hex_number(pos, ':', &pos); + if (!pos) { + return NULL; + } + + // not used right now. + uint8_t bus = read_next_hex_number(pos + 1, '/', &pos); + if (!pos) { + return NULL; + } + + PciAddress *address = pci_address_new(); + address->domain = domain; + // now read all of the devices + for (int n = 0; ; n++) { + PciDevice *dev = g_new0(PciDevice, 1); + char *next = strchr(pos + 1, '/'); + if (!parse_pci_device(pos + 1, next, dev)) { + g_free(dev); + break; + } + address->devices = g_list_append(address->devices, dev); + pos = next; + if (!pos) + break; + } + return address; +} + +// format should be something like pci/$domain/$slot.$fn/$slot.$fn +PciAddress* parse_pci_address_from_spice(char *input) +{ + const char * const prefix = "pci/"; + if (strncmp(input, prefix, strlen(prefix)) != 0) + return NULL; + + char *pos = input + strlen(prefix); + int domain = read_next_hex_number(pos, '/', &pos); + if (!pos) { + return NULL; + } + + PciAddress *address = pci_address_new(); + address->domain = domain; + // now read all of the devices + for (int n = 0; ; n++) { + PciDevice *dev = g_new0(PciDevice, 1); + char *next = strchr(pos + 1, '/'); + + dev->slot = read_next_hex_number(pos + 1, '.', &pos); + if (!pos) { + g_free(dev); + break; + } + + dev->function = strtol(pos + 1, &pos, 16); + if (!pos || (next != NULL && next != pos)) { + g_free(dev); + break; + } + + address->devices = g_list_append(address->devices, dev); + pos = next; + if (!pos) + break; + } + return address; +} + +bool compare_addresses(PciAddress *a, PciAddress *b) +{ + // only check domain, slot, and function + if (!(a->domain == b->domain + && g_list_length(a->devices) == g_list_length(b->devices))) { + return false; + } + + for (GList *la = a->devices, *lb = b->devices; + la != NULL; + la = la->next, lb = lb->next) { + PciDevice *deva = la->data; + PciDevice *devb = lb->data; + + if (deva->slot != devb->slot + || deva->function != devb->function) { + return false; + } + } + return true; +} + +// Connector type names from xorg modesetting driver +static const char * const modesetting_output_names[] = { + [DRM_MODE_CONNECTOR_Unknown] = "None" , + [DRM_MODE_CONNECTOR_VGA] = "VGA" , + [DRM_MODE_CONNECTOR_DVII] = "DVI-I" , + [DRM_MODE_CONNECTOR_DVID] = "DVI-D" , + [DRM_MODE_CONNECTOR_DVIA] = "DVI-A" , + [DRM_MODE_CONNECTOR_Composite] = "Composite" , + [DRM_MODE_CONNECTOR_SVIDEO] = "SVIDEO" , + [DRM_MODE_CONNECTOR_LVDS] = "LVDS" , + [DRM_MODE_CONNECTOR_Component] = "Component" , + [DRM_MODE_CONNECTOR_9PinDIN] = "DIN" , + [DRM_MODE_CONNECTOR_DisplayPort] = "DP" , + [DRM_MODE_CONNECTOR_HDMIA] = "HDMI" , + [DRM_MODE_CONNECTOR_HDMIB] = "HDMI-B" , + [DRM_MODE_CONNECTOR_TV] = "TV" , + [DRM_MODE_CONNECTOR_eDP] = "eDP" , + [DRM_MODE_CONNECTOR_VIRTUAL] = "Virtual" , + [DRM_MODE_CONNECTOR_DSI] = "DSI" , + [DRM_MODE_CONNECTOR_DPI] = "DPI" , +}; +// Connector type names from qxl driver +const char * const qxl_output_names[] = { + [DRM_MODE_CONNECTOR_Unknown] = "None" , + [DRM_MODE_CONNECTOR_VGA] = "VGA" , + [DRM_MODE_CONNECTOR_DVII] = "DVI" , + [DRM_MODE_CONNECTOR_DVID] = "DVI" , + [DRM_MODE_CONNECTOR_DVIA] = "DVI" , + [DRM_MODE_CONNECTOR_Composite] = "Composite" , + [DRM_MODE_CONNECTOR_SVIDEO] = "S-video" , + [DRM_MODE_CONNECTOR_LVDS] = "LVDS" , + [DRM_MODE_CONNECTOR_Component] = "CTV" , + [DRM_MODE_CONNECTOR_9PinDIN] = "DIN" , + [DRM_MODE_CONNECTOR_DisplayPort] = "DisplayPort" , + [DRM_MODE_CONNECTOR_HDMIA] = "HDMI" , + [DRM_MODE_CONNECTOR_HDMIB] = "HDMI" , + [DRM_MODE_CONNECTOR_TV] = "TV" , + [DRM_MODE_CONNECTOR_eDP] = "eDP" , + [DRM_MODE_CONNECTOR_VIRTUAL] = "Virtual" , +}; + + +void drm_conn_name_full(drmModeConnector *conn, const char * const *names, int nnames, char *dest, size_t dlen) +{ + const char *type; + + if (conn->connector_type_id < nnames && + names[conn->connector_type]) { + type = names[conn->connector_type]; + } else { + type = "unknown"; + } + snprintf(dest, dlen, "%s-%d", type, conn->connector_type_id); +} + +void drm_conn_name_qxl(drmModeConnector *conn, char *dest, size_t dlen) +{ + return drm_conn_name_full(conn, qxl_output_names, + sizeof(qxl_output_names)/sizeof(qxl_output_names[0]), + dest, dlen); +} + +// FIXME: there are some cases (for example, in my Lenovo T460p laptop with +// intel graphics) where the modesetting driver uses a name such as DP-3-1 +// instead of DP-4. These outputs are not likely to be common in virtual +// machines, so it may not matter much (?) +void drm_conn_name_modesetting(drmModeConnector *conn, char *dest, size_t dlen) +{ + return drm_conn_name_full(conn, modesetting_output_names, + sizeof(modesetting_output_names)/sizeof(modesetting_output_names[0]), + dest, dlen); +} + +static const char *connections[] = {"", "*CONNECTED*", "disconnected", "unknown connection"}; + +// verify that we parse the BDF notation correctly +bool test_bdf(const char* string, int domain, uint8_t bus, uint8_t slot, uint8_t function) +{ + PciDevice pci_dev; + return (parse_pci_device(string, NULL, &pci_dev) + && (pci_dev.domain == domain) + && (pci_dev.bus == bus) + && (pci_dev.slot == slot) + && (pci_dev.function == function)); +} + +void test_bdf_parsing() +{ + // valid input + assert(test_bdf("0000:00:02.1", 0, 0, 2, 1)); + assert(test_bdf("00:00:02.1", 0, 0, 2, 1)); + assert(test_bdf("0000:00:03.0", 0, 0, 3, 0)); + assert(test_bdf("0000:00:1d.1", 0, 0, 29, 1)); + assert(test_bdf("0000:09:02.1", 0, 9, 2, 1)); + assert(test_bdf("0000:1d:02.1", 0, 29, 2, 1)); + assert(test_bdf("0000:00:02.d", 0, 0, 2, 13)); + assert(test_bdf("000f:00:02.d", 15, 0, 2, 13)); + assert(test_bdf("000f:00:02.d", 15, 0, 2, 13)); + assert(test_bdf("000f:00:02.d", 15, 0, 2, 13)); + assert(test_bdf("000f:00:02.d", 15, 0, 2, 13)); + assert(test_bdf("ffff:ff:ff.f", 65535, 255, 255, 15)); + assert(test_bdf("0:0:2.1", 0, 0, 2, 1)); + + // invalid input + assert(!test_bdf("0000:00:02:0", 0, 0, 0, 0)); + assert(!test_bdf("-0001:00:02.1", 0, 0, 2, 1)); + assert(!test_bdf("0000.00.02.0", 0, 0, 0, 0)); + assert(!test_bdf("000f:00:02", 0, 0, 0, 0)); + assert(!test_bdf("000f:00", 0, 0, 0, 0)); + assert(!test_bdf("000f", 0, 0, 0, 0)); + assert(!test_bdf("random string", 0, 0, 0, 0)); + assert(!test_bdf("12345", 0, 0, 0, 0)); +} + +#define assert_device(dev, domain_, bus_, slot_, function_) \ +{ \ + PciDevice* dev_ = (dev); \ + assert(dev_ != NULL); \ + assert(dev_->domain == domain_); \ + assert(dev_->bus == bus_); \ + assert(dev_->slot == slot_); \ + assert(dev_->function == function_); \ +} + +void test_sysfs_parsing() +{ + PciAddress *addr = parse_pci_address_from_sysfs_path("../../devices/pci0000:00/0000:00:02.0/drm/card0"); + assert(addr != NULL); + assert(addr->domain == 0); + assert(g_list_length(addr->devices) == 1); + assert_device(addr->devices->data, 0, 0, 2, 0); + pci_address_free(addr); + + addr = parse_pci_address_from_sysfs_path("../../devices/pciffff:ff/ffff:ff:ff.f/drm/card0"); + assert(addr != NULL); + assert(addr->domain == 65535); + assert(g_list_length(addr->devices) == 1); + assert_device(addr->devices->data, 65535, 255, 255, 15); + pci_address_free(addr); + + addr = parse_pci_address_from_sysfs_path("../../devices/pci0000:00/0000:00:03.0/0000:01:01.0/0000:02:03.0/virtio2/drm/card0"); + assert(addr != NULL); + assert(addr->domain == 0); + assert(g_list_length(addr->devices) == 3); + assert_device(addr->devices->data, 0, 0, 3, 0); + assert_device(addr->devices->next->data, 0, 1, 1, 0); + assert_device(addr->devices->next->next->data, 0, 2, 3, 0); + pci_address_free(addr); +} + +void test_spice_parsing() +{ + PciAddress *addr = parse_pci_address_from_spice("pci/0000/02.0"); + assert(addr != NULL); + assert(addr->domain == 0); + assert(g_list_length(addr->devices) == 1); + assert_device(addr->devices->data, 0, 0, 2, 0); + pci_address_free(addr); + + addr = parse_pci_address_from_spice("pci/ffff/ff.f"); + assert(addr != NULL); + assert(addr->domain == 65535); + assert(g_list_length(addr->devices) == 1); + assert_device(addr->devices->data, 0, 0, 255, 15); + pci_address_free(addr); + + addr = parse_pci_address_from_spice("pci/0000/02.1/03.0"); + assert(addr != NULL); + assert(addr->domain == 0); + assert(g_list_length(addr->devices) == 2); + assert_device(addr->devices->data, 0, 0, 2, 1); + assert_device(addr->devices->next->data, 0, 0, 3, 0); + pci_address_free(addr); + + addr = parse_pci_address_from_spice("pci/000a/01.0/02.1/03.0"); + assert(addr != NULL); + assert(addr->domain == 10); + assert(g_list_length(addr->devices) == 3); + assert_device(addr->devices->data, 0, 0, 1, 0); + assert_device(addr->devices->next->data, 0, 0, 2, 1); + assert_device(addr->devices->next->next->data, 0, 0, 3, 0); + pci_address_free(addr); + + addr = parse_pci_address_from_spice("pcx/0000/02.1/03.0"); + assert(addr == NULL); + + addr = parse_pci_address_from_spice("0000/02.0"); + assert(addr == NULL); + + addr = parse_pci_address_from_spice("0000/02.1/03.0"); + assert(addr == NULL); +} + +void test_compare_addresses() +{ + { + PciDevice da1 = {1, 0, 3, 0}; + PciDevice da2 = {1, 1, 1, 0}; + PciDevice da3 = {1, 2, 3, 0}; + PciAddress a1 = {1, NULL}; + a1.domain = 0; + a1.devices = g_list_append(a1.devices, &da1); + a1.devices = g_list_append(a1.devices, &da2); + a1.devices = g_list_append(a1.devices, &da3); + + PciDevice db1 = {1, 0, 3, 0}; + PciDevice db2 = {1, 1, 1, 0}; + PciDevice db3 = {1, 2, 3, 0}; + PciAddress a2 = {1, NULL}; + a2.domain = 0; + a2.devices = g_list_append(a2.devices, &db1); + a2.devices = g_list_append(a2.devices, &db2); + a2.devices = g_list_append(a2.devices, &db3); + + assert(compare_addresses(&a1, &a2)); + } + { + PciDevice da1 = {1, 0, 3, 0}; + PciDevice da2 = {1, 1, 1, 0}; + PciDevice da3 = {1, 2, 3, 0}; + PciAddress a1 = {1, NULL}; + a1.domain = 0; + a1.devices = g_list_append(a1.devices, &da1); + a1.devices = g_list_append(a1.devices, &da2); + a1.devices = g_list_append(a1.devices, &da3); + + // a 'spice' format PCI address will not provide domain or bus for each + // device, only slot and function. So first two numbers for each device + // will always be set to 0 + PciDevice db1 = {0, 0, 3, 0}; + PciDevice db2 = {0, 0, 1, 0}; + PciDevice db3 = {0, 0, 3, 0}; + PciAddress a2 = {1, NULL}; + a2.domain = 0; + a2.devices = g_list_append(a2.devices, &db1); + a2.devices = g_list_append(a2.devices, &db2); + a2.devices = g_list_append(a2.devices, &db3); + + assert(compare_addresses(&a1, &a2)); + } + // different number of devices + { + PciDevice da1 = {0, 0, 3, 0}; + PciDevice da2 = {0, 1, 1, 0}; + PciDevice da3 = {0, 2, 3, 0}; + PciAddress a1 = {0, NULL}; + a1.domain = 0; + a1.devices = g_list_append(a1.devices, &da1); + a1.devices = g_list_append(a1.devices, &da2); + a1.devices = g_list_append(a1.devices, &da3); + + PciDevice db1 = {0, 0, 3, 0}; + PciDevice db2 = {0, 1, 1, 0}; + PciAddress a2 = {0, NULL}; + a2.domain = 0; + a2.devices = g_list_append(a2.devices, &db1); + a2.devices = g_list_append(a2.devices, &db2); + + assert(!compare_addresses(&a1, &a2)); + } + // mismatched function + { + PciDevice da1 = {0, 0, 2, 0}; + PciAddress a1 = {0, NULL}; + a1.domain = 0; + a1.devices = g_list_append(a1.devices, &da1); + + PciDevice db1 = {0, 0, 2, 1}; + PciAddress a2 = {0, NULL}; + a2.domain = 0; + a2.devices = g_list_append(a2.devices, &db1); + + assert(!compare_addresses(&a1, &a2)); + } + // mismatched slot + { + PciDevice da1 = {0, 0, 2, 0}; + PciAddress a1 = {0, NULL}; + a1.domain = 0; + a1.devices = g_list_append(a1.devices, &da1); + + PciDevice db1 = {0, 0, 1, 0}; + PciAddress a2 = {0, NULL}; + a2.domain = 0; + a2.devices = g_list_append(a2.devices, &db1); + + assert(!compare_addresses(&a1, &a2)); + } + // mismatched domain + { + PciDevice da1 = {0, 0, 2, 0}; + PciAddress a1 = {0, NULL}; + a1.domain = 1; + a1.devices = g_list_append(a1.devices, &da1); + + PciDevice db1 = {0, 0, 2, 0}; + PciAddress a2 = {0, NULL}; + a2.domain = 0; + a2.devices = g_list_append(a2.devices, &db1); + + assert(!compare_addresses(&a1, &a2)); + } +} + +void run_tests() +{ + test_bdf_parsing(); + test_sysfs_parsing(); + test_spice_parsing(); + test_compare_addresses(); +} + +#define PCI_VENDOR_ID_REDHAT 0x1b36 +#define PCI_VENDOR_ID_REDHAT_QUMRANET 0x1af4 // virtio-gpu +#define PCI_VENDOR_ID_INTEL 0x8086 +#define PCI_VENDOR_ID_NVIDIA 0x10de + +#define PCI_DEVICE_ID_QXL 0x0100 +#define PCI_DEVICE_ID_VIRTIO_GPU 0x1050 + +static bool read_hex_value_from_file(const char *path, int* value) +{ + if (value == NULL || path == NULL) + return false; + + int fd = open(path, O_RDONLY); + char buf[255]; + bool result = false; + if (fd < 1) + return false; + + size_t nread = 0, n = 0; + while (nread < sizeof(buf) + && (n = read(fd, buf + nread, sizeof(buf) - nread))) { + nread += n; + } + if (n < 0) { + goto cleanup; + } + buf[nread] = '\0'; + char *endptr = NULL; + *value = strtol(buf, &endptr, 16); + // if we didn't read the entire string up to a newline, something went wrong. + if (endptr == buf || *endptr != '\n') { + goto cleanup; + } + result = true; + +cleanup: + //closing the fd here causes an abort... + //close(fd); + return result; +} + +int main(int argc, char* argv[]) +{ + if (argc < 3) { + if (argc == 2 && (strcmp(argv[1], "test") == 0)) { + run_tests(); + printf("All tests succeeded\n"); + return EXIT_SUCCESS; + } + errx(EXIT_FAILURE, "Usage: %s PCIADDR DEVICEDISPLAYID", argv[0]); + } + debug = (getenv("DEBUG") != NULL); + + // PCI address should be in the following format: + // pci/$domain/$slot.$fn/$slot.$fn + PciAddress *user_pci_addr = parse_pci_address_from_spice(argv[1]); + if (!user_pci_addr) { + errx(EXIT_FAILURE, "Couldn't parse PCI address '%s'. Address should be the form 'pci/$domain/$slot.$fn/$slot.fn...", argv[1]); + } + int device_display_id = atoi(argv[2]); + + // look up xrandr outputs + Display *xdisplay = XOpenDisplay(NULL); + if (!xdisplay) { + errx(EXIT_FAILURE, "Failed to open X dislay"); + } + + int rr_event_base, rr_error_base; + if (!XRRQueryExtension(xdisplay, &rr_event_base, &rr_error_base)) { + errx(EXIT_FAILURE, "Failed to initialize XRandr extension"); + } + + XRRScreenResources *xres = XRRGetScreenResourcesCurrent(xdisplay, DefaultRootWindow(xdisplay)); + if (!xres) { + errx(EXIT_FAILURE, "Unable to get Xorg screen resources"); + } + + // Look for a device that matches the PCI address parsed above. Loop + // through the list of cards reported by the DRM subsytem + bool found_device = false; + for (int i = 0; i < 10; ++i) { + char dev_path[64]; + struct stat buf; + + // device node for the card is needed to access libdrm functionality + snprintf(dev_path, sizeof(dev_path), DRM_DEV_NAME, DRM_DIR_NAME, i); + if (stat(dev_path, &buf) != 0) { + // no card exists, exit loop + DEBUG("No card %i exists\n", i); + break; + } + + // the sysfs directory for the card will allow us to determine the + // pci address for the device + char sys_path[64]; + snprintf(sys_path, sizeof(sys_path), "/sys/class/drm/card%d", i); + DEBUG("sys path: %s\n", sys_path); + + // the file /sys/class/drm/card0 is a symlink to a file that + // specifies the device's address. It usually points to something + // like /sys/devices/pci0000:00/0000:00:02.0/drm/card0 + char device_link[PATH_MAX]; + if (realpath(sys_path, device_link) == NULL) { + err(EXIT_FAILURE, "Failed to get the real path of %s", sys_path); + } + DEBUG("Device %s is at %s\n", dev_path, device_link); + + PciAddress *drm_pci_addr = parse_pci_address_from_sysfs_path(device_link); + if (!drm_pci_addr) { + DEBUG("Can't determine pci address from '%s'", device_link); + continue; + } + + if (compare_addresses(user_pci_addr, drm_pci_addr)) { + bool found_output = false; + char vendor_id_path[150], device_id_path[150]; + snprintf(vendor_id_path, sizeof(vendor_id_path), "%s/device/vendor", sys_path); + int vendor_id = 0; + if (!read_hex_value_from_file(vendor_id_path, &vendor_id)) { + DEBUG("Unable to read vendor ID of card\n"); + } else { + DEBUG("Vendor id of this card is 0x%x\n", vendor_id); + } + snprintf(device_id_path, sizeof(device_id_path), "%s/device/device", sys_path); + int device_id = 0; + if (!read_hex_value_from_file(device_id_path, &device_id)) { + DEBUG("Unable to read device ID of card\n"); + } else { + DEBUG("Device id of this card is 0x%x\n", device_id); + } + + int fd = open(dev_path, O_RDWR); + if (fd < 0) { + err(EXIT_FAILURE, "Unable to open file %s", dev_path); + } + + drmModeResPtr res = drmModeGetResources(fd); + if (res) { + // find the drm output that is equal to device_display_id + if (device_display_id >= res->count_connectors) { + errx(EXIT_FAILURE, "Specified display id %i is higher than the maximum display id provided by this device (%i)", + device_display_id, res->count_connectors - 1); + } + + drmModeConnectorPtr conn = drmModeGetConnector(fd, res->connectors[device_display_id]); + drmModeFreeResources(res); + res = NULL; + + bool increment_xrandr_name = false; + if (vendor_id == PCI_VENDOR_ID_REDHAT && device_id == PCI_DEVICE_ID_QXL) { + // Older QXL drivers numbered their outputs starting with + // 0. This contrasts with most drivers who start numbering + // outputs with 1. In this case, the output name will need + // to be incremented before comparing to the expected drm + // connector name + for (int i = 0; i < xres->noutput; ++i) { + XRROutputInfo *oinfo = XRRGetOutputInfo(xdisplay, xres, xres->outputs[i]); + // the QXL device doesn't use the drm names directly, but + // starts numbering from 0 instead of 1, so we need to + // detect this condition and add 1 to all output names in + // this scenario before comparing + if (i == 0) { + // check the first output to see if it ends with '-0' + char *postfix = oinfo->name + (strlen(oinfo->name) - 2); + if (strcmp(postfix, "-0") == 0) { + DEBUG("Need to increment xrandr name...\n"); + increment_xrandr_name = true; + break; + } + } + XRRFreeOutputInfo(oinfo); + } + } + // Compare the name of the xrandr output against what we would + // expect based on the drm connection type. The xrandr names + // are driver-specific, so we need to special-case some + // drivers. Most hardware these days uses the 'modesetting' + // driver, but the QXL device uses its own driver which has + // different naming conventions + char expected_name[100]; + if (vendor_id == PCI_VENDOR_ID_REDHAT && device_id == PCI_DEVICE_ID_QXL) { + drm_conn_name_qxl(conn, expected_name, sizeof(expected_name)); + } else { + drm_conn_name_modesetting(conn, expected_name, sizeof(expected_name)); + } + + // Loop through xrandr outputs and check whether the xrandr + // output name matches the drm connector name + for (int i = 0; i < xres->noutput; ++i) { + int oid = xres->outputs[i]; + XRROutputInfo *oinfo = XRRGetOutputInfo(xdisplay, xres, oid); + char xname[100]; + // NOTE: this will increment the counter for every XRandr + // output, not only the ones associated with the current + // device. This should only matter in the unlikely scenario + // where X is configured with multiple devices and the + // incremented name of an output on another device + // conflicts with the name of the output we're looking for, + if (increment_xrandr_name) { + DEBUG("Original xrandr name is %s\n", oinfo->name); + char *id_pos = strrchr(oinfo->name, '-') + 1; + size_t prefix_len = id_pos - oinfo->name; + memcpy(xname, oinfo->name, prefix_len); + int newid = atoi(id_pos) + 1; + DEBUG("New id suffix should be -%i\n", newid); + int sz = snprintf(xname + prefix_len, sizeof(xname - prefix_len), "%i", newid); + if (sz < 0) { + errx(EXIT_FAILURE, "Unable to increment name"); + } + xname[prefix_len + sz] = '\0'; + } else { + strncpy(xname, oinfo->name, sizeof(xname) - 1); + xname[sizeof(xname)] = '\0'; + } + + DEBUG("Checking whether xrandr output %i (%s) matches expected name %s\n", i, xname, expected_name); + if (strcmp(xname, expected_name) == 0) { + found_output = true; + printf(" Found matching X Output: name=%s id=%i (%s)\n", + oinfo->name, + (int)oid, + connections[oinfo->connection + 1]); + XRRFreeOutputInfo(oinfo); + break; + } + XRRFreeOutputInfo(oinfo); + } + drmModeFreeConnector(conn); + conn = NULL; + } else { + if (vendor_id == PCI_VENDOR_ID_NVIDIA) { + DEBUG("This device is an Nvidia device that doesn't provide information about drm connectors. We'll simply look up the XRandr output with index %i\n", device_display_id); + // the proprietary nvidia driver does not provide outputs + // via drm, so the only thing we can do is just assume that + // it is the only device assigned to X, and use the xrandr + // output order to determine the proper display. + if (device_display_id >= xres->noutput) { + errx(EXIT_FAILURE, "The device display id %i does not exist", device_display_id); + } + int oid = xres->outputs[device_display_id]; + XRROutputInfo *oinfo = XRRGetOutputInfo(xdisplay, xres, oid); + printf(" Found matching X Output: name=%s id=%i (%s)\n", + oinfo->name, + (int)oid, + connections[oinfo->connection + 1]); + XRRFreeOutputInfo(oinfo); + found_output = true; + } else { + errx(EXIT_FAILURE, "Unable to get DRM resources for card"); + } + } + + + if (!found_output) { + errx(EXIT_FAILURE, "Couldn't find an XRandr output for the specified device"); + } + + // we found the right card, abort the loop + found_device = true; + break; + } else { + DEBUG("card addr '%s' does not match requested addr '%s'\n", device_link, argv[1]); + } + } + XRRFreeScreenResources(xres); + xres = NULL; + + if (!found_device) { + errx(EXIT_FAILURE, "Couldn't find a card with the given PCI address"); + } + return EXIT_SUCCESS; +} -- 2.17.2 _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel