[RFC 2/9] mfd: Add driver for Multifunction USB Device
Lee Jones
lee.jones at linaro.org
Thu Feb 27 09:09:01 UTC 2020
I'd really like someone from USB to have a look through this too.
I'll do a quick first pass and provide some general comments though.
On Sun, 16 Feb 2020, Noralf Trønnes wrote:
> A Multifunction USB Device is a device that supports functions like gpio
> and display or any other function that can be represented as a USB regmap.
> Interrupts over USB is also supported if such an endpoint is present.
Do you have a datasheet?
> Signed-off-by: Noralf Trønnes <noralf at tronnes.org>
> ---
> drivers/mfd/Kconfig | 8 +
> drivers/mfd/Makefile | 1 +
> drivers/mfd/mud.c | 580 ++++++++++++++++++++++++++++++++++++++++
As interesting as a "mud driver" sounds. Something more forthcoming
might be better, "multi_usb" as a very quick example, but perhaps
something more imaginative/distinguishing would be better.
> include/linux/mfd/mud.h | 16 ++
> 4 files changed, 605 insertions(+)
> create mode 100644 drivers/mfd/mud.c
> create mode 100644 include/linux/mfd/mud.h
>
> diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
> index 52818dbcfe1f..9950794d907e 100644
> --- a/drivers/mfd/Kconfig
> +++ b/drivers/mfd/Kconfig
> @@ -1968,6 +1968,14 @@ config MFD_STMFX
> additional drivers must be enabled in order to use the functionality
> of the device.
>
> +config MFD_MUD
> + tristate "Multifunction USB Device core driver"
> + depends on USB
> + select MFD_CORE
> + select REGMAP_USB
> + help
> + Select this to get support for the Multifunction USB Device.
> +
> menu "Multimedia Capabilities Port drivers"
> depends on ARCH_SA1100
>
> diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
> index 29e6767dd60c..0adfab9afaed 100644
> --- a/drivers/mfd/Makefile
> +++ b/drivers/mfd/Makefile
> @@ -255,4 +255,5 @@ obj-$(CONFIG_MFD_ROHM_BD70528) += rohm-bd70528.o
> obj-$(CONFIG_MFD_ROHM_BD718XX) += rohm-bd718x7.o
> obj-$(CONFIG_MFD_STMFX) += stmfx.o
> obj-$(CONFIG_MFD_RPISENSE_CORE) += rpisense-core.o
> +obj-$(CONFIG_MFD_MUD) += mud.o
>
> diff --git a/drivers/mfd/mud.c b/drivers/mfd/mud.c
> new file mode 100644
> index 000000000000..f5f31478656d
> --- /dev/null
> +++ b/drivers/mfd/mud.c
> @@ -0,0 +1,580 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright 2020 Noralf Trønnes
> + */
> +
> +#include <linux/bitmap.h>
> +#include <linux/interrupt.h>
> +#include <linux/irq.h>
> +#include <linux/irqdomain.h>
> +#include <linux/list.h>
> +#include <linux/mfd/core.h>
> +#include <linux/mfd/mud.h>
> +#include <linux/module.h>
> +#include <linux/regmap.h>
> +#include <linux/regmap_usb.h>
> +#include <linux/seq_file.h>
> +#include <linux/slab.h>
> +#include <linux/spinlock.h>
> +#include <linux/usb.h>
> +#include <linux/workqueue.h>
> +
> +/* Temporary debugging aid */
> +#undef dev_dbg
> +#define dev_dbg dev_info
> +
> +#define mdebug(fmt, ...) \
> +do { \
> + if (1) \
> + pr_debug(fmt, ##__VA_ARGS__); \
> +} while (0)
No thank you.
We already have quite a few debugging facilities in the kernel.
> +struct mud_irq_event {
> + struct list_head node;
> + DECLARE_BITMAP(status, REGMAP_USB_MAX_MAPS);
> +};
> +
> +struct mud_irq {
> + struct irq_domain *domain;
> + unsigned int num_irqs;
> +
> + struct workqueue_struct *workq;
> + struct work_struct work;
> + struct urb *urb;
> +
> + spinlock_t lock; /* Protect the values below */
> + unsigned long *mask;
> + u16 tag;
> + struct list_head eventlist;
> +
> + unsigned int stats_illegal;
> + unsigned int stats_already_seen;
> + unsigned int stats_lost;
> +};
I think this should have a Kerneldoc header.
> +struct mud_device {
s/device/ddata/
> + struct usb_device *usb;
> + struct mud_irq *mirq;
> + struct mfd_cell *cells;
Why does this need to be in here?
> + unsigned int num_cells;
Why do these need to be stored in device data?
> +};
> +
> +static void mud_irq_work(struct work_struct *work)
> +{
> + struct mud_irq *mirq = container_of(work, struct mud_irq, work);
> + struct mud_irq_event *event;
> + unsigned long n, flags;
> + unsigned int irq;
> +
> + mdebug("%s: IN\n", __func__);
All of these prints need to go. They have no place in an upstreamed
driver and the whole thing reads much better without them.
> + while (true) {
> + spin_lock_irqsave(&mirq->lock, flags);
> + event = list_first_entry_or_null(&mirq->eventlist, struct mud_irq_event, node);
> + if (event) {
> + list_del(&event->node);
> + mdebug(" status: %*pb\n", mirq->num_irqs, event->status);
> + bitmap_and(event->status, event->status, mirq->mask, mirq->num_irqs);
> + }
> + spin_unlock_irqrestore(&mirq->lock, flags);
'\n'
> + if (!event)
> + break;
> +
> + for_each_set_bit(n, event->status, mirq->num_irqs) {
> + irq = irq_find_mapping(mirq->domain, n);
> + mdebug(" n=%lu irq=%u\n", n, irq);
> + if (irq)
> + handle_nested_irq(irq);
> + }
> +
> + kfree(event);
> + }
> +
> + mdebug("%s: OUT\n", __func__);
> +}
> +
> +#define BYTES_PER_LONG (BITS_PER_LONG / BITS_PER_BYTE)
> +
> +static void mud_irq_queue(struct urb *urb)
> +{
> + u8 *buf = urb->transfer_buffer + sizeof(u16);
Comment.
> + struct mud_irq *mirq = urb->context;
> + struct device *dev = &urb->dev->dev;
> + struct mud_irq_event *event = NULL;
> + unsigned int i, tag, diff;
> + unsigned long flags;
> +
> + if (urb->actual_length != urb->transfer_buffer_length) {
> + dev_err_once(dev, "Interrupt packet wrong length: %u\n",
> + urb->actual_length);
> + mirq->stats_illegal++;
> + return;
> + }
> +
> + spin_lock_irqsave(&mirq->lock, flags);
> +
> + tag = le16_to_cpup(urb->transfer_buffer);
> + if (tag == mirq->tag) {
> + dev_dbg(dev, "Interrupt tag=%u already seen, ignoring\n", tag);
> + mirq->stats_already_seen++;
> + goto unlock;
> + }
> +
> + if (tag > mirq->tag)
> + diff = tag - mirq->tag;
> + else
> + diff = U16_MAX - mirq->tag + tag + 1;
Comment.
> + if (diff > 1) {
> + dev_err_once(dev, "Interrupts lost: %u\n", diff - 1);
> + mirq->stats_lost += diff - 1;
> + }
> +
> + event = kzalloc(sizeof(*event), GFP_ATOMIC);
> + if (!event) {
> + mirq->stats_lost += 1;
> + goto unlock;
> + }
This hides a potential OOM issue.
Not sure what to do about that. Maybe the USB guys have an idea.
> + list_add_tail(&event->node, &mirq->eventlist);
> +
> + for (i = 0; i < (urb->transfer_buffer_length - sizeof(u16)); i++) {
> + unsigned long *val = &event->status[i / BYTES_PER_LONG];
> + unsigned int mod = i % BYTES_PER_LONG;
> +
> + if (!mod)
> + *val = buf[i];
> + else
> + *val |= ((unsigned long)buf[i]) << (mod * BITS_PER_BYTE);
> + }
Comment. In fact, comments throughout please.
(I'm going to stop saying "comment" from here on).
> + mdebug("%s: tag=%u\n", __func__, tag);
> +
> + mirq->tag = tag;
> +unlock:
> + spin_unlock_irqrestore(&mirq->lock, flags);
> +
> + if (event)
> + queue_work(mirq->workq, &mirq->work);
> +}
> +
> +static void mud_irq_urb_completion(struct urb *urb)
> +{
> + struct device *dev = &urb->dev->dev;
> + int ret;
> +
> + mdebug("%s: actual_length=%u\n", __func__, urb->actual_length);
> +
> + switch (urb->status) {
> + case 0:
> + mud_irq_queue(urb);
> + break;
> + case -EPROTO: /* FIXME: verify: dwc2 reports this on disconnect */
What does this mean? Why can't you fix it now?
> + case -ECONNRESET:
> + case -ENOENT:
> + case -ESHUTDOWN:
> + dev_dbg(dev, "irq urb shutting down with status: %d\n", urb->status);
s/irq/IRQ/ in all comments and prints.
Same with URB?
> + return;
> + default:
> + dev_dbg(dev, "irq urb failure with status: %d\n", urb->status);
> + break;
So it's failed, but you're going to attempt to submit it anyway?
> + }
> +
> + ret = usb_submit_urb(urb, GFP_ATOMIC);
> + if (ret && ret != -ENODEV)
> + dev_err(dev, "irq usb_submit_urb failed with result %d\n", ret);
> +}
> +
> +static void mud_irq_mask(struct irq_data *data)
> +{
> + struct mud_irq *mirq = irq_data_get_irq_chip_data(data);
> + unsigned long flags;
> +
> + mdebug("%s: hwirq=%lu\n", __func__, data->hwirq);
> +
> + spin_lock_irqsave(&mirq->lock, flags);
> + clear_bit(data->hwirq, mirq->mask);
> + spin_unlock_irqrestore(&mirq->lock, flags);
> +}
> +
> +static void mud_irq_unmask(struct irq_data *data)
> +{
> + struct mud_irq *mirq = irq_data_get_irq_chip_data(data);
> + unsigned long flags;
> +
> + mdebug("%s: hwirq=%lu\n", __func__, data->hwirq);
> +
> + spin_lock_irqsave(&mirq->lock, flags);
> + set_bit(data->hwirq, mirq->mask);
> + spin_unlock_irqrestore(&mirq->lock, flags);
> +}
> +
> +static struct irq_chip mud_irq_chip = {
> + .name = "mud-irq",
> + .irq_mask = mud_irq_mask,
> + .irq_unmask = mud_irq_unmask,
> +};
This tabbing seems excessive.
> +static void __maybe_unused
> +mud_irq_domain_debug_show(struct seq_file *m, struct irq_domain *d,
> + struct irq_data *data, int ind)
Best not to over-shorten variable names for no good reason.
'm', 'd' and 'ind' aren't good choices.
> +{
> + struct mud_irq *mirq = d ? d->host_data : irq_data_get_irq_chip_data(data);
> + unsigned long flags;
> +
> + spin_lock_irqsave(&mirq->lock, flags);
> +
> + seq_printf(m, "%*sTag: %u\n", ind, "", mirq->tag);
> + seq_printf(m, "%*sIllegal: %u\n", ind, "", mirq->stats_illegal);
> + seq_printf(m, "%*sAlready seen: %u\n", ind, "", mirq->stats_already_seen);
> + seq_printf(m, "%*sLost: %u\n", ind, "", mirq->stats_lost);
> +
> + spin_unlock_irqrestore(&mirq->lock, flags);
> +}
> +
> +static int mud_irq_domain_map(struct irq_domain *d, unsigned int virq,
> + irq_hw_number_t hwirq)
> +{
> + irq_set_chip_data(virq, d->host_data);
> + irq_set_chip_and_handler(virq, &mud_irq_chip, handle_simple_irq);
> + irq_set_nested_thread(virq, true);
> + irq_set_noprobe(virq);
> +
> + return 0;
> +}
> +
> +static void mud_irq_domain_unmap(struct irq_domain *d, unsigned int virq)
> +{
> + irq_set_chip_and_handler(virq, NULL, NULL);
> + irq_set_chip_data(virq, NULL);
> +}
> +
> +static const struct irq_domain_ops mud_irq_ops = {
> + .map = mud_irq_domain_map,
> + .unmap = mud_irq_domain_unmap,
> +#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
> + .debug_show = mud_irq_domain_debug_show,
> +#endif
> +};
> +
> +static int mud_irq_start(struct mud_irq *mirq)
> +{
> + if (!mirq)
> + return 0;
> +
> + return usb_submit_urb(mirq->urb, GFP_KERNEL);
> +}
> +
> +static void mud_irq_stop(struct mud_irq *mirq)
> +{
> + if (!mirq)
> + return;
> +
> + usb_kill_urb(mirq->urb);
> + flush_work(&mirq->work);
> +}
> +
> +static void mud_irq_release(struct mud_irq *mirq)
> +{
> + if (!mirq)
> + return;
> +
> + if (mirq->workq)
> + destroy_workqueue(mirq->workq);
> +
> + if (mirq->domain) {
> + irq_hw_number_t hwirq;
> +
> + for (hwirq = 0; hwirq < mirq->num_irqs; hwirq++)
> + irq_dispose_mapping(irq_find_mapping(mirq->domain, hwirq));
> +
> + irq_domain_remove(mirq->domain);
> + }
> +
> + usb_free_coherent(mirq->urb->dev, mirq->urb->transfer_buffer_length,
> + mirq->urb->transfer_buffer, mirq->urb->transfer_dma);
> + usb_free_urb(mirq->urb);
> + bitmap_free(mirq->mask);
> + kfree(mirq);
> +}
> +
> +static struct mud_irq *mud_irq_create(struct usb_interface *interface,
> + unsigned int num_irqs)
> +{
> + struct usb_device *usb = interface_to_usbdev(interface);
> + struct device *dev = &interface->dev;
> + struct usb_endpoint_descriptor *ep;
> + struct fwnode_handle *fn;
> + struct urb *urb = NULL;
> + struct mud_irq *mirq;
> + void *buf = NULL;
> + size_t buf_size;
> + int ret;
> +
> + mdebug("%s: dev->id=%d\n", __func__, dev->id);
> +
> + ret = usb_find_int_in_endpoint(interface->cur_altsetting, &ep);
> + if (ret == -ENXIO)
> + return NULL;
> + if (ret)
> + return ERR_PTR(ret);
> +
> + mirq = kzalloc(sizeof(*mirq), GFP_KERNEL);
> + if (!mirq)
> + return ERR_PTR(-ENOMEM);
> +
> + mirq->mask = bitmap_zalloc(num_irqs, GFP_KERNEL);
> + if (!mirq->mask) {
> + ret = -ENOMEM;
> + goto release;
> + }
> +
> + spin_lock_init(&mirq->lock);
> + mirq->num_irqs = num_irqs;
> +
> + urb = usb_alloc_urb(0, GFP_KERNEL);
> + if (!urb) {
> + ret = -ENOMEM;
> + goto release;
> + }
> +
> + buf_size = usb_endpoint_maxp(ep);
> + if (buf_size != (sizeof(u16) + DIV_ROUND_UP(num_irqs, BITS_PER_BYTE))) {
> + dev_err(dev, "Interrupt endpoint wMaxPacketSize too small: %zu\n", buf_size);
> + ret = -EINVAL;
> + goto release;
> + }
> +
> + buf = usb_alloc_coherent(usb, buf_size, GFP_KERNEL, &urb->transfer_dma);
> + if (!buf) {
> + usb_free_urb(urb);
> + ret = -ENOMEM;
> + goto release;
> + }
> +
> + usb_fill_int_urb(urb, usb,
> + usb_rcvintpipe(usb, usb_endpoint_num(ep)),
> + buf, buf_size, mud_irq_urb_completion,
> + mirq, ep->bInterval);
> + urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
> +
> + mirq->urb = urb;
> +
> + if (dev->of_node) {
> + fn = of_node_to_fwnode(dev->of_node);
> + } else {
> + fn = irq_domain_alloc_named_fwnode("mud-irq");
> + if (!fn) {
> + ret = -ENOMEM;
> + goto release;
> + }
> + }
> +
> + mirq->domain = irq_domain_create_linear(fn, num_irqs, &mud_irq_ops, mirq);
> + if (!dev->of_node)
> + irq_domain_free_fwnode(fn);
> + if (!mirq->domain) {
> + ret = -ENOMEM;
> + goto release;
> + }
> +
> + INIT_LIST_HEAD(&mirq->eventlist);
> + INIT_WORK(&mirq->work, mud_irq_work);
> +
> + mirq->workq = alloc_workqueue("mud-irq/%s", WQ_HIGHPRI, 0, dev_name(dev));
> + if (!mirq->workq) {
> + ret = -ENOMEM;
> + goto release;
> + }
> +
> + return mirq;
> +
> +release:
> + mud_irq_release(mirq);
> +
> + return ERR_PTR(ret);
> +}
> +
> +static int mud_probe_regmap(struct usb_interface *interface, struct mfd_cell *cell,
> + unsigned int index, struct mud_irq *mirq)
> +{
> + struct mud_cell_pdata *pdata;
> + struct resource *res = NULL;
> + int ret;
> +
> + pdata = kzalloc(sizeof(*pdata), GFP_KERNEL);
> + if (!pdata)
> + return -ENOMEM;
> +
> + ret = regmap_usb_get_map_descriptor(interface, index, &pdata->desc);
Can you give an example of what a desc might look like?
I'm particularly interested in pdata->desc.name.
> + if (ret)
> + goto error;
This will attempt to free 'res' which is currently NULL.
> + mdebug("%s: name='%s' index=%u\n", __func__, pdata->desc.name, index);
> + mdebug(" bRegisterValueBits=%u\n", pdata->desc.bRegisterValueBits);
> + mdebug(" bCompression=0x%02x\n", pdata->desc.bCompression);
> + mdebug(" bMaxTransferSizeOrder=%u (%ukB)\n",
> + pdata->desc.bMaxTransferSizeOrder,
> + (1 << pdata->desc.bMaxTransferSizeOrder) / 1024);
> +
> + if (mirq) {
> + res = kzalloc(sizeof(*res), GFP_KERNEL);
> + if (!res) {
> + ret = -ENOMEM;
> + goto error;
This will attempt to free 'res' which is currently NULL.
> + }
> +
> + res->flags = IORESOURCE_IRQ;
> + res->start = irq_create_mapping(mirq->domain, index);
> + mdebug(" res->start=%u\n", (unsigned int)res->start);
> + res->end = res->start;
> +
> + cell->resources = res;
> + cell->num_resources = 1;
> + }
> +
> + pdata->interface = interface;
This looks like something that should be stored in ddata.
> + pdata->index = index;
Don't usually like indexes - what is this used for?
> + cell->name = pdata->desc.name;
> + cell->platform_data = pdata;
> + cell->pdata_size = sizeof(*pdata);
> + /*
> + * A Multifunction USB Device can have multiple functions of the same
> + * type. mfd_add_device() in its current form will only match on the
> + * first node in the Device Tree.
> + */
> + cell->of_compatible = cell->name;
> +
> + return 0;
> +
> +error:
> + kfree(res);
I think you should remove this line, as it's never useful here.
> + kfree(pdata);
> +
> + return ret;
> +}
> +
> +static void mud_free(struct mud_device *mud)
> +{
> + unsigned int i;
> +
> + mud_irq_release(mud->mirq);
> +
> + for (i = 0; i < mud->num_cells; i++) {
> + kfree(mud->cells[i].platform_data);
> + kfree(mud->cells[i].resources);
> + }
> +
> + kfree(mud->cells);
> + kfree(mud);
> +}
> +
> +static int mud_probe(struct usb_interface *interface,
> + const struct usb_device_id *id)
> +{
> + struct device *dev = &interface->dev;
> + unsigned int i, num_regmaps;
> + struct mud_device *mud;
> + int ret;
> +
> + mdebug("%s: interface->dev.of_node=%px usb->dev.of_node=%px",
> + __func__, interface->dev.of_node,
> + usb_get_dev(interface_to_usbdev(interface))->dev.of_node);
> +
> + ret = regmap_usb_get_interface_descriptor(interface, &num_regmaps);
> + if (ret)
> + return ret;
> + if (!num_regmaps)
> + return -EINVAL;
> +
> + mdebug("%s: num_regmaps=%u\n", __func__, num_regmaps);
> +
> + mud = kzalloc(sizeof(*mud), GFP_KERNEL);
> + if (!mud)
> + return -ENOMEM;
> +
> + mud->mirq = mud_irq_create(interface, num_regmaps);
> + if (IS_ERR(mud->mirq)) {
> + ret = PTR_ERR(mud->mirq);
> + goto err_free;
> + }
> +
> + mud->num_cells = num_regmaps;
> + mud->cells = kcalloc(num_regmaps, sizeof(*mud->cells), GFP_KERNEL);
> + if (!mud->cells)
> + goto err_free;
> +
> + for (i = 0; i < num_regmaps; i++) {
> + ret = mud_probe_regmap(interface, &mud->cells[i], i, mud->mirq);
> + if (ret) {
> + dev_err(dev, "Failed to probe regmap index %i (error %d)\n", i, ret);
> + goto err_free;
> + }
> + }
> +
> + ret = mud_irq_start(mud->mirq);
> + if (ret) {
> + dev_err(dev, "Failed to start irq (error %d)\n", ret);
> + goto err_free;
> + }
> +
> + ret = mfd_add_hotplug_devices(dev, mud->cells, mud->num_cells);
> + if (ret) {
> + dev_err(dev, "Failed to add mfd devices to core.");
"MFD" is not a thing. It's made up.
"Failed to register child devices" makes more sense.
NIT: I don't see full stops in any of your other messages.
> + goto err_stop;
> + }
> +
> + mud->usb = usb_get_dev(interface_to_usbdev(interface));
> +
> + usb_set_intfdata(interface, mud);
> +
> + if (mud->usb->product)
> + dev_info(dev, "%s\n", mud->usb->product);
> +
> + return 0;
> +
> +err_stop:
> + mud_irq_stop(mud->mirq);
> +err_free:
> + mud_free(mud);
> +
> + return ret;
> +}
> +
> +static void mud_disconnect(struct usb_interface *interface)
> +{
> + struct mud_device *mud = usb_get_intfdata(interface);
> +
> + mfd_remove_devices(&interface->dev);
> + mud_irq_stop(mud->mirq);
> + usb_put_dev(mud->usb);
> + mud_free(mud);
> +
> + dev_dbg(&interface->dev, "disconnected\n");
> +}
> +
> +static const struct usb_device_id mud_table[] = {
> + /*
> + * FIXME:
> + * Apply for a proper pid: https://github.com/openmoko/openmoko-usb-oui
> + *
> + * Or maybe the Linux Foundation will provide one from their vendor id.
> + */
Probably not a good idea to take this into the upstream kernel without
a valid, registered PID. Suggest you do this *first*.
> + { USB_DEVICE_INTERFACE_CLASS(0x1d50, 0x6150, USB_CLASS_VENDOR_SPEC) },
> + { }
> +};
> +
> +MODULE_DEVICE_TABLE(usb, mud_table);
> +
> +static struct usb_driver mud_driver = {
> + .name = "mud",
> + .probe = mud_probe,
> + .disconnect = mud_disconnect,
> + .id_table = mud_table,
> +};
> +
> +module_usb_driver(mud_driver);
> +
> +MODULE_DESCRIPTION("Generic USB Device mfd core driver");
> +MODULE_AUTHOR("Noralf Trønnes <noralf at tronnes.org>");
> +MODULE_LICENSE("GPL");
> diff --git a/include/linux/mfd/mud.h b/include/linux/mfd/mud.h
> new file mode 100644
> index 000000000000..b2059fa57429
> --- /dev/null
> +++ b/include/linux/mfd/mud.h
> @@ -0,0 +1,16 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +
> +#ifndef __LINUX_MUD_H
> +#define __LINUX_MUD_H
> +
> +#include <linux/regmap_usb.h>
> +
> +struct usb_interface;
> +
> +struct mud_cell_pdata {
> + struct usb_interface *interface;
Shouldn't be here - more of a ddata thing.
> + unsigned int index;
Indexes are generally not a good idea.
> + struct regmap_usb_map_descriptor desc;
Shouldn't be here - more of a ddata thing.
> +};
> +
> +#endif
--
Lee Jones [李琼斯]
Linaro Services Technical Lead
Linaro.org │ Open source software for ARM SoCs
Follow Linaro: Facebook | Twitter | Blog
More information about the dri-devel
mailing list