ref:main
/**
* @file src/platform/linux/qemu/frame_store.cpp
* @brief Definitions for the shared-memory frame store of the QEMU capture backend.
*/
// class header include
#include "frame_store.h"
// standard includes
#include <algorithm>
#include <cstring>
// platform includes
#include <sys/mman.h>
#include <sys/stat.h>
// local includes
#include "src/logging.h"
using namespace std::literals;
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.
*/
void log_unsupported(std::uint32_t format) {
static std::mutex logged_mutex;
static std::vector<std::uint32_t> logged;
std::lock_guard lock {logged_mutex};
if (std::ranges::find(logged, format) == logged.end()) {
logged.push_back(format);
BOOST_LOG(warning) << "qemu: unsupported pixman format 0x"sv << std::hex << format << std::dec << "; ignoring scanout"sv;
}
}
/**
* @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.
* @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) {
return false;
}
return (std::size_t) (stride * (height - 1) + width * 4) <= available;
}
} // namespace
frame_store_t::~frame_store_t() {
std::lock_guard lock {mutex};
unmap_locked();
}
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) {
if (!is_supported(format)) {
log_unsupported(format);
return;
}
if (!covers(data.size(), width, height, stride)) {
BOOST_LOG(warning) << "qemu: scanout data doesn't cover "sv << width << 'x' << height << " with stride "sv << stride;
return;
}
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);
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) {
if (!is_supported(format)) {
log_unsupported(format);
return;
}
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;
}
std::lock_guard lock {mutex};
if (frame_width == 0 || map_addr) {
return;
}
// clip the rectangle to the frame, skipping the source rows and columns that fall outside
int x0 = std::max(x, 0);
int y0 = std::max(y, 0);
int x1 = std::min<std::int64_t>((std::int64_t) x + width, frame_width);
int y1 = std::min<std::int64_t>((std::int64_t) y + height, frame_height);
if (x0 >= x1 || y0 >= y1) {
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);
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) {
if (fd.get() < 0) {
BOOST_LOG(warning) << "qemu: ScanoutMap without a descriptor"sv;
return;
}
if (!is_supported(format)) {
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)) {
BOOST_LOG(warning) << "qemu: ScanoutMap descriptor doesn't cover "sv << width << 'x' << height << " at offset "sv << offset;
return;
}
std::size_t size = (std::size_t) offset + (std::size_t) stride * height;
size = std::min<std::size_t>(size, st.st_size);
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;
}
std::lock_guard lock {mutex};
unmap_locked();
map_fd = std::move(fd);
map_addr = (const std::uint8_t *) addr;
map_size = size;
map_offset = offset;
map_stride = stride;
map_format = format;
resize_locked((int) width, (int) height);
blit_locked(0, 0, (int) width, (int) height, map_addr + map_offset, map_stride, map_format);
touch_locked();
}
void frame_store_t::update_map(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) {
std::lock_guard lock {mutex};
if (!map_addr) {
return;
}
int x0 = std::max(x, 0);
int y0 = std::max(y, 0);
int x1 = std::min<std::int64_t>((std::int64_t) x + width, frame_width);
int y1 = std::min<std::int64_t>((std::int64_t) y + height, frame_height);
if (x0 >= x1 || y0 >= y1) {
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);
touch_locked();
}
void frame_store_t::disable() {
std::lock_guard lock {mutex};
std::ranges::fill(pixels, 0);
if (frame_width > 0) {
touch_locked();
}
}
void frame_store_t::disconnected() {
std::lock_guard lock {mutex};
is_disconnected = true;
frame_ready.notify_all();
}
bool frame_store_t::wait_for_frame(std::chrono::milliseconds timeout) {
std::unique_lock lock {mutex};
frame_ready.wait_for(lock, timeout, [this]() {
return change_sequence > 0 || is_disconnected;
});
return change_sequence > 0 && !is_disconnected;
}
int frame_store_t::width() const {
std::lock_guard lock {mutex};
return frame_width;
}
int frame_store_t::height() const {
std::lock_guard lock {mutex};
return frame_height;
}
bool frame_store_t::connected() const {
std::lock_guard lock {mutex};
return !is_disconnected;
}
std::uint64_t frame_store_t::sequence() const {
std::lock_guard lock {mutex};
return change_sequence;
}
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 &timestamp) {
std::lock_guard lock {mutex};
if (is_disconnected) {
return frame_status_e::disconnected;
}
if (width != frame_width || height != frame_height) {
return frame_status_e::size_changed;
}
if (change_sequence == last_sequence) {
return frame_status_e::unchanged;
}
std::memcpy(dst, pixels.data(), pixels.size());
last_sequence = change_sequence;
timestamp = change_time;
return frame_status_e::new_frame;
}
void frame_store_t::resize_locked(int new_width, int new_height) {
const bool mapped = map_addr != nullptr;
if (new_width != frame_width || new_height != frame_height || mapped != logged_mapped) {
BOOST_LOG(info) << "qemu: scanout "sv << new_width << 'x' << new_height << (mapped ? " via shared memory map"sv : " via D-Bus messages"sv);
logged_mapped = mapped;
}
frame_width = new_width;
frame_height = new_height;
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) {
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];
}
}
}
void frame_store_t::touch_locked() {
change_sequence += 1;
change_time = std::chrono::steady_clock::now();
frame_ready.notify_all();
}
void frame_store_t::unmap_locked() {
if (map_addr) {
munmap((void *) map_addr, map_size);
map_addr = nullptr;
map_size = 0;
}
map_fd = fd_t {};
}
} // namespace qemu