@@ -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};