@@ -1,17 +1,25 @@
/**
* @file src/platform/linux/qemu/capture.cpp
* @brief Definitions for the QEMU D-Bus display capture backend (`capture = qemu`).
* @details Like kmsgrab there are two display classes: `display_ram_t` for frames in system
* memory (shared memory scanouts, or DMABUFs read back for software encoding) and
* `display_vram_t`, which hands DMABUF scanouts to the VAAPI, CUDA and Vulkan encoders without
* copying them.
*/
// standard includes
#include <cstring>
#include <limits>
#include <mutex>
#include <optional>
#include <thread>
// local includes
#include "frame_store.h"
#include "render_node.h"
#include "session.h"
#include "src/config.h"
#include "src/logging.h"
#include "src/platform/common.h"
#include "src/platform/linux/graphics.h"
#include "src/platform/linux/misc.h"
#include "src/video.h"
@@ -22,5 +30,8 @@
#ifdef SUNSHINE_BUILD_CUDA
#include "src/platform/linux/cuda.h"
#endif
#ifdef SUNSHINE_BUILD_VULKAN
#include "src/platform/linux/vulkan_encode.h"
#endif
using namespace std::literals;
@@ -37,82 +48,138 @@
};
/**
* @brief A listener registered on one console, with the frame it reconstructs.
* @brief Display backend that streams a QEMU console from shared memory scanouts.
*/
class display_t: public platf::display_t {
public:
/**
* @brief Create an uninitialized display.
*
* @param mem_type Memory type the encoder expects.
*/
explicit display_t(platf::mem_type_e mem_type):
mem_type {mem_type} {
struct console_connection_t {
std::shared_ptr<session_t> session; ///< Session to the VM.
std::shared_ptr<frame_store_t> store; ///< Frame reconstructed from listener calls.
std::unique_ptr<listener_registration_t> registration; ///< Keeps the listener registered.
console_info_t console; ///< The selected console.
std::string vm_name; ///< VM name, for logs.
int width {0}; ///< Frame width at registration.
int height {0}; ///< Frame height at registration.
};
/**
* @brief Connect to QEMU, register a listener on the selected console, and wait for a frame.
*
* @param display_name Console id or label; empty selects the first graphical console.
* @param dmabuf_readback Whether DMABUF scanouts should be read back into system memory.
* @return The connection, or nothing on failure.
*/
std::optional<console_connection_t> connect_console(const std::string &display_name, bool dmabuf_readback) {
console_connection_t conn;
conn.session = shared_session(config::video.qemu_dbus_address);
if (!conn.session) {
return std::nullopt;
}
/**
* @brief Connect to QEMU, register a listener on the selected console, and wait for a frame.
*
* @param display_name Console id or label; empty selects the first graphical console.
auto vm = conn.session->vm();
auto console = find_console(vm, display_name);
if (!console) {
BOOST_LOG(warning) << "qemu: console ["sv << display_name << "] not found, using the first graphical console"sv;
console = find_console(vm, "");
}
if (!console) {
BOOST_LOG(error) << "qemu: VM ["sv << vm.name << "] has no graphical console"sv;
return std::nullopt;
}
conn.console = *console;
conn.vm_name = vm.name;
* @param config Stream configuration.
* @return 0 on success, -1 on failure.
*/
int init(const std::string &display_name, const ::video::config_t &config) {
delay = ::video::capture_frame_interval(config);
conn.store = std::make_shared<frame_store_t>(dmabuf_readback);
conn.registration = conn.session->register_listener(console->id, conn.store);
if (!conn.registration) {
session = shared_session(config::video.qemu_dbus_address);
if (!session) {
return std::nullopt;
}
return -1;
}
auto vm = session->vm();
// QEMU sends the current surface right after registration
if (conn.store->wait_for_frame(2s)) {
conn.width = conn.store->width();
conn.height = conn.store->height();
} else {
BOOST_LOG(warning) << "qemu: no scanout received yet; using the console size"sv;
conn.width = (int) console->width;
conn.height = (int) console->height;
}
if (conn.width <= 0 || conn.height <= 0) {
BOOST_LOG(error) << "qemu: console "sv << console->id << " has no size"sv;
return std::nullopt;
}
return conn;
}
auto console = find_console(vm, display_name);
if (!console) {
BOOST_LOG(warning) << "qemu: console ["sv << display_name << "] not found, using the first graphical console"sv;
console = find_console(vm, "");
}
if (!console) {
BOOST_LOG(error) << "qemu: VM ["sv << vm.name << "] has no graphical console"sv;
return -1;
}
/**
* @brief Check that QEMU renders DMABUF scanouts on the GPU the encoder uses.
*
* @param mem_type Memory type of the encoder.
* @param session Session to the VM, to find the QEMU process.
* @return False when the GPUs definitely differ.
store = std::make_shared<frame_store_t>();
registration = session->register_listener(console->id, store);
if (!registration) {
return -1;
}
*/
bool render_node_matches(platf::mem_type_e mem_type, const session_t &session) {
const auto qemu_node = qemu_render_node(session.qemu_pid());
if (!qemu_node) {
BOOST_LOG(warning) << "qemu: can't tell which render node QEMU uses; make sure it is the GPU Sunshine encodes on"sv;
return true;
}
switch (mem_type) {
case platf::mem_type_e::vaapi:
// QEMU sends the current surface right after registration
if (store->wait_for_frame(2s)) {
width = store->width();
height = store->height();
} else {
BOOST_LOG(warning) << "qemu: no scanout received yet; using the console size"sv;
width = (int) console->width;
height = (int) console->height;
{
const auto encoder_node = platf::resolve_render_device();
if (!same_device(*qemu_node, encoder_node)) {
BOOST_LOG(error) << "qemu: QEMU renders on "sv << *qemu_node << " but VAAPI encodes on "sv << encoder_node << "; set adapter_name = "sv << *qemu_node << " or start QEMU with rendernode="sv << encoder_node;
return false;
}
return true;
}
case platf::mem_type_e::cuda:
{
const auto driver = render_node_driver(*qemu_node);
if (!driver.empty() && driver != "nvidia") {
BOOST_LOG(error) << "qemu: QEMU renders on "sv << *qemu_node << " ("sv << driver << ") but NVENC needs an NVIDIA render node; start QEMU with rendernode= set to the NVIDIA GPU"sv;
return false;
}
return true;
}
default:
BOOST_LOG(info) << "qemu: QEMU renders on "sv << *qemu_node;
return true;
}
}
}
if (width <= 0 || height <= 0) {
BOOST_LOG(error) << "qemu: console "sv << console->id << " has no size"sv;
return -1;
}
/**
* @brief Display backend that streams one QEMU console; subclasses decide how frames are handed out.
*/
class display_base_t: public platf::display_t {
public:
/**
* @brief Take over a registered console connection.
*
* @param mem_type Memory type the encoder expects.
* @param conn Registered console.
* @param config Stream configuration.
*/
display_base_t(platf::mem_type_e mem_type, console_connection_t &&conn, const ::video::config_t &config):
mem_type {mem_type},
delay {::video::capture_frame_interval(config)},
conn {std::move(conn)} {
width = this->conn.width;
height = this->conn.height;
env_width = width;
env_height = height;
logical_width = width;
logical_height = height;
env_logical_width = width;
env_logical_height = height;
BOOST_LOG(info) << "qemu: streaming VM ["sv << vm.name << "] console "sv << console->id << " ["sv << console->label << "] at "sv << width << 'x' << height;
return 0;
}
/**
* @brief Push frames as soon as the guest damages the display, at most one per client frame interval.
* @details Waits on the frame store instead of a fixed tick, so damage is copied right after
* QEMU reports it. Without damage, the pipeline gets a heartbeat once per frame interval (so it
* can stop or reconfigure the capture) and the encoder's minimum frame rate repeats the last
* frame. The listener thread never waits for this loop.
* @details Waits on the frame store instead of a fixed tick, so damage is handed to the encoder
* right after QEMU reports it. Without damage, the pipeline gets a heartbeat once per frame
* interval (so it can stop or reconfigure the capture) and the encoder's minimum frame rate
* repeats the last frame. The listener thread never waits for this loop.
*
* @param push_captured_image_cb Callback receiving captured images and heartbeats.
* @param pull_free_image_cb Callback providing an image to fill.
@@ -125,7 +192,7 @@
while (true) {
auto now = std::chrono::steady_clock::now();
if (store->wait_for_change(copied_sequence, now + delay)) {
if (conn.store->wait_for_change(copied_sequence, now + delay)) {
// rate limit: never push more often than the client's frame interval
auto earliest = last_push + delay;
if (std::chrono::steady_clock::now() < earliest) {
@@ -134,7 +201,10 @@
}
std::shared_ptr<platf::img_t> img_out;
auto status = platf::capture_e::reinit;
auto status = snapshot(pull_free_image_cb, img_out, cursor && *cursor);
if (!gone_or_resized()) {
status = snapshot(pull_free_image_cb, img_out, cursor && *cursor);
}
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
@@ -147,6 +217,10 @@
break;
case platf::capture_e::ok:
last_push = std::chrono::steady_clock::now();
if (img_out->frame_timestamp) {
damage_to_capture_logger.first_point(*img_out->frame_timestamp);
damage_to_capture_logger.second_point_now_and_log();
}
if (!push_captured_image_cb(std::move(img_out), true)) {
return platf::capture_e::ok;
}
@@ -158,19 +232,86 @@
}
}
protected:
/**
* @brief Copy the latest frame if the guest changed it.
* @brief Hand out the latest frame if the guest changed it.
*
* @param pull_free_image_cb Callback providing an image to fill.
* @param img_out Filled image on `ok`.
* @param draw_cursor Whether to draw the guest cursor.
* @return `ok` with a new frame, `timeout` when nothing changed, `reinit` when the scanout
* changed in a way that needs a new display, or `interrupted`.
*/
* @param draw_cursor Whether to blend the guest cursor into the image.
virtual 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) = 0;
/**
* @brief Report whether QEMU went away or the scanout size changed.
*
* @return True when the display has to be re-created.
* @return `ok` with a new frame, `timeout` when nothing changed, `reinit` when the size
* changed or QEMU went away, or `interrupted`.
*/
[[nodiscard]] bool gone_or_resized() const {
return !conn.session->alive() || !conn.store->connected() || conn.store->width() != width || conn.store->height() != height;
}
platf::mem_type_e mem_type; ///< Memory type the encoder expects.
std::chrono::nanoseconds delay; ///< Client frame interval.
console_connection_t conn; ///< Registered console.
std::uint64_t copied_sequence {0}; ///< Store sequence of the last frame handed out.
private:
logging::time_delta_periodic_logger damage_to_capture_logger {debug, "qemu: damage received to frame captured"}; ///< Latency from QEMU's call to the capture.
};
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) {
if (!session->alive() || !store->connected() || store->width() != width || store->height() != height) {
/**
* @brief Display that copies frames into system memory, for software encoders or an upload to the GPU.
*/
class display_ram_t: public display_base_t {
public:
using display_base_t::display_base_t;
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<qemu::img_t>();
img->width = width;
img->height = height;
img->pixel_pitch = 4;
img->row_pitch = img->pixel_pitch * width;
img->data = new std::uint8_t[(std::size_t) height * img->row_pitch];
return img;
}
int dummy_img(platf::img_t *img) override {
if (!img) {
return -1;
}
std::memset(img->data, 0, (std::size_t) img->height * img->row_pitch);
return 0;
}
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == platf::mem_type_e::vaapi) {
return va::make_avcodec_encode_device(width, height, false);
}
#endif
#ifdef SUNSHINE_BUILD_CUDA
if (mem_type == platf::mem_type_e::cuda) {
return cuda::make_avcodec_encode_device(width, height, false);
}
#endif
#ifdef SUNSHINE_BUILD_VULKAN
if (mem_type == platf::mem_type_e::vulkan) {
return vk::make_avcodec_encode_device_ram(width, height);
}
#endif
return std::make_unique<platf::avcodec_encode_device_t>();
}
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 (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 (store->sequence() == copied_sequence) {
if (conn.store->sequence() == copied_sequence) {
return platf::capture_e::timeout;
}
@@ -180,60 +321,162 @@
}
std::chrono::steady_clock::time_point timestamp;
switch (store->copy_if_newer(copied_sequence, width, height, img_out->data, timestamp, draw_cursor)) {
switch (conn.store->copy_if_newer(copied_sequence, width, height, img_out->data, timestamp, draw_cursor)) {
case frame_status_e::new_frame:
img_out->frame_timestamp = timestamp;
damage_to_capture_logger.first_point(timestamp);
damage_to_capture_logger.second_point_now_and_log();
return platf::capture_e::ok;
case frame_status_e::unchanged:
return platf::capture_e::timeout;
case frame_status_e::size_changed:
case frame_status_e::disconnected:
default:
return platf::capture_e::reinit;
}
}
};
/**
* @brief Display that hands DMABUF scanouts to a GPU encoder without copying them.
* @details Modeled on kmsgrab's `display_vram_t`: each frame carries duplicated descriptors in an
* `egl::img_descriptor_t`, which the encoder imports with `egl::import_source`. The descriptor
* sequence only changes when QEMU scans out a new buffer, so the encoder keeps its imported EGL
* image across damage updates and releases the previous one when the next buffer arrives.
*/
class display_vram_t: public display_base_t {
public:
/**
* @brief Take over a console that sends DMABUF scanouts.
*
* @param mem_type Memory type the encoder expects.
* @param conn Registered console.
* @param config Stream configuration.
* @param offset_x Left edge of the display rectangle in the buffer.
* @param offset_y Top edge of the display rectangle in the buffer.
*/
display_vram_t(platf::mem_type_e mem_type, console_connection_t &&conn, const ::video::config_t &config, int offset_x, int offset_y):
display_base_t(mem_type, std::move(conn), config),
offset_x {offset_x},
offset_y {offset_y} {
}
std::shared_ptr<platf::img_t> alloc_img() override {
auto img = std::make_shared<qemu::img_t>();
auto img = std::make_shared<egl::img_descriptor_t>();
img->width = width;
img->height = height;
img->serial = std::numeric_limits<decltype(img->serial)>::max();
img->data = nullptr;
img->pixel_pitch = 4;
img->row_pitch = img->pixel_pitch * width;
img->data = new std::uint8_t[(std::size_t) height * img->row_pitch];
img->sequence = 0;
std::fill_n(img->sd.fds, 4, -1);
return img;
}
int dummy_img(platf::img_t *img) override {
if (!img) {
return -1;
}
std::memset(img->data, 0, (std::size_t) img->height * img->row_pitch);
// images with sequence 0 are drawn as black by the encoder
return img ? 0 : -1;
return 0;
}
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
#ifdef SUNSHINE_BUILD_VAAPI
if (mem_type == platf::mem_type_e::vaapi) {
return va::make_avcodec_encode_device(width, height, offset_x, offset_y, true);
}
#endif
#ifdef SUNSHINE_BUILD_VULKAN
if (mem_type == platf::mem_type_e::vulkan) {
return vk::make_avcodec_encode_device_vram(width, height, offset_x, offset_y);
return va::make_avcodec_encode_device(width, height, false);
}
#endif
#ifdef SUNSHINE_BUILD_CUDA
if (mem_type == platf::mem_type_e::cuda) {
return cuda::make_avcodec_encode_device(width, height, false);
return cuda::make_avcodec_gl_encode_device(width, height, offset_x, offset_y);
}
#endif
BOOST_LOG(error) << "qemu: unsupported pixel format for DMABUF capture: "sv << platf::from_pix_fmt(pix_fmt);
return nullptr;
}
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->sequence() == copied_sequence) {
return platf::capture_e::timeout;
}
dmabuf_frame_t frame;
switch (conn.store->dmabuf_if_newer(copied_sequence, width, height, frame)) {
case frame_status_e::new_frame:
break;
case frame_status_e::unchanged:
return platf::capture_e::timeout;
default:
return platf::capture_e::reinit;
}
if (!frame.disabled && ((int) frame.buffer.x != offset_x || (int) frame.buffer.y != offset_y)) {
BOOST_LOG(info) << "qemu: DMABUF scanout moved to +"sv << frame.buffer.x << '+' << frame.buffer.y << "; re-creating the display"sv;
return platf::capture_e::reinit;
}
if (!pull_free_image_cb(img_out)) {
return platf::capture_e::interrupted;
}
auto img = (egl::img_descriptor_t *) img_out.get();
img->reset();
img->frame_timestamp = frame.timestamp;
if (frame.disabled) {
// sequence 0 makes the encoder draw black; import the buffer again once the display is back
img->sequence = 0;
img->width = width;
img->height = height;
img->data = nullptr;
reimport = true;
return platf::capture_e::ok;
}
if (frame.generation != imported_generation || reimport) {
imported_generation = frame.generation;
reimport = false;
sequence += 1;
}
img->sequence = sequence;
img->y_invert = !frame.buffer.y0_top;
auto &sd = img->sd;
sd.width = (int) frame.buffer.backing_width;
sd.height = (int) frame.buffer.backing_height;
sd.fourcc = frame.buffer.fourcc;
sd.modifier = frame.buffer.modifier;
for (std::uint32_t plane = 0; plane < 4; ++plane) {
const bool used = plane < frame.buffer.num_planes;
sd.fds[plane] = used ? frame.buffer.fds[plane].release() : -1;
sd.pitches[plane] = used ? frame.buffer.strides[plane] : 0;
sd.offsets[plane] = used ? frame.buffer.offsets[plane] : 0;
}
auto cursor = draw_cursor ? conn.store->cursor(img->serial) : cursor_state_t {};
if (cursor.drawable()) {
if (img->serial != cursor.serial) {
img->buffer = std::move(cursor.pixels);
img->serial = cursor.serial;
}
img->x = cursor.x - cursor.hot_x;
img->y = cursor.y - cursor.hot_y;
img->src_w = cursor.width;
img->src_h = cursor.height;
return std::make_unique<platf::avcodec_encode_device_t>();
img->width = cursor.width;
img->height = cursor.height;
img->pixel_pitch = 4;
img->row_pitch = img->pixel_pitch * img->width;
img->data = img->buffer.data();
} else {
img->data = nullptr;
}
return platf::capture_e::ok;
}
private:
platf::mem_type_e mem_type; ///< Memory type the encoder expects.
std::chrono::nanoseconds delay {}; ///< Capture interval for the client frame rate.
std::shared_ptr<session_t> session; ///< Session to the VM.
std::shared_ptr<frame_store_t> store; ///< Frame reconstructed from listener calls.
std::unique_ptr<listener_registration_t> registration; ///< Keeps the listener registered.
std::uint64_t copied_sequence {0}; ///< Store sequence of the last copied frame.
logging::time_delta_periodic_logger damage_to_capture_logger {debug, "qemu: damage received to frame captured"}; ///< Latency from QEMU's call to the capture copy.
int offset_x; ///< Left edge of the display rectangle in the buffer.
int offset_y; ///< Top edge of the display rectangle in the buffer.
std::uint64_t sequence {0}; ///< Descriptor sequence; incremented when the encoder must import a new buffer.
std::uint64_t imported_generation {0}; ///< Store generation of the buffer the encoder imported last.
bool reimport {false}; ///< Whether a black frame replaced the imported buffer.
};
} // namespace qemu
@@ -254,8 +497,10 @@
/**
* @brief List the graphical consoles of the VM.
* @details The console selected by label in `output_name` is listed under its label, so
* Sunshine's display selection finds it.
*
* @return Console ids, or an empty list when QEMU is unreachable.
* @return Console names, or an empty list when QEMU is unreachable.
*/
std::vector<std::string> qemu_display_names() {
auto session = qemu::shared_session(config::video.qemu_dbus_address);
@@ -271,6 +516,8 @@
/**
* @brief Create a QEMU display capture backend.
* @details GPU encoders get the zero-copy DMABUF display when QEMU sends DMABUF scanouts, and
* the system-memory display with an upload otherwise.
*
* @param hwdevice_type Memory type the encoder expects.
* @param display_name Console id or label.
@@ -278,15 +525,54 @@
* @return Display backend, or nullptr on failure.
*/
std::shared_ptr<display_t> qemu_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
if (hwdevice_type != mem_type_e::system && hwdevice_type != mem_type_e::vaapi && hwdevice_type != mem_type_e::cuda) {
if (hwdevice_type != mem_type_e::system && hwdevice_type != mem_type_e::vaapi && hwdevice_type != mem_type_e::cuda && hwdevice_type != mem_type_e::vulkan) {
BOOST_LOG(error) << "qemu: could not initialize display with the given hw device type"sv;
return nullptr;
}
#ifndef SUNSHINE_BUILD_CUDA
if (hwdevice_type == mem_type_e::cuda) {
BOOST_LOG(warning) << "qemu: attempting to use NVENC without CUDA support"sv;
return nullptr;
}
#endif
const bool gpu = hwdevice_type != mem_type_e::system;
auto display = std::make_shared<qemu::display_t>(hwdevice_type);
if (display->init(display_name, config)) {
auto conn = qemu::connect_console(display_name, !gpu);
if (!conn) {
return nullptr;
}
const auto console_id = conn->console.id;
const auto label = conn->console.label;
const auto vm_name = conn->vm_name;
std::shared_ptr<display_t> display;
if (conn->store->kind() == qemu::scanout_kind_e::dmabuf) {
if (!gpu && !conn->store->dmabuf_readable()) {
BOOST_LOG(error) << "qemu: QEMU sends DMABUF scanouts that can't be read from system memory; use a hardware encoder"sv;
return nullptr;
}
if (gpu) {
if (!qemu::render_node_matches(hwdevice_type, *conn->session)) {
return nullptr;
}
qemu::dmabuf_frame_t frame;
std::uint64_t probe_sequence = 0;
int offset_x = 0;
int offset_y = 0;
if (conn->store->dmabuf_if_newer(probe_sequence, conn->width, conn->height, frame) == qemu::frame_status_e::new_frame) {
offset_x = (int) frame.buffer.x;
offset_y = (int) frame.buffer.y;
}
display = std::make_shared<qemu::display_vram_t>(hwdevice_type, std::move(*conn), config, offset_x, offset_y);
}
} else if (gpu) {
BOOST_LOG(info) << "qemu: QEMU sends shared memory scanouts; uploading frames to the GPU"sv;
}
if (!display) {
display = std::make_shared<qemu::display_ram_t>(hwdevice_type, std::move(*conn), config);
}
BOOST_LOG(info) << "qemu: streaming VM ["sv << vm_name << "] console "sv << console_id << " ["sv << label << "] at "sv << display->width << 'x' << display->height;
return display;
}
} // namespace platf