diff options
Diffstat (limited to 'hw/usb/combined-packet.c')
-rw-r--r-- | hw/usb/combined-packet.c | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/hw/usb/combined-packet.c b/hw/usb/combined-packet.c new file mode 100644 index 0000000000..652bf02628 --- /dev/null +++ b/hw/usb/combined-packet.c @@ -0,0 +1,179 @@ +/* + * 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; + + 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? */ + if ((p->iov.size % ep->max_packet_size) != 0 || !p->short_not_ok || + next == NULL) { + 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; + } + } +} |