[Spice-devel] [RFC linux vdagent] Add POC for getting xrandr output from monitor ID
Jonathon Jongsma
jjongsma at redhat.com
Mon Nov 12 22:15:35 UTC 2018
On Thu, 2018-11-08 at 14:21 +0100, Lukáš Hrázký wrote:
> 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/virt
> > io2/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.
It appears that these extra outputs are associated with the docking
station. When I run 'xrandr' after booting the laptop without the
docking station, they do not appear. But after I dock the laptop, they
do appear.
>
> > +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)/siz
> > eof(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?
Good question. I had forgotten to address that comment. Personally, I'm
not sure that it makes it cleaner and more elegant though.
To look up a PCI device by address, I could use the /sys filesystem. So
for pci/0000/02.0, I would translate the domain 0000 into the
filesystem path '/sys/devices/pci0000:00/' assuming bus '00'? And then
look inside that directory for the the path '0000:00:02.0'. And then
look for a path named 'drm' within that directory to determine whether
it's a drm card or not? Is the existence of the 'drm' directory the
thing that we should use to determine whether this is a drm card?
Or a slightly more complicated example (based on my laptop): Let's look
for this device:
02:00.0 3D controller: NVIDIA Corporation GM108M [GeForce 940MX] (rev
a2)
So it's device 00.0 on bus 02. Here's the first part of lspci -t:
-[0000:00]-+-00.0
+-01.0-[01]--
+-01.2-[02]----00.0
So it's attached to the PCI bridge at 01.2. Since the bus number ('02')
is not a part of our device address format, we would only be provided
the following address: pci/0000/01.2/00.0
>From this we can look for the PCI bridge at path
/sys/devices/pci0000:00/0000:00:01.2. Within this directory, we can
look for a path of ... it's unclear because we're not provided the bus
number, so we can't necessarily construct the path '0000:02:00.0' that
represents this device. We could simply look for files within the
directory that end with '00.0' and just assume that the one we find is
the correct one. But that doesn't seem cleaner and more elegant to me.
Or maybe a better option would be to look at /sys/bus/pci/ to find the
device? Unfortunately this doesn't really help much either.
$ ls /sys/bus/pci/devices
0000:00:00.0
0000:00:01.0
0000:00:01.2
0000:00:02.0
0000:00:14.0
0000:00:14.2
0000:00:16.0
0000:00:16.3
0000:00:17.0
0000:00:1c.0
0000:00:1c.4
0000:00:1f.0
0000:00:1f.2
0000:00:1f.3
0000:00:1f.4
0000:00:1f.6
0000:02:00.0
0000:03:00.0
0000:04:00.0
So, for the second example above I can look inside the directory
/sys/class/pci/devices/0000:00:01.2 and again we're faced with the bus
number issue. The path 0000:02:00.0 does exist, but we don't know the
bus number '02' so constructing that path is not straightforward.
Perhaps there's a library that we could use that makes this device
lookup simpler, but it seems a bit overkill since it can be done quite
easily by just looking in /sys/class/drm as I've done. Or maybe you
know a clear way to do this?
>
> > + 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?
Probably a better approach indeed.
>
> 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