qemu-patch-raspberry4/hw/usb/combined-packet.c
Hans de Goede 579967bea6 combined-packet: Add a workaround for Linux usbfs + live migration
Older versions (anything but the latest) of Linux usbfs + libusb(x),
will submit larger (bulk) transfers split into multiple 16k submissions,
which means that rather then all tds getting linked into the queue in
one atomic operarion they get linked in a bunch at a time, which could
cause problems if:
1) We scan the queue while libusb is in the middle of submitting a split
   bulk transfer
2) While this bulk transfer is pending we migrate to another host.

The problem is that after 2, the new host will rescan the queue and
combine the packets in one large transfer, where as 1) has caused the
original host to see them as 2 transfers. This patch fixes this by stopping
combinging if we detect a 16k transfer with its int_req flag set.

This should not adversely effect performance for other cases as:
1) Linux never sets the interrupt flag on packets other then the last
2) Windows does set the in_req flag on each td, but will submit large
transfers in 20k tds thus never triggering the check

Signed-off-by: Hans de Goede <hdegoede@redhat.com>
Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
2012-11-01 15:17:58 +01:00

183 lines
6 KiB
C

/*
* QEMU USB packet combining code (for input pipelining)
*
* Copyright(c) 2012 Red Hat, Inc.
*
* Red Hat Authors:
* Hans de Goede <hdegoede@redhat.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or(at your option) any later version.
*
* This library 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
* Lesser 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 "qemu-common.h"
#include "hw/usb.h"
#include "iov.h"
#include "trace.h"
static void usb_combined_packet_add(USBCombinedPacket *combined, USBPacket *p)
{
qemu_iovec_concat(&combined->iov, &p->iov, 0, p->iov.size);
QTAILQ_INSERT_TAIL(&combined->packets, p, combined_entry);
p->combined = combined;
}
static void usb_combined_packet_remove(USBCombinedPacket *combined,
USBPacket *p)
{
assert(p->combined == combined);
p->combined = NULL;
QTAILQ_REMOVE(&combined->packets, p, combined_entry);
}
/* Also handles completion of non combined packets for pipelined input eps */
void usb_combined_input_packet_complete(USBDevice *dev, USBPacket *p)
{
USBCombinedPacket *combined = p->combined;
USBEndpoint *ep = p->ep;
USBPacket *next;
enum { completing, complete, leftover };
int result, state = completing;
bool short_not_ok;
if (combined == NULL) {
usb_packet_complete_one(dev, p);
goto leave;
}
assert(combined->first == p && p == QTAILQ_FIRST(&combined->packets));
result = combined->first->result;
short_not_ok = QTAILQ_LAST(&combined->packets, packets_head)->short_not_ok;
QTAILQ_FOREACH_SAFE(p, &combined->packets, combined_entry, next) {
if (state == completing) {
/* Distribute data over uncombined packets */
if (result >= p->iov.size) {
p->result = p->iov.size;
} else {
/* Send short or error packet to complete the transfer */
p->result = result;
state = complete;
}
p->short_not_ok = short_not_ok;
usb_combined_packet_remove(combined, p);
usb_packet_complete_one(dev, p);
result -= p->result;
} else {
/* Remove any leftover packets from the queue */
state = leftover;
p->result = USB_RET_REMOVE_FROM_QUEUE;
dev->port->ops->complete(dev->port, p);
}
}
/*
* If we had leftover packets the hcd driver will have cancelled them
* and usb_combined_packet_cancel has already freed combined!
*/
if (state != leftover) {
g_free(combined);
}
leave:
/* Check if there are packets in the queue waiting for our completion */
usb_ep_combine_input_packets(ep);
}
/* May only be called for combined packets! */
void usb_combined_packet_cancel(USBDevice *dev, USBPacket *p)
{
USBCombinedPacket *combined = p->combined;
assert(combined != NULL);
usb_combined_packet_remove(combined, p);
if (p == combined->first) {
usb_device_cancel_packet(dev, p);
}
if (QTAILQ_EMPTY(&combined->packets)) {
g_free(combined);
}
}
/*
* Large input transfers can get split into multiple input packets, this
* function recombines them, removing the short_not_ok checks which all but
* the last packet of such splits transfers have, thereby allowing input
* transfer pipelining (which we cannot do on short_not_ok transfers)
*/
void usb_ep_combine_input_packets(USBEndpoint *ep)
{
USBPacket *p, *u, *next, *prev = NULL, *first = NULL;
USBPort *port = ep->dev->port;
int ret, totalsize;
assert(ep->pipeline);
assert(ep->pid == USB_TOKEN_IN);
QTAILQ_FOREACH_SAFE(p, &ep->queue, queue, next) {
/* Empty the queue on a halt */
if (ep->halted) {
p->result = USB_RET_REMOVE_FROM_QUEUE;
port->ops->complete(port, p);
continue;
}
/* Skip packets already submitted to the device */
if (p->state == USB_PACKET_ASYNC) {
prev = p;
continue;
}
usb_packet_check_state(p, USB_PACKET_QUEUED);
/*
* If the previous (combined) packet has the short_not_ok flag set
* stop, as we must not submit packets to the device after a transfer
* ending with short_not_ok packet.
*/
if (prev && prev->short_not_ok) {
break;
}
if (first) {
if (first->combined == NULL) {
USBCombinedPacket *combined = g_new0(USBCombinedPacket, 1);
combined->first = first;
QTAILQ_INIT(&combined->packets);
qemu_iovec_init(&combined->iov, 2);
usb_combined_packet_add(combined, first);
}
usb_combined_packet_add(first->combined, p);
} else {
first = p;
}
/* Is this packet the last one of a (combined) transfer? */
totalsize = (p->combined) ? p->combined->iov.size : p->iov.size;
if ((p->iov.size % ep->max_packet_size) != 0 || !p->short_not_ok ||
next == NULL ||
/* Work around for Linux usbfs bulk splitting + migration */
(totalsize == 16348 && p->int_req)) {
ret = usb_device_handle_data(ep->dev, first);
assert(ret == USB_RET_ASYNC);
if (first->combined) {
QTAILQ_FOREACH(u, &first->combined->packets, combined_entry) {
usb_packet_set_state(u, USB_PACKET_ASYNC);
}
} else {
usb_packet_set_state(first, USB_PACKET_ASYNC);
}
first = NULL;
prev = p;
}
}
}