ref:52f749fe564e7814d1cd60d6cd0ec18981e49095

feat(linux): convert every packed pixman format QEMU emits in qemu capture

QEMU passes the guest surface format through to D-Bus listeners, so 16-bit VGA modes arrive as r5g6b5 and other guests use 24-bit or BGRA-ordered layouts. Decode the pixman format code generically (bpp, channel order and widths, bit replication for narrow channels) with copy and swap fast paths for the 32-bit formats, and use it for Scanout/Update and ScanoutMap/UpdateMap. Unsupported formats are still logged once and ignored. Also map DRM fourccs to pixman layouts for the DMABUF readback path. Refs #3 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPNw4PCgkEfhyCjQT19wsb
SHA: 52f749fe564e7814d1cd60d6cd0ec18981e49095
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-12 22:40
Parents: 98cb3d2
8 files changed +438 -70
Type
cmake/compile_definitions/linux.cmake +2 −0
@@ -325,6 +325,8 @@
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/capture.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/frame_store.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/frame_store.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/pixel_format.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/pixel_format.cpp"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/session.h"
"${CMAKE_SOURCE_DIR}/src/platform/linux/qemu/session.cpp")
endif()
src/platform/linux/qemu/frame_store.cpp +36 −53
@@ -21,26 +21,6 @@
namespace qemu {
namespace {
/**
* @brief Report whether a pixman format is 32 bpp with red and blue swapped relative to BGRX.
*
* @param format Pixman format code.
* @return True for `x8b8g8r8` and `a8b8g8r8`.
*/
bool is_rgbx(std::uint32_t format) {
return format == pixman_format::x8b8g8r8 || format == pixman_format::a8b8g8r8;
}
/**
* @brief Report whether the frame store can convert a pixman format.
*
* @param format Pixman format code.
* @return True for the supported 32 bpp formats.
*/
bool is_supported(std::uint32_t format) {
return format == pixman_format::x8r8g8b8 || format == pixman_format::a8r8g8b8 || is_rgbx(format);
}
/**
* @brief Log an unsupported format once per format code.
*
* @param format Pixman format code.
@@ -56,18 +36,33 @@
}
/**
* @brief Decode a pixman format, logging unsupported formats once per format code.
*
* @param format Pixman format code.
* @return Layout, or nothing when the format can't be converted.
*/
std::optional<pixel_layout_t> layout_or_log(std::uint32_t format) {
auto layout = pixman_layout(format);
if (!layout) {
log_unsupported(format);
}
return layout;
}
/**
* @brief Check that a pixel buffer covers a rectangle.
*
* @param available Bytes available.
* @param width Rectangle width.
* @param height Rectangle height.
* @param stride Bytes per row.
* @param bytes_per_pixel Bytes per pixel.
* @return True when the buffer is large enough and the stride fits the width.
*/
bool covers(std::size_t available, std::int64_t width, std::int64_t height, std::int64_t stride) {
if (width <= 0 || height <= 0 || stride < width * 4) {
bool covers(std::size_t available, std::int64_t width, std::int64_t height, std::int64_t stride, int bytes_per_pixel) {
if (width <= 0 || height <= 0 || stride < width * bytes_per_pixel) {
return false;
}
return (std::size_t) (stride * (height - 1) + width * bytes_per_pixel) <= available;
return (std::size_t) (stride * (height - 1) + width * 4) <= available;
}
} // namespace
@@ -78,11 +73,11 @@
}
void frame_store_t::scanout(std::uint32_t width, std::uint32_t height, std::uint32_t stride, std::uint32_t format, std::span<const std::uint8_t> data) {
auto layout = layout_or_log(format);
if (!layout) {
if (!is_supported(format)) {
log_unsupported(format);
return;
}
if (!covers(data.size(), width, height, stride)) {
if (!covers(data.size(), width, height, stride, layout->bytes_per_pixel)) {
BOOST_LOG(warning) << "qemu: scanout data doesn't cover "sv << width << 'x' << height << " with stride "sv << stride;
return;
}
@@ -90,16 +85,16 @@
std::lock_guard lock {mutex};
unmap_locked();
resize_locked((int) width, (int) height);
blit_locked(0, 0, (int) width, (int) height, data.data(), stride, format);
blit_locked(0, 0, (int) width, (int) height, data.data(), stride, *layout);
touch_locked();
}
void frame_store_t::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) {
auto layout = layout_or_log(format);
if (!layout) {
if (!is_supported(format)) {
log_unsupported(format);
return;
}
if (!covers(data.size(), width, height, stride, layout->bytes_per_pixel)) {
if (!covers(data.size(), width, height, stride)) {
BOOST_LOG(warning) << "qemu: update data doesn't cover "sv << width << 'x' << height << " with stride "sv << stride;
return;
}
@@ -118,7 +113,7 @@
return;
}
auto src = data.data() + (std::size_t) (y0 - y) * stride + (std::size_t) (x0 - x) * 4;
blit_locked(x0, y0, x1 - x0, y1 - y0, src, stride, format);
auto src = data.data() + (std::size_t) (y0 - y) * stride + (std::size_t) (x0 - x) * layout->bytes_per_pixel;
blit_locked(x0, y0, x1 - x0, y1 - y0, src, stride, *layout);
touch_locked();
}
@@ -128,13 +123,13 @@
BOOST_LOG(warning) << "qemu: ScanoutMap without a descriptor"sv;
return;
}
if (!is_supported(format)) {
auto layout = layout_or_log(format);
if (!layout) {
log_unsupported(format);
return;
}
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)) {
if (fstat(fd.get(), &st) != 0 || !covers(st.st_size > offset ? (std::size_t) st.st_size - offset : 0, width, height, stride)) {
BOOST_LOG(warning) << "qemu: ScanoutMap descriptor doesn't cover "sv << width << 'x' << height << " at offset "sv << offset;
return;
}
@@ -154,9 +149,9 @@
map_size = size;
map_offset = offset;
map_stride = stride;
map_format = format;
map_layout = *layout;
resize_locked((int) width, (int) height);
blit_locked(0, 0, (int) width, (int) height, map_addr + map_offset, map_stride, map_format);
blit_locked(0, 0, (int) width, (int) height, map_addr + map_offset, map_stride, map_layout);
touch_locked();
}
@@ -175,7 +170,7 @@
return;
}
auto src = map_addr + map_offset + (std::size_t) y0 * map_stride + (std::size_t) x0 * 4;
blit_locked(x0, y0, x1 - x0, y1 - y0, src, map_stride, map_format);
auto src = map_addr + map_offset + (std::size_t) y0 * map_stride + (std::size_t) x0 * map_layout.bytes_per_pixel;
blit_locked(x0, y0, x1 - x0, y1 - y0, src, map_stride, map_layout);
touch_locked();
}
@@ -251,21 +246,9 @@
pixels.assign((std::size_t) new_width * new_height * 4, 0);
}
void frame_store_t::blit_locked(int x, int y, int w, int h, const std::uint8_t *src, std::uint32_t src_stride, std::uint32_t format) {
void frame_store_t::blit_locked(int x, int y, int w, int h, const std::uint8_t *src, std::uint32_t src_stride, const pixel_layout_t &layout) {
const auto dst_stride = (std::size_t) frame_width * 4;
const bool swap = is_rgbx(format);
for (int row = 0; row < h; ++row) {
auto s = src + (std::size_t) row * src_stride;
auto d = pixels.data() + (std::size_t) (y + row) * dst_stride + (std::size_t) x * 4;
if (!swap) {
std::memcpy(d, s, (std::size_t) w * 4);
continue;
}
for (int col = 0; col < w; ++col, s += 4, d += 4) {
d[0] = s[2];
d[1] = s[1];
d[2] = s[0];
d[3] = s[3];
}
convert_row(layout, src + (std::size_t) row * src_stride, pixels.data() + (std::size_t) (y + row) * dst_stride + (std::size_t) x * 4, w);
}
}
src/platform/linux/qemu/frame_store.h +4 −3
@@ -14,6 +14,7 @@
#include <vector>
// local includes
#include "pixel_format.h"
#include "session.h"
namespace qemu {
@@ -114,9 +115,9 @@
* @param h Rectangle height.
* @param src Pointer to the top-left source pixel of the rectangle.
* @param src_stride Source bytes per row.
* @param format Source pixman format; must be supported.
* @param layout Source pixel layout.
*/
void blit_locked(int x, int y, int w, int h, const std::uint8_t *src, std::uint32_t src_stride, const pixel_layout_t &layout);
void blit_locked(int x, int y, int w, int h, const std::uint8_t *src, std::uint32_t src_stride, std::uint32_t format);
/**
* @brief Mark the frame as changed now.
@@ -143,6 +144,6 @@
std::size_t map_size {0}; ///< Length of the mapping.
std::uint32_t map_offset {0}; ///< Offset of the first pixel in the mapping.
std::uint32_t map_stride {0}; ///< Bytes per row in the mapping.
pixel_layout_t map_layout; ///< Pixel layout of the mapping.
std::uint32_t map_format {0}; ///< Pixman format of the mapping.
};
} // namespace qemu
src/platform/linux/qemu/pixel_format.cpp +138 −0
@@ -1,0 +1,138 @@
/**
* @file src/platform/linux/qemu/pixel_format.cpp
* @brief Definitions for converting QEMU scanout pixel formats to Sunshine's BGRX layout.
*/
// class header include
#include "pixel_format.h"
// standard includes
#include <bit>
#include <cstring>
namespace qemu {
namespace {
constexpr int type_argb = 2; ///< `PIXMAN_TYPE_ARGB`: channels aligned to the least significant bit, blue lowest.
constexpr int type_abgr = 3; ///< `PIXMAN_TYPE_ABGR`: channels aligned to the least significant bit, red lowest.
constexpr int type_bgra = 8; ///< `PIXMAN_TYPE_BGRA`: channels aligned to the most significant bit, blue highest.
constexpr int type_rgba = 9; ///< `PIXMAN_TYPE_RGBA`: channels aligned to the most significant bit, red highest.
/**
* @brief Scale a channel of any width up to 8 bits, replicating high bits into the low ones.
*
* @param value Channel value.
* @param bits Channel width in bits, 1 to 15.
* @return 8-bit value.
*/
std::uint8_t expand(std::uint32_t value, int bits) {
if (bits >= 8) {
return (std::uint8_t) (value >> (bits - 8));
}
std::uint32_t result = 0;
for (int filled = 0; filled < 8; filled += bits) {
result = (result << bits) | value;
}
int total = ((8 + bits - 1) / bits) * bits;
return (std::uint8_t) (result >> (total - 8));
}
} // namespace
std::optional<pixel_layout_t> pixman_layout(std::uint32_t format) {
static_assert(std::endian::native == std::endian::little, "the QEMU capture backend assumes a little-endian host");
const int bpp = (int) (format >> 24);
const int type = (int) ((format >> 16) & 0xff);
const int a = (int) ((format >> 12) & 0xf);
const int r = (int) ((format >> 8) & 0xf);
const int g = (int) ((format >> 4) & 0xf);
const int b = (int) (format & 0xf);
if ((bpp != 16 && bpp != 24 && bpp != 32) || r == 0 || g == 0 || b == 0 || a + r + g + b > bpp) {
return std::nullopt;
}
pixel_layout_t layout;
layout.bytes_per_pixel = bpp / 8;
layout.bits = {b, g, r};
switch (type) {
case type_argb:
layout.shift = {0, b, b + g};
break;
case type_abgr:
layout.shift = {r + g, r, 0};
break;
case type_bgra:
layout.shift = {bpp - b, bpp - b - g, bpp - b - g - r};
break;
case type_rgba:
layout.shift = {bpp - r - g - b, bpp - r - g, bpp - r};
break;
default:
return std::nullopt;
}
if (bpp == 32 && r == 8 && g == 8 && b == 8) {
if (layout.shift == std::array<int, 3> {0, 8, 16}) {
layout.kind = pixel_layout_t::kind_e::bgrx;
} else if (layout.shift == std::array<int, 3> {16, 8, 0}) {
layout.kind = pixel_layout_t::kind_e::rgbx;
}
}
return layout;
}
std::optional<std::uint32_t> pixman_from_drm_fourcc(std::uint32_t fourcc) {
switch (fourcc) {
case drm_fourcc::xrgb8888:
return pixman_format::x8r8g8b8;
case drm_fourcc::argb8888:
return pixman_format::a8r8g8b8;
case drm_fourcc::xbgr8888:
return pixman_format::x8b8g8r8;
case drm_fourcc::abgr8888:
return pixman_format::a8b8g8r8;
case drm_fourcc::bgrx8888:
return pixman_format::b8g8r8x8;
case drm_fourcc::bgra8888:
return pixman_format::b8g8r8a8;
case drm_fourcc::rgbx8888:
return pixman_format::r8g8b8x8;
case drm_fourcc::rgba8888:
return pixman_format::r8g8b8a8;
case drm_fourcc::rgb565:
return pixman_format::r5g6b5;
default:
return std::nullopt;
}
}
void convert_row(const pixel_layout_t &layout, const std::uint8_t *src, std::uint8_t *dst, int width) {
switch (layout.kind) {
case pixel_layout_t::kind_e::bgrx:
std::memcpy(dst, src, (std::size_t) width * 4);
return;
case pixel_layout_t::kind_e::rgbx:
for (int x = 0; x < width; ++x, src += 4, dst += 4) {
dst[0] = src[2];
dst[1] = src[1];
dst[2] = src[0];
dst[3] = src[3];
}
return;
case pixel_layout_t::kind_e::packed:
break;
}
const auto bpp = layout.bytes_per_pixel;
for (int x = 0; x < width; ++x, src += bpp, dst += 4) {
std::uint32_t value = 0;
for (int i = 0; i < bpp; ++i) {
value |= (std::uint32_t) src[i] << (8 * i);
}
for (int c = 0; c < 3; ++c) {
const auto mask = (1u << layout.bits[c]) - 1;
dst[c] = expand((value >> layout.shift[c]) & mask, layout.bits[c]);
}
dst[3] = 0xff;
}
}
} // namespace qemu
src/platform/linux/qemu/pixel_format.h +97 −0
@@ -1,0 +1,97 @@
/**
* @file src/platform/linux/qemu/pixel_format.h
* @brief Declarations for converting QEMU scanout pixel formats to Sunshine's BGRX layout.
*/
#pragma once
// standard includes
#include <array>
#include <cstdint>
#include <optional>
namespace qemu {
/**
* @brief Pixman format codes used by QEMU display scanouts.
* @details Values follow pixman's `PIXMAN_FORMAT(bpp, type, a, r, g, b)` encoding. They are
* defined here so the capture backend does not need to link against pixman.
*/
namespace pixman_format {
inline constexpr std::uint32_t x8r8g8b8 = 0x20020888; ///< 32 bpp, B G R X byte order on little-endian hosts.
inline constexpr std::uint32_t a8r8g8b8 = 0x20028888; ///< 32 bpp, B G R A byte order on little-endian hosts.
inline constexpr std::uint32_t x8b8g8r8 = 0x20030888; ///< 32 bpp, R G B X byte order on little-endian hosts.
inline constexpr std::uint32_t a8b8g8r8 = 0x20038888; ///< 32 bpp, R G B A byte order on little-endian hosts.
inline constexpr std::uint32_t b8g8r8x8 = 0x20080888; ///< 32 bpp, X R G B byte order on little-endian hosts.
inline constexpr std::uint32_t b8g8r8a8 = 0x20088888; ///< 32 bpp, A R G B byte order on little-endian hosts.
inline constexpr std::uint32_t r8g8b8x8 = 0x20090888; ///< 32 bpp, X B G R byte order on little-endian hosts.
inline constexpr std::uint32_t r8g8b8a8 = 0x20098888; ///< 32 bpp, A B G R byte order on little-endian hosts.
inline constexpr std::uint32_t x2r10g10b10 = 0x20020aaa; ///< 32 bpp, 10 bits per color channel.
inline constexpr std::uint32_t r8g8b8 = 0x18020888; ///< 24 bpp, B G R byte order on little-endian hosts.
inline constexpr std::uint32_t b8g8r8 = 0x18030888; ///< 24 bpp, R G B byte order on little-endian hosts.
inline constexpr std::uint32_t r5g6b5 = 0x10020565; ///< 16 bpp, used by QEMU for 16-bit guest modes.
inline constexpr std::uint32_t b5g6r5 = 0x10030565; ///< 16 bpp with red and blue swapped.
inline constexpr std::uint32_t x1r5g5b5 = 0x10020555; ///< 15 bpp in a 16-bit pixel.
inline constexpr std::uint32_t a8 = 0x08018000; ///< 8 bpp alpha only; not a displayable format.
} // namespace pixman_format
/**
* @brief DRM fourcc codes and modifiers used by DMABUF scanouts.
*/
namespace drm_fourcc {
inline constexpr std::uint32_t xrgb8888 = 0x34325258; ///< 'XR24', same layout as pixman x8r8g8b8.
inline constexpr std::uint32_t argb8888 = 0x34325241; ///< 'AR24', same layout as pixman a8r8g8b8.
inline constexpr std::uint32_t xbgr8888 = 0x34324258; ///< 'XB24', same layout as pixman x8b8g8r8.
inline constexpr std::uint32_t abgr8888 = 0x34324241; ///< 'AB24', same layout as pixman a8b8g8r8.
inline constexpr std::uint32_t bgrx8888 = 0x34325842; ///< 'BX24', same layout as pixman b8g8r8x8.
inline constexpr std::uint32_t bgra8888 = 0x34324142; ///< 'BA24', same layout as pixman b8g8r8a8.
inline constexpr std::uint32_t rgbx8888 = 0x34325852; ///< 'RX24', same layout as pixman r8g8b8x8.
inline constexpr std::uint32_t rgba8888 = 0x34324152; ///< 'RA24', same layout as pixman r8g8b8a8.
inline constexpr std::uint32_t rgb565 = 0x36314752; ///< 'RG16', same layout as pixman r5g6b5.
inline constexpr std::uint64_t mod_linear = 0; ///< `DRM_FORMAT_MOD_LINEAR`.
inline constexpr std::uint64_t mod_invalid = 0x00ffffffffffffffULL; ///< `DRM_FORMAT_MOD_INVALID`.
} // namespace drm_fourcc
/**
* @brief How to decode one packed pixel of a pixman format.
*/
struct pixel_layout_t {
/**
* @brief Conversion strategy for a row.
*/
enum class kind_e {
bgrx, ///< Already B, G, R, X bytes: copy.
rgbx, ///< R, G, B, X bytes: swap red and blue.
packed, ///< Any other packed layout: decode each pixel with shifts and masks.
};
kind_e kind {kind_e::packed}; ///< Conversion strategy.
int bytes_per_pixel {4}; ///< Bytes per source pixel.
std::array<int, 3> shift {}; ///< Bit position of the B, G, R channels in the pixel value.
std::array<int, 3> bits {}; ///< Width in bits of the B, G, R channels.
};
/**
* @brief Decode a pixman format code into a pixel layout.
*
* @param format Pixman format code.
* @return Layout, or nothing for formats that aren't packed 16, 24 or 32 bpp RGB.
*/
std::optional<pixel_layout_t> pixman_layout(std::uint32_t format);
/**
* @brief Map a DRM fourcc to the pixman format with the same memory layout.
*
* @param fourcc DRM fourcc code.
* @return Pixman format, or nothing when the fourcc has no packed RGB equivalent here.
*/
std::optional<std::uint32_t> pixman_from_drm_fourcc(std::uint32_t fourcc);
/**
* @brief Convert one row of pixels to B, G, R, X bytes.
*
* @param layout Source pixel layout.
* @param src First source pixel.
* @param dst First destination pixel; receives `4 * width` bytes.
* @param width Number of pixels.
*/
void convert_row(const pixel_layout_t &layout, const std::uint8_t *src, std::uint8_t *dst, int width);
} // namespace qemu
src/platform/linux/qemu/session.h +3 −12
@@ -17,20 +17,11 @@
#include <string_view>
#include <vector>
// local includes
#include "pixel_format.h"
namespace qemu {
using namespace std::literals;
/**
* @brief Pixman format codes used by QEMU display scanouts.
* @details Values follow pixman's `PIXMAN_FORMAT(bpp, type, a, r, g, b)` encoding. They are
* defined here so the capture backend does not need to link against pixman.
*/
namespace pixman_format {
inline constexpr std::uint32_t x8r8g8b8 = 0x20020888; ///< 32 bpp, B G R X byte order on little-endian hosts.
inline constexpr std::uint32_t a8r8g8b8 = 0x20028888; ///< 32 bpp, B G R A byte order on little-endian hosts.
inline constexpr std::uint32_t x8b8g8r8 = 0x20030888; ///< 32 bpp, R G B X byte order on little-endian hosts.
inline constexpr std::uint32_t a8b8g8r8 = 0x20038888; ///< 32 bpp, R G B A byte order on little-endian hosts.
} // namespace pixman_format
/**
* @brief Move-only owner of a POSIX file descriptor.
tests/unit/platform/linux/qemu/test_frame_store.cpp +40 −2
@@ -101,6 +101,8 @@
format_case {qemu::pixman_format::a8r8g8b8, {0, 1, 2, 3}},
format_case {qemu::pixman_format::x8b8g8r8, {2, 1, 0, 3}},
format_case {qemu::pixman_format::a8b8g8r8, {2, 1, 0, 3}},
format_case {qemu::pixman_format::b8g8r8x8, {3, 2, 1, 0}},
format_case {qemu::pixman_format::r8g8b8a8, {1, 2, 3, 0}},
}) {
qemu::frame_store_t store;
store.scanout(2, 2, 8, format, make_pixels(2, 2, 8, order));
@@ -116,7 +118,7 @@
// @tag requirements: [REQ-CAP-002]
TEST(QemuFrameStoreTest, IgnoresUnsupportedFormatAndShortData) {
qemu::frame_store_t store;
store.scanout(2, 2, 4, 0x10020565, std::vector<std::uint8_t>(8)); // r5g6b5
store.scanout(2, 2, 2, qemu::pixman_format::a8, std::vector<std::uint8_t>(4)); // alpha only
EXPECT_FALSE(store.wait_for_frame(0ms));
store.scanout(2, 2, 8, qemu::pixman_format::x8r8g8b8, std::vector<std::uint8_t>(12)); // too short
@@ -130,6 +132,42 @@
}
// @tag requirements: [REQ-CAP-002]
TEST(QemuFrameStoreTest, ConvertsR5G6B5ScanoutUpdateAndMap) {
// 3x2 frame with a 1-byte stride padding: red, green, blue on the first row, white on the second
const std::vector<std::uint8_t> data {0x00, 0xf8, 0xe0, 0x07, 0x1f, 0x00, 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xee};
qemu::frame_store_t store;
store.scanout(3, 2, 7, qemu::pixman_format::r5g6b5, data);
std::vector<std::uint8_t> frame(3 * 2 * 4);
std::uint64_t seq = 0;
std::chrono::steady_clock::time_point timestamp;
ASSERT_EQ(store.copy_if_newer(seq, 3, 2, frame.data(), timestamp), qemu::frame_status_e::new_frame);
EXPECT_EQ(bgr_at(frame, 3, 0, 0), (std::array<std::uint8_t, 3> {0, 0, 0xff}));
EXPECT_EQ(bgr_at(frame, 3, 1, 0), (std::array<std::uint8_t, 3> {0, 0xff, 0}));
EXPECT_EQ(bgr_at(frame, 3, 2, 0), (std::array<std::uint8_t, 3> {0xff, 0, 0}));
EXPECT_EQ(bgr_at(frame, 3, 2, 1), (std::array<std::uint8_t, 3> {0xff, 0xff, 0xff}));
// a 1x1 black update at (1, 1), and a 2x1 update clipped at the left edge
store.update(1, 1, 1, 1, 2, qemu::pixman_format::r5g6b5, std::vector<std::uint8_t> {0x00, 0x00});
store.update(-1, 0, 2, 1, 4, qemu::pixman_format::r5g6b5, std::vector<std::uint8_t> {0x00, 0x00, 0x1f, 0x00});
ASSERT_EQ(store.copy_if_newer(seq, 3, 2, frame.data(), timestamp), qemu::frame_status_e::new_frame);
EXPECT_EQ(bgr_at(frame, 3, 1, 1), (std::array<std::uint8_t, 3> {0, 0, 0}));
EXPECT_EQ(bgr_at(frame, 3, 0, 0), (std::array<std::uint8_t, 3> {0xff, 0, 0}));
// the same pixels through the shared memory map, 16 bytes per row less than 4 bytes per pixel
int fd = memfd_create("frame-store-rgb565", MFD_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(ftruncate(fd, data.size()), 0);
ASSERT_EQ(pwrite(fd, data.data(), data.size(), 0), (ssize_t) data.size());
store.scanout_map(qemu::fd_t {fd}, 0, 3, 2, 7, qemu::pixman_format::r5g6b5);
ASSERT_EQ(store.copy_if_newer(seq, 3, 2, frame.data(), timestamp), qemu::frame_status_e::new_frame);
EXPECT_EQ(bgr_at(frame, 3, 0, 0), (std::array<std::uint8_t, 3> {0, 0, 0xff}));
store.update_map(1, 0, 2, 1);
ASSERT_EQ(store.copy_if_newer(seq, 3, 2, frame.data(), timestamp), qemu::frame_status_e::new_frame);
EXPECT_EQ(bgr_at(frame, 3, 2, 0), (std::array<std::uint8_t, 3> {0xff, 0, 0}));
}
// @tag requirements: [REQ-CAP-002]
TEST(QemuFrameStoreTest, UpdateBlitsDamageRectangleAndClips) {
qemu::frame_store_t store;
store.scanout(4, 3, 16, qemu::pixman_format::x8r8g8b8, std::vector<std::uint8_t>(4 * 3 * 4, 0));
@@ -232,6 +270,6 @@
int fd = memfd_create("frame-store-format", MFD_CLOEXEC);
ASSERT_GE(fd, 0);
ASSERT_EQ(ftruncate(fd, 64), 0);
store.scanout_map(qemu::fd_t {fd}, 0, 2, 2, 8, 0x10020565);
store.scanout_map(qemu::fd_t {fd}, 0, 2, 2, 8, qemu::pixman_format::a8);
EXPECT_FALSE(store.wait_for_frame(0ms));
}
tests/unit/platform/linux/qemu/test_pixel_format.cpp +118 −0
@@ -1,0 +1,118 @@
/**
* @file tests/unit/platform/linux/qemu/test_pixel_format.cpp
* @brief Test the conversion of QEMU scanout pixel formats to BGRX.
*/
#ifdef SUNSHINE_BUILD_QEMU
// test includes
#include "../../../../tests_common.h"
// standard includes
#include <array>
#include <vector>
// local includes
#include <src/platform/linux/qemu/pixel_format.h>
namespace {
/**
* @brief One format conversion case: raw source bytes of one pixel and the expected BGR result.
*/
struct format_case_t {
std::uint32_t format; ///< Pixman format.
std::vector<std::uint8_t> pixel; ///< Source bytes of one pixel, little-endian.
std::array<std::uint8_t, 3> bgr; ///< Expected B, G, R bytes.
};
/**
* @brief Convert a single pixel.
*
* @param format Pixman format.
* @param pixel Source bytes.
* @return Converted B, G, R bytes.
*/
std::array<std::uint8_t, 3> convert_one(std::uint32_t format, const std::vector<std::uint8_t> &pixel) {
auto layout = qemu::pixman_layout(format);
EXPECT_TRUE(layout.has_value()) << std::hex << format;
if (!layout) {
return {};
}
EXPECT_EQ(layout->bytes_per_pixel, (int) pixel.size()) << std::hex << format;
std::array<std::uint8_t, 4> out {};
qemu::convert_row(*layout, pixel.data(), out.data(), 1);
return {out[0], out[1], out[2]};
}
} // namespace
// @tag requirements: [REQ-CAP-002]
TEST(QemuPixelFormatTest, ConvertsEveryPackedFormatQemuEmits) {
const std::vector<format_case_t> cases {
// 32 bpp: pixel value 0xAARRGGBB-style layouts, stored little-endian
{qemu::pixman_format::x8r8g8b8, {0x10, 0x20, 0x30, 0x00}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::a8r8g8b8, {0x10, 0x20, 0x30, 0xff}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::x8b8g8r8, {0x30, 0x20, 0x10, 0x00}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::a8b8g8r8, {0x30, 0x20, 0x10, 0xff}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::b8g8r8x8, {0x00, 0x30, 0x20, 0x10}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::b8g8r8a8, {0xff, 0x30, 0x20, 0x10}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::r8g8b8x8, {0x00, 0x10, 0x20, 0x30}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::r8g8b8a8, {0xff, 0x10, 0x20, 0x30}, {0x10, 0x20, 0x30}},
// 0x3ff red, 0x200 green, 0x004 blue -> 0xff, 0x80, 0x01
{qemu::pixman_format::x2r10g10b10, {0x04, 0x00, 0xf8, 0x3f}, {0x01, 0x80, 0xff}},
// 24 bpp
{qemu::pixman_format::r8g8b8, {0x10, 0x20, 0x30}, {0x10, 0x20, 0x30}},
{qemu::pixman_format::b8g8r8, {0x30, 0x20, 0x10}, {0x10, 0x20, 0x30}},
// 16 bpp: full-scale channels expand to 0xff, zero stays zero
{qemu::pixman_format::r5g6b5, {0x00, 0xf8}, {0x00, 0x00, 0xff}},
{qemu::pixman_format::r5g6b5, {0xe0, 0x07}, {0x00, 0xff, 0x00}},
{qemu::pixman_format::r5g6b5, {0x1f, 0x00}, {0xff, 0x00, 0x00}},
// mid-scale red 0b10000 -> 0x84 (bit replication), green 0b100000 -> 0x82
{qemu::pixman_format::r5g6b5, {0x00, 0x84}, {0x00, 0x82, 0x84}},
{qemu::pixman_format::b5g6r5, {0x1f, 0x00}, {0x00, 0x00, 0xff}},
{qemu::pixman_format::x1r5g5b5, {0x00, 0x7c}, {0x00, 0x00, 0xff}},
{qemu::pixman_format::x1r5g5b5, {0xe0, 0x03}, {0x00, 0xff, 0x00}},
};
for (const auto &c : cases) {
EXPECT_EQ(convert_one(c.format, c.pixel), c.bgr) << "format 0x" << std::hex << c.format;
}
}
// @tag requirements: [REQ-CAP-002]
TEST(QemuPixelFormatTest, UsesCopyAndSwapFastPaths) {
EXPECT_EQ(qemu::pixman_layout(qemu::pixman_format::x8r8g8b8)->kind, qemu::pixel_layout_t::kind_e::bgrx);
EXPECT_EQ(qemu::pixman_layout(qemu::pixman_format::a8r8g8b8)->kind, qemu::pixel_layout_t::kind_e::bgrx);
EXPECT_EQ(qemu::pixman_layout(qemu::pixman_format::x8b8g8r8)->kind, qemu::pixel_layout_t::kind_e::rgbx);
EXPECT_EQ(qemu::pixman_layout(qemu::pixman_format::a8b8g8r8)->kind, qemu::pixel_layout_t::kind_e::rgbx);
EXPECT_EQ(qemu::pixman_layout(qemu::pixman_format::r5g6b5)->kind, qemu::pixel_layout_t::kind_e::packed);
// a whole row through the swap path
auto layout = *qemu::pixman_layout(qemu::pixman_format::x8b8g8r8);
const std::vector<std::uint8_t> row {1, 2, 3, 4, 5, 6, 7, 8};
std::vector<std::uint8_t> out(8);
qemu::convert_row(layout, row.data(), out.data(), 2);
EXPECT_EQ(out, (std::vector<std::uint8_t> {3, 2, 1, 4, 7, 6, 5, 8}));
}
// @tag requirements: [REQ-CAP-002]
TEST(QemuPixelFormatTest, RejectsNonRgbFormats) {
EXPECT_FALSE(qemu::pixman_layout(qemu::pixman_format::a8).has_value());
EXPECT_FALSE(qemu::pixman_layout(0x08040000).has_value()); // c8, palette
EXPECT_FALSE(qemu::pixman_layout(0x08050000).has_value()); // g8, gray
EXPECT_FALSE(qemu::pixman_layout(0x0c028444).has_value()); // a4r4g4b4 at 12 bpp: not byte-aligned
EXPECT_FALSE(qemu::pixman_layout(0x20020fff).has_value()); // channels wider than the pixel
EXPECT_FALSE(qemu::pixman_layout(0).has_value());
}
// @tag requirements: [REQ-CAP-002, REQ-CAP-003]
TEST(QemuPixelFormatTest, MapsDrmFourccToPixman) {
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::xrgb8888), qemu::pixman_format::x8r8g8b8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::argb8888), qemu::pixman_format::a8r8g8b8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::xbgr8888), qemu::pixman_format::x8b8g8r8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::abgr8888), qemu::pixman_format::a8b8g8r8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::bgrx8888), qemu::pixman_format::b8g8r8x8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::bgra8888), qemu::pixman_format::b8g8r8a8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::rgbx8888), qemu::pixman_format::r8g8b8x8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::rgba8888), qemu::pixman_format::r8g8b8a8);
EXPECT_EQ(qemu::pixman_from_drm_fourcc(qemu::drm_fourcc::rgb565), qemu::pixman_format::r5g6b5);
EXPECT_FALSE(qemu::pixman_from_drm_fourcc(0x3231564e).has_value()); // NV12
}
#endif