Commit 3c770d7292 for qemu.org

commit 3c770d72925eb06c1478612233e103e3de9b311f
Author: Junjie Cao <junjie.cao@intel.com>
Date:   Mon Sep 21 13:46:31 2026 +0800

    tests/qtest/usb-hcd-xhci: test isoch pacing with MFINDEX above 2^32

    Add the minimum needed to drive a transfer ring from the test. Use it to
    queue an isoch TD with SIA on usb-audio after stepping the clock past
    2^32 microframes, and check that the TD waits for the next interval
    boundary. Before the ROUND_UP() change it completed as soon as the
    doorbell was rung.

    The machine is started with pit=off. With the i8254 present the clock
    step takes 26s in an ASan build instead of well under a second.

    Signed-off-by: Junjie Cao <junjie.cao@intel.com>
    Acked-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
    Message-ID: <eb57b4b9bd78f08d248fa1ae7ed5d2d5958c9ef4.1789968699.git.junjie.cao@intel.com>
    Signed-off-by: Thomas Huth <thuth@redhat.com>

diff --git a/tests/qtest/usb-hcd-xhci-test.c b/tests/qtest/usb-hcd-xhci-test.c
index b58fa1e2da..61eca37b15 100644
--- a/tests/qtest/usb-hcd-xhci-test.c
+++ b/tests/qtest/usb-hcd-xhci-test.c
@@ -10,8 +10,68 @@
 #include "qemu/osdep.h"
 #include "libqtest-single.h"
 #include "libqos/usb.h"
+#include "libqos/malloc-pc.h"
 #include "qobject/qdict.h"

+/* capability registers */
+#define XHCI_CAPLENGTH          0x00
+#define XHCI_HCSPARAMS1         0x04
+#define XHCI_DBOFF              0x14
+#define XHCI_RTSOFF             0x18
+/* operational registers */
+#define XHCI_USBCMD             0x00
+#define XHCI_USBSTS             0x04
+#define XHCI_CRCR               0x18
+#define XHCI_DCBAAP             0x30
+#define XHCI_CONFIG             0x38
+#define XHCI_PORTSC(n)          (0x400 + 0x10 * (n))
+/* interrupter 0, relative to the runtime registers */
+#define XHCI_ERSTSZ             0x28
+#define XHCI_ERSTBA             0x30
+#define XHCI_ERDP               0x38
+
+#define USBCMD_RS               (1 << 0)
+#define USBCMD_HCRST            (1 << 1)
+#define USBSTS_HCH              (1 << 0)
+#define PORTSC_CCS              (1 << 0)
+#define PORTSC_PR               (1 << 4)
+#define PORTSC_PP               (1 << 9)
+#define CRCR_RCS                (1 << 0)
+#define ERDP_EHB                (1 << 3)
+
+#define TRB_C                   (1 << 0)
+#define TRB_TR_IOC              (1 << 5)
+#define TRB_TR_SIA              (1U << 31)
+#define TRB_TYPE(t)             ((t) << 10)
+#define TRB_GET_TYPE(control)   (((control) >> 10) & 0x3f)
+#define TRB_GET_CCODE(status)   ((status) >> 24)
+#define TRB_GET_SLOT(control)   ((control) >> 24)
+
+#define TR_ISOCH                5
+#define CR_ENABLE_SLOT          9
+#define CR_ADDRESS_DEVICE       11
+#define CR_CONFIGURE_ENDPOINT   12
+#define ER_TRANSFER             32
+#define ER_COMMAND_COMPLETE     33
+#define CC_SUCCESS              1
+
+#define EP_TYPE_ISOCH_OUT       1
+#define EP_TYPE_CONTROL         4
+
+#define XHCI_RING_TRBS          64
+#define XHCI_MICROFRAME_NS      125000
+
+typedef struct XHCITest {
+    QTestState *qts;
+    QGuestAllocator alloc;
+    QPCIBus *bus;
+    struct qhc hc;
+    uint32_t oper, runtime, doorbell;
+    uint64_t cmd_ring, event_ring, input_ctx;
+    unsigned int cmd_idx, event_idx;
+    unsigned int port, slot;
+} XHCITest;
+
 static void wait_device_deleted_event(QTestState *qtest, const char *id)
 {
     QDict *resp, *data;
@@ -109,6 +169,227 @@ static void test_usb_ccid_hotplug(void)
     qtest_qmp_device_del(qts, "ccid");
 }

+static uint32_t xhci_readl(XHCITest *x, uint32_t off)
+{
+    return qpci_io_readl(x->hc.dev, x->hc.bar, off);
+}
+
+static void xhci_writel(XHCITest *x, uint32_t off, uint32_t val)
+{
+    qpci_io_writel(x->hc.dev, x->hc.bar, off, val);
+}
+
+static void xhci_writeq(XHCITest *x, uint32_t off, uint64_t val)
+{
+    xhci_writel(x, off, val);
+    xhci_writel(x, off + 4, val >> 32);
+}
+
+static uint64_t xhci_alloc_page(XHCITest *x)
+{
+    uint64_t addr = guest_alloc(&x->alloc, 0x1000);
+
+    qtest_memset(x->qts, addr, 0, 0x1000);
+    return addr;
+}
+
+static void xhci_write_trb(XHCITest *x, uint64_t addr, uint64_t parameter,
+                           uint32_t status, uint32_t control)
+{
+    qtest_writeq(x->qts, addr, parameter);
+    qtest_writel(x->qts, addr + 8, status);
+    qtest_writel(x->qts, addr + 12, control);
+}
+
+/* Fetch the next event if there is one. Does not advance the clock. */
+static bool xhci_next_event(XHCITest *x, uint32_t *status, uint32_t *control)
+{
+    uint64_t addr = x->event_ring + 16 * x->event_idx;
+    uint32_t c = qtest_readl(x->qts, addr + 12);
+
+    if (!(c & TRB_C)) {
+        return false;
+    }
+    if (status) {
+        *status = qtest_readl(x->qts, addr + 8);
+    }
+    if (control) {
+        *control = c;
+    }
+    x->event_idx++;
+    g_assert_cmpuint(x->event_idx, <, XHCI_RING_TRBS);
+    xhci_writeq(x, x->runtime + XHCI_ERDP, (addr + 16) | ERDP_EHB);
+    return true;
+}
+
+static unsigned int xhci_command(XHCITest *x, uint64_t parameter,
+                                 uint32_t control)
+{
+    uint32_t status;
+
+    g_assert_cmpuint(x->cmd_idx, <, XHCI_RING_TRBS);
+    xhci_write_trb(x, x->cmd_ring + 16 * x->cmd_idx++, parameter, 0,
+                   control | TRB_C);
+    xhci_writel(x, x->doorbell, 0);
+
+    g_assert_true(xhci_next_event(x, &status, &control));
+    g_assert_cmpuint(TRB_GET_TYPE(control), ==, ER_COMMAND_COMPLETE);
+    g_assert_cmpuint(TRB_GET_CCODE(status), ==, CC_SUCCESS);
+    return TRB_GET_SLOT(control);
+}
+
+/*
+ * Start qemu-xhci with one USB device, run the controller and bring the
+ * device to the Addressed state.
+ */
+static void xhci_test_start(XHCITest *x, const char *usb_device)
+{
+    uint64_t dcbaa, erst, ep0_ring;
+    unsigned int maxports;
+
+    memset(x, 0, sizeof(*x));
+    /* pit=off: a long clock step would run the i8254 timer all the way */
+    x->qts = qtest_initf("-machine pc,pit=off -nodefaults "
+                         "-device qemu-xhci,id=xhci,addr=04.0 %s", usb_device);
+    pc_alloc_init(&x->alloc, x->qts, ALLOC_NO_FLAGS);
+    x->bus = qpci_new_pc(x->qts, NULL);
+    qusb_pci_init_one(x->bus, &x->hc, QPCI_DEVFN(4, 0), 0);
+
+    x->oper = qpci_io_readb(x->hc.dev, x->hc.bar, XHCI_CAPLENGTH);
+    x->runtime = xhci_readl(x, XHCI_RTSOFF) & ~0x1f;
+    x->doorbell = xhci_readl(x, XHCI_DBOFF) & ~0x3;
+    maxports = xhci_readl(x, XHCI_HCSPARAMS1) >> 24;
+
+    xhci_writel(x, x->oper + XHCI_USBCMD, USBCMD_HCRST);
+    g_assert_false(xhci_readl(x, x->oper + XHCI_USBCMD) & USBCMD_HCRST);
+
+    dcbaa = xhci_alloc_page(x);
+    erst = xhci_alloc_page(x);
+    x->cmd_ring = xhci_alloc_page(x);
+    x->event_ring = xhci_alloc_page(x);
+    x->input_ctx = xhci_alloc_page(x);
+
+    xhci_writel(x, x->oper + XHCI_CONFIG, 1);
+    xhci_writeq(x, x->oper + XHCI_DCBAAP, dcbaa);
+    qtest_writeq(x->qts, erst, x->event_ring);
+    qtest_writel(x->qts, erst + 8, XHCI_RING_TRBS);
+    xhci_writel(x, x->runtime + XHCI_ERSTSZ, 1);
+    xhci_writeq(x, x->runtime + XHCI_ERSTBA, erst);
+    xhci_writeq(x, x->runtime + XHCI_ERDP, x->event_ring | ERDP_EHB);
+    xhci_writeq(x, x->oper + XHCI_CRCR, x->cmd_ring | CRCR_RCS);
+    xhci_writel(x, x->oper + XHCI_USBCMD, USBCMD_RS);
+    g_assert_false(xhci_readl(x, x->oper + XHCI_USBSTS) & USBSTS_HCH);
+
+    for (x->port = 0; x->port < maxports; x->port++) {
+        if (xhci_readl(x, x->oper + XHCI_PORTSC(x->port)) & PORTSC_CCS) {
+            break;
+        }
+    }
+    g_assert_cmpuint(x->port, <, maxports);
+    xhci_writel(x, x->oper + XHCI_PORTSC(x->port), PORTSC_PP | PORTSC_PR);
+    while (xhci_next_event(x, NULL, NULL)) {
+        /* drop the port status change events */
+    }
+
+    x->slot = xhci_command(x, 0, TRB_TYPE(CR_ENABLE_SLOT));
+    qtest_writeq(x->qts, dcbaa + 8 * x->slot, xhci_alloc_page(x));
+
+    /* input control context: add slot and ep0 */
+    qtest_writel(x->qts, x->input_ctx + 0x04, 0x3);
+    /* slot context: one context entry, root hub port */
+    qtest_writel(x->qts, x->input_ctx + 0x20, 1 << 27);
+    qtest_writel(x->qts, x->input_ctx + 0x24, (x->port + 1) << 16);
+    /* ep0 context */
+    ep0_ring = xhci_alloc_page(x);
+    qtest_writel(x->qts, x->input_ctx + 0x44,
+                 (64 << 16) | (EP_TYPE_CONTROL << 3));
+    qtest_writeq(x->qts, x->input_ctx + 0x48, ep0_ring | 1);
+    xhci_command(x, x->input_ctx,
+                 TRB_TYPE(CR_ADDRESS_DEVICE) | (x->slot << 24));
+}
+
+/* Returns the address of the transfer ring. */
+static uint64_t xhci_configure_ep(XHCITest *x, unsigned int epid,
+                                  unsigned int type, unsigned int interval,
+                                  unsigned int max_packet)
+{
+    uint64_t ring = xhci_alloc_page(x);
+    uint64_t epctx = x->input_ctx + 0x20 * (epid + 1);
+
+    qtest_memset(x->qts, x->input_ctx, 0, 0x1000);
+    qtest_writel(x->qts, x->input_ctx + 0x04, (1 << epid) | 1);
+    qtest_writel(x->qts, x->input_ctx + 0x20, epid << 27);
+    qtest_writel(x->qts, x->input_ctx + 0x24, (x->port + 1) << 16);
+    qtest_writel(x->qts, epctx + 0x00, interval << 16);
+    qtest_writel(x->qts, epctx + 0x04, (max_packet << 16) | (type << 3));
+    qtest_writeq(x->qts, epctx + 0x08, ring | 1);
+    xhci_command(x, x->input_ctx,
+                 TRB_TYPE(CR_CONFIGURE_ENDPOINT) | (x->slot << 24));
+    return ring;
+}
+
+static void xhci_test_end(XHCITest *x)
+{
+    g_free(x->hc.dev);
+    qpci_free_pc(x->bus);
+    alloc_destroy(&x->alloc);
+    qtest_quit(x->qts);
+}
+
+static bool xhci_test_supported(const char *usb_device)
+{
+    const char *arch = qtest_get_arch();
+
+    if (strcmp(arch, "i386") != 0 && strcmp(arch, "x86_64") != 0) {
+        g_test_skip("Test only runs on x86 (pc machine)");
+        return false;
+    }
+    if (!qtest_has_device("qemu-xhci") || !qtest_has_device(usb_device)) {
+        g_test_skip("Devices not available");
+        return false;
+    }
+    return true;
+}
+
+/*
+ * An isoch TD with SIA set is run at the next interval boundary. That has to
+ * hold once the microframe index no longer fits in 32 bits as well.
+ */
+static void test_xhci_isoch_mfindex_32bit(void)
+{
+    const unsigned int interval = 6;
+    uint32_t control;
+    uint64_t ring;
+    XHCITest x;
+
+    if (!xhci_test_supported("usb-audio")) {
+        return;
+    }
+
+    xhci_test_start(&x, "-audiodev none,id=snd0 "
+                        "-device usb-audio,audiodev=snd0");
+    ring = xhci_configure_ep(&x, 2, EP_TYPE_ISOCH_OUT, interval, 64);
+
+    /* Go past 2^32 microframes and stop off an interval boundary. */
+    qtest_clock_step(x.qts, (1ULL << 32) * XHCI_MICROFRAME_NS);
+    qtest_clock_step(x.qts, 5 * XHCI_MICROFRAME_NS);
+
+    xhci_write_trb(&x, ring, xhci_alloc_page(&x), 64,
+                   TRB_TYPE(TR_ISOCH) | TRB_TR_SIA | TRB_TR_IOC | TRB_C);
+    xhci_writel(&x, x.doorbell + 4 * x.slot, 2);
+    g_assert_false(xhci_next_event(&x, NULL, NULL));
+
+    /*
+     * The streaming interface has not been enabled, so usb-audio stalls the
+     * TD. What matters is when that happens.
+     */
+    qtest_clock_step(x.qts, XHCI_MICROFRAME_NS << interval);
+    g_assert_true(xhci_next_event(&x, NULL, &control));
+    g_assert_cmpuint(TRB_GET_TYPE(control), ==, ER_TRANSFER);
+
+    xhci_test_end(&x);
+}
+
 int main(int argc, char **argv)
 {
     int ret;
@@ -123,6 +404,8 @@ int main(int argc, char **argv)
     if (qtest_has_device("usb-ccid")) {
         qtest_add_func("/xhci/pci/hotplug/usb-ccid", test_usb_ccid_hotplug);
     }
+    qtest_add_func("/xhci/pci/isoch/mfindex-32bit",
+                   test_xhci_isoch_mfindex_32bit);

     qtest_start("-device nec-usb-xhci,id=xhci"
                 " -drive id=drive0,if=none,file=null-co://,"