▸
src/platform/linux/qemu/capture.cpp
+16
−3
@@ -311,9 +311,16 @@
protected:
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, bool draw_cursor) override {
if (conn.store->kind() == scanout_kind_e::dmabuf) {
if (mem_type != platf::mem_type_e::system) {
BOOST_LOG(info) << "qemu: QEMU switched to DMABUF scanouts; re-creating the display for zero-copy encoding"sv;
return platf::capture_e::reinit;
}
if (!conn.store->dmabuf_readable()) {
// re-creating the display reports the error instead of streaming black frames
BOOST_LOG(warning) << "qemu: QEMU switched to DMABUF scanouts that can't be read from system memory; re-creating the display"sv;
return platf::capture_e::reinit;
}
if (mem_type != platf::mem_type_e::system && conn.store->kind() == scanout_kind_e::dmabuf) {
BOOST_LOG(info) << "qemu: QEMU switched to DMABUF scanouts; re-creating the display for zero-copy encoding"sv;
return platf::capture_e::reinit;
}
if (conn.store->sequence() == copied_sequence) {
return platf::capture_e::timeout;
@@ -462,6 +469,12 @@
}
img->x = cursor.x - cursor.hot_x;
img->y = cursor.y - cursor.hot_y;
if (mem_type == platf::mem_type_e::vulkan) {
// the Vulkan encoder takes the cursor in buffer coordinates and subtracts the crop offset;
// the GL path (VAAPI, CUDA) takes it in display coordinates
img->x += offset_x;
img->y += offset_y;
}
img->src_w = cursor.width;
img->src_h = cursor.height;
img->width = cursor.width;
▸
src/platform/linux/qemu/frame_store.cpp
+27
−11
@@ -164,20 +164,26 @@
touch_locked();
}
void frame_store_t::scanout_map(fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) {
bool frame_store_t::scanout_map(fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) {
// QEMU falls back to Scanout/Update messages after a rejection; stop applying damage to the old map
auto reject = [this]() {
std::lock_guard lock {mutex};
unmap_locked();
return false;
};
if (fd.get() < 0) {
BOOST_LOG(warning) << "qemu: ScanoutMap without a descriptor"sv;
return;
return reject();
}
auto layout = layout_or_log(format);
if (!layout) {
return;
return reject();
}
struct stat st {};
if (fstat(fd.get(), &st) != 0 || !covers(st.st_size > offset ? (std::size_t) st.st_size - offset : 0, width, height, stride, layout->bytes_per_pixel)) {
BOOST_LOG(warning) << "qemu: ScanoutMap descriptor doesn't cover "sv << width << 'x' << height << " at offset "sv << offset;
return reject();
return;
}
std::size_t size = (std::size_t) offset + (std::size_t) stride * height;
@@ -185,7 +191,7 @@
auto addr = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd.get(), 0);
if (addr == MAP_FAILED) {
BOOST_LOG(warning) << "qemu: couldn't map scanout: "sv << std::strerror(errno);
return reject();
return;
}
std::lock_guard lock {mutex};
@@ -203,6 +209,7 @@
resize_locked((int) width, (int) height);
blit_locked(0, 0, (int) width, (int) height, map_addr + map_offset, map_stride, map_layout);
touch_locked();
return true;
}
void frame_store_t::update_map(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) {
@@ -244,7 +251,8 @@
dmabuf_generation += 1;
scanout_kind = scanout_kind_e::dmabuf;
is_disabled = false;
resize_locked((int) dmabuf.width, (int) dmabuf.height);
// GPU encoders take the descriptors; don't keep a system memory copy of the frame
resize_locked((int) dmabuf.width, (int) dmabuf.height, readback);
if (readback) {
map_dmabuf_locked();
if (dmabuf_addr) {
@@ -252,9 +260,6 @@
} else {
std::ranges::fill(pixels, 0);
}
} else {
// GPU encoders take the descriptors; don't keep a system memory copy of the frame
pixels.clear();
}
touch_locked();
}
@@ -394,12 +399,18 @@
return change_sequence;
}
std::size_t frame_store_t::frame_memory() const {
std::lock_guard lock {mutex};
return pixels.capacity();
}
frame_status_e frame_store_t::copy_if_newer(std::uint64_t &last_sequence, int width, int height, std::uint8_t *dst, std::chrono::steady_clock::time_point ×tamp, bool draw_cursor) {
std::lock_guard lock {mutex};
if (is_disconnected) {
return frame_status_e::disconnected;
}
// without pixels (a DMABUF handed to GPU encoders) there is nothing to copy: the caller needs a new display
if (width != frame_width || height != frame_height) {
if (width != frame_width || height != frame_height || pixels.size() != (std::size_t) width * height * 4) {
return frame_status_e::size_changed;
}
if (change_sequence == last_sequence) {
@@ -524,7 +535,7 @@
dmabuf = dmabuf_scanout_t {};
}
void frame_store_t::resize_locked(int new_width, int new_height, bool allocate) {
void frame_store_t::resize_locked(int new_width, int new_height) {
int transport = 0;
if (scanout_kind == scanout_kind_e::dmabuf) {
transport = 2;
@@ -538,6 +549,11 @@
}
frame_width = new_width;
frame_height = new_height;
if (!allocate) {
pixels.clear();
pixels.shrink_to_fit();
return;
}
// every caller overwrites the whole frame, so only a size change needs new memory
const auto size = (std::size_t) new_width * new_height * 4;
if (pixels.size() != size) {
▸
src/platform/linux/qemu/frame_store.h
+14
−3
@@ -115,7 +115,7 @@
void scanout(std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format, std::span<const std::uint8_t> data) override;
void update(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, std::uint32_t stride, std::uint32_t format, std::span<const std::uint8_t> data) override;
void scanout_map(fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) override;
bool scanout_map(fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) override;
void update_map(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) override;
void scanout_dmabuf(dmabuf_scanout_t buffer) override;
void update_dmabuf(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) override;
@@ -186,6 +186,14 @@
[[nodiscard]] std::uint64_t sequence() const;
/**
* @brief System memory reserved for the frame's pixels.
* @details Zero while a DMABUF scanout is handed to GPU encoders without readback.
*
* @return Capacity of the pixel buffer in bytes.
*/
[[nodiscard]] std::size_t frame_memory() const;
/**
* @brief Copy the frame if it changed since the caller last copied it.
*
* @param last_sequence Sequence of the caller's last copy; updated on `new_frame`.
@@ -194,6 +202,7 @@
* @param dst Destination with `4 * width` bytes per row.
* @param timestamp Receipt time of the call that produced the copied frame; set on `new_frame`.
* @param draw_cursor Whether to blend the guest cursor into the copy.
* @return Poll result.
* @return Poll result; `size_changed` also when the frame has no pixels in system memory (a
* DMABUF scanout without readback).
*/
frame_status_e copy_if_newer(std::uint64_t &last_sequence, int width, int height, std::uint8_t *dst, std::chrono::steady_clock::time_point ×tamp, bool draw_cursor = false);
@@ -230,8 +239,10 @@
*
* @param new_width New width in pixels.
* @param new_height New height in pixels.
* @param allocate Whether the frame needs pixels in system memory; without them the pixel
* buffer is released.
*/
void resize_locked(int new_width, int new_height);
void resize_locked(int new_width, int new_height, bool allocate = true);
/**
* @brief Convert and copy a rectangle of source pixels into the frame.
▸
src/platform/linux/qemu/session.cpp
+18
−5
@@ -19,6 +19,7 @@
#include <utility>
// platform includes
#include <fcntl.h>
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <sys/socket.h>
@@ -1270,7 +1271,9 @@
auto offsets = (const guint32 *) g_variant_get_fixed_array(offset, &offset_count, sizeof(guint32));
auto strides = (const guint32 *) g_variant_get_fixed_array(stride, &stride_count, sizeof(guint32));
const gsize fd_count = g_variant_n_children(dmabuf);
// QEMU stops adding descriptors at the first plane without its own (-1, sharing the previous
if (num_planes < 1 || num_planes > 4 || fd_count < num_planes || offset_count < num_planes || stride_count < num_planes || !fd_list) {
// plane's buffer) but still sends num_planes
if (num_planes < 1 || num_planes > 4 || fd_count < 1 || fd_count > num_planes || offset_count < num_planes || stride_count < num_planes || !fd_list) {
BOOST_LOG(error) << "qemu: ScanoutDMABUF2 with "sv << num_planes << " planes, "sv << fd_count << " descriptors, "sv << offset_count << " offsets and "sv << stride_count << " strides"sv;
g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "inconsistent planes");
return TRUE;
@@ -1279,9 +1282,15 @@
dmabuf_scanout_t buffer;
for (guint plane = 0; plane < num_planes; ++plane) {
GError *err = nullptr;
int fd = -1;
if (plane < fd_count) {
auto handle = g_variant_get_child_value(dmabuf, plane);
fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(handle), &err);
g_variant_unref(handle);
} else {
// EGL_MESA_image_dma_buf_export: a plane without a descriptor is in the previous plane's buffer
auto handle = g_variant_get_child_value(dmabuf, plane);
int fd = g_unix_fd_list_get(fd_list, g_variant_get_handle(handle), &err);
g_variant_unref(handle);
fd = fcntl(buffer.fds[plane - 1].get(), F_DUPFD_CLOEXEC, 3);
}
if (fd < 0) {
return_invalid_descriptor(invocation, err, "ScanoutDMABUF2");
return TRUE;
@@ -1338,7 +1347,11 @@
return_invalid_descriptor(invocation, err, "ScanoutMap");
return TRUE;
}
if (!((listener_impl_t *) self)->listener->scanout_map(fd_t {fd}, offset, width, height, stride, format)) {
((listener_impl_t *) self)->listener->scanout_map(fd_t {fd}, offset, width, height, stride, format);
// QEMU stops sharing memory with this listener only when the call fails
g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "scanout map not usable");
return TRUE;
}
qemu_dbus_display1_listener_unix_map_complete_scanout_map(object, invocation, nullptr);
return TRUE;
}
▸
src/platform/linux/qemu/session.h
+3
−1
@@ -175,8 +175,10 @@
* @param height Display height in pixels.
* @param stride Bytes per row.
* @param format Pixman format code.
* @return False when the listener can't use the map; QEMU then gets an error and falls back to
* `Scanout` and `Update` messages.
*/
virtual void scanout_map(fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) = 0;
virtual bool scanout_map(fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) = 0;
/**
* @brief Handle damage on the current shared memory scanout.
▸
src_assets/linux/assets/shaders/vulkan/rgb2yuv.comp
+3
−1
@@ -55,8 +55,10 @@
vec2 scale = vec2(pc.src_size) / vec2(pc.dst_size);
vec2 uv = (vec2(pc.src_offset) + (vec2(d) + 0.5) * scale) * inv_tex;
// Bottom-up buffers: crop the source rectangle in buffer memory order, then flip inside it
// (QEMU's rule, also used by the GL path); for a full-size buffer this is 1.0 - uv.y.
if (pc.y_invert != 0)
uv.y = 1.0 - uv.y;
uv.y = (float(pc.src_offset.y + pc.src_size.y) - (float(d.y) + 0.5) * scale.y) * inv_tex.y;
vec3 rgb = texture(rgb_in, uv).rgb;
if (pc.cursor_size.x > 0)
▸
tests/unit/platform/linux/qemu/test_capture.cpp
+98
−3
@@ -801,11 +801,12 @@
// new buffer: new sequence
EXPECT_EQ(seen[2].sequence, 2);
EXPECT_EQ(seen[2].inode, second.inode());
// cursor: drawn by the encoder's cursor shader at the pointer minus the hot spot
// cursor: drawn by the encoder's cursor shader at the pointer minus the hot spot, in buffer
// coordinates for Vulkan (which subtracts the display rectangle's offset (2, 1) again)
EXPECT_EQ(seen[3].sequence, 2);
EXPECT_TRUE(seen[3].has_cursor);
EXPECT_EQ(seen[3].cursor_x, 4);
EXPECT_EQ(seen[3].cursor_x, 2);
EXPECT_EQ(seen[3].cursor_y, 2);
EXPECT_EQ(seen[3].cursor_y, 3);
EXPECT_EQ(seen[3].cursor_w, 2);
// Disable: black frame (sequence 0, no buffer)
EXPECT_EQ(seen[4].sequence, 0);
@@ -815,6 +816,68 @@
EXPECT_EQ(seen[5].inode, second.inode());
}
// @tag requirements: [REQ-CAP-003, REQ-CAP-005]
TEST_F(QemuCaptureTest, GlEncodersTakeTheCursorInDisplayCoordinates) {
memfd_buffer_t buffer {8, 6, 0x10, 0x20, 0x30};
ASSERT_NE(buffer.map, nullptr);
qemu_test::fake_qemu_t::dmabuf2_t layout;
layout.fds = {buffer.fd};
layout.offsets = {0};
layout.strides = {32};
layout.x = 2;
layout.y = 1;
layout.width = 4;
layout.height = 3;
layout.backing_width = 8;
layout.backing_height = 6;
layout.fourcc = qemu::drm_fourcc::xrgb8888;
layout.modifier = qemu::drm_fourcc::mod_linear;
layout.y0_top = false;
std::thread sender {[&]() {
if (fake->wait_for_listener(1)) {
fake->scanout_dmabuf2(1, layout);
}
}};
auto display = platf::qemu_display(platf::mem_type_e::vaapi, "1", stream_config());
sender.join();
if (!display && qemu::default_render_node()) {
GTEST_SKIP() << "QEMU's render node doesn't match VAAPI's on this host";
}
ASSERT_NE(display, nullptr);
std::thread actor;
std::optional<std::pair<int, int>> cursor;
auto status = run_capture(
*display,
[&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
if (!frame_captured) {
return true;
}
auto d = std::dynamic_pointer_cast<egl::img_descriptor_t>(img);
EXPECT_NE(d, nullptr);
if (d && d->data) {
cursor = std::make_pair(d->x, d->y);
return false;
}
if (!actor.joinable()) {
actor = std::thread {[&]() {
fake->cursor_define(1, 2, 2, 1, 0, std::vector<std::uint8_t>(16, 0xff));
fake->mouse_set(1, 3, 2, true);
}};
}
return d != nullptr;
},
true
);
if (actor.joinable()) {
actor.join();
}
EXPECT_EQ(status, platf::capture_e::ok);
// sws_t::load_vram draws the cursor into the already cropped frame: no crop offset
EXPECT_EQ(cursor, (std::optional<std::pair<int, int>> {{2, 2}}));
}
// @tag requirements: [REQ-CAP-003, REQ-CAP-004]
TEST_F(QemuCaptureTest, GpuEncoderUploadsSharedMemoryAndSwitchesToDmabuf) {
auto display = [&]() {
@@ -881,6 +944,38 @@
// a tiled GPU buffer can't be read from system memory
EXPECT_EQ(open(0x0100000000000001ULL), nullptr);
}
// @tag requirements: [REQ-CAP-003, REQ-CAP-004]
TEST_F(QemuCaptureTest, SoftwareEncoderReinitsWhenQemuSwitchesToAnUnreadableDmabuf) {
auto display = open_display("1", 1, 4, 4);
ASSERT_NE(display, nullptr);
// same size, but a tiled GPU buffer: streaming on would show black forever
memfd_buffer_t buffer {4, 4, 9, 9, 9};
ASSERT_NE(buffer.map, nullptr);
std::atomic_bool switched {false};
int frames_after_switch = 0;
std::thread switcher;
auto status = run_capture(*display, [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) {
if (!frame_captured) {
return true;
}
if (switched) {
frames_after_switch += 1;
} else if (!switcher.joinable()) {
switcher = std::thread {[&]() {
fake->scanout_dmabuf(1, buffer.fd, 4, 4, 16, qemu::drm_fourcc::xrgb8888, 0x0100000000000001ULL, true);
switched = true;
}};
}
return true;
});
if (switcher.joinable()) {
switcher.join();
}
EXPECT_EQ(status, platf::capture_e::reinit);
EXPECT_LE(frames_after_switch, 1) << "black frames of an unreadable DMABUF were streamed";
}
// @tag requirements: [REQ-CAP-003]
▸
tests/unit/platform/linux/qemu/test_dmabuf_gpu.cpp
+109
−3
@@ -1,8 +1,9 @@
/**
* @file tests/unit/platform/linux/qemu/test_dmabuf_gpu.cpp
* @brief Test importing QEMU-style DMABUF scanouts through EGL on a real GPU.
* @details Needs `/dev/udmabuf` and a DRM render node with GBM. Hosts without them (containers,
* WSL2) skip these tests, so REQ-CAP-003's EGL import is only verified on native Linux GPU hosts.
* @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
* 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
@@ -23,7 +24,17 @@
#include <src/platform/linux/graphics.h>
#include <src/platform/linux/qemu/pixel_format.h>
#include <src/platform/linux/qemu/render_node.h>
#include <src/video_colorspace.h>
#ifdef SUNSHINE_BUILD_VULKAN
#include <src/platform/linux/vulkan_encode.h>
extern "C" {
#include <libavutil/frame.h>
#include <libavutil/hwcontext.h>
}
#endif
namespace {
/**
* @brief GPU resources for one test: render node, GBM device, EGL display and context.
@@ -175,4 +186,99 @@
EXPECT_EQ(pixel(0, 1), (std::array<std::uint8_t, 2> {40, 40}));
EXPECT_EQ(pixel(1, 1), (std::array<std::uint8_t, 2> {80, 40}));
}
#ifdef SUNSHINE_BUILD_VULKAN
// @tag requirements: [REQ-CAP-003]
TEST(QemuDmabufGpuTest, VulkanCropsAndFlipsBottomUpScanoutLikeQemu) {
// an 8x6 bottom-up buffer whose 4x2 display rectangle starts at (2, 1): QEMU shows memory rows 2
// and 1, in that order; flipping the whole buffer would show rows 4 and 3
constexpr int backing_width = 8;
constexpr int backing_height = 6;
constexpr int offset_x = 2;
constexpr int offset_y = 1;
constexpr int width = 4;
constexpr int height = 2;
const auto skip_reason = "; REQ-CAP-003 Vulkan import is verified only on native Linux GPU hosts";
int dmabuf = make_udmabuf(backing_width, backing_height);
if (dmabuf < 0) {
GTEST_SKIP() << "/dev/udmabuf is not available" << 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);
ASSERT_NE(frames_ref, nullptr);
auto frames = (AVHWFramesContext *) frames_ref->data;
frames->format = AV_PIX_FMT_VULKAN;
frames->sw_format = AV_PIX_FMT_NV12;
frames->width = width;
frames->height = height;
device->init_hwframes(frames);
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;
}
AVFrame *frame = av_frame_alloc();
frame->format = AV_PIX_FMT_VULKAN;
frame->width = width;
frame->height = height;
ASSERT_EQ(device->set_frame(frame, frames_ref), 0); // the device owns the frame now
device->colorspace = video::sunshine_colorspace_t {video::colorspace_e::rec709, true, 8};
device->apply_colorspace();
egl::img_descriptor_t img;
img.sequence = 1;
img.y_invert = true;
img.data = nullptr;
img.sd.width = backing_width;
img.sd.height = backing_height;
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.pitches[0] = backing_width * 4;
img.sd.offsets[0] = 0;
const int converted = device->convert(img);
close(dmabuf);
img.sd.fds[0] = -1;
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;
}
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
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};
const float y = colors->color_vec_y[0] * rgb[0] + colors->color_vec_y[1] * rgb[1] + colors->color_vec_y[2] * rgb[2] + colors->color_vec_y[3];
return (int) std::lround((y * colors->range_y[0] + colors->range_y[1]) * 255.0f);
};
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const int actual = download->data[0][y * download->linesize[0] + x];
EXPECT_NEAR(actual, expected_y(offset_x + x, offset_y + height - 1 - y), 2) << "display pixel " << x << ',' << y;
}
}
av_frame_free(&download);
device.reset();
av_buffer_unref(&frames_ref);
av_buffer_unref(&hw_device);
}
#endif
#endif
▸
tests/unit/platform/linux/qemu/test_frame_store.cpp
+47
−4
@@ -225,7 +225,7 @@
std::copy(pixels.begin(), pixels.end(), map + offset);
qemu::frame_store_t store;
store.scanout_map(qemu::fd_t {dup(fd)}, offset, width, height, stride, qemu::pixman_format::x8r8g8b8);
EXPECT_TRUE(store.scanout_map(qemu::fd_t {dup(fd)}, offset, width, height, stride, qemu::pixman_format::x8r8g8b8));
std::vector<std::uint8_t> frame(width * height * 4);
std::uint64_t seq = 0;
@@ -258,21 +258,31 @@
// @tag requirements: [REQ-CAP-002]
TEST(QemuFrameStoreTest, SharedMapRejectsBadDescriptorOrGeometry) {
qemu::frame_store_t store;
store.scanout_map(qemu::fd_t {}, 0, 2, 2, 8, qemu::pixman_format::x8r8g8b8);
EXPECT_FALSE(store.scanout_map(qemu::fd_t {}, 0, 2, 2, 8, qemu::pixman_format::x8r8g8b8));
EXPECT_FALSE(store.wait_for_frame(0ms));
// file smaller than the advertised geometry: mapping it would fault on read
int small_fd = memfd_create("frame-store-small", MFD_CLOEXEC);
ASSERT_GE(small_fd, 0);
ASSERT_EQ(ftruncate(small_fd, 4), 0);
store.scanout_map(qemu::fd_t {small_fd}, 0, 2, 2, 8, qemu::pixman_format::x8r8g8b8);
EXPECT_FALSE(store.scanout_map(qemu::fd_t {small_fd}, 0, 2, 2, 8, qemu::pixman_format::x8r8g8b8));
EXPECT_FALSE(store.wait_for_frame(0ms));
int fd = memfd_create("frame-store-format", MFD_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(ftruncate(fd, 64), 0);
EXPECT_FALSE(store.scanout_map(qemu::fd_t {dup(fd)}, 0, 2, 2, 8, qemu::pixman_format::a8));
store.scanout_map(qemu::fd_t {fd}, 0, 2, 2, 8, qemu::pixman_format::a8);
EXPECT_FALSE(store.wait_for_frame(0ms));
// after a rejected map QEMU sends Update messages for the new surface: the previous map must not
// keep receiving UpdateMap damage or block those updates
ASSERT_TRUE(store.scanout_map(qemu::fd_t {dup(fd)}, 0, 2, 2, 8, qemu::pixman_format::x8r8g8b8));
EXPECT_FALSE(store.scanout_map(qemu::fd_t {fd}, 0, 2, 2, 8, qemu::pixman_format::a8));
auto before = store.sequence();
store.update_map(0, 0, 1, 1);
EXPECT_EQ(store.sequence(), before);
store.update(0, 0, 1, 1, 4, qemu::pixman_format::x8r8g8b8, std::vector<std::uint8_t>(4, 0x42));
EXPECT_GT(store.sequence(), before);
}
// @tag requirements: [REQ-CAP-002]
@@ -586,6 +596,39 @@
store.disconnected();
EXPECT_EQ(store.dmabuf_if_newer(seq, 4, 3, frame), qemu::frame_status_e::disconnected);
}
// @tag requirements: [REQ-CAP-003, REQ-NFR-001]
TEST(QemuFrameStoreTest, DmabufWithoutReadbackKeepsNoFrameInSystemMemory) {
fake_dmabuf_t dmabuf {8, 6, 32};
ASSERT_NE(dmabuf.map, nullptr);
qemu::frame_store_t store {false};
store.scanout(4, 3, 16, qemu::pixman_format::x8r8g8b8, make_pixels(4, 3, 16));
EXPECT_GE(store.frame_memory(), 4u * 3 * 4);
// guests that flip buffers send a scanout per frame: none of them may allocate or clear a frame
for (int i = 0; i < 3; ++i) {
store.scanout_dmabuf(dmabuf.scanout(2, 1, 4, 3, 8, 6, 32, false));
EXPECT_EQ(store.frame_memory(), 0u) << "scanout " << i;
}
store.update_dmabuf(0, 0, 4, 3);
store.disable();
EXPECT_EQ(store.frame_memory(), 0u);
// a copy of a frame that has no pixels must not report a new frame
std::vector<std::uint8_t> frame(4 * 3 * 4, 0x55);
std::uint64_t seq = 0;
std::chrono::steady_clock::time_point timestamp;
EXPECT_EQ(store.copy_if_newer(seq, 4, 3, frame.data(), timestamp), qemu::frame_status_e::size_changed);
EXPECT_EQ(seq, 0u);
EXPECT_EQ(frame, std::vector<std::uint8_t>(4 * 3 * 4, 0x55));
// back to memory scanouts: the frame is allocated again
store.scanout(4, 3, 16, qemu::pixman_format::x8r8g8b8, make_pixels(4, 3, 16));
EXPECT_GE(store.frame_memory(), 4u * 3 * 4);
ASSERT_EQ(store.copy_if_newer(seq, 4, 3, frame.data(), timestamp), qemu::frame_status_e::new_frame);
EXPECT_EQ(bgr_at(frame, 4, 3, 2), expected_bgr(3, 2));
}
// @tag requirements: [REQ-CAP-003]
▸
tests/unit/platform/linux/qemu/test_p2p.cpp
+2
−1
@@ -43,7 +43,8 @@
void update(std::int32_t, std::int32_t, std::int32_t, std::int32_t, std::uint32_t, std::uint32_t, std::span<const std::uint8_t>) override {
}
void scanout_map(qemu::fd_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t) override {
bool scanout_map(qemu::fd_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t) override {
return true;
}
void update_map(std::int32_t, std::int32_t, std::int32_t, std::int32_t) override {
▸
tests/unit/platform/linux/qemu/test_session.cpp
+60
−2
@@ -15,6 +15,7 @@
#include <sys/stat.h>
// local includes
#include <src/platform/linux/qemu/frame_store.h>
#include <src/platform/linux/qemu/session.h>
using namespace std::literals;
@@ -38,6 +39,7 @@
qemu::dmabuf_scanout_t last_dmabuf;
std::vector<ino_t> dmabuf_inodes;
std::atomic<int> disconnects {0};
bool accept_map {true};
void scanout(std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format, std::span<const std::uint8_t> data) override {
std::lock_guard lock {mutex};
@@ -61,9 +63,12 @@
last_data.assign(data.begin(), data.end());
}
void scanout_map(qemu::fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) override {
bool scanout_map(qemu::fd_t fd, std::uint32_t offset, std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format) override {
std::lock_guard lock {mutex};
calls.emplace_back("scanout_map");
if (!accept_map) {
return false;
}
last_offset = offset;
last_width = width;
last_height = height;
@@ -76,6 +81,7 @@
mapped_bytes.assign(bytes + offset, bytes + size);
munmap(addr, size);
}
return true;
}
void update_map(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) override {
@@ -346,6 +352,38 @@
EXPECT_EQ(listener->last_height, 2);
}
// @tag requirements: [REQ-CAP-002]
TEST_F(QemuSessionTest, RejectedSharedMapScanoutReturnsAnError) {
start_fake();
auto session = qemu::session_t::connect(bus->address());
ASSERT_NE(session, nullptr);
auto listener = std::make_shared<recording_listener_t>();
listener->accept_map = false;
auto registration = session->register_listener(1, listener);
ASSERT_NE(registration, nullptr);
ASSERT_TRUE(fake->wait_for_listener(1));
int fd = memfd_create("fake-scanout-rejected", MFD_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(ftruncate(fd, 16), 0);
// QEMU stops sharing memory and falls back to Scanout/Update messages only when ScanoutMap fails
EXPECT_FALSE(fake->scanout_map(1, fd, 0, 2, 2, 8, qemu::pixman_format::x8r8g8b8));
close(fd);
EXPECT_EQ(listener->snapshot_calls(), (std::vector<std::string> {"scanout_map"}));
// the frame store refuses maps it can't show
auto store = std::make_shared<qemu::frame_store_t>();
auto store_registration = session->register_listener(2, store);
ASSERT_NE(store_registration, nullptr);
ASSERT_TRUE(fake->wait_for_listener(2));
fd = memfd_create("fake-scanout-format", MFD_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(ftruncate(fd, 16), 0);
EXPECT_FALSE(fake->scanout_map(2, fd, 0, 2, 2, 8, qemu::pixman_format::a8));
EXPECT_TRUE(fake->scanout_map(2, fd, 0, 2, 2, 8, qemu::pixman_format::x8r8g8b8));
close(fd);
}
// @tag requirements: [REQ-CAP-001]
TEST_F(QemuSessionTest, DeliversDisableAndCursor) {
start_fake();
@@ -465,12 +503,32 @@
EXPECT_TRUE(got.y0_top);
}
// QEMU stops sending descriptors at the first plane that shares the previous plane's buffer (fd -1)
// but keeps num_planes: those planes get a copy of the last descriptor
buffer.fds = {y_fd};
ASSERT_TRUE(fake->scanout_dmabuf2(1, buffer));
{
std::lock_guard lock {listener->mutex};
const auto &got = listener->last_dmabuf;
ASSERT_EQ(got.num_planes, 2);
EXPECT_EQ(listener->dmabuf_inodes, (std::vector<ino_t> {inode_of(y_fd), inode_of(y_fd)}));
EXPECT_NE(got.fds[0].get(), got.fds[1].get()) << "each plane owns its descriptor";
EXPECT_EQ(got.fds[2].get(), -1);
EXPECT_EQ(got.offsets[1], 16);
}
// inconsistent plane counts are rejected with an error, and not delivered
buffer.fds = {y_fd, uv_fd};
buffer.num_planes = 3;
EXPECT_FALSE(fake->scanout_dmabuf2(1, buffer));
buffer.num_planes = 0;
EXPECT_FALSE(fake->scanout_dmabuf2(1, buffer));
EXPECT_EQ(listener->snapshot_calls(), (std::vector<std::string> {"scanout_dmabuf"}));
buffer.num_planes = 2;
buffer.fds = {};
EXPECT_FALSE(fake->scanout_dmabuf2(1, buffer));
buffer.fds = {y_fd, uv_fd, y_fd};
EXPECT_FALSE(fake->scanout_dmabuf2(1, buffer));
EXPECT_EQ(listener->snapshot_calls(), (std::vector<std::string> {"scanout_dmabuf", "scanout_dmabuf"}));
close(y_fd);
close(uv_fd);