get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/1.1/patches/2230516/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2230516,
    "url": "http://patchwork.ozlabs.org/api/1.1/patches/2230516/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260429135717.3048713-6-joshualant@gmail.com/",
    "project": {
        "id": 14,
        "url": "http://patchwork.ozlabs.org/api/1.1/projects/14/?format=api",
        "name": "QEMU Development",
        "link_name": "qemu-devel",
        "list_id": "qemu-devel.nongnu.org",
        "list_email": "qemu-devel@nongnu.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20260429135717.3048713-6-joshualant@gmail.com>",
    "date": "2026-04-29T13:48:39",
    "name": "[RFC,QEMU,05/10] cxl-vcs-switch: Initial support for CXL VCS.",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "59a46338e8836da2fbfd2f215dd2921ba755c032",
    "submitter": {
        "id": 93283,
        "url": "http://patchwork.ozlabs.org/api/1.1/people/93283/?format=api",
        "name": "Joshua Lant",
        "email": "joshualant@gmail.com"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260429135717.3048713-6-joshualant@gmail.com/mbox/",
    "series": [
        {
            "id": 502144,
            "url": "http://patchwork.ozlabs.org/api/1.1/series/502144/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/list/?series=502144",
            "date": "2026-04-29T13:48:40",
            "name": "Initial Support for VCS Switching",
            "version": 1,
            "mbox": "http://patchwork.ozlabs.org/series/502144/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2230516/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/2230516/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>",
        "X-Original-To": "incoming@patchwork.ozlabs.org",
        "Delivered-To": "patchwork-incoming@legolas.ozlabs.org",
        "Authentication-Results": [
            "legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256\n header.s=20251104 header.b=jYV3Ynpj;\n\tdkim-atps=neutral",
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists1p.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=patchwork.ozlabs.org)"
        ],
        "Received": [
            "from lists1p.gnu.org (lists1p.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4g5Stc3bbRz1yK5\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 30 Apr 2026 06:04:07 +1000 (AEST)",
            "from localhost ([::1] helo=lists1p.gnu.org)\n\tby lists1p.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces@nongnu.org>)\n\tid 1wIB7v-0003Vj-N9; Wed, 29 Apr 2026 16:03:19 -0400",
            "from eggs.gnu.org ([2001:470:142:3::10])\n by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <joshualant@gmail.com>)\n id 1wI5QV-0008Jl-R7\n for qemu-devel@nongnu.org; Wed, 29 Apr 2026 09:58:08 -0400",
            "from mail-wm1-x331.google.com ([2a00:1450:4864:20::331])\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128)\n (Exim 4.90_1) (envelope-from <joshualant@gmail.com>)\n id 1wI5QR-0007hs-R3\n for qemu-devel@nongnu.org; Wed, 29 Apr 2026 09:58:06 -0400",
            "by mail-wm1-x331.google.com with SMTP id\n 5b1f17b1804b1-48334ee0aeaso128830005e9.1\n for <qemu-devel@nongnu.org>; Wed, 29 Apr 2026 06:58:03 -0700 (PDT)",
            "from node1.manccluster.local (revolution.cs.man.ac.uk.\n [130.88.198.135]) by smtp.gmail.com with ESMTPSA id\n ffacd0b85a97d-447b3d48517sm5205950f8f.5.2026.04.29.06.58.00\n (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n Wed, 29 Apr 2026 06:58:01 -0700 (PDT)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=gmail.com; s=20251104; t=1777471082; x=1778075882; darn=nongnu.org;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=mwHA3nK3tLpyQIT1y1aThj/ymGzWpwwSiiTYweRTIgw=;\n b=jYV3YnpjAahmWAP75TP9MJLT9r94bhW+9vIb1nqfNn+QplexstLdeQ/jBShvluXh5C\n iAR4thWIvedYhMo7WvT6E4akEf32sZ8xGFxXDJAM0i0Do/CpdbERXjOXbd+QToTfdvXD\n ufjUDVs3rEnhhJ0Zo3ztplXCF/VNUqcr9NgT7st9HFUOYvFgh7CVb4SRJkwSEpuKVSzM\n t+g07AkqDCCqBkpVEixA0/8us2U57Ns62Lz+tDR3kmrNWDr2fn0ZUSLn6RtfqN/VGB62\n XdSBSDwdKznCuFq/2eObDYYhfQaPsav0o0RgcLz8URaeJvJcrBsdbpqvt+FooZhxu6fq\n O+8A==",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1777471082; x=1778075882;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n :to:cc:subject:date:message-id:reply-to;\n bh=mwHA3nK3tLpyQIT1y1aThj/ymGzWpwwSiiTYweRTIgw=;\n b=meUwZlgUmDvzAozFry/068V0cUkJpbrcnmmRi5D04ia431aCrAxtsStsEGLJz2P9zh\n O0/+jslVgwzVvIu7byQtIqoTgcuFFELAzvTPZ21zplOpeDIFPnefXFUJz1uFmRRqEWcn\n 2PQTDcSHl+9+tVhu6hL1j4XLq8xFsH2U9AC1O9FRR6z3tuiVZ7NpRnM/7JcM97Rm+Tsl\n Cm894mSj0aq3HSPIMAELSZXKn1iJPYVfW2WuTIsjcP94LKLefzBgH5QKYKol2tC5OL3+\n Ca5u1MAgKkixkCgJ7H+TG77yFQP2GiuqMUP6cwiGN78SIPBwJliwI53CkDYhPJc3qjqb\n UbNQ==",
        "X-Gm-Message-State": "AOJu0YztjVDItzRUB+hB2qf6WOgCHf46oZu4cckUH6Dnidz43s/+Pn7h\n XWbfUM9+3os+3fHXEfhnbz6349oKuq0fXLA2jwQfj0mwWY/PkiORSW19PxCERVIK",
        "X-Gm-Gg": "AeBDievbVkxcM5OARxQC+MoR2HfeLymkiJ3J7SBNh+6IrdOG37KpcLR8i/74clmN06s\n lBX7mYhNSkF5LtbGg4MGE0L12HD3wuHk6siShWDVNg1gbZWVO+QpY6Uy++a8w9fdtmRyKIo9ixY\n 5YKt+oNGEL0SPPoVo+Ng33xKb7/b8BzrJj8ftC/Fun9WquwQqd/MzzqKyvL64d5yXlxgMMgS9s2\n 2nbEa87+FroTUw30/+vUVDnARwrfXWrBPLseZZYCa0IclZIHCqa66915WTc0Y3skuHGMj8giuhV\n EuErus1F9EaJIlppBbTcfRlnL3wl4kaTmQVqtvjoQdkh9i8iLYF7qVJMloh6ZoNft2D3D0m6ytv\n sYEptpU9yN41fTbXwBONOIa3s56by1NFRnNeL9R8E7YpIDS6WugokmSyc3QYoZGDKpAXIHh4uPr\n 7uPZsCEId6cm0kebhcXZZIF2b+kB5XBbny1ahCPKWkGGGYCx8IHwulpR09F35PjHufFbR0Bj9L",
        "X-Received": "by 2002:a05:600c:3f18:b0:485:35d3:ce59 with SMTP id\n 5b1f17b1804b1-48a7b51beebmr72235865e9.10.1777471082058;\n Wed, 29 Apr 2026 06:58:02 -0700 (PDT)",
        "From": "Joshua Lant <joshualant@gmail.com>",
        "To": "linux-cxl@vger.kernel.org",
        "Cc": "qemu-devel@nongnu.org, Jonathan.Cameron@huawei.com,\n arpit1.kumar@samsung.com, Joshua Lant <joshualant@gmail.com>",
        "Subject": "[RFC QEMU PATCH 05/10] cxl-vcs-switch: Initial support for CXL VCS.",
        "Date": "Wed, 29 Apr 2026 14:48:39 +0100",
        "Message-ID": "<20260429135717.3048713-6-joshualant@gmail.com>",
        "X-Mailer": "git-send-email 2.47.3",
        "In-Reply-To": "<20260429135717.3048713-1-joshualant@gmail.com>",
        "References": "<20260429135717.3048713-1-joshualant@gmail.com>",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=UTF-8",
        "Content-Transfer-Encoding": "8bit",
        "Received-SPF": "pass client-ip=2a00:1450:4864:20::331;\n envelope-from=joshualant@gmail.com; helo=mail-wm1-x331.google.com",
        "X-Spam_score_int": "-20",
        "X-Spam_score": "-2.1",
        "X-Spam_bar": "--",
        "X-Spam_report": "(-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,\n DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001,\n RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001,\n SPF_PASS=-0.001 autolearn=ham autolearn_force=no",
        "X-Spam_action": "no action",
        "X-Mailman-Approved-At": "Wed, 29 Apr 2026 16:03:16 -0400",
        "X-BeenThere": "qemu-devel@nongnu.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "qemu development <qemu-devel.nongnu.org>",
        "List-Unsubscribe": "<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.nongnu.org/archive/html/qemu-devel>",
        "List-Post": "<mailto:qemu-devel@nongnu.org>",
        "List-Help": "<mailto:qemu-devel-request@nongnu.org?subject=help>",
        "List-Subscribe": "<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>",
        "Errors-To": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org",
        "Sender": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org"
    },
    "content": "Add initial support for VCS (multi-USP/multi-logic-device) switch\nemulation in CXL. Currently the object only supports the identify/bind/unbind\ncommands (0x5200/0x5201/0x5202), as well as support for device hiding\nlisteners. This enables preliminary testing of the VCS capability,\nbut will not allow for complete emulated flow as described in\nspec (CXL v3.2, 7.2.3). Nor will it allow for multiple USP's to be\ndisributed over multiple QEMU instances.\n\nSigned-off-by: Joshua Lant <joshualant@gmail.com>\n---\n hw/cxl/cxl-vcs-switch.c         | 524 ++++++++++++++++++++++++++++++++\n hw/cxl/meson.build              |   1 +\n include/hw/cxl/cxl_vcs_switch.h | 134 ++++++++\n qapi/qom.json                   |  19 ++\n 4 files changed, 678 insertions(+)\n create mode 100644 hw/cxl/cxl-vcs-switch.c\n create mode 100644 include/hw/cxl/cxl_vcs_switch.h",
    "diff": "diff --git a/hw/cxl/cxl-vcs-switch.c b/hw/cxl/cxl-vcs-switch.c\nnew file mode 100644\nindex 0000000000..9a492330cc\n--- /dev/null\n+++ b/hw/cxl/cxl-vcs-switch.c\n@@ -0,0 +1,524 @@\n+/*\n+ * CXL VCS Capable Switch Object\n+ *\n+ * Copyright(C) 2026 University of Manchester.\n+ * Author: Joshua Lant <joshualant@gmail.com>.\n+ *\n+ * This work is licensed under the terms of the GNU GPL, version 2. See the\n+ * COPYING file in the top-level directory.\n+ *\n+ * SPDX-License-Identifier: GPL-v2-only\n+ */\n+#include \"hw/cxl/cxl_vcs_switch.h\"\n+#include \"qobject/qdict.h\"\n+#include \"monitor/qdev.h\"\n+\n+/* Primary FM here is used when the FM is local to the QEMU instance. Setting\n+ * local-fm=false in cli results in instantiation of remote listener for FM\n+ * commands (TODO: to be implemented) */\n+static bool cxl_vcs_get_local_fm(Object *obj, Error **errp)\n+{\n+    CXLVCSSwitch *vcs = CXL_VCS_SWITCH(obj);\n+    return vcs->local_fm;\n+}\n+static void cxl_vcs_set_local_fm(Object *obj, bool value, Error **errp)\n+{\n+    CXLVCSSwitch *vcs = CXL_VCS_SWITCH(obj);\n+    vcs->local_fm = value;\n+}\n+\n+/* Binds an already realized MLD to a VPPB in the switch */\n+static CXLRetCode cxl_vcs_bind_realized_vppb(CXLVCSSwitch *sw, uint8_t vcs_id,\n+        uint8_t vppb_id, uint8_t dsp_ppb_id, uint8_t ld_id)\n+{\n+    /* TODO: Implement... Required for handling MLD devices... */\n+    return CXL_MBOX_UNSUPPORTED;\n+}\n+\n+/* Binds a hidden device (simple SLD) to a VPPB in the switch */\n+static CXLRetCode cxl_vcs_bind_qdict_vppb(CXLVCSSwitch *sw, uint8_t vcs_id,\n+        uint8_t vppb_id, uint8_t dsp_ppb_id, uint8_t ld_id)\n+{\n+    Error *local_err = NULL;\n+    CXLVPPBInfo *vppb;\n+    CXLDownstreamPPB *dspppb;\n+    QDict *bind_opts;\n+    DeviceState *dev;\n+\n+    // Upstream to bind\n+    if(!sw->usp_ppbs[vcs_id]) {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+    vppb = sw->usp_ppbs[vcs_id]->info->vppbs[vppb_id];\n+    if(!vppb) {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+    PCIDevice *vppb_dev = PCI_DEVICE(vppb->dsp);\n+\n+    //Downstream to bind\n+    dspppb = sw->dsp_ppbs[dsp_ppb_id];\n+    if(!dspppb) {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+\n+    // Get DSP bus id from qdev\n+    bind_opts = qdict_clone_shallow(dspppb->opts);\n+    qdict_put_str(bind_opts, \"bus\", vppb_dev->qdev.id);\n+    qdict_del(bind_opts, \"vcs\");\n+    qdict_del(bind_opts, \"dsppb\");\n+\n+    uint8_t prev_binding_status = vppb->binding_status;\n+    vppb->binding_status = CXL_VPPB_BINDING_STATUS_IN_PROGRESS;\n+    dev = qdev_device_add_from_qdict(bind_opts, dspppb->from_json, &local_err);\n+    if(!dev || local_err) {\n+        vppb->binding_status = prev_binding_status;\n+        return CXL_MBOX_INTERNAL_ERROR;\n+    }\n+\n+    vppb->bound_port_id = dsp_ppb_id;\n+    vppb->bound_ld_id = CXL_INVALID_BOUND_LD_ID; // TODO: support MLDs.\n+    vppb->binding_status = CXL_VPPB_BINDING_STATUS_BOUND_PORT;\n+    dspppb->dev = dev;\n+    dspppb->is_bound = true;\n+    dspppb->bound_vcs_id = vcs_id;\n+    dspppb->bound_vppb_id = vppb_id;\n+    qobject_unref(bind_opts);\n+\n+    return CXL_MBOX_SUCCESS;\n+}\n+\n+CXLRetCode cxl_vcs_bind_vppb(CXLVCSSwitch *sw, uint8_t vcs_id, uint8_t vppb_id,\n+        uint8_t dsp_ppb_id, uint16_t ld_id)\n+{\n+    if(cxl_vcs_get_local_fm(OBJECT(sw), NULL)) {\n+        if(ld_id == CXL_UNSUPPORTED_LD_ID) {\n+            return cxl_vcs_bind_qdict_vppb(sw, vcs_id, vppb_id,\n+                    dsp_ppb_id, ld_id);\n+        } else {\n+            // TODO: Connect up already realized device\n+            // (needed for MLD support).\n+            return cxl_vcs_bind_realized_vppb(sw, vcs_id, vppb_id,\n+                    dsp_ppb_id, ld_id);\n+        }\n+    }\n+    else {\n+        // TODO: Send bind command to remote QEMU process...\n+        return CXL_MBOX_UNSUPPORTED;\n+    }\n+}\n+\n+static CXLRetCode cxl_vcs_unbind_qdict_vppb(CXLVCSSwitch *sw, uint8_t vcs_id,\n+                                       uint8_t vppb_id, uint8_t options)\n+{\n+    Error *local_err = NULL;\n+    CXLVPPBInfo *vppb_info;\n+    CXLDownstreamPPB *dspppb = NULL;\n+\n+    if(!sw->usp_ppbs[vcs_id]) {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+\n+    vppb_info = sw->usp_ppbs[vcs_id]->info->vppbs[vppb_id];\n+    if (!vppb_info ||\n+        (vppb_info->binding_status == CXL_VPPB_BINDING_STATUS_UNBOUND) ||\n+        (vppb_info->binding_status == CXL_VPPB_BINDING_STATUS_IN_PROGRESS)) {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+\n+    for (int i = 0; i < sw->num_dsp_ppbs; i++) {\n+        if (sw->dsp_ppbs[i] &&\n+                sw->dsp_ppbs[i]->is_bound &&\n+                sw->dsp_ppbs[i]->bound_vcs_id == vcs_id &&\n+                sw->dsp_ppbs[i]->bound_vppb_id == vppb_id) {\n+            dspppb = sw->dsp_ppbs[i];\n+            break;\n+        }\n+    }\n+\n+    if (!dspppb) {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+\n+    if (!dspppb->dev) {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+\n+    /* Options from cxl v3.2 (Table 7-34), Bits[3:0].\n+      — 0h = Wait for port Link Down before unbinding\n+      — 1h = Simulate Managed Hot-Remove\n+      — 2h = Simulate Surprise Hot-Remove */\n+    options = options & 0x0F;\n+    if (options == CXL_VPPB_UNBIND_WAIT_FOR_LINK_DOWN) {\n+        // TODO: implement\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+    else if(options == CXL_VPPB_UNBIND_MANAGED_HOT_REMOVE) {\n+        // unrealize listener will fire once guest notifies the port...\n+        qdev_unplug(dspppb->dev, &local_err);\n+        vppb_info->binding_status = CXL_VPPB_BINDING_STATUS_IN_PROGRESS;\n+        if (local_err) {\n+            return CXL_MBOX_INTERNAL_ERROR;\n+        }\n+        return CXL_MBOX_SUCCESS;\n+    }\n+    else if (options == CXL_VPPB_UNBIND_SURPRISE_HOT_REMOVE) {\n+        // TODO: implement, This isn't a true surprise removal...\n+        qdev_unplug(dspppb->dev, &local_err);\n+        if (local_err) {\n+            return CXL_MBOX_INTERNAL_ERROR;\n+        }\n+        object_unparent(OBJECT(dspppb->dev));\n+        dspppb->is_bound      = false;\n+        dspppb->bound_vcs_id  = 0;\n+        dspppb->bound_vppb_id = 0;\n+\n+        /* Clear VPPB binding state so Get Virtual Switch Info reflects unbound */\n+        vppb_info->binding_status = CXL_VPPB_BINDING_STATUS_UNBOUND;\n+        vppb_info->bound_port_id  = 0;\n+        vppb_info->bound_ld_id    = CXL_INVALID_BOUND_LD_ID;\n+        dspppb->dev = NULL;\n+        /* TODO: Generate Virtual CXL Switch Event Record per CXL spec\n+         * section 7.6.6.6  */\n+        return CXL_MBOX_SUCCESS;\n+    }\n+    else {\n+        return CXL_MBOX_INVALID_INPUT;\n+    }\n+\n+}\n+\n+CXLRetCode cxl_vcs_unbind_vppb(CXLVCSSwitch *sw, uint8_t vcs_id,\n+        uint8_t vppb_id, uint16_t option)\n+{\n+    /*TODO: Only currently unbinding an unrealizing whole device.\n+     * Implement for MLD partial unbinding of single extents etc.\n+     * */\n+    return cxl_vcs_unbind_qdict_vppb(sw, vcs_id, vppb_id, option);\n+}\n+\n+void cxl_vcs_identify_switch_device(CXLVCSSwitch *sw)\n+{\n+    //TODO: implement\n+}\n+void cxl_vcs_get_physical_port_state(CXLVCSSwitch *sw)\n+{\n+    //TODO: implement\n+}\n+void cxl_vcs_physical_port_control(CXLVCSSwitch *sw)\n+{\n+    //TODO: implement\n+}\n+void cxl_vcs_get_virtual_switch_info(CXLVCSSwitch *sw)\n+{\n+    //TODO: implement\n+}\n+\n+void cxl_vcs_register_usp(CXLVCSSwitch *sw, CXLUpstreamPort *usp,\n+        Error **errp)\n+{\n+    uint8_t ppb = usp->ppb;\n+\n+    if(strcmp(object_get_canonical_path_component(OBJECT(sw)), usp->vcs_name)) {\n+        error_setg(errp, \"VCS id for USP and switch do not match...\\n\");\n+    }\n+\n+    if (ppb >= sw->num_usp_ppbs) {\n+        error_setg(errp, \"vcs '%s': ppb %u >= usp-ppbs %u\",\n+                   object_get_canonical_path_component(OBJECT(sw)),\n+                   ppb, sw->num_usp_ppbs);\n+        return;\n+    }\n+    if (sw->usp_ppbs[ppb]) {\n+        error_setg(errp, \"vcs '%s': USP slot %u already registered\",\n+                   object_get_canonical_path_component(OBJECT(sw)), ppb);\n+        return;\n+    }\n+\n+    sw->usp_ppbs[ppb] = g_new0(CXLUpstreamPPB, 1);\n+    sw->usp_ppbs[ppb]->usp = usp;\n+    sw->usp_ppbs[ppb]->info = g_new0(CXLVCSInfoBlock, 1);\n+    sw->usp_ppbs[ppb]->info->vcs_id = ppb;\n+    sw->usp_ppbs[ppb]->info->usp_id = ppb;\n+    sw->usp_ppbs[ppb]->info->num_vppbs = 0;\n+    sw->usp_ppbs[ppb]->info->vcs_state = CXL_VCS_STATE_ENABLED;\n+    usp->swcci.vcs = sw;\n+}\n+\n+void cxl_vcs_register_vppb(CXLVCSSwitch *sw, CXLUpstreamPort *usp,\n+        CXLDownstreamPort *dsp, Error **errp)\n+{\n+    CXLUpstreamPPB *vcs;\n+\n+    for(int i = 0; i < CXL_MAX_VCS_PORTS; i++) {\n+        if(sw->usp_ppbs[i]) {\n+            if(usp == sw->usp_ppbs[i]->usp) {\n+                vcs = sw->usp_ppbs[i];\n+                break;\n+            }\n+        } else {\n+            error_setg(errp, \"The USP was not found in the VCS list...\");\n+            return;\n+        }\n+    }\n+\n+    for(int i = 0; i < CXL_MAX_VPPB_PER_VCS; i++) {\n+        if(!vcs->info->vppbs[i]) {\n+            // Free vppb slot.. lets allocate and populate it...\n+            CXLVPPBInfo *vppb = g_new0(CXLVPPBInfo, 1);\n+            vppb->dsp = PCI_DEVICE(dsp);\n+            vppb->binding_status = CXL_VPPB_BINDING_STATUS_UNBOUND;\n+            vcs->info->vppbs[i] = vppb;\n+            vcs->info->num_vppbs = vcs->info->num_vppbs + 1;\n+            return;\n+        }\n+    }\n+\n+    error_setg(errp, \"No free VPPB slots in the VCS...\");\n+    return;\n+}\n+\n+void cxl_vcs_register_qdict_dsppb(CXLVCSSwitch *sw, const QDict *opts,\n+        bool from_json, Error **errp)\n+{\n+    QDict *dev_opts;\n+    int ppb;\n+    const char *ppb_str = qdict_get_try_str(opts, \"dsppb\");\n+    ppb = atoi(ppb_str);\n+    if(ppb == -1) {\n+        error_setg(errp, \"No ppb id given in cli.\");\n+        return;\n+    }\n+\n+    if (ppb >= sw->num_dsp_ppbs) {\n+        error_setg(errp, \"vcs '%s': dsppb %u >= dsp-ppbs %u\",\n+                   object_get_canonical_path_component(OBJECT(sw)),\n+                   ppb, sw->num_dsp_ppbs);\n+        return;\n+    }\n+    if (sw->dsp_ppbs[ppb]) {\n+        error_setg(errp, \"vcs '%s': DSP PPB slot %u already occupied\",\n+                   object_get_canonical_path_component(OBJECT(sw)), ppb);\n+        return;\n+    }\n+\n+    dev_opts = qdict_clone_shallow(opts);\n+    qdict_del(dev_opts, \"vcs\");\n+    qdict_del(dev_opts, \"dsppb\");\n+    qdict_del(dev_opts, \"bus\");\n+\n+    sw->dsp_ppbs[ppb] = g_new0(CXLDownstreamPPB, 1);\n+    sw->dsp_ppbs[ppb]->opts      = dev_opts;\n+    sw->dsp_ppbs[ppb]->from_json = from_json;\n+    sw->dsp_ppbs[ppb]->is_bound  = false;\n+}\n+\n+void cxl_vcs_register_dsppb(CXLVCSSwitch *sw, const QDict *opts,\n+        bool from_json, Error **errp)\n+{\n+\n+    DeviceState *dev = qdev_new(qdict_get_str(opts, \"driver\"));\n+    qdev_set_id(dev, g_strdup(qdict_get_try_str(opts, \"id\")), errp);\n+    int ppb;\n+    const char *ppb_str = qdict_get_try_str(opts, \"dsppb\");\n+    ppb = atoi(ppb_str);\n+    if(ppb == -1) {\n+        error_setg(errp, \"No ppb id given in cli.\");\n+        return;\n+    }\n+\n+    QDict *dev_opts = qdict_clone_shallow(opts);\n+    qdict_del(dev_opts, \"driver\");\n+    qdict_del(dev_opts, \"bus\");\n+    qdict_del(dev_opts, \"id\");\n+    qdict_del(dev_opts, \"vcs\");\n+    qdict_del(dev_opts, \"dsppb\");\n+    object_set_properties_from_keyval(OBJECT(dev), dev_opts, from_json, errp);\n+    qobject_unref(dev_opts);\n+    // store, don't realize\n+    sw->dsp_ppbs[ppb]->dev = dev;\n+}\n+\n+static bool cxl_vcs_switch_can_be_deleted(UserCreatable *uc)\n+{\n+    return false;\n+}\n+\n+/* When a device is instantiated downstream of a VCS's PPB, we\n+ * store the qdicts from the CLI, to realize the device at a\n+ * future time.\n+ */\n+bool cxl_vcs_hide_device_listener(DeviceListener *listener, const QDict *opts,\n+                        bool from_json, Error **errp)\n+{\n+    CXLVCSSwitch *vcs = container_of(listener, CXLVCSSwitch, listener);\n+    const char *vcs_id_str   = qdict_get_try_str(opts, \"vcs\");\n+    const char *ppb_str = qdict_get_try_str(opts, \"dsppb\");\n+    int ppb;\n+\n+    /* Not our device — don't claim it */\n+    if (!vcs_id_str || !ppb_str) {\n+        return false;\n+    }\n+    if(vcs_id_str) {\n+        if(strcmp(vcs_id_str,\n+                    object_get_canonical_path_component(OBJECT(vcs)))) {\n+            return false;\n+        }\n+    }\n+    if(ppb_str) {\n+        ppb = atoi(ppb_str);\n+        if(vcs->dsp_ppbs[ppb]) {\n+            error_setg(errp,\n+                \"The ppb %s is already populated by another device.\", ppb_str);\n+            return false;\n+        }\n+    }\n+\n+    cxl_vcs_register_qdict_dsppb(vcs, opts, from_json, errp);\n+\n+    return true;\n+}\n+\n+/*\n+ * Listener is added for unbinding. When a device is unbound using the\n+ * Fabric Manager with 'managed' hot-remove option, a notificaiton is\n+ * sent to the guest, which should perform a graceful teardown and notify\n+ * the port of completion. At this point this listener unrealizes the device.\n+ */\n+static void cxl_vcs_ppb_unrealize_listener(DeviceListener *listener,\n+        DeviceState *dev)\n+{\n+    CXLVCSSwitch *sw = container_of(listener, CXLVCSSwitch, listener);\n+    CXLDownstreamPPB *dspppb;\n+    CXLVPPBInfo *vppb_info;\n+\n+    for (int i = 0; i < sw->num_dsp_ppbs; i++) {\n+        if (sw->dsp_ppbs[i] && (sw->dsp_ppbs[i]->dev == dev)) {\n+            dspppb = sw->dsp_ppbs[i];\n+            break;\n+        }\n+    }\n+    if(!dspppb) {\n+        return;\n+    }\n+\n+    vppb_info =\n+        sw->usp_ppbs[dspppb->bound_vcs_id]->info->vppbs[dspppb->bound_vppb_id];\n+    object_unparent(OBJECT(dspppb->dev));\n+    dspppb->dev = NULL;\n+    dspppb->is_bound      = false;\n+    dspppb->bound_vcs_id  = 0;\n+    dspppb->bound_vppb_id = 0;\n+    vppb_info->binding_status = CXL_VPPB_BINDING_STATUS_UNBOUND;\n+    vppb_info->bound_port_id  = 0;\n+    vppb_info->bound_ld_id    = CXL_INVALID_BOUND_LD_ID;\n+    /* TODO: generate Virtual CXL Switch Event Record */\n+}\n+\n+static void cxl_vcs_switch_complete(UserCreatable *uc, Error **errp)\n+{\n+    CXLVCSSwitch *sw = CXL_VCS_SWITCH(uc);\n+    sw->listener.hide_device = cxl_vcs_hide_device_listener;\n+    sw->listener.unrealize = cxl_vcs_ppb_unrealize_listener;\n+    device_listener_register(&sw->listener);\n+}\n+\n+static void vcs_get_usp_ppbs(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    CXLVCSSwitch *vcs = CXL_VCS_SWITCH(obj);\n+    uint8_t val = vcs->num_usp_ppbs;\n+    visit_type_uint8(v, name, &val, errp);\n+}\n+\n+static void vcs_set_usp_ppbs(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    CXLVCSSwitch *vcs = CXL_VCS_SWITCH(obj);\n+    uint8_t val;\n+    if (!visit_type_uint8(v, name, &val, errp)) {\n+        return;\n+    }\n+    vcs->num_usp_ppbs = val;\n+}\n+static void vcs_get_dsp_ppbs(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    CXLVCSSwitch *vcs = CXL_VCS_SWITCH(obj);\n+    uint8_t val = vcs->num_dsp_ppbs;\n+    visit_type_uint8(v, name, &val, errp);\n+}\n+\n+static void vcs_set_dsp_ppbs(Object *obj, Visitor *v, const char *name,\n+                              void *opaque, Error **errp)\n+{\n+    CXLVCSSwitch *vcs = CXL_VCS_SWITCH(obj);\n+    uint8_t val;\n+    if (!visit_type_uint8(v, name, &val, errp)) {\n+        return;\n+    }\n+    vcs->num_dsp_ppbs = val;\n+}\n+\n+static void cxl_vcs_class_init(ObjectClass *oc, const void *data)\n+{\n+    CXLVCSSwitchClass *cc = CXL_VCS_SWITCH_CLASS(oc);\n+    UserCreatableClass *ucc = USER_CREATABLE_CLASS(oc);\n+    ucc->complete = cxl_vcs_switch_complete;\n+    ucc->can_be_deleted = cxl_vcs_switch_can_be_deleted;\n+    cc->identify_switch_device = cxl_vcs_identify_switch_device;\n+    cc->get_physical_port_state = cxl_vcs_get_physical_port_state;\n+    cc->physical_port_control = cxl_vcs_physical_port_control;\n+    cc->get_virtual_switch_info = cxl_vcs_get_virtual_switch_info;\n+    cc->bind_vppb = cxl_vcs_bind_vppb;\n+    cc->unbind_vppb = cxl_vcs_unbind_vppb;\n+\n+    object_class_property_add_bool(oc, \"local-fm\",\n+                              cxl_vcs_get_local_fm, cxl_vcs_set_local_fm);\n+    object_class_property_set_description(oc, \"local-fm\",\n+        \"true = FM authority (mctp connected guest), \\\n+        false = slave listener (IPC)\");\n+    object_class_property_add(oc, \"usp-ppbs\", \"uint8\",\n+                          vcs_get_usp_ppbs, vcs_set_usp_ppbs,\n+                          NULL, NULL);\n+    object_class_property_set_description(oc, \"usp-ppbs\",\n+        \"Number of upstream ports in the switch\");\n+    object_class_property_add(oc, \"dsp-ppbs\", \"uint8\",\n+                          vcs_get_dsp_ppbs, vcs_set_dsp_ppbs,\n+                          NULL, NULL);\n+    object_class_property_set_description(oc, \"dsp-ppbs\",\n+        \"Number of downstream ports in the switch\");\n+}\n+\n+static void cxl_vcs_instance_init(Object *obj)\n+{\n+    // TODO: Nothing here yet...\n+}\n+\n+static void cxl_vcs_instance_finalize(Object *obj)\n+{\n+    // TODO: Nothing here yet...\n+}\n+\n+static const InterfaceInfo cxl_vcs_interfaces[] = {\n+    { TYPE_USER_CREATABLE },\n+    { }\n+};\n+\n+static const TypeInfo cxl_vcs_info = {\n+    .name = TYPE_CXL_VCS_SWITCH,\n+    .parent = TYPE_OBJECT,\n+    .instance_size = sizeof(CXLVCSSwitch),\n+    .class_size = sizeof(CXLVCSSwitchClass),\n+    .class_init = cxl_vcs_class_init,\n+    .instance_init = cxl_vcs_instance_init,\n+    .instance_finalize = cxl_vcs_instance_finalize,\n+    .interfaces = cxl_vcs_interfaces,\n+};\n+\n+static void cxl_vcs_register(void)\n+{\n+    type_register_static(&cxl_vcs_info);\n+}\n+\n+type_init(cxl_vcs_register)\ndiff --git a/hw/cxl/meson.build b/hw/cxl/meson.build\nindex ccad565c5c..c37563cd05 100644\n--- a/hw/cxl/meson.build\n+++ b/hw/cxl/meson.build\n@@ -7,6 +7,7 @@ system_ss.add(when: 'CONFIG_CXL',\n                    'cxl-cdat.c',\n                    'cxl-events.c',\n                    'switch-mailbox-cci.c',\n+                   'cxl-vcs-switch.c',\n                ),\n                if_false: files(\n                    'cxl-host-stubs.c',\ndiff --git a/include/hw/cxl/cxl_vcs_switch.h b/include/hw/cxl/cxl_vcs_switch.h\nnew file mode 100644\nindex 0000000000..6576870bf3\n--- /dev/null\n+++ b/include/hw/cxl/cxl_vcs_switch.h\n@@ -0,0 +1,134 @@\n+/*\n+ * CXL VCS Capable Switch Object Header\n+ *\n+ * Copyright(C) 2026 University of Manchester\n+ * Author: Joshua Lant <joshualant@gmail.com>.\n+ *\n+ * This work is licensed under the terms of the GNU GPL, version 2. See the\n+ * COPYING file in the top-level directory.\n+ *\n+ * SPDX-License-Identifier: GPL-v2-only\n+ */\n+\n+#ifndef CXL_VCS_SWITCH_H\n+#define CXL_VCS_SWITCH_H\n+\n+#include \"qemu/osdep.h\"\n+#include \"qapi/error.h\"\n+#include \"qom/object.h\"\n+#include \"qom/object_interfaces.h\"\n+#include \"hw/pci-bridge/cxl_upstream_port.h\"\n+#include \"hw/pci-bridge/cxl_downstream_port.h\"\n+#include \"include/hw/cxl/cxl_device.h\"\n+\n+#define TYPE_CXL_VCS_SWITCH \"cxl-vcs-switch\"\n+OBJECT_DECLARE_TYPE(CXLVCSSwitch, CXLVCSSwitchClass, CXL_VCS_SWITCH);\n+\n+#define CXL_VPPB_BINDING_STATUS_UNBOUND        0x00\n+#define CXL_VPPB_BINDING_STATUS_IN_PROGRESS    0x01\n+#define CXL_VPPB_BINDING_STATUS_BOUND_PORT     0x02\n+#define CXL_VPPB_BINDING_STATUS_BOUND_LD       0x03\n+#define CXL_VPPB_BINDING_STATUS_BOUND_PID      0x04\n+\n+#define CXL_VPPB_UNBIND_WAIT_FOR_LINK_DOWN      0x0\n+#define CXL_VPPB_UNBIND_MANAGED_HOT_REMOVE      0x1\n+#define CXL_VPPB_UNBIND_SURPRISE_HOT_REMOVE     0x2\n+\n+#define CXL_UNSUPPORTED_LD_ID 0xFFFF\n+#define CXL_INVALID_BOUND_LD_ID 0xFF\n+\n+#define CXL_VCS_STATE_INVALID_ID        0xFF\n+#define CXL_VCS_STATE_DISABLED 0x0\n+#define CXL_VCS_STATE_ENABLED 0x1\n+\n+#define CXL_MAX_VCS_PORTS 0x8\n+#define CXL_MAX_VPPB_PER_VCS 0x8\n+#define CXL_MAX_USP_PPBS CXL_MAX_VCS_PORTS\n+#define CXL_MAX_DSP_PPBS CXL_MAX_VPPB_PER_VCS\n+\n+#define CXL_MAX_PPBS (CXL_MAX_USP_PPBS + CXL_MAX_DSP_PPBS)\n+#define VPPB_LIST_LIMIT 8\n+\n+struct CXLVCSSwitchClass {\n+    ObjectClass parent_class;\n+    void (*identify_switch_device)(CXLVCSSwitch *sw);\n+    void (*get_physical_port_state)(CXLVCSSwitch *sw);\n+    void (*physical_port_control)(CXLVCSSwitch *sw);\n+    void (*get_virtual_switch_info)(CXLVCSSwitch *sw);\n+    CXLRetCode (*bind_vppb)(CXLVCSSwitch *sw, uint8_t vcs_id, uint8_t vppb_id,\n+            uint8_t dsppb_id, uint16_t ld_id);\n+    CXLRetCode (*unbind_vppb)(CXLVCSSwitch *sw, uint8_t vcs_id, uint8_t vppb_id,\n+            uint16_t option);\n+};\n+\n+/* CXL r3.2 Table 7-32: Get Virtual CXL Switch Info VCS Info Block Format */\n+typedef struct CXLVPPBInfo {\n+        PCIDevice *dsp;\n+        uint8_t binding_status;\n+        uint8_t bound_port_id;\n+        uint8_t bound_ld_id;\n+        uint8_t rsv1;\n+} CXLVPPBInfo;\n+\n+typedef struct CXLVCSInfoBlock {\n+        uint8_t vcs_id;\n+        uint8_t vcs_state;\n+        uint8_t usp_id;\n+        uint8_t num_vppbs;\n+        struct CXLVPPBInfo *vppbs[CXL_MAX_VPPB_PER_VCS];\n+} CXLVCSInfoBlock;\n+\n+/* Physical Upstream and Downstream PCI-PCI Bridge (PPBs) structs */\n+typedef struct CXLUpstreamPPB {\n+    CXLUpstreamPort   *usp;\n+    struct CXLVCSInfoBlock *info;\n+} CXLUpstreamPPB;\n+\n+typedef struct CXLDownstreamPPB {\n+    DeviceState *dev;\n+    PCIDevice   *pdev;\n+    // store opts and json in case of simple device hiding.\n+    QDict       *opts;\n+    bool        from_json;\n+    bool        is_bound;\n+    uint8_t     bound_vcs_id;\n+    uint8_t     bound_vppb_id;\n+} CXLDownstreamPPB;\n+\n+typedef struct CXLVCSSwitch {\n+    Object parent;\n+    uint8_t num_usp_ppbs;\n+    uint8_t num_dsp_ppbs;\n+    uint8_t num_vppbs_per_vcs;\n+    CXLCCI swcci;\n+    CXLCCI mctpcci;\n+    DeviceListener listener;\n+    CXLUpstreamPPB *usp_ppbs[CXL_MAX_USP_PPBS];\n+    CXLDownstreamPPB *dsp_ppbs[CXL_MAX_DSP_PPBS];\n+    // true if FM is in this guest, false if remote FM.\n+    bool local_fm;\n+} CXLVCSSwitch;\n+\n+// Called by USP/DSP/EP CXL devices to build the VCS structures.\n+void cxl_vcs_register_usp(CXLVCSSwitch *sw, CXLUpstreamPort *usp, Error **errp);\n+void cxl_vcs_register_vppb(CXLVCSSwitch *sw, CXLUpstreamPort *usp,\n+        CXLDownstreamPort *dsp, Error **errp);\n+void cxl_vcs_register_dsppb(CXLVCSSwitch *sw, const QDict *opts,\n+        bool from_json, Error **errp);\n+void cxl_vcs_register_qdict_dsppb(CXLVCSSwitch *sw, const QDict *opts,\n+        bool from_json, Error **errp);\n+bool cxl_vcs_hide_device_listener(DeviceListener *listener,\n+        const QDict *device_opts, bool from_json, Error **errp);\n+\n+\n+// Called by the CCI mailbox utils for FMAPI control of VCS.\n+CXLRetCode cxl_vcs_bind_vppb(CXLVCSSwitch *sw, uint8_t vcs_id, uint8_t vppb_id,\n+        uint8_t dsppb_id, uint16_t ld_id);\n+CXLRetCode cxl_vcs_unbind_vppb(CXLVCSSwitch *sw, uint8_t vcs_id,\n+        uint8_t vppb_id, uint16_t option);\n+void cxl_vcs_identify_switch_device(CXLVCSSwitch *sw);\n+void cxl_vcs_get_physical_port_state(CXLVCSSwitch *sw);\n+void cxl_vcs_physical_port_control(CXLVCSSwitch *sw);\n+void cxl_vcs_get_virtual_switch_info(CXLVCSSwitch *sw);\n+\n+#endif /* CXL_VCS_SWITCH_H */\ndiff --git a/qapi/qom.json b/qapi/qom.json\nindex 6f5c9de0f0..f66ef6b68b 100644\n--- a/qapi/qom.json\n+++ b/qapi/qom.json\n@@ -357,6 +357,23 @@\n   'data': { 'chardev': 'str' },\n   'if': 'CONFIG_VHOST_CRYPTO' }\n \n+##\n+# @CXLVCSSwitchProperties:\n+#\n+# Properties for cxl-vcs-switch objects.\n+#\n+# @usp-ppbs: number of physical upstream PPBs in the switch\n+# @dsp-ppbs: number of physical downstream PPBs in the switch\n+# @local-fm: true if this instance of qemu contains the MCTP device\n+# (FM capabilities), false if vcs will listen for incoming traffic\n+# from the remote FM.\n+##\n+{ 'struct': 'CXLVCSSwitchProperties',\n+  'data': {\n+    '*usp-ppbs': 'uint8',\n+    '*dsp-ppbs': 'uint8',\n+    '*local-fm':   'bool'} }\n+\n ##\n # @DBusVMStateProperties:\n #\n@@ -1194,6 +1211,7 @@\n     'cryptodev-backend-lkcf',\n     { 'name': 'cryptodev-vhost-user',\n       'if': 'CONFIG_VHOST_CRYPTO' },\n+    'cxl-vcs-switch',\n     'dbus-vmstate',\n     'filter-buffer',\n     'filter-dump',\n@@ -1272,6 +1290,7 @@\n       'cryptodev-backend-lkcf':     'CryptodevBackendProperties',\n       'cryptodev-vhost-user':       { 'type': 'CryptodevVhostUserProperties',\n                                       'if': 'CONFIG_VHOST_CRYPTO' },\n+      'cxl-vcs-switch':             'CXLVCSSwitchProperties',\n       'dbus-vmstate':               'DBusVMStateProperties',\n       'filter-buffer':              'FilterBufferProperties',\n       'filter-dump':                'FilterDumpProperties',\n",
    "prefixes": [
        "RFC",
        "QEMU",
        "05/10"
    ]
}