[Spice-devel] [RFC linux vdagent] Add POC for getting xrandr output from monitor ID
Lukáš Hrázký
lhrazky at redhat.com
Thu Nov 8 13:21:30 UTC 2018
Hello,
On Mon, 2018-11-05 at 17:09 -0600, Jonathon Jongsma wrote:
> 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 at redhat.com>
> + *
> + * 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 (?)
Did you find out why it does that? Maybe it even is a bug? It kind of
looks like it.
> +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) {
I raised this question on the previous version and didn't get a
reply...
Why do you iterate over the first 10 drm cards and test their PCI
address against the input? Wouldn't it be better to look up the PCI
device by the input address and then just verify it's a drm card?
You'd get rid of the arbitrary 10 limit, it should be more efficient
(no iterating over the cards, though that doesn't matter here) and it
should be cleaner and more elegant?
> + 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,
So in that situation it seems quite possible there would already be
duplicit xrandr output names (or maybe xrandr - or drm? - has some
mechanism to disambiguate names which would again break our algorithm)?
But supposing there aren't, it would be better to decrement the name
we're looking for instead of increment the names we're looking at, not
knowing if they're QXL and we're supposed to do that?
Cheers,
Lukas
> + 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;
> +}
More information about the Spice-devel
mailing list