On Wed, Dec 03, 2025 at 02:18:17PM -0700, Stephen Bates wrote:
> This patch introduces a PCI MMIO Bridge device that enables PCI devices
> to perform MMIO operations on other PCI devices via command packets. This
> provides software-defined PCIe peer-to-peer (P2P) communication without
> requiring specific hardware topology.
> 
> Configuration:
>   qemu-system-x86_64 -machine q35 \
>       -device pci-mmio-bridge,shadow-gpa=0x80000000,shadow-size=4096
> 
> - shadow-gpa: Guest physical address (default: 0x80000000, 0=auto)
> - shadow-size: Buffer size in bytes (default: 4096, min: 4096)
> - poll-interval-ns: Polling interval (default: 1000000 = 1ms)
> - enabled: Enable/disable bridge (default: true)
> 
> The bridge exposes shadow buffer information via a vendor-specific PCI config
> space:
> 
>   Offset 0x40: GPA bits [31:0]
>   Offset 0x44: GPA bits [63:32]
>   Offset 0x48: Buffer size
>   Offset 0x4C: Queue depth
> 
> Guest software reads these registers to locate the command queue and
> can then submit MMIO operations by writing command packets to guest
> RAM and updating the producer index.
> 
> Limitations:
> - Currently supports only bus 0 (main PCI bus)
> - No access control or security checks between devices
> - Polling-based completion (no interrupt notification)
> - Cannot target VFIO device BARs directly (future work)
> 
> Testing:
>   make check-qtest-x86_64
> 
> The qtest suite validates PCI device discovery, config space registers,
> command queue operations, and proper handling of invalid commands.
> 
> Signed-off-by: Stephen Bates <[email protected]>


Not at all excited to support another device with an ad hoc
guest/host communication protocol.

Why not build a virtio-pci-bridge? 

> ---
>  MAINTAINERS                             |   8 +
>  docs/system/device-emulation.rst        |   1 +
>  docs/system/devices/pci-mmio-bridge.rst | 302 +++++++++++++
>  hw/pci/meson.build                      |   1 +
>  hw/pci/pci-mmio-bridge.c                | 568 ++++++++++++++++++++++++
>  hw/pci/trace-events                     |  18 +
>  include/hw/pci/pci-mmio-bridge.h        | 162 +++++++
>  include/hw/pci/pci.h                    |   1 +
>  tests/qtest/meson.build                 |   1 +
>  tests/qtest/pci-mmio-bridge-test.c      | 417 +++++++++++++++++
>  10 files changed, 1479 insertions(+)
>  create mode 100644 docs/system/devices/pci-mmio-bridge.rst
>  create mode 100644 hw/pci/pci-mmio-bridge.c
>  create mode 100644 include/hw/pci/pci-mmio-bridge.h
>  create mode 100644 tests/qtest/pci-mmio-bridge-test.c
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index a07086ed76..6bbbeca57e 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -2098,6 +2098,14 @@ F: docs/pci*
>  F: docs/specs/*pci*
>  F: docs/system/sriov.rst
>  
> +PCI MMIO Bridge
> +M: Michael S. Tsirkin <[email protected]>
> +S: Supported
> +F: hw/pci/pci-mmio-bridge.c
> +F: include/hw/pci/pci-mmio-bridge.h
> +F: tests/qtest/pci-mmio-bridge-test.c
> +F: docs/system/devices/pci-mmio-bridge.rst
> +
>  PCIE DOE
>  M: Huai-Cheng Kuo <[email protected]>
>  M: Chris Browy <[email protected]>
> diff --git a/docs/system/device-emulation.rst 
> b/docs/system/device-emulation.rst
> index 911381643f..472b388162 100644
> --- a/docs/system/device-emulation.rst
> +++ b/docs/system/device-emulation.rst
> @@ -91,6 +91,7 @@ Emulated Devices
>     devices/keyboard.rst
>     devices/net.rst
>     devices/nvme.rst
> +   devices/pci-mmio-bridge.rst
>     devices/usb.rst
>     devices/vhost-user.rst
>     devices/virtio-gpu.rst
> diff --git a/docs/system/devices/pci-mmio-bridge.rst 
> b/docs/system/devices/pci-mmio-bridge.rst
> new file mode 100644
> index 0000000000..6cd069af0b
> --- /dev/null
> +++ b/docs/system/devices/pci-mmio-bridge.rst
> @@ -0,0 +1,302 @@
> +.. SPDX-License-Identifier: GPL-2.0-or-later
> +
> +===============
> +PCI MMIO Bridge
> +===============
> +
> +The PCI MMIO Bridge is an emulated PCI device that provides a mechanism for
> +PCI devices to perform MMIO (Memory-Mapped I/O) operations on other PCI
> +devices via command packets. This enables software-defined PCIe peer-to-peer
> +(P2P) communication between any combination of emulated and real PCI devices.
> +
> +Overview
> +========
> +
> +The PCI MMIO Bridge uses a hybrid architecture:
> +
> +1. **PCI Device**: Provides guest OS discovery via standard enumeration.
> +2. **Shadow Buffer**: Allocated in guest RAM.
> +3. **PCI Config Space**: Exposes shadow buffer GPA in vendor-specific
> +   registers.
> +
> +VFIO can only map guest RAM not emulated PCI MMIO space. And, at the present
> +time, VFIO cannot map MMIO space into an IOVA mapping. Therefore the PCI MMIO
> +Bridge uses a small amount of guest RAM mapped into its Guest Physical 
> Address
> +(GPA) space.
> +
> +Command Queue Structure
> +=======================
> +
> +The PCI MMIO Bridge uses a ring buffer in guest RAM. The first 24 bytes
> +of the ring buffer contain metadata. The rest of the ring buffer contains
> +command slots:
> +
> +.. code-block:: c
> +
> +   struct pci_mmio_bridge_ring_meta {
> +       uint32_t producer_idx;  /* Written by initiator */
> +       uint32_t consumer_idx;  /* Written by QEMU */
> +       uint32_t queue_depth;   /* Number of available slots */
> +       uint32_t reserved[3];
> +   };
> +
> +   struct pci_mmio_bridge_command {
> +       uint16_t target_bdf;    /* Bus:Device:Function */
> +       uint8_t  target_bar;    /* Which BAR on target */
> +       uint8_t  reserved1;
> +       uint32_t offset;        /* Offset within BAR */
> +       uint32_t value;         /* Write value / read result */
> +       uint8_t  command;       /* 0=NOP, 1=WRITE, 2=READ */
> +       uint8_t  size;          /* 1, 2, 4, or 8 bytes */
> +       uint8_t  status;        /* 0=pending, 1=complete, 2=error */
> +       uint8_t  reserved2;
> +       uint32_t sequence;      /* For ordering */
> +       uint32_t reserved3;
> +   };
> +
> +With the default 4KiB shadow buffer size, the queue provides 169 command 
> slots.
> +
> +Processing Model
> +================
> +
> +QEMU polls the command queue periodically (default 1ms interval). When
> +the producer index differs from the consumer index:
> +
> +1. QEMU reads pending command(s) from the queue
> +2. For each command:
> +
> +   * Locates the target PCI device (emulated or real) by BDF
> +   * Dispatches MMIO read/write to the target device's BAR
> +   * Updates the command status field (complete or error)
> +   * Updates the result value (for reads)
> +
> +3. Updates the consumer index
> +4. Reschedules the next poll
> +
> +Device Configuration
> +====================
> +
> +The PCI MMIO Bridge appears as a standard PCI device:
> +
> +- **Vendor ID**: 0x1b36 (Red Hat/QEMU)
> +- **Device ID**: 0x0015 (PCI MMIO Bridge)
> +- **Class**: 0x08/0x80 (System/Other)
> +
> +Basic Usage
> +-----------
> +
> +.. code-block:: console
> +
> +   qemu-system-x86_64 \
> +       -machine q35 \
> +       -device pci-mmio-bridge,id=mmio-bridge
> +
> +This creates a bridge with defaults:
> +
> +- Shadow GPA: 0x80000000
> +- Shadow size: 4096 bytes
> +- Polling interval: 1ms
> +
> +Custom Configuration
> +---------------------
> +
> +.. code-block:: console
> +
> +   qemu-system-x86_64 \
> +       -machine q35 \
> +       -device pci-mmio-bridge,id=mmio-bridge,\
> +               shadow-gpa=0x90000000,\
> +               shadow-size=8192,\
> +               poll-interval-ns=500000
> +
> +Properties:
> +
> +- ``shadow-gpa``: Guest physical address for buffer (0 = auto, default:
> +  0x80000000)
> +- ``shadow-size``: Buffer size in bytes (default: 4096, min: 4096)
> +- ``poll-interval-ns``: Polling interval in nanoseconds (default:
> +  1000000)
> +- ``enabled``: Enable/disable bridge (default: true)
> +- ``addr``: PCI slot address (e.g., ``addr=5.0`` for slot 5)
> +
> +Guest Software Interface
> +========================
> +
> +Guest drivers can read the vendor-specific config space to find the GPA of
> +the shadow buffer:
> +
> +.. code-block:: c
> +
> +   #define PCI_MMIO_BRIDGE_CAP_OFFSET  0x40
> +   #define PCI_MMIO_BRIDGE_CAP_GPA_LO  0x00
> +   #define PCI_MMIO_BRIDGE_CAP_GPA_HI  0x04
> +   #define PCI_MMIO_BRIDGE_CAP_SIZE    0x08
> +   #define PCI_MMIO_BRIDGE_CAP_DEPTH   0x0C
> +
> +   /* Read shadow buffer GPA */
> +   uint32_t gpa_lo = pci_read_config_dword(pdev,
> +                                          PCI_MMIO_BRIDGE_CAP_OFFSET + 0);
> +   uint32_t gpa_hi = pci_read_config_dword(pdev,
> +                                          PCI_MMIO_BRIDGE_CAP_OFFSET + 4);
> +   uint64_t shadow_gpa = ((uint64_t)gpa_hi << 32) | gpa_lo;
> +
> +   /* Read buffer size and queue depth */
> +   uint32_t shadow_size = pci_read_config_dword(pdev,
> +                                                PCI_MMIO_BRIDGE_CAP_OFFSET + 
> 8);
> +   uint32_t queue_depth = pci_read_config_dword(pdev,
> +                                                PCI_MMIO_BRIDGE_CAP_OFFSET + 
> 12);
> +
> +Linux Kernel Driver Example
> +---------------------------
> +
> +Example PCI driver that discovers and uses the bridge:
> +
> +.. code-block:: c
> +
> +   #include <linux/pci.h>
> +   #include <linux/module.h>
> +   #include <linux/io.h>
> +
> +   #define PCI_VENDOR_ID_REDHAT_QEMU  0x1b36
> +   #define PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE  0x0015
> +
> +   /* Config space offsets */
> +   #define CAP_OFFSET  0x40
> +   #define CAP_GPA_LO  0x00
> +   #define CAP_GPA_HI  0x04
> +   #define CAP_SIZE    0x08
> +   #define CAP_DEPTH   0x0C
> +
> +   struct mmio_bridge_dev {
> +       struct pci_dev *pdev;
> +       void __iomem *shadow_buf;
> +       uint64_t shadow_gpa;
> +       uint32_t shadow_size;
> +       uint32_t queue_depth;
> +   };
> +
> +   static int mmio_bridge_probe(struct pci_dev *pdev,
> +                                const struct pci_device_id *id)
> +   {
> +       struct mmio_bridge_dev *dev;
> +       uint32_t gpa_lo, gpa_hi;
> +       int err;
> +
> +       dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
> +       if (!dev)
> +           return -ENOMEM;
> +
> +       dev->pdev = pdev;
> +
> +       err = pci_enable_device(pdev);
> +       if (err)
> +           return err;
> +
> +       /* Read shadow buffer location from config space */
> +       pci_read_config_dword(pdev, CAP_OFFSET + CAP_GPA_LO, &gpa_lo);
> +       pci_read_config_dword(pdev, CAP_OFFSET + CAP_GPA_HI, &gpa_hi);
> +       pci_read_config_dword(pdev, CAP_OFFSET + CAP_SIZE, &dev->shadow_size);
> +       pci_read_config_dword(pdev, CAP_OFFSET + CAP_DEPTH, 
> &dev->queue_depth);
> +
> +       dev->shadow_gpa = ((uint64_t)gpa_hi << 32) | gpa_lo;
> +
> +       pr_info("PCI MMIO Bridge: GPA=0x%llx size=%u depth=%u\n",
> +               dev->shadow_gpa, dev->shadow_size, dev->queue_depth);
> +
> +       /* Map shadow buffer (guest RAM, not MMIO) */
> +       dev->shadow_buf = ioremap(dev->shadow_gpa, dev->shadow_size);
> +       if (!dev->shadow_buf) {
> +           err = -ENOMEM;
> +           goto err_disable;
> +       }
> +
> +       pci_set_drvdata(pdev, dev);
> +
> +       return 0;
> +
> +   err_disable:
> +       pci_disable_device(pdev);
> +       return err;
> +   }
> +
> +   static void mmio_bridge_remove(struct pci_dev *pdev)
> +   {
> +       struct mmio_bridge_dev *dev = pci_get_drvdata(pdev);
> +
> +       if (dev->shadow_buf)
> +           iounmap(dev->shadow_buf);
> +
> +       pci_disable_device(pdev);
> +   }
> +
> +   static const struct pci_device_id mmio_bridge_ids[] = {
> +       { PCI_DEVICE(PCI_VENDOR_ID_REDHAT_QEMU,
> +                    PCI_DEVICE_ID_MMIO_BRIDGE) },
> +       { 0, }
> +   };
> +   MODULE_DEVICE_TABLE(pci, mmio_bridge_ids);
> +
> +   static struct pci_driver mmio_bridge_driver = {
> +       .name       = "pci-mmio-bridge",
> +       .id_table   = mmio_bridge_ids,
> +       .probe      = mmio_bridge_probe,
> +       .remove     = mmio_bridge_remove,
> +   };
> +
> +   module_pci_driver(mmio_bridge_driver);
> +   MODULE_LICENSE("GPL");
> +
> +Submitting Commands
> +-------------------
> +
> +Example code for writing to a PCI device's BAR:
> +
> +.. code-block:: c
> +
> +   #include <linux/io.h>
> +
> +   volatile struct pci_mmio_bridge_ring_meta *meta;
> +   volatile struct pci_mmio_bridge_command *cmds;
> +
> +   void init_bridge(void __iomem *shadow_buf)
> +   {
> +       meta = (struct pci_mmio_bridge_ring_meta *)shadow_buf;
> +       cmds = (struct pci_mmio_bridge_command *)(shadow_buf +
> +                                          sizeof(*meta));
> +
> +       /* Verify queue is initialized */
> +       if (meta->queue_depth == 0) {
> +           pr_err("PCI MMIO bridge not available\n");
> +           return;
> +       }
> +       pr_info("Bridge ready: %u slots\n",
> +               meta->queue_depth);
> +   }
> +
> +   int pci_mmio_write_bar(u16 bdf, u8 bar, u32 offset,
> +                          u32 value, u8 size)
> +   {
> +       u32 slot = meta->producer_idx % meta->queue_depth;
> +       struct pci_mmio_bridge_command cmd = {
> +           .target_bdf = bdf,
> +           .target_bar = bar,
> +           .offset = offset,
> +           .value = value,
> +           .command = 1,  /* WRITE */
> +           .size = size,
> +           .status = 0,   /* PENDING */
> +       };
> +
> +       /* Write command to queue */
> +       cmds[slot] = cmd;
> +       wmb();
> +
> +       /* Update producer index */
> +       meta->producer_idx++;
> +
> +       /* Poll for completion (or use interrupt) */
> +       while (cmds[slot].status == 0)
> +           cpu_relax();
> +
> +       return (cmds[slot].status == 1) ? 0 : -EIO;
> +   }
> diff --git a/hw/pci/meson.build b/hw/pci/meson.build
> index b9c34b2acf..670645921f 100644
> --- a/hw/pci/meson.build
> +++ b/hw/pci/meson.build
> @@ -6,6 +6,7 @@ pci_ss.add(files(
>    'pci_bridge.c',
>    'pci_host.c',
>    'pci-hmp-cmds.c',
> +  'pci-mmio-bridge.c',
>    'pci-qmp-cmds.c',
>    'pcie_sriov.c',
>    'shpc.c',
> diff --git a/hw/pci/pci-mmio-bridge.c b/hw/pci/pci-mmio-bridge.c
> new file mode 100644
> index 0000000000..a6cdf81885
> --- /dev/null
> +++ b/hw/pci/pci-mmio-bridge.c
> @@ -0,0 +1,568 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * QEMU PCI MMIO Bridge Implementation
> + *
> + * Provides PCI device-to-device MMIO capability via a command queue.
> + *
> + * Copyright (c) 2025 Stephen Bates <[email protected]>
> + */
> +
> +#include "qemu/osdep.h"
> +#include "qemu/memalign.h"
> +#include "qemu/main-loop.h"
> +#include "hw/pci/pci-mmio-bridge.h"
> +#include "hw/pci/pci.h"
> +#include "hw/pci/pci_device.h"
> +#include "hw/pci/pci_bus.h"
> +#include "system/address-spaces.h"
> +#include "qemu/timer.h"
> +#include "qemu/log.h"
> +#include "qapi/error.h"
> +#include "trace.h"
> +#include "hw/qdev-properties.h"
> +#include "hw/resettable.h"
> +#include "qemu/module.h"
> +#include "exec/target_page.h"
> +
> +/* Default polling interval: 1ms */
> +#define DEFAULT_POLL_INTERVAL_NS (1000 * 1000)
> +
> +/* Helper: Extract bus number from BDF */
> +static inline uint8_t bdf_to_bus(uint16_t bdf)
> +{
> +    return (bdf >> 8) & 0xFF;
> +}
> +
> +/* Helper: Extract devfn from BDF */
> +static inline uint8_t bdf_to_devfn(uint16_t bdf)
> +{
> +    return bdf & 0xFF;
> +}
> +
> +/*
> + * Find PCI device by BDF
> + *
> + * Searches the configured PCI bus for a device matching the given BDF.
> + * Returns NULL if not found.
> + */
> +static PCIDevice *pci_mmio_bridge_find_device(PCIMMIOBridge *bridge,
> +                                              uint16_t bdf)
> +{
> +    uint8_t bus_num = bdf_to_bus(bdf);
> +    uint8_t devfn = bdf_to_devfn(bdf);
> +    PCIDevice *dev;
> +
> +    if (!bridge->pci_bus) {
> +        return NULL;
> +    }
> +
> +    /* For now, only support bus 0 (main PCI bus) */
> +    if (bus_num != 0) {
> +        qemu_log_mask(LOG_GUEST_ERROR,
> +                      "pci-mmio-bridge: Multi-bus not yet supported (BDF 
> %04x)\n",
> +                      bdf);
> +        return NULL;
> +    }
> +
> +    dev = bridge->pci_bus->devices[devfn];
> +    if (!dev) {
> +        trace_pci_mmio_bridge_device_not_found(bdf);
> +    }
> +
> +    return dev;
> +}
> +
> +/*
> + * Execute a single MMIO command
> + *
> + * Dispatches the command to the appropriate target device's MMIO handler.
> + */
> +static void pci_mmio_bridge_execute_command(
> +    PCIMMIOBridge *bridge, struct pci_mmio_bridge_command *cmd)
> +{
> +    PCIDevice *target;
> +    uint64_t value = 0;
> +    MemTxResult result;
> +
> +    /* Validate command type */
> +    switch (cmd->command) {
> +    case PCI_MMIO_BRIDGE_CMD_WRITE:
> +    case PCI_MMIO_BRIDGE_CMD_READ:
> +        break;
> +    default:
> +        qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_ERROR);
> +        smp_wmb();  /* Ensure status visible before return */
> +        trace_pci_mmio_bridge_invalid_command(cmd->command);
> +        return;
> +    }
> +
> +    /* Find target device */
> +    target = pci_mmio_bridge_find_device(bridge, cmd->target_bdf);
> +    if (!target) {
> +        qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_ERROR);
> +        smp_wmb();  /* Ensure status is visible to guest */
> +        trace_pci_mmio_bridge_device_not_found(cmd->target_bdf);
> +        return;
> +    }
> +
> +    /* Validate BAR number */
> +    if (cmd->target_bar >= PCI_NUM_REGIONS) {
> +        qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_ERROR);
> +        smp_wmb();  /* Ensure status visible before return */
> +        trace_pci_mmio_bridge_invalid_bar(cmd->target_bdf, cmd->target_bar);
> +        return;
> +    }
> +
> +    /* Get target BAR address */
> +    hwaddr bar_addr = pci_get_bar_addr(target, cmd->target_bar);
> +    if (bar_addr == PCI_BAR_UNMAPPED) {
> +        qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_ERROR);
> +        smp_wmb();  /* Ensure status visible to guest */
> +        trace_pci_mmio_bridge_bar_not_mapped(cmd->target_bdf, 
> cmd->target_bar);
> +        return;
> +    }
> +
> +    /* Validate size */
> +    if (cmd->size != 1 && cmd->size != 2 && cmd->size != 4 && cmd->size != 
> 8) {
> +        qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_ERROR);
> +        smp_wmb();  /* Ensure status visible to guest */
> +        trace_pci_mmio_bridge_invalid_size(cmd->size);
> +        return;
> +    }
> +
> +    /* Calculate full MMIO address (BAR base + offset) */
> +    hwaddr mmio_addr = bar_addr + cmd->offset;
> +
> +    /* Execute the operation using address_space to access MMIO space */
> +    switch (cmd->command) {
> +    case PCI_MMIO_BRIDGE_CMD_WRITE:
> +        result = address_space_write(&address_space_memory, mmio_addr,
> +                                      MEMTXATTRS_UNSPECIFIED,
> +                                      &cmd->value, cmd->size);
> +        if (result == MEMTX_OK) {
> +            qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_COMPLETE);
> +            smp_wmb();  /* Ensure status visible to guest */
> +            bridge->total_writes++;
> +            trace_pci_mmio_bridge_write(cmd->target_bdf, cmd->target_bar,
> +                                        cmd->offset, cmd->value, cmd->size);
> +        } else {
> +            qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_ERROR);
> +            smp_wmb();  /* Ensure status visible to guest */
> +            bridge->total_errors++;
> +            trace_pci_mmio_bridge_write_failed(cmd->target_bdf, 
> cmd->target_bar,
> +                                               cmd->offset, result);
> +        }
> +        break;
> +
> +    case PCI_MMIO_BRIDGE_CMD_READ:
> +        result = address_space_read(&address_space_memory, mmio_addr,
> +                                     MEMTXATTRS_UNSPECIFIED,
> +                                     &value, cmd->size);
> +        if (result == MEMTX_OK) {
> +            cmd->value = value;
> +            smp_wmb();  /* Ensure value is visible before status update */
> +            qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_COMPLETE);
> +            smp_wmb();  /* Ensure status visible to guest */
> +            bridge->total_reads++;
> +            trace_pci_mmio_bridge_read(cmd->target_bdf, cmd->target_bar,
> +                                       cmd->offset, value, cmd->size);
> +        } else {
> +            qatomic_set(&cmd->status, PCI_MMIO_BRIDGE_STATUS_ERROR);
> +            smp_wmb();  /* Ensure status visible to guest */
> +            bridge->total_errors++;
> +            trace_pci_mmio_bridge_read_failed(cmd->target_bdf, 
> cmd->target_bar,
> +                                              cmd->offset, result);
> +        }
> +        break;
> +    }
> +
> +    /* Ensure status is visible */
> +    smp_wmb();
> +}
> +
> +/*
> + * Polling timer callback
> + *
> + * Checks shadow buffer for new commands and processes them.
> + * This is exported for use by the PCI device wrapper.
> + */
> +void pci_mmio_bridge_poll(void *opaque)
> +{
> +    PCIMMIOBridge *bridge = opaque;
> +    uint32_t producer_idx, consumer_idx;
> +    uint32_t commands_processed = 0;
> +
> +    if (!bridge->enabled) {
> +        goto reschedule;
> +    }
> +
> +    bridge->total_polls++;
> +
> +    /*
> +     * Read metadata from guest memory using address_space_read. This ensures
> +     * we see any VFIO device writes and not cached copies.
> +     */
> +    struct pci_mmio_bridge_ring_meta metadata;
> +    address_space_read(&address_space_memory, bridge->shadow_gpa,
> +                       MEMTXATTRS_UNSPECIFIED, &metadata, sizeof(metadata));
> +
> +    producer_idx = metadata.producer_idx;
> +    consumer_idx = bridge->head;
> +
> +    /* Process all pending commands */
> +    while (consumer_idx != producer_idx) {
> +        uint32_t slot = consumer_idx % bridge->queue_depth;
> +        hwaddr cmd_addr = bridge->shadow_gpa +
> +                          sizeof(struct pci_mmio_bridge_ring_meta) +
> +                          (slot * sizeof(struct pci_mmio_bridge_command));
> +
> +        /* Read command from guest memory to see GPU writes */
> +        struct pci_mmio_bridge_command cmd;
> +        address_space_read(&address_space_memory, cmd_addr,
> +                           MEMTXATTRS_UNSPECIFIED, &cmd, sizeof(cmd));
> +
> +        if (cmd.status == PCI_MMIO_BRIDGE_STATUS_PENDING) {
> +            pci_mmio_bridge_execute_command(bridge, &cmd);
> +
> +            /* Write back status using address_space_write */
> +            address_space_write(&address_space_memory, cmd_addr,
> +                                MEMTXATTRS_UNSPECIFIED, &cmd, sizeof(cmd));
> +
> +            bridge->total_commands++;
> +            commands_processed++;
> +        }
> +
> +        consumer_idx++;
> +    }
> +
> +    /* Update our head position */
> +    bridge->head = consumer_idx;
> +
> +    if (commands_processed > 0) {
> +        trace_pci_mmio_bridge_poll_processed(commands_processed);
> +    }
> +
> +reschedule:
> +    /* Reschedule for next poll cycle */
> +    if (bridge->poll_timer) {
> +        timer_mod(bridge->poll_timer,
> +                  qemu_clock_get_ns(QEMU_CLOCK_REALTIME) +
> +                  bridge->poll_interval_ns);
> +    }
> +}
> +
> +/*
> + * Initialize PCI MMIO Bridge
> + */
> +PCIMMIOBridge *pci_mmio_bridge_init(PCIBus *pci_bus,
> +                                         hwaddr gpa, uint32_t size,
> +                                         uint64_t poll_interval_ns,
> +                                         Error **errp)
> +{
> +    PCIMMIOBridge *bridge;
> +    struct pci_mmio_bridge_ring_meta *meta;
> +
> +    /* Validate parameters */
> +    if (!pci_bus) {
> +        error_setg(errp, "PCI bus must be provided");
> +        return NULL;
> +    }
> +
> +    if (size < TARGET_PAGE_SIZE) {
> +        error_setg(errp, "Shadow buffer size must be at least 4096 bytes");
> +        return NULL;
> +    }
> +
> +    if (!QEMU_IS_ALIGNED(gpa, TARGET_PAGE_SIZE)) {
> +        error_setg(errp, "Shadow buffer GPA must be page-aligned");
> +        return NULL;
> +    }
> +
> +    /* Allocate bridge state */
> +    bridge = g_new0(PCIMMIOBridge, 1);
> +
> +    /* Store PCI bus reference */
> +    bridge->pci_bus = pci_bus;
> +
> +    bridge->shadow_gpa = gpa;
> +    bridge->shadow_size = size;
> +
> +    /* Allocate guest RAM for shadow buffer with unique name */
> +    char *mr_name = g_strdup_printf("pci-mmio-bridge-shadow@0x%"PRIx64, gpa);
> +    memory_region_init_ram(&bridge->shadow_mr, NULL, mr_name, size,
> +                           &error_fatal);
> +    g_free(mr_name);
> +
> +    /* Add to system memory at specified GPA */
> +    memory_region_add_subregion(get_system_memory(), gpa, 
> &bridge->shadow_mr);
> +
> +    /* Get host virtual address for direct access */
> +    bridge->shadow_hva = memory_region_get_ram_ptr(&bridge->shadow_mr);
> +    memset(bridge->shadow_hva, 0, size);
> +
> +    /* Calculate queue depth (reserve first slot for metadata) */
> +    bridge->queue_depth = (size / sizeof(struct pci_mmio_bridge_command)) - 
> 1;
> +
> +    /* Initialize ring buffer metadata in first slot */
> +    meta = (struct pci_mmio_bridge_ring_meta *)bridge->shadow_hva;
> +    meta->producer_idx = 0;
> +    meta->consumer_idx = 0;
> +    meta->queue_depth = bridge->queue_depth;
> +    meta->reserved = 0;
> +
> +    /* Initialize polling infrastructure */
> +    bridge->poll_interval_ns = poll_interval_ns ? poll_interval_ns :
> +                                                   DEFAULT_POLL_INTERVAL_NS;
> +
> +    /* Use bottom-half for immediate processing (for qtest) */
> +    bridge->poll_bh = qemu_bh_new(pci_mmio_bridge_poll, bridge);
> +
> +    /* Also create timer for periodic polling when BH isn't triggered */
> +    bridge->poll_timer = timer_new_ns(QEMU_CLOCK_REALTIME,
> +                                      pci_mmio_bridge_poll, bridge);
> +    bridge->enabled = true;
> +
> +    /* Start periodic polling */
> +    timer_mod(bridge->poll_timer,
> +              qemu_clock_get_ns(QEMU_CLOCK_REALTIME) +
> +              bridge->poll_interval_ns);
> +
> +    trace_pci_mmio_bridge_init(gpa, size, bridge->queue_depth,
> +                               bridge->poll_interval_ns);
> +
> +    return bridge;
> +}
> +
> +/*
> + * Cleanup PCI MMIO Bridge
> + */
> +void pci_mmio_bridge_cleanup(PCIMMIOBridge *bridge)
> +{
> +    if (!bridge) {
> +        return;
> +    }
> +
> +    trace_pci_mmio_bridge_cleanup(bridge->total_commands, 
> bridge->total_writes,
> +                                  bridge->total_reads, bridge->total_errors);
> +
> +    /* Stop polling */
> +    if (bridge->poll_timer) {
> +        timer_free(bridge->poll_timer);
> +    }
> +    if (bridge->poll_bh) {
> +        qemu_bh_delete(bridge->poll_bh);
> +    }
> +
> +    /* Remove from system memory and cleanup */
> +    memory_region_del_subregion(get_system_memory(), &bridge->shadow_mr);
> +    object_unparent(OBJECT(&bridge->shadow_mr));
> +
> +    /* Free backing memory */
> +    qemu_vfree(bridge->shadow_hva);
> +
> +    g_free(bridge);
> +}
> +
> +/*
> + * Enable/disable bridge
> + */
> +void pci_mmio_bridge_set_enabled(PCIMMIOBridge *bridge, bool enabled)
> +{
> +    if (bridge) {
> +        bridge->enabled = enabled;
> +        trace_pci_mmio_bridge_set_enabled(enabled);
> +    }
> +}
> +
> +/*
> + * Get statistics
> + */
> +void pci_mmio_bridge_get_stats(PCIMMIOBridge *bridge,
> +                               uint64_t *total_commands,
> +                               uint64_t *total_writes,
> +                               uint64_t *total_reads,
> +                               uint64_t *total_errors)
> +{
> +    if (bridge) {
> +        if (total_commands) {
> +            *total_commands = bridge->total_commands;
> +        }
> +        if (total_writes) {
> +            *total_writes = bridge->total_writes;
> +        }
> +        if (total_reads) {
> +            *total_reads = bridge->total_reads;
> +        }
> +        if (total_errors) {
> +            *total_errors = bridge->total_errors;
> +        }
> +    }
> +}
> +
> +/*
> + * Manually trigger one poll cycle
> + *
> + * This is primarily for testing - it processes pending commands immediately
> + * without waiting for the timer.
> + */
> +void pci_mmio_bridge_poll_once(PCIMMIOBridge *bridge)
> +{
> +    if (bridge) {
> +        pci_mmio_bridge_poll(bridge);
> +    }
> +}
> +
> +/*
> + * PCI Device Wrapper
> + */
> +
> +static void pci_mmio_bridge_pci_realize(PCIDevice *pci_dev, Error **errp)
> +{
> +    PCIMMIOBridge_NEW *s = PCI_MMIO_BRIDGE(pci_dev);
> +    uint8_t *pci_conf = pci_dev->config;
> +    Error *local_err = NULL;
> +
> +    /* Set PCI config space */
> +    pci_config_set_vendor_id(pci_conf, PCI_VENDOR_ID_REDHAT);
> +    pci_config_set_device_id(pci_conf, PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE);
> +    pci_config_set_class(pci_conf, PCI_CLASS_SYSTEM_OTHER);
> +    pci_config_set_revision(pci_conf, 0x01);
> +
> +    /* Subsystem vendor/device ID */
> +    pci_set_word(pci_conf + PCI_SUBSYSTEM_VENDOR_ID,
> +                 PCI_VENDOR_ID_REDHAT);
> +    pci_set_word(pci_conf + PCI_SUBSYSTEM_ID, 0x1100);
> +
> +    /* Validate shadow_size */
> +    if (s->shadow_size < TARGET_PAGE_SIZE) {
> +        error_setg(errp, "shadow-size must be at least %d bytes",
> +            TARGET_PAGE_SIZE);
> +        return;
> +    }
> +
> +    /* Default GPA if not specified (below 4GB for 32-bit compatibility) */
> +    if (s->shadow_gpa == 0) {
> +        s->shadow_gpa = 0x80000000ULL;  /* Default: 2GB mark */
> +    }
> +
> +    /* Use existing bridge initialization (creates guest RAM) */
> +    s->bridge = pci_mmio_bridge_init(pci_get_bus(pci_dev),
> +                                     s->shadow_gpa,
> +                                     s->shadow_size,
> +                                     s->poll_interval_ns,
> +                                     &local_err);
> +    if (local_err) {
> +        error_propagate(errp, local_err);
> +        return;
> +    }
> +
> +    s->bridge->enabled = s->enabled;
> +
> +    /* Expose shadow buffer GPA and size via vendor-specific config space */
> +    pci_set_long(pci_conf + PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                 PCI_MMIO_BRIDGE_CAP_GPA_LO,
> +                 (uint32_t)(s->shadow_gpa & 0xFFFFFFFF));
> +    pci_set_long(pci_conf + PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                 PCI_MMIO_BRIDGE_CAP_GPA_HI,
> +                 (uint32_t)(s->shadow_gpa >> 32));
> +    pci_set_long(pci_conf + PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                 PCI_MMIO_BRIDGE_CAP_SIZE,
> +                 s->shadow_size);
> +    pci_set_long(pci_conf + PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                 PCI_MMIO_BRIDGE_CAP_DEPTH,
> +                 s->bridge->queue_depth);
> +
> +    trace_pci_mmio_bridge_realize(s->shadow_gpa, s->shadow_size,
> +                                      s->bridge->queue_depth);
> +}
> +
> +static void pci_mmio_bridge_pci_exit(PCIDevice *pci_dev)
> +{
> +    PCIMMIOBridge_NEW *s = PCI_MMIO_BRIDGE(pci_dev);
> +
> +    if (!s->bridge) {
> +        return;
> +    }
> +
> +    trace_pci_mmio_bridge_exit(s->bridge->total_commands,
> +                                   s->bridge->total_writes,
> +                                   s->bridge->total_reads,
> +                                   s->bridge->total_errors);
> +
> +    /* Use bridge cleanup (handles guest RAM removal) */
> +    pci_mmio_bridge_cleanup(s->bridge);
> +    s->bridge = NULL;
> +}
> +
> +static void pci_mmio_bridge_pci_reset(Object *obj, ResetType type)
> +{
> +    PCIMMIOBridge_NEW *s = PCI_MMIO_BRIDGE(obj);
> +    struct pci_mmio_bridge_ring_meta *meta;
> +
> +    if (!s->bridge || !s->bridge->shadow_hva) {
> +        return;
> +    }
> +
> +    /* Reset ring buffer state in guest RAM */
> +    meta = (struct pci_mmio_bridge_ring_meta *)s->bridge->shadow_hva;
> +    meta->producer_idx = 0;
> +    meta->consumer_idx = 0;
> +    s->bridge->head = 0;
> +
> +    /* Reset statistics */
> +    s->bridge->total_commands = 0;
> +    s->bridge->total_writes = 0;
> +    s->bridge->total_reads = 0;
> +    s->bridge->total_errors = 0;
> +    s->bridge->total_polls = 0;
> +
> +    trace_pci_mmio_bridge_reset();
> +}
> +
> +static const Property pci_mmio_bridge_pci_properties[] = {
> +    DEFINE_PROP_UINT64("shadow-gpa", PCIMMIOBridge_NEW, shadow_gpa, 0),
> +    DEFINE_PROP_UINT32("shadow-size", PCIMMIOBridge_NEW, shadow_size,
> +                       4096),
> +    DEFINE_PROP_UINT64("poll-interval-ns", PCIMMIOBridge_NEW,
> +                       poll_interval_ns, 1000000),
> +    DEFINE_PROP_BOOL("enabled", PCIMMIOBridge_NEW, enabled, true)
> +};
> +
> +static void pci_mmio_bridge_pci_class_init(ObjectClass *klass,
> +                                            const void *data)
> +{
> +    DeviceClass *dc = DEVICE_CLASS(klass);
> +    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);
> +    ResettableClass *rc = RESETTABLE_CLASS(klass);
> +
> +    k->realize = pci_mmio_bridge_pci_realize;
> +    k->exit = pci_mmio_bridge_pci_exit;
> +    k->vendor_id = PCI_VENDOR_ID_REDHAT;
> +    k->device_id = PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE;
> +    k->class_id = PCI_CLASS_SYSTEM_OTHER;
> +    k->revision = 0x01;
> +
> +    dc->desc = "PCI MMIO Bridge (device-to-device MMIO proxy)";
> +    rc->phases.hold = pci_mmio_bridge_pci_reset;
> +    device_class_set_props(dc, pci_mmio_bridge_pci_properties);
> +    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
> +}
> +
> +static const TypeInfo pci_mmio_bridge_pci_info = {
> +    .name          = TYPE_PCI_MMIO_BRIDGE,
> +    .parent        = TYPE_PCI_DEVICE,
> +    .instance_size = sizeof(PCIMMIOBridge_NEW),
> +    .class_init    = pci_mmio_bridge_pci_class_init,
> +    .interfaces = (const InterfaceInfo[]) {
> +        { INTERFACE_CONVENTIONAL_PCI_DEVICE },
> +        { }
> +    }
> +};
> +
> +static void pci_mmio_bridge_pci_register_types(void)
> +{
> +    type_register_static(&pci_mmio_bridge_pci_info);
> +}
> +
> +type_init(pci_mmio_bridge_pci_register_types)
> +
> diff --git a/hw/pci/trace-events b/hw/pci/trace-events
> index 02c80d3ec8..0919ec0a68 100644
> --- a/hw/pci/trace-events
> +++ b/hw/pci/trace-events
> @@ -28,3 +28,21 @@ pcie_cap_slot_write_config(const char *parent, const char 
> *child, const char *pd
>  
>  # shpc.c
>  shpc_slot_command(const char *parent, int pci_slot, const char *child, const 
> char *old_pic, const char *new_pic, const char *old_aic, const char *new_aic, 
> const char *old_state, const char *new_state) "%s[%d] > %s: pic: %s->%s, aic: 
> %s->%s, state: %s->%s"
> +
> +# pci-mmio-bridge.c
> +pci_mmio_bridge_init(uint64_t gpa, uint32_t size, uint32_t queue_depth, 
> uint64_t poll_interval_ns) "gpa=0x%"PRIx64" size=%u queue_depth=%u 
> poll_interval=%"PRIu64"ns"
> +pci_mmio_bridge_cleanup(uint64_t total_commands, uint64_t total_writes, 
> uint64_t total_reads, uint64_t total_errors) "total_commands=%"PRIu64" 
> writes=%"PRIu64" reads=%"PRIu64" errors=%"PRIu64
> +pci_mmio_bridge_set_enabled(bool enabled) "enabled=%d"
> +pci_mmio_bridge_write(uint16_t bdf, uint8_t bar, uint32_t offset, uint64_t 
> value, uint8_t size) "BDF=0x%04x BAR=%u offset=0x%x value=0x%"PRIx64" size=%u"
> +pci_mmio_bridge_read(uint16_t bdf, uint8_t bar, uint32_t offset, uint64_t 
> value, uint8_t size) "BDF=0x%04x BAR=%u offset=0x%x value=0x%"PRIx64" size=%u"
> +pci_mmio_bridge_write_failed(uint16_t bdf, uint8_t bar, uint32_t offset, int 
> result) "BDF=0x%04x BAR=%u offset=0x%x result=%d"
> +pci_mmio_bridge_read_failed(uint16_t bdf, uint8_t bar, uint32_t offset, int 
> result) "BDF=0x%04x BAR=%u offset=0x%x result=%d"
> +pci_mmio_bridge_device_not_found(uint16_t bdf) "BDF=0x%04x not found"
> +pci_mmio_bridge_invalid_bar(uint16_t bdf, uint8_t bar) "BDF=0x%04x invalid 
> BAR=%u"
> +pci_mmio_bridge_bar_not_mapped(uint16_t bdf, uint8_t bar) "BDF=0x%04x BAR=%u 
> not mapped"
> +pci_mmio_bridge_invalid_command(uint8_t command) "invalid command=%u"
> +pci_mmio_bridge_invalid_size(uint8_t size) "invalid size=%u"
> +pci_mmio_bridge_poll_processed(uint32_t count) "processed %u commands"
> +pci_mmio_bridge_realize(uint64_t gpa, uint32_t size, uint32_t queue_depth) 
> "shadow_gpa=0x%"PRIx64" size=%u queue_depth=%u"
> +pci_mmio_bridge_exit(uint64_t commands, uint64_t writes, uint64_t reads, 
> uint64_t errors) "commands=%"PRIu64" writes=%"PRIu64" reads=%"PRIu64" 
> errors=%"PRIu64
> +pci_mmio_bridge_reset(void) "resetting bridge state"
> diff --git a/include/hw/pci/pci-mmio-bridge.h 
> b/include/hw/pci/pci-mmio-bridge.h
> new file mode 100644
> index 0000000000..38d852cb08
> --- /dev/null
> +++ b/include/hw/pci/pci-mmio-bridge.h
> @@ -0,0 +1,162 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * QEMU PCI MMIO Bridge
> + *
> + * Copyright (c) 2025 Stephen Bates <[email protected]>
> + */
> +
> +#ifndef HW_PCI_MMIO_BRIDGE_H
> +#define HW_PCI_MMIO_BRIDGE_H
> +
> +#include "hw/pci/pci.h"
> +#include "hw/pci/pci_device.h"
> +#include "qom/object.h"
> +
> +/*
> + * MMIO Bridge Command Packet Structure
> + *
> + */
> +struct pci_mmio_bridge_command {
> +    uint16_t target_bdf;      /* Bus:Device:Function (bus|devfn) */
> +    uint8_t  target_bar;      /* Which BAR on target device (0-5) */
> +    uint8_t  reserved1;
> +    uint32_t offset;          /* Offset within BAR */
> +
> +    uint64_t value;           /* Value to write, or returned value for read 
> */
> +
> +    uint8_t  command;         /* Command type (see CMD_* below) */
> +    uint8_t  size;            /* Transfer size: 1, 2, 4, or 8 bytes */
> +    uint8_t  status;          /* Status (see STATUS_* below) */
> +    uint8_t  reserved2;
> +    uint32_t sequence;        /* Sequence number for ordering */
> +} QEMU_PACKED;
> +
> +/* Command types */
> +#define PCI_MMIO_BRIDGE_CMD_NOP    0
> +#define PCI_MMIO_BRIDGE_CMD_WRITE  1
> +#define PCI_MMIO_BRIDGE_CMD_READ   2
> +
> +/* Status codes */
> +#define PCI_MMIO_BRIDGE_STATUS_PENDING   0
> +#define PCI_MMIO_BRIDGE_STATUS_COMPLETE  1
> +#define PCI_MMIO_BRIDGE_STATUS_ERROR     2
> +
> +/* Ring buffer metadata (stored in first command slot) */
> +struct pci_mmio_bridge_ring_meta {
> +    uint32_t producer_idx;    /* Guest/device write index (tail) */
> +    uint32_t consumer_idx;    /* QEMU read index (head) */
> +    uint32_t queue_depth;     /* Total number of command slots */
> +    uint32_t reserved;
> +} QEMU_PACKED;
> +
> +/*
> + * PCI MMIO Bridge State
> + *
> + */
> +typedef struct PCIMMIOBridge {
> +    /* Shadow buffer (guest RAM for DMA access) */
> +    MemoryRegion shadow_mr;
> +    hwaddr shadow_gpa;        /* Guest physical address */
> +    void *shadow_hva;         /* Host virtual address for polling */
> +    uint32_t shadow_size;     /* Size in bytes */
> +
> +    /* PCI bus to search for devices */
> +    struct PCIBus *pci_bus;   /* Main PCI bus */
> +
> +    /* Ring buffer management */
> +    uint32_t queue_depth;     /* Number of command slots available */
> +    uint32_t head;            /* Consumer index (QEMU's position) */
> +
> +    /* Polling infrastructure */
> +    QEMUTimer *poll_timer;
> +    QEMUBH *poll_bh;
> +    uint64_t poll_interval_ns;
> +    bool enabled;
> +    bool use_bh;              /* Use BH instead of timer (for qtest) */
> +
> +    /* Statistics for monitoring/debugging */
> +    uint64_t total_commands;
> +    uint64_t total_writes;
> +    uint64_t total_reads;
> +    uint64_t total_errors;
> +    uint64_t total_polls;
> +} PCIMMIOBridge;
> +
> +/*
> + * Initialize the PCI MMIO bridge
> + *
> + * @pci_bus: PCI bus to monitor for devices
> + * @gpa: Guest physical address for shadow buffer (must be page-aligned)
> + * @size: Size of shadow buffer in bytes (must be >= 4096)
> + * @poll_interval_ns: Polling interval in nanoseconds (0 = default)
> + * @errp: Error pointer
> + *
> + * Returns: Pointer to bridge state on success, NULL on error
> + */
> +PCIMMIOBridge *pci_mmio_bridge_init(struct PCIBus *pci_bus,
> +                                         hwaddr gpa, uint32_t size,
> +                                         uint64_t poll_interval_ns,
> +                                         Error **errp);
> +
> +/*
> + * Cleanup the PCI MMIO bridge
> + */
> +void pci_mmio_bridge_cleanup(PCIMMIOBridge *bridge);
> +
> +/*
> + * Enable/disable the bridge
> + */
> +void pci_mmio_bridge_set_enabled(PCIMMIOBridge *bridge, bool enabled);
> +
> +/*
> + * Get bridge statistics (for QMP/monitor)
> + */
> +void pci_mmio_bridge_get_stats(PCIMMIOBridge *bridge,
> +                               uint64_t *total_commands,
> +                               uint64_t *total_writes,
> +                               uint64_t *total_reads,
> +                               uint64_t *total_errors);
> +
> +/*
> + * Manually trigger one poll cycle (for testing)
> + *
> + * This is useful in qtest environments where timers don't automatically 
> fire.
> + */
> +void pci_mmio_bridge_poll_once(PCIMMIOBridge *bridge);
> +
> +/*
> + * Internal polling callback
> + */
> +void pci_mmio_bridge_poll(void *opaque);
> +
> +
> +/*
> + * PCI Device Interface
> + */
> +
> +/*
> + * Vendor-specific capability offsets in PCI config space
> + * These expose the shadow buffer GPA and size to guest drivers
> + */
> +#define PCI_MMIO_BRIDGE_CAP_OFFSET  0x40
> +#define PCI_MMIO_BRIDGE_CAP_GPA_LO  0x00  /* Lower 32 bits of GPA */
> +#define PCI_MMIO_BRIDGE_CAP_GPA_HI  0x04  /* Upper 32 bits of GPA */
> +#define PCI_MMIO_BRIDGE_CAP_SIZE    0x08  /* Buffer size */
> +#define PCI_MMIO_BRIDGE_CAP_DEPTH   0x0C  /* Queue depth */
> +
> +#define TYPE_PCI_MMIO_BRIDGE "pci-mmio-bridge"
> +OBJECT_DECLARE_SIMPLE_TYPE(PCIMMIOBridge_NEW, PCI_MMIO_BRIDGE)
> +
> +struct PCIMMIOBridge_NEW {
> +    PCIDevice parent_obj;
> +
> +    /* Core bridge state */
> +    PCIMMIOBridge *bridge;
> +
> +    /* Configuration properties */
> +    uint64_t shadow_gpa;       /* Guest physical address (0 = auto) */
> +    uint32_t shadow_size;      /* Size of shadow buffer */
> +    uint64_t poll_interval_ns; /* Polling interval */
> +    bool enabled;              /* Whether bridge is active */
> +};
> +#endif /* HW_PCI_MMIO_BRIDGE_H */
> diff --git a/include/hw/pci/pci.h b/include/hw/pci/pci.h
> index 6b7d3ac8a3..d5c6e546a8 100644
> --- a/include/hw/pci/pci.h
> +++ b/include/hw/pci/pci.h
> @@ -121,6 +121,7 @@ extern bool pci_available;
>  #define PCI_DEVICE_ID_REDHAT_ACPI_ERST   0x0012
>  #define PCI_DEVICE_ID_REDHAT_UFS         0x0013
>  #define PCI_DEVICE_ID_REDHAT_RISCV_IOMMU 0x0014
> +#define PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE 0x0015
>  #define PCI_DEVICE_ID_REDHAT_QXL         0x0100
>  
>  #define FMT_PCIBUS                      PRIx64
> diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
> index 669d07c06b..718f2e0606 100644
> --- a/tests/qtest/meson.build
> +++ b/tests/qtest/meson.build
> @@ -74,6 +74,7 @@ qtests_i386 = \
>    (config_all_devices.has_key('CONFIG_HDA') ? ['intel-hda-test'] : []) +     
>                \
>    (config_all_devices.has_key('CONFIG_I82801B11') ? ['i82801b11-test'] : []) 
> +             \
>    (config_all_devices.has_key('CONFIG_IOH3420') ? ['ioh3420-test'] : []) +   
>                \
> +  ['pci-mmio-bridge-test'] +              \
>    (config_all_devices.has_key('CONFIG_LPC_ICH9') ? ['lpc-ich9-test'] : []) + 
>              \
>    (config_all_devices.has_key('CONFIG_MC146818RTC') ? ['rtc-test'] : []) +   
>                \
>    (config_all_devices.has_key('CONFIG_USB_UHCI') ? ['usb-hcd-uhci-test'] : 
> []) +            \
> diff --git a/tests/qtest/pci-mmio-bridge-test.c 
> b/tests/qtest/pci-mmio-bridge-test.c
> new file mode 100644
> index 0000000000..ad795a2f7e
> --- /dev/null
> +++ b/tests/qtest/pci-mmio-bridge-test.c
> @@ -0,0 +1,417 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * QTest testcases for PCI MMIO Bridge (PCI Device)
> + *
> + * Copyright (c) 2025 Stephen Bates <[email protected]>
> + */
> +
> +#include "qemu/osdep.h"
> +#include "libqtest.h"
> +#include "libqos/pci.h"
> +#include "libqos/pci-pc.h"
> +#include "hw/pci/pci_regs.h"
> +#include "hw/pci/pci-mmio-bridge.h"
> +
> +/* Helper: Read shadow buffer GPA from PCI config space */
> +static uint64_t read_shadow_gpa(QPCIDevice *dev)
> +{
> +    uint32_t gpa_lo = qpci_config_readl(dev, PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                                        PCI_MMIO_BRIDGE_CAP_GPA_LO);
> +    uint32_t gpa_hi = qpci_config_readl(dev, PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                                        PCI_MMIO_BRIDGE_CAP_GPA_HI);
> +    return ((uint64_t)gpa_hi << 32) | gpa_lo;
> +}
> +
> +/* Helper: Read shadow buffer size from PCI config space */
> +static uint32_t read_shadow_size(QPCIDevice *dev)
> +{
> +    return qpci_config_readl(dev, PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                            PCI_MMIO_BRIDGE_CAP_SIZE);
> +}
> +
> +/* Helper: Read queue depth from PCI config space */
> +static uint32_t read_queue_depth(QPCIDevice *dev)
> +{
> +    return qpci_config_readl(dev, PCI_MMIO_BRIDGE_CAP_OFFSET +
> +                            PCI_MMIO_BRIDGE_CAP_DEPTH);
> +}
> +
> +/* Helper: Find bridge device on PCI bus */
> +static QPCIDevice *find_pci_mmio_bridge(QPCIBus *bus)
> +{
> +    for (int devfn = 0; devfn < 256; devfn++) {
> +        QPCIDevice *dev = qpci_device_find(bus, devfn);
> +        if (dev) {
> +            uint16_t vid = qpci_config_readw(dev, PCI_VENDOR_ID);
> +            uint16_t did = qpci_config_readw(dev, PCI_DEVICE_ID);
> +            if (vid == PCI_VENDOR_ID_REDHAT &&
> +                did == PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE) {
> +                return dev;
> +            }
> +            g_free(dev);
> +        }
> +    }
> +    return NULL;
> +}
> +
> +/* Helper: Find pci-testdev on PCI bus */
> +static QPCIDevice *find_pci_testdev(QPCIBus *bus)
> +{
> +    for (int devfn = 0; devfn < 256; devfn++) {
> +        QPCIDevice *dev = qpci_device_find(bus, devfn);
> +        if (dev) {
> +            uint16_t did = qpci_config_readw(dev, PCI_DEVICE_ID);
> +            if (did == 0x5050) {  /* pci-testdev device ID */
> +                return dev;
> +            }
> +            g_free(dev);
> +        }
> +    }
> +    return NULL;
> +}
> +
> +/* Test: Device discovery via PCI bus enumeration */
> +static void test_pci_device_discovery(void)
> +{
> +    QTestState *qts;
> +    QPCIBus *pcibus;
> +    QPCIDevice *dev;
> +    uint16_t vendor_id, device_id;
> +    uint8_t class_id;
> +
> +    qts = qtest_init("-machine q35 "
> +                     "-device pci-mmio-bridge,id=bridge0");
> +
> +    pcibus = qpci_new_pc(qts, NULL);
> +    g_assert_nonnull(pcibus);
> +
> +    /* Find the bridge device */
> +    dev = find_pci_mmio_bridge(pcibus);
> +    g_assert_nonnull(dev);
> +
> +    /* Verify PCI IDs */
> +    vendor_id = qpci_config_readw(dev, PCI_VENDOR_ID);
> +    device_id = qpci_config_readw(dev, PCI_DEVICE_ID);
> +    class_id = qpci_config_readb(dev, PCI_CLASS_DEVICE);
> +
> +    g_assert_cmpuint(vendor_id, ==, PCI_VENDOR_ID_REDHAT);
> +    g_assert_cmpuint(device_id, ==, PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE);
> +    g_assert_cmpuint(class_id, ==, 0x80); /* PCI_CLASS_SYSTEM_OTHER */
> +
> +    g_free(dev);
> +    qpci_free_pc(pcibus);
> +    qtest_quit(qts);
> +}
> +
> +/* Test: Shadow buffer GPA is exposed via config space */
> +static void test_pci_shadow_gpa_access(void)
> +{
> +    QTestState *qts;
> +    QPCIBus *pcibus;
> +    QPCIDevice *dev;
> +    uint64_t shadow_gpa;
> +    uint32_t shadow_size, queue_depth;
> +    struct pci_mmio_bridge_ring_meta meta;
> +
> +    qts = qtest_init("-machine q35 "
> +                     "-device pci-mmio-bridge,id=bridge0,shadow-size=4096");
> +
> +    pcibus = qpci_new_pc(qts, NULL);
> +    dev = find_pci_mmio_bridge(pcibus);
> +    g_assert_nonnull(dev);
> +
> +    /* Enable device */
> +    qpci_device_enable(dev);
> +
> +    /* Read shadow buffer location from PCI config space */
> +    shadow_gpa = read_shadow_gpa(dev);
> +    shadow_size = read_shadow_size(dev);
> +    queue_depth = read_queue_depth(dev);
> +
> +    /* Verify config space values */
> +    g_assert_cmpuint(shadow_gpa, >, 0);
> +    g_assert_cmpuint(shadow_size, ==, 4096);
> +    g_assert_cmpuint(queue_depth, ==, 169);  /* (4096/24)-1 */
> +
> +    /* Read ring metadata from guest RAM at shadow_gpa */
> +    qtest_memread(qts, shadow_gpa, &meta, sizeof(meta));
> +
> +    /* Verify metadata is initialized */
> +    g_assert_cmpuint(meta.producer_idx, ==, 0);
> +    g_assert_cmpuint(meta.consumer_idx, ==, 0);
> +    g_assert_cmpuint(meta.queue_depth, ==, 169);
> +
> +    g_free(dev);
> +    qpci_free_pc(pcibus);
> +    qtest_quit(qts);
> +}
> +
> +/* Test: Shadow size property */
> +static void test_pci_shadow_size_property(void)
> +{
> +    QTestState *qts;
> +    QPCIBus *pcibus;
> +    QPCIDevice *dev;
> +    uint64_t shadow_gpa;
> +    uint32_t shadow_size, queue_depth;
> +    struct pci_mmio_bridge_ring_meta meta;
> +
> +    qts = qtest_init("-machine q35 "
> +                     "-device pci-mmio-bridge,shadow-size=8192");
> +
> +    pcibus = qpci_new_pc(qts, NULL);
> +    dev = find_pci_mmio_bridge(pcibus);
> +    g_assert_nonnull(dev);
> +
> +    qpci_device_enable(dev);
> +
> +    /* Read shadow buffer info from config space */
> +    shadow_gpa = read_shadow_gpa(dev);
> +    shadow_size = read_shadow_size(dev);
> +    queue_depth = read_queue_depth(dev);
> +
> +    /* Verify size is 8192 and queue depth matches: (8192/24)-1 = 340 */
> +    g_assert_cmpuint(shadow_size, ==, 8192);
> +    g_assert_cmpuint(queue_depth, ==, 340);
> +
> +    /* Verify metadata in guest RAM */
> +    qtest_memread(qts, shadow_gpa, &meta, sizeof(meta));
> +    g_assert_cmpuint(meta.queue_depth, ==, 340);
> +
> +    g_free(dev);
> +    qpci_free_pc(pcibus);
> +    qtest_quit(qts);
> +}
> +
> +/* Test: Basic write command via shadow buffer */
> +static void test_pci_write_command(void)
> +{
> +    QTestState *qts;
> +    QPCIBus *pcibus;
> +    QPCIDevice *bridge_dev, *test_dev;
> +    QPCIBar test_bar;
> +    uint64_t shadow_gpa;
> +    struct pci_mmio_bridge_command cmd = {0};
> +    struct pci_mmio_bridge_ring_meta meta;
> +    uint32_t test_value_read;
> +
> +    qts = qtest_init("-machine q35 "
> +                     "-device pci-mmio-bridge,id=bridge0 "
> +                     "-device pci-testdev,id=testdev0");
> +
> +    pcibus = qpci_new_pc(qts, NULL);
> +
> +    /* Get bridge device and shadow GPA */
> +    bridge_dev = find_pci_mmio_bridge(pcibus);
> +    g_assert_nonnull(bridge_dev);
> +    qpci_device_enable(bridge_dev);
> +    shadow_gpa = read_shadow_gpa(bridge_dev);
> +
> +    /* Get test device */
> +    test_dev = find_pci_testdev(pcibus);
> +    if (!test_dev) {
> +        g_test_skip("pci-testdev not available");
> +        g_free(bridge_dev);
> +        qpci_free_pc(pcibus);
> +        qtest_quit(qts);
> +        return;
> +    }
> +    qpci_device_enable(test_dev);
> +    test_bar = qpci_iomap(test_dev, 0, NULL);
> +
> +    /* Prepare write command */
> +    cmd.target_bdf = (0 << 8) | test_dev->devfn;
> +    cmd.target_bar = 0;
> +    cmd.offset = 0;
> +    cmd.value = 0xDEADBEEF;
> +    cmd.command = PCI_MMIO_BRIDGE_CMD_WRITE;
> +    cmd.size = 4;
> +    cmd.status = PCI_MMIO_BRIDGE_STATUS_PENDING;
> +    cmd.sequence = 1;
> +
> +    /* Write command to slot 1 in guest RAM (slot 0 is metadata) */
> +    qtest_memwrite(qts, shadow_gpa + sizeof(meta), &cmd, sizeof(cmd));
> +
> +    /* Update producer index to signal command */
> +    meta.producer_idx = 1;
> +    qtest_memwrite(qts, shadow_gpa, &meta.producer_idx, 4);
> +
> +    /* Give QEMU time to process (BH should trigger) */
> +    qtest_clock_step(qts, 10000000); /* 10ms */
> +
> +    /* Read back command to check status */
> +    qtest_memread(qts, shadow_gpa + sizeof(meta), &cmd, sizeof(cmd));
> +    g_assert_cmpuint(cmd.status, ==, PCI_MMIO_BRIDGE_STATUS_COMPLETE);
> +
> +    /* Verify write reached target device */
> +    test_value_read = qpci_io_readl(test_dev, test_bar, 0);
> +    g_assert_cmpuint(test_value_read, ==, 0xDEADBEEF);
> +
> +    g_free(bridge_dev);
> +    g_free(test_dev);
> +    qpci_free_pc(pcibus);
> +    qtest_quit(qts);
> +}
> +
> +/* Test: Basic read command via shadow buffer */
> +static void test_pci_read_command(void)
> +{
> +    QTestState *qts;
> +    QPCIBus *pcibus;
> +    QPCIDevice *bridge_dev, *test_dev;
> +    QPCIBar test_bar;
> +    uint64_t shadow_gpa;
> +    struct pci_mmio_bridge_command cmd = {0};
> +    struct pci_mmio_bridge_ring_meta meta;
> +    uint32_t expected_value = 0xCAFEBABE;
> +
> +    qts = qtest_init("-machine q35 "
> +                     "-device pci-mmio-bridge,id=bridge0 "
> +                     "-device pci-testdev,id=testdev0");
> +
> +    pcibus = qpci_new_pc(qts, NULL);
> +
> +    bridge_dev = find_pci_mmio_bridge(pcibus);
> +    g_assert_nonnull(bridge_dev);
> +    qpci_device_enable(bridge_dev);
> +    shadow_gpa = read_shadow_gpa(bridge_dev);
> +
> +    test_dev = find_pci_testdev(pcibus);
> +    if (!test_dev) {
> +        g_test_skip("pci-testdev not available");
> +        g_free(bridge_dev);
> +        qpci_free_pc(pcibus);
> +        qtest_quit(qts);
> +        return;
> +    }
> +    qpci_device_enable(test_dev);
> +    test_bar = qpci_iomap(test_dev, 0, NULL);
> +
> +    /* Write a value to test device first */
> +    qpci_io_writel(test_dev, test_bar, 0, expected_value);
> +
> +    /* Prepare read command */
> +    cmd.target_bdf = (0 << 8) | test_dev->devfn;
> +    cmd.target_bar = 0;
> +    cmd.offset = 0;
> +    cmd.value = 0;
> +    cmd.command = PCI_MMIO_BRIDGE_CMD_READ;
> +    cmd.size = 4;
> +    cmd.status = PCI_MMIO_BRIDGE_STATUS_PENDING;
> +    cmd.sequence = 1;
> +
> +    /* Write command to slot 1 in guest RAM */
> +    qtest_memwrite(qts, shadow_gpa + sizeof(meta), &cmd, sizeof(cmd));
> +
> +    /* Signal command */
> +    meta.producer_idx = 1;
> +    qtest_memwrite(qts, shadow_gpa, &meta.producer_idx, 4);
> +
> +    /* Wait for processing */
> +    qtest_clock_step(qts, 10000000);
> +
> +    /* Read back command */
> +    qtest_memread(qts, shadow_gpa + sizeof(meta), &cmd, sizeof(cmd));
> +    g_assert_cmpuint(cmd.status, ==, PCI_MMIO_BRIDGE_STATUS_COMPLETE);
> +    g_assert_cmpuint(cmd.value, ==, expected_value);
> +
> +    g_free(bridge_dev);
> +    g_free(test_dev);
> +    qpci_free_pc(pcibus);
> +    qtest_quit(qts);
> +}
> +
> +/* Test: Device reset clears statistics */
> +static void test_pci_device_reset(void)
> +{
> +    QTestState *qts;
> +    QPCIBus *pcibus;
> +    QPCIDevice *dev;
> +    uint64_t shadow_gpa;
> +    struct pci_mmio_bridge_ring_meta meta;
> +
> +    qts = qtest_init("-machine q35 "
> +                     "-device pci-mmio-bridge,id=bridge0");
> +
> +    pcibus = qpci_new_pc(qts, NULL);
> +    dev = find_pci_mmio_bridge(pcibus);
> +    g_assert_nonnull(dev);
> +
> +    qpci_device_enable(dev);
> +    shadow_gpa = read_shadow_gpa(dev);
> +
> +    /* Write some data to producer index */
> +    meta.producer_idx = 5;
> +    qtest_memwrite(qts, shadow_gpa, &meta.producer_idx, 4);
> +
> +    /* Reset device */
> +    qtest_qmp_send(qts, "{ 'execute': 'system_reset' }");
> +    qtest_qmp_receive(qts);
> +    qtest_clock_step(qts, 1000000);
> +
> +    /* Verify producer index reset to 0 */
> +    qtest_memread(qts, shadow_gpa, &meta, sizeof(meta));
> +    g_assert_cmpuint(meta.producer_idx, ==, 0);
> +    g_assert_cmpuint(meta.consumer_idx, ==, 0);
> +
> +    g_free(dev);
> +    qpci_free_pc(pcibus);
> +    qtest_quit(qts);
> +}
> +
> +/* Test: Multiple bridges can coexist */
> +static void test_pci_multiple_bridges(void)
> +{
> +    QTestState *qts;
> +    QPCIBus *pcibus;
> +    QPCIDevice *dev1, *dev2;
> +    uint16_t device_id;
> +
> +    qts = qtest_init("-machine q35 "
> +                     "-device pci-mmio-bridge,id=bridge0,addr=4.0,"
> +                     "shadow-gpa=0x80000000 "
> +                     "-device pci-mmio-bridge,id=bridge1,addr=5.0,"
> +                     "shadow-gpa=0x81000000");
> +
> +    pcibus = qpci_new_pc(qts, NULL);
> +
> +    /* Find first bridge */
> +    dev1 = qpci_device_find(pcibus, QPCI_DEVFN(4, 0));
> +    g_assert_nonnull(dev1);
> +    device_id = qpci_config_readw(dev1, PCI_DEVICE_ID);
> +    g_assert_cmpuint(device_id, ==, PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE);
> +
> +    /* Find second bridge */
> +    dev2 = qpci_device_find(pcibus, QPCI_DEVFN(5, 0));
> +    g_assert_nonnull(dev2);
> +    device_id = qpci_config_readw(dev2, PCI_DEVICE_ID);
> +    g_assert_cmpuint(device_id, ==, PCI_DEVICE_ID_REDHAT_MMIO_BRIDGE);
> +
> +    g_free(dev1);
> +    g_free(dev2);
> +    qpci_free_pc(pcibus);
> +    qtest_quit(qts);
> +}
> +
> +int main(int argc, char **argv)
> +{
> +    g_test_init(&argc, &argv, NULL);
> +
> +    qtest_add_func("/pci-mmio-bridge-pci/device-discovery",
> +                   test_pci_device_discovery);
> +    qtest_add_func("/pci-mmio-bridge-pci/shadow-gpa-access",
> +                   test_pci_shadow_gpa_access);
> +    qtest_add_func("/pci-mmio-bridge-pci/shadow-size-property",
> +                   test_pci_shadow_size_property);
> +    qtest_add_func("/pci-mmio-bridge-pci/write-command",
> +                   test_pci_write_command);
> +    qtest_add_func("/pci-mmio-bridge-pci/read-command",
> +                   test_pci_read_command);
> +    qtest_add_func("/pci-mmio-bridge-pci/device-reset",
> +                   test_pci_device_reset);
> +    qtest_add_func("/pci-mmio-bridge-pci/multiple-bridges",
> +                   test_pci_multiple_bridges);
> +
> +    return g_test_run();
> +}
> +
> -- 
> 2.43.0
> 
> 
> -- 
> 
> Cheers
> 
> Stephen Bates, PhD.


Reply via email to