fangorn/sunshine-qemu
public
ref:main
/**
* @file src/platform/linux/qemu/capture.cpp
* @brief Definitions for the QEMU D-Bus display capture backend (`capture = qemu`).
*/
// standard includes
#include <cstring>
#include <mutex>
// local includes
#include "frame_store.h"
#include "session.h"
#include "src/config.h"
#include "src/logging.h"
#include "src/platform/common.h"
#include "src/platform/linux/misc.h"
#include "src/video.h"
#ifdef SUNSHINE_BUILD_VAAPI
#include "src/platform/linux/vaapi.h"
#endif
#ifdef SUNSHINE_BUILD_CUDA
#include "src/platform/linux/cuda.h"
#endif
using namespace std::literals;
namespace qemu {
/**
* @brief System-memory image owned by the QEMU capture backend.
*/
struct img_t: public platf::img_t {
~img_t() override {
delete[] data;
data = nullptr;
}
};
/**
* @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} {
}
/**
* @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 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);
session = shared_session(config::video.qemu_dbus_address);
if (!session) {
return -1;
}
auto vm = 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 -1;
}
store = std::make_shared<frame_store_t>();
registration = session->register_listener(console->id, store);
if (!registration) {
return -1;
}
// 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;
}
if (width <= 0 || height <= 0) {
BOOST_LOG(error) << "qemu: console "sv << console->id << " has no size"sv;
return -1;
}
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;
}
platf::capture_e capture(const push_captured_image_cb_t &push_captured_image_cb, const pull_free_image_cb_t &pull_free_image_cb, bool *cursor) override {
auto next_frame = std::chrono::steady_clock::now();
sleep_overshoot_logger.reset();
while (true) {
platf::handle_pacing(next_frame, delay, sleep_overshoot_logger);
std::shared_ptr<platf::img_t> img_out;
auto status = snapshot(pull_free_image_cb, img_out);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
case platf::capture_e::interrupted:
return status;
case platf::capture_e::timeout:
if (!push_captured_image_cb(std::move(img_out), false)) {
return platf::capture_e::ok;
}
break;
case platf::capture_e::ok:
if (!push_captured_image_cb(std::move(img_out), true)) {
return platf::capture_e::ok;
}
break;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
return status;
}
}
}
/**
* @brief Copy 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`.
* @return `ok` with a new frame, `timeout` when nothing changed, `reinit` when the size
* changed or QEMU went away, or `interrupted`.
*/
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out) {
if (!session->alive() || !store->connected() || store->width() != width || store->height() != height) {
return platf::capture_e::reinit;
}
if (store->sequence() == copied_sequence) {
return platf::capture_e::timeout;
}
if (!pull_free_image_cb(img_out)) {
return platf::capture_e::interrupted;
}
std::chrono::steady_clock::time_point timestamp;
switch (store->copy_if_newer(copied_sequence, width, height, img_out->data, timestamp)) {
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;
}
}
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
return std::make_unique<platf::avcodec_encode_device_t>();
}
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.
};
} // namespace qemu
namespace platf {
/**
* @brief Check whether the QEMU backend was selected and QEMU is reachable.
* @details Never connects unless `capture = qemu`, so other backends are unaffected.
*
* @return True when `capture = qemu` and a QEMU display was found.
*/
bool verify_qemu() {
if (config::video.capture != "qemu") {
return false;
}
return qemu::shared_session(config::video.qemu_dbus_address) != nullptr;
}
/**
* @brief List the graphical consoles of the VM.
*
* @return Console ids, 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);
if (!session) {
return {};
}
auto names = qemu::graphic_console_names(session->vm());
for (const auto &name : names) {
BOOST_LOG(info) << "qemu: found console "sv << name;
}
return names;
}
/**
* @brief Create a QEMU display capture backend.
*
* @param hwdevice_type Memory type the encoder expects.
* @param display_name Console id or label.
* @param config Stream configuration.
* @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) {
BOOST_LOG(error) << "qemu: could not initialize display with the given hw device type"sv;
return nullptr;
}
auto display = std::make_shared<qemu::display_t>(hwdevice_type);
if (display->init(display_name, config)) {
return nullptr;
}
return display;
}
} // namespace platf