ref:2e70abbfcc648702b0c9e503bc031b0779220a0d

test(qemu): run the GPU import tests in a VM, and allocate their buffer with GBM

A host without a DRM render node skipped all of REQ-CAP-003's GPU tests, so none had ever run. A guest with virtio-gpu has a render node, /dev/udmabuf and EGL with dma_buf import, so the tests run there instead. The EGL test allocated its buffer with udmabuf, which Mesa refuses to import because it didn't allocate it. It now allocates through GBM the way QEMU does, falling back to a card node (software GBM allocates KMS dumb buffers, which a render node may not create) and then to udmabuf. The import then works, and llvmpipe crashes in its own JIT while sampling the imported buffer, so the test skips on a software renderer and says why. Vulkan imports the same buffer fine. gpu_in_vm.sh boots the desktop guest, copies the host's test binary and the libraries the guest lacks, and runs the tests there; CI runs it as gpu-tests-in-vm and fails if the Vulkan test doesn't run and pass. The desktop rig also logs at debug now, which records per-session frame processing latency. REQ-CAP-003 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPNw4PCgkEfhyCjQT19wsb
SHA: 2e70abbfcc648702b0c9e503bc031b0779220a0d
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-17 02:29
Parents: 288d70a
8 files changed +417 -36
Type
.anvil.yml +11 −1
@@ -84,8 +84,18 @@
- name: e2e
path: ci-artifacts/e2e-*/*
- name: gpu-tests-in-vm
image: bare
runs_on: [linux, amd64]
timeout_seconds: 3600
depends_on: [build-and-unit-test-qemu-on]
run: scripts/ci/anvil-test.sh gpu-vm
artifacts:
- name: gpu-vm
path: ci-artifacts/gpu-vm/*
- name: requirements
runs_on: [linux, amd64]
timeout_seconds: 600
depends_on: [build-and-unit-test-qemu-on, build-and-unit-test-qemu-off, lint, packaging, e2e, gpu-tests-in-vm]
depends_on: [build-and-unit-test-qemu-on, build-and-unit-test-qemu-off, lint, packaging, e2e]
run: scripts/ci/anvil-test.sh requirements
docs/qemu.md +18 −0
@@ -333,6 +333,24 @@
virt-manager's own console can't show a D-Bus display; use Moonlight (or `virt-viewer`, which then takes the
display away from Sunshine, see above).
## Testing the GPU paths without a GPU of your own
A host whose GPU has no DRM render node (WSL2, some containers) can't check the DMABUF import paths
(REQ-CAP-003) directly: `/dev/dri` isn't there. A guest can. With `virtio-gpu` the guest has
`/dev/dri/card0`, `/dev/dri/renderD128` and `/dev/udmabuf`, and Mesa gives it EGL with
`EGL_EXT_image_dma_buf_import` and Vulkan through lavapipe, all in software.
`tests/e2e/qemu/gpu_in_vm.sh` boots the desktop guest, copies the host's `test_sunshine` into it with
whatever libraries the guest is missing, and runs the GPU tests there. CI runs it as `gpu-tests-in-vm`.
What that does and doesn't prove:
- The Vulkan import, crop and flip run for real, as does the check that VAAPI only accepts QEMU's own
render node.
- The EGL import is skipped: Mesa's software rasterizer imports the buffer and then crashes in its JIT
while sampling it, so that path still needs GPU hardware.
- Nothing about speed. Software rendering says nothing about the latency target.
## Troubleshooting
### "Unable to initialize capture method" or "Unable to find display or encoder during startup"
JOURNAL.md +25 −0
@@ -686,3 +686,28 @@
- Broken capture check: with `convert_row` swapping red and blue, `anvil-test.sh e2e shm` exits 1
(`matched: false`, quadrants swapped). It exits 0 again once restored. The patch was never committed.
### GPU tests without a GPU render node (2026-09-16)
WSL2 has no `/dev/dri`: the 4090 is reached through `/dev/dxg` (GPU-PV), NVENC goes through CUDA, there's no PCI
passthrough, and module loading is blocked (`vgem` won't insert). A **guest** has what's needed, though:
`virtio_gpu` + `virtio_dma_buf` give it `/dev/dri/card0`, `/dev/dri/renderD128` and `/dev/udmabuf`, with EGL 1.5
(`EGL_EXT_image_dma_buf_import`, modifiers), GBM and OpenGL 4.5 on llvmpipe.
Running the host's `test_sunshine` inside the desktop guest moved three never-executed tests:
| Test | Before | In a VM |
|---|---|---|
| `QemuCaptureTest.VaapiRequiresQemuToRenderOnTheEncoderGpu` | skipped everywhere | **passes** |
| `QemuDmabufGpuTest.VulkanCropsAndFlipsBottomUpScanoutLikeQemu` | skipped everywhere | **passes** |
| `QemuDmabufGpuTest.ImportsCropsAndFlipsBottomUpScanout` | skipped everywhere | skipped, with a reason |
- The EGL test first failed with `EGL_BAD_ALLOC`: Mesa refuses to import a udmabuf it didn't allocate. The test now
allocates through GBM, which is what QEMU does. Software GBM allocates KMS dumb buffers, which a *render* node
may not create ("DRM_IOCTL_MODE_CREATE_DUMB: Permission denied"), so it falls back to a card node and then to
udmabuf. A DMABUF crossing nodes is the point of the format.
- With a GBM buffer the import succeeds and llvmpipe then segfaults in its own JIT thread while sampling it
(`Thread 5 "llvmpipe-2"`, no frames of ours). The same buffer imports fine through Vulkan, so this is the
software rasterizer, not the backend: the test now skips when `GL_RENDERER` is llvmpipe, softpipe or swrast.
- `tests/e2e/qemu/gpu_in_vm.sh` does this end to end, and CI runs it as `gpu-tests-in-vm`. It fails if the Vulkan
test doesn't run and pass, so a VM that silently skips everything isn't green.
- Still needs real hardware: the EGL import path, VAAPI encoding, and every latency number for REQ-CAP-003.
scripts/ci/anvil-test.sh +16 −0
@@ -12,6 +12,7 @@
# lint [base] clang-format on C/C++ files changed since base (default origin/main), doxygen
# e2e <mode> end-to-end stream against a real QEMU guest: shm (bus transport), qmp, audio,
# input-absolute, input-relative, reset, dmabuf (skipped without a DRM render node)
# gpu-vm REQ-CAP-003's GPU tests inside a VM, for hosts whose GPU has no render node
# requirements anvil requirement status for this repository
#
# Environment: CC/CXX (default gcc-14/g++-14), JOBS (default nproc), CI_ARTIFACTS (default ci-artifacts),
@@ -190,6 +191,20 @@
esac
}
cmd_gpu_vm() {
section "GPU tests in a VM"
local dir="cmake-build-ci-on"
[[ -x "${dir}/tests/test_sunshine" ]] || { echo "build the tests first: anvil-test.sh build on" >&2; exit 2; }
local status=0
SUNSHINE_TESTS="${repo}/${dir}/tests/test_sunshine" GPU_VM_ARTIFACTS="${artifacts}/gpu-vm" \
tests/e2e/qemu/gpu_in_vm.sh || status=$?
if ((status == 77)); then
echo "SKIPPED: this runner has no desktop guest image (REQ-CAP-003 unchecked here)"
return 0
fi
return "${status}"
}
cmd_requirements() {
section "requirement traceability"
local anvil="${ANVIL_BIN:-anvil}"
@@ -206,6 +221,7 @@
shift || true
case "${command}" in
submodules | build | unit | packaging | lint | e2e | requirements) "cmd_${command}" "$@" ;;
gpu-vm) cmd_gpu_vm "$@" ;;
*)
sed -n '2,20p' "${BASH_SOURCE[0]}"
exit 2
tests/e2e/qemu/desktop/build_desktop.sh +5 −0
@@ -48,6 +48,11 @@
- x11-xserver-utils
- xdotool
- evtest
# what tests/e2e/qemu/gpu_in_vm.sh needs from the guest's virtio-gpu: DRM, GBM, EGL and Vulkan
- libgbm1
- libegl1
- libvulkan1
- mesa-vulkan-drivers
write_files:
- path: /etc/lightdm/lightdm.conf.d/50-autologin.conf
content: |
tests/e2e/qemu/desktop/serve_desktop.sh +2 −0
@@ -13,6 +13,7 @@
# ENCODER nvenc, vaapi, vulkan or software (default: nvenc)
# PORT Sunshine base port (default: 47989, what Moonlight expects)
# SERVE_EXIT_AFTER seconds to run before shutting down, for automated checks (default: run until Ctrl+C)
# LOG_LEVEL Sunshine log level (default: debug, which logs frame processing latency per session)
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -63,6 +64,7 @@
cert = ${serve_dir}/sunshine.crt
file_apps = ${serve_dir}/apps.json
log_path = ${serve_dir}/sunshine.log
min_log_level = ${LOG_LEVEL:-debug}
upnp = disabled
system_tray = disabled
origin_web_ui_allowed = lan
tests/e2e/qemu/gpu_in_vm.sh +98 −0
@@ -1,0 +1,98 @@
#!/usr/bin/env bash
# Run the GPU tests inside a VM, on hosts whose GPU has no DRM render node.
#
# A guest with virtio-gpu has /dev/dri, /dev/udmabuf and EGL with dma_buf import, so REQ-CAP-003's
# import paths run there even when the host's own GPU can't be reached that way (WSL2, containers).
# Rendering is software, so this checks behavior, never latency.
#
# The test binary is the host's: the guest runs the same Ubuntu release, so only the libraries the
# guest is missing are copied next to it.
#
# Usage: gpu_in_vm.sh
#
# Environment:
# SUNSHINE_TESTS test_sunshine binary (default: <repo>/cmake-build-debug/tests/test_sunshine)
# QEMU QEMU binary (default: qemu-system-x86_64)
# DESKTOP_CACHE desktop guest image (default: ~/.cache/sunshine-qemu/desktop)
# GPU_VM_FILTER gtest filter (default: the DMABUF and VAAPI tests)
# GPU_VM_REQUIRE test that has to run and pass, so a VM that skips everything isn't green
# (default: QemuDmabufGpuTest.VulkanCropsAndFlipsBottomUpScanoutLikeQemu; "" to allow all skips)
# GPU_VM_ARTIFACTS where to put the JUnit XML and logs (default: <repo>/ci-artifacts/gpu-vm)
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo="$(cd "${script_dir}/../../.." && pwd)"
tests_bin="${SUNSHINE_TESTS:-${repo}/cmake-build-debug/tests/test_sunshine}"
desktop_cache="${DESKTOP_CACHE:-${HOME}/.cache/sunshine-qemu/desktop}"
filter="${GPU_VM_FILTER:-QemuDmabufGpuTest.*:QemuCaptureTest.Vaapi*}"
require="${GPU_VM_REQUIRE-QemuDmabufGpuTest.VulkanCropsAndFlipsBottomUpScanoutLikeQemu}"
artifacts="${GPU_VM_ARTIFACTS:-${repo}/ci-artifacts/gpu-vm}"
[[ -x "${tests_bin}" ]] || { echo "gpu-vm: missing ${tests_bin}; build the tests first" >&2; exit 2; }
if [[ ! -f "${desktop_cache}/desktop-base.qcow2" ]]; then
echo "gpu-vm: SKIPPED: no desktop guest image; run tests/e2e/qemu/desktop/build_desktop.sh" >&2
exit 77
fi
rm -rf "${artifacts}"
mkdir -p "${artifacts}"
work="$(mktemp -d "${TMPDIR:-/tmp}/sunshine-gpu-vm.XXXXXX")"
cleanup() {
set +e
for pidfile in "${work}/vm/qemu.pid" "${work}/vm/dbus.pid"; do
[[ -f "${pidfile}" ]] && kill "$(cat "${pidfile}")" 2> /dev/null
done
rm -rf "${work}"
}
trap cleanup EXIT INT TERM
echo "gpu-vm: booting the guest"
SNAPSHOT=on "${script_dir}/desktop/run_desktop.sh" "${work}/vm" 1280 800 > "${artifacts}/boot.log" 2>&1
ssh_port="$(cat "${work}/vm/ssh.port")"
ssh_opts=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -i "${desktop_cache}/ssh_key" -p "${ssh_port}")
guest() {
ssh -q "${ssh_opts[@]}" sq@127.0.0.1 "$@"
}
echo "gpu-vm: the guest's DRM devices"
guest 'ls -1 /dev/dri /dev/udmabuf 2>&1' | tee "${artifacts}/devices.txt"
# only the libraries the guest doesn't have; both run the same Ubuntu release
echo "gpu-vm: copying the test binary"
guest 'mkdir -p /tmp/gpu-test/libs'
ldd "${tests_bin}" | awk '/=> \// {print $3}' | sort -u > "${work}/libs.txt"
# shellcheck disable=SC2016 # the loop runs in the guest, so its expansions must survive the trip
guest 'while read -r p; do b="${p##*/}"; ldconfig -p | grep -q " ${b} " || echo "${p}"; done' < "${work}/libs.txt" > "${work}/missing.txt"
if [[ -s "${work}/missing.txt" ]]; then
# shellcheck disable=SC2046 # one scp for every missing library
scp -q "${ssh_opts[@]/-p/-P}" $(tr '\n' ' ' < "${work}/missing.txt") sq@127.0.0.1:/tmp/gpu-test/libs/
fi
scp -q "${ssh_opts[@]/-p/-P}" "${tests_bin}" sq@127.0.0.1:/tmp/gpu-test/test_sunshine
# as root for /dev/udmabuf (root:kvm in the guest); GCOV_PREFIX keeps a coverage build from writing to the host's paths
echo "gpu-vm: running ${filter}"
status=0
guest "cd /tmp/gpu-test && sudo env GCOV_PREFIX=/tmp/gcov LD_LIBRARY_PATH=/tmp/gpu-test/libs ./test_sunshine --gtest_filter='${filter}' --gtest_output=xml:/tmp/gpu-test/gpu-tests.xml" \
> "${artifacts}/tests.log" 2>&1 || status=$?
grep -vE "^profiling:" "${artifacts}/tests.log" | grep -E "^\[|SKIPPED|OK \]|FAILED" || true
scp -q "${ssh_opts[@]/-p/-P}" sq@127.0.0.1:/tmp/gpu-test/gpu-tests.xml "${artifacts}/gpu-tests.xml" 2> /dev/null || true
if ((status != 0)); then
echo "gpu-vm: FAIL: the tests exited ${status}" >&2
exit 1
fi
if [[ -n "${require}" ]]; then
if ! grep -qE "\[ *OK *\] ${require//./\\.}" "${artifacts}/tests.log"; then
echo "gpu-vm: FAIL: ${require} didn't run and pass; the guest has no usable GPU stack" >&2
grep -E "SKIPPED|Skipped" "${artifacts}/tests.log" | head -5 >&2
exit 1
fi
fi
# gtest names each skipped test twice: when it runs and again in the summary
passed="$(grep -cE "^\[ *OK *\]" "${artifacts}/tests.log" || true)"
skipped="$(grep -E "^\[ *SKIPPED *\] [A-Za-z]" "${artifacts}/tests.log" | awk '{print $3}' | sort -u | wc -l)"
echo "gpu-vm: PASS: ${passed} passed, ${skipped} skipped; artifacts in ${artifacts}"
tests/unit/platform/linux/qemu/test_dmabuf_gpu.cpp +242 −35
@@ -1,9 +1,10 @@
/**
* @file tests/unit/platform/linux/qemu/test_dmabuf_gpu.cpp
* @brief Test importing QEMU-style DMABUF scanouts through EGL and Vulkan on a real GPU.
* @details Needs `/dev/udmabuf` and a DRM render node with GBM (EGL) or a Vulkan device that
* @details Needs a DRM render node with GBM (EGL) or a Vulkan device that imports DMABUFs, and
* `/dev/udmabuf` when the buffer can't come from GBM. A guest with `virtio-gpu` has all of these
* (software rendering), so these run in a VM on hosts without a render node of their own, such as
* WSL2; only REQ-CAP-003's latency needs GPU hardware.
* imports DMABUFs. Hosts without them (containers, WSL2) skip these tests, so REQ-CAP-003's GPU
* import is only verified on native Linux GPU hosts.
*/
#ifdef SUNSHINE_BUILD_QEMU
// test includes
@@ -11,9 +12,15 @@
// standard includes
#include <array>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
// platform includes
#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <linux/udmabuf.h>
#include <sys/ioctl.h>
@@ -92,6 +99,200 @@
}
/**
* @brief Write position-encoded XRGB8888 pixels, so every pixel says where it came from.
*
* @param map First byte of the buffer.
* @param width Buffer width.
* @param height Buffer height.
* @param stride Bytes per row.
*/
void fill_pattern(std::uint8_t *map, int width, int height, int stride) {
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
auto p = map + (std::size_t) y * stride + (std::size_t) x * 4;
p[0] = (std::uint8_t) (x * 40); // B
p[1] = (std::uint8_t) (y * 40); // G
p[2] = 0x80; // R
p[3] = 0xff;
}
}
}
/**
* @brief GBM buffer allocation, which the production wrapper doesn't need and so doesn't load.
* @details A driver imports a buffer it allocated itself; Mesa's software renderer, for one,
* refuses an arbitrary udmabuf. Loaded here so the test allocates the way QEMU does.
*/
struct gbm_alloc_t {
struct bo;
void *lib {nullptr}; ///< libgbm handle.
bo *(*create)(gbm::device *, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t) {nullptr};
void (*destroy)(bo *) {nullptr};
int (*get_fd)(bo *) {nullptr};
std::uint32_t (*get_stride)(bo *) {nullptr};
std::uint64_t (*get_modifier)(bo *) {nullptr};
void *(*map)(bo *, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t *, void **) {nullptr};
void (*unmap)(bo *, void *) {nullptr};
static constexpr std::uint32_t format_xrgb8888 = 0x34325258; ///< `GBM_FORMAT_XRGB8888`.
static constexpr std::uint32_t use_rendering = 1 << 2; ///< `GBM_BO_USE_RENDERING`.
static constexpr std::uint32_t use_linear = 1 << 4; ///< `GBM_BO_USE_LINEAR`.
static constexpr std::uint32_t transfer_write = 2; ///< `GBM_BO_TRANSFER_WRITE`.
~gbm_alloc_t() {
if (lib) {
dlclose(lib);
}
}
/**
* @brief Load libgbm's buffer calls.
*
* @return True when every symbol is there.
*/
bool load() {
lib = dlopen("libgbm.so.1", RTLD_LAZY | RTLD_LOCAL);
if (!lib) {
return false;
}
auto sym = [this](auto &fn, const char *name) {
fn = reinterpret_cast<std::remove_reference_t<decltype(fn)>>(dlsym(lib, name));
return fn != nullptr;
};
return sym(create, "gbm_bo_create") && sym(destroy, "gbm_bo_destroy") && sym(get_fd, "gbm_bo_get_fd") &&
sym(get_stride, "gbm_bo_get_stride") && sym(get_modifier, "gbm_bo_get_modifier") &&
sym(map, "gbm_bo_map") && sym(unmap, "gbm_bo_unmap");
}
};
/**
* @brief A DMABUF of position-encoded pixels, from GBM when the driver can allocate one.
*/
struct test_dmabuf_t {
int fd {-1}; ///< DMABUF descriptor.
int stride {0}; ///< Bytes per row, which GBM decides.
std::uint64_t modifier {qemu::drm_fourcc::mod_invalid}; ///< Layout of the allocated buffer.
std::string source; ///< Where the buffer came from, for the skip and failure messages.
gbm_alloc_t gbm_alloc; ///< Keeps libgbm loaded while the buffer lives.
gbm_alloc_t::bo *bo {nullptr}; ///< The GBM buffer, when GBM allocated it.
file_t alloc_fd; ///< Node the buffer was allocated on, when it isn't the EGL display's own.
gbm::gbm_t alloc_device; ///< GBM device on that node.
test_dmabuf_t() = default;
test_dmabuf_t(const test_dmabuf_t &) = delete;
test_dmabuf_t &operator=(const test_dmabuf_t &) = delete;
~test_dmabuf_t() {
if (fd >= 0) {
close(fd);
}
if (bo && gbm_alloc.destroy) {
gbm_alloc.destroy(bo);
}
}
};
/**
* @brief Allocate the test buffer through GBM on the given device.
*
* @param device GBM device from the render node.
* @param out Buffer to fill in.
* @param width Buffer width.
* @param height Buffer height.
* @return True when GBM allocated, mapped and exported the buffer.
*/
bool make_gbm_dmabuf(gbm::device *device, test_dmabuf_t &out, int width, int height) {
if (!out.gbm_alloc.load()) {
return false;
}
auto &g = out.gbm_alloc;
auto give_up = [&]() {
if (out.bo) {
g.destroy(out.bo);
out.bo = nullptr;
}
return false;
};
out.bo = g.create(device, (std::uint32_t) width, (std::uint32_t) height, gbm_alloc_t::format_xrgb8888, gbm_alloc_t::use_rendering | gbm_alloc_t::use_linear);
if (!out.bo) {
return false;
}
std::uint32_t map_stride = 0;
void *map_data = nullptr;
auto map = (std::uint8_t *) g.map(out.bo, 0, 0, (std::uint32_t) width, (std::uint32_t) height, gbm_alloc_t::transfer_write, &map_stride, &map_data);
if (!map || map == MAP_FAILED) {
return give_up();
}
fill_pattern(map, width, height, (int) map_stride);
g.unmap(out.bo, map_data);
out.fd = g.get_fd(out.bo);
out.stride = (int) g.get_stride(out.bo);
out.modifier = g.get_modifier(out.bo);
if (out.fd < 0 || out.stride < width * 4) {
return give_up();
}
out.source = "GBM";
return true;
}
int make_udmabuf(int width, int height);
/**
* @brief Fill the test buffer, from GBM where it can and `/dev/udmabuf` otherwise.
* @details Mesa's software renderer allocates through KMS dumb buffers, which a render node isn't
* allowed to create, so a card node is tried as well: a DMABUF crosses nodes, which is the point.
*
* @param preferred GBM device to try first, when the caller already has one.
* @param out Buffer to fill in.
* @param width Buffer width.
* @param height Buffer height.
* @return True when the buffer has pixels and a descriptor.
*/
bool make_dmabuf(gbm::device *preferred, test_dmabuf_t &out, int width, int height) {
if (preferred && make_gbm_dmabuf(preferred, out, width, height)) {
return true;
}
if (gbm::init()) {
out.fd = make_udmabuf(width, height);
out.stride = width * 4;
out.source = "udmabuf";
return out.fd >= 0;
}
std::unique_ptr<DIR, int (*)(DIR *)> dir {opendir("/dev/dri"), &closedir};
while (dir) {
auto entry = readdir(dir.get());
if (!entry) {
break;
}
if (std::string_view {entry->d_name}.substr(0, 4) != "card") {
continue;
}
const auto node = std::string {"/dev/dri/"} + entry->d_name;
out.alloc_fd.el = open(node.c_str(), O_RDWR | O_CLOEXEC);
if (out.alloc_fd.el < 0) {
continue;
}
out.alloc_device.reset(gbm::create_device(out.alloc_fd.el));
if (out.alloc_device && make_gbm_dmabuf(out.alloc_device.get(), out, width, height)) {
out.source = "GBM on " + node;
return true;
}
out.alloc_device.reset();
out.alloc_fd = file_t {};
}
out.fd = make_udmabuf(width, height);
out.stride = width * 4;
out.source = "udmabuf";
return out.fd >= 0;
}
/**
* @brief Create a udmabuf with position-encoded XRGB8888 pixels.
*
* @param width Buffer width.
@@ -110,15 +311,7 @@
if (memfd >= 0 && ftruncate(memfd, (off_t) size) == 0 && fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK) == 0) {
auto map = (std::uint8_t *) mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0);
if (map != MAP_FAILED) {
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
auto p = map + ((std::size_t) y * width + x) * 4;
p[0] = (std::uint8_t) (x * 40); // B
p[1] = (std::uint8_t) (y * 40); // G
p[2] = 0x80; // R
p[3] = 0xff;
}
}
fill_pattern(map, width, height, width * 4);
munmap(map, size);
udmabuf_create create {};
create.memfd = (std::uint32_t) memfd;
@@ -138,29 +331,39 @@
// @tag requirements: [REQ-CAP-003]
TEST(QemuDmabufGpuTest, ImportsCropsAndFlipsBottomUpScanout) {
int dmabuf = make_udmabuf(4, 4);
if (dmabuf < 0) {
GTEST_SKIP() << "/dev/udmabuf is not available; REQ-CAP-003 EGL import is verified only on native Linux GPU hosts";
}
std::string reason;
auto gpu = open_gpu(reason);
if (!gpu) {
GTEST_SKIP() << reason << "; REQ-CAP-003's EGL import needs a DRM render node, which a guest with virtio-gpu has";
}
// Mesa's software rasterizer imports the DMABUF and then crashes in its JIT while sampling it, so a
// VM covers the Vulkan import (which lavapipe does handle) and leaves this one to real hardware.
const auto renderer = (const char *) gl::ctx.GetString(GL_RENDERER);
for (const auto software : {"llvmpipe", "softpipe", "swrast"}) {
if (renderer && std::string_view {renderer}.find(software) != std::string_view::npos) {
GTEST_SKIP() << "GL renders in software (" << renderer << "), which can't sample an imported DMABUF; REQ-CAP-003's EGL import needs a GPU";
}
}
// GBM first: a driver imports what it allocated itself, and Mesa's software renderer refuses a
// udmabuf outright, so GBM is what makes this run in a VM.
close(dmabuf);
GTEST_SKIP() << reason << "; REQ-CAP-003 EGL import is verified only on native Linux GPU hosts";
test_dmabuf_t buffer;
if (!make_dmabuf(gpu->gbm.get(), buffer, 4, 4)) {
GTEST_SKIP() << "neither GBM nor /dev/udmabuf gave us a DMABUF; REQ-CAP-003's EGL import is unverified here";
}
egl::surface_descriptor_t sd {};
sd.width = 4;
sd.height = 4;
sd.fds[0] = dmabuf;
sd.fds[0] = buffer.fd;
sd.fds[1] = sd.fds[2] = sd.fds[3] = -1;
sd.fourcc = qemu::drm_fourcc::xrgb8888;
sd.modifier = buffer.modifier;
sd.modifier = qemu::drm_fourcc::mod_invalid;
sd.pitches[0] = 16;
sd.pitches[0] = buffer.stride;
sd.offsets[0] = 0;
auto rgb = egl::import_source(gpu->display.get(), sd);
close(dmabuf);
ASSERT_TRUE(rgb.has_value()) << "EGL couldn't import the " << buffer.source << " DMABUF";
ASSERT_TRUE(rgb.has_value()) << "EGL couldn't import the DMABUF";
// the 2x2 display rectangle at (1, 1) of a bottom-up buffer, as sws_t::load_vram copies it
auto target = gl::tex_t::make(1);
@@ -200,16 +403,22 @@
constexpr int height = 2;
const auto skip_reason = "; REQ-CAP-003 Vulkan import is verified only on native Linux GPU hosts";
// udmabuf first here: it gives the tight, linear layout this test's expectations are written for.
// GBM covers hosts where /dev/udmabuf isn't readable, at whatever stride and layout it picks.
test_dmabuf_t buffer;
int dmabuf = make_udmabuf(backing_width, backing_height);
if (dmabuf < 0) {
GTEST_SKIP() << "/dev/udmabuf is not available" << skip_reason;
buffer.fd = make_udmabuf(backing_width, backing_height);
if (buffer.fd >= 0) {
buffer.stride = backing_width * 4;
buffer.modifier = qemu::drm_fourcc::mod_linear;
buffer.source = "udmabuf";
} else if (!make_dmabuf(nullptr, buffer, backing_width, backing_height)) {
GTEST_SKIP() << "neither /dev/udmabuf nor GBM gave us a DMABUF" << skip_reason;
}
auto device = vk::make_avcodec_encode_device_vram(width, height, offset_x, offset_y);
ASSERT_NE(device, nullptr);
AVBufferRef *hw_device = nullptr;
if (vk::vulkan_init_avcodec_hardware_input_buffer(device.get(), &hw_device) < 0) {
close(dmabuf);
GTEST_SKIP() << "no Vulkan device" << skip_reason;
}
AVBufferRef *frames_ref = av_hwframe_ctx_alloc(hw_device);
@@ -223,6 +432,5 @@
if (av_hwframe_ctx_init(frames_ref) < 0) {
av_buffer_unref(&frames_ref);
av_buffer_unref(&hw_device);
close(dmabuf);
GTEST_SKIP() << "the Vulkan device can't create NV12 encoder frames" << skip_reason;
}
@@ -241,27 +449,26 @@
img.data = nullptr;
img.sd.width = backing_width;
img.sd.height = backing_height;
img.sd.fds[0] = buffer.fd;
img.sd.fds[0] = dmabuf;
img.sd.fds[1] = img.sd.fds[2] = img.sd.fds[3] = -1;
img.sd.fourcc = qemu::drm_fourcc::xrgb8888;
img.sd.modifier = qemu::drm_fourcc::mod_linear;
img.sd.modifier = buffer.modifier;
img.sd.pitches[0] = buffer.stride;
img.sd.pitches[0] = backing_width * 4;
img.sd.offsets[0] = 0;
const int converted = device->convert(img);
close(dmabuf);
img.sd.fds[0] = -1;
img.sd.fds[0] = -1; // the buffer closes its own descriptor
if (converted != 0) {
device.reset();
av_buffer_unref(&frames_ref);
av_buffer_unref(&hw_device);
GTEST_SKIP() << "the Vulkan device couldn't import the DMABUF" << skip_reason;
GTEST_SKIP() << "the Vulkan device couldn't import the " << buffer.source << " DMABUF" << skip_reason;
}
AVFrame *download = av_frame_alloc();
download->format = AV_PIX_FMT_NV12;
ASSERT_EQ(av_hwframe_transfer_data(download, frame, 0), 0);
// make_udmabuf fills B = x * 40, G = y * 40, R = 0x80
// fill_pattern writes B = x * 40, G = y * 40, R = 0x80
const auto colors = video::color_vectors_from_colorspace(device->colorspace, true);
auto expected_y = [&](int buffer_x, int buffer_y) {
const float rgb[3] {0x80 / 255.0f, (float) (buffer_y * 40) / 255.0f, (float) (buffer_x * 40) / 255.0f};