mirror of
https://github.com/LizardByte/Sunshine.git
synced 2026-08-07 18:36:34 +00:00
refactor(linux/xdgportal): Split pipewire code into it's own file so it can be reused (#5008)
This commit is contained in:
@@ -244,17 +244,24 @@ if(GIO_FOUND)
|
||||
list(APPEND PLATFORM_LIBRARIES ${GIO_LIBRARIES})
|
||||
endif()
|
||||
|
||||
# XDG portal
|
||||
# Pipewire
|
||||
if(${SUNSHINE_ENABLE_PORTAL})
|
||||
pkg_check_modules(PIPEWIRE libpipewire-0.3 REQUIRED)
|
||||
else()
|
||||
set(PIPEWIRE_FOUND OFF)
|
||||
endif()
|
||||
|
||||
if(PIPEWIRE_FOUND AND GIO_FOUND)
|
||||
add_compile_definitions(SUNSHINE_BUILD_PORTAL)
|
||||
if(PIPEWIRE_FOUND)
|
||||
include_directories(SYSTEM ${PIPEWIRE_INCLUDE_DIRS})
|
||||
list(APPEND PLATFORM_LIBRARIES ${PIPEWIRE_LIBRARIES})
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/linux/pipewire.cpp")
|
||||
endif()
|
||||
|
||||
# XDG portal
|
||||
set(PORTAL_FOUND OFF)
|
||||
if(PIPEWIRE_FOUND AND GIO_FOUND AND ${SUNSHINE_ENABLE_PORTAL})
|
||||
set(PORTAL_FOUND ON)
|
||||
add_compile_definitions(SUNSHINE_BUILD_PORTAL)
|
||||
list(APPEND PLATFORM_TARGET_FILES
|
||||
"${CMAKE_SOURCE_DIR}/src/platform/linux/portalgrab.cpp")
|
||||
endif()
|
||||
@@ -262,7 +269,7 @@ endif()
|
||||
if(NOT ${CUDA_FOUND}
|
||||
AND NOT ${WAYLAND_FOUND}
|
||||
AND NOT ${X11_FOUND}
|
||||
AND NOT ${PIPEWIRE_FOUND}
|
||||
AND NOT ${PORTAL_FOUND}
|
||||
AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND})
|
||||
AND NOT ${LIBVA_FOUND})
|
||||
message(FATAL_ERROR "Couldn't find either cuda, libva, pipewire, wayland, x11, or (libdrm and libcap)")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@
|
||||
// local includes
|
||||
#include "cuda.h"
|
||||
#include "graphics.h"
|
||||
#include "pipewire.cpp"
|
||||
#include "src/main.h"
|
||||
#include "src/platform/common.h"
|
||||
#include "src/video.h"
|
||||
@@ -39,12 +40,6 @@
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
// Buffer and limit constants
|
||||
constexpr int SPA_POD_BUFFER_SIZE = 4096;
|
||||
constexpr int MAX_PARAMS = 200;
|
||||
constexpr int MAX_DMABUF_FORMATS = 200;
|
||||
constexpr int MAX_DMABUF_MODIFIERS = 200;
|
||||
|
||||
// Portal configuration constants
|
||||
constexpr uint32_t SOURCE_TYPE_MONITOR = 1;
|
||||
constexpr uint32_t CURSOR_MODE_EMBEDDED = 2;
|
||||
@@ -741,14 +736,6 @@ namespace portal {
|
||||
public:
|
||||
static runtime_t &instance();
|
||||
|
||||
bool is_maxframerate_failed() const {
|
||||
return maxframerate_failed_;
|
||||
}
|
||||
|
||||
void set_maxframerate_failed() {
|
||||
maxframerate_failed_ = true;
|
||||
}
|
||||
|
||||
bool is_portal_secured() const {
|
||||
return is_portal_secured_;
|
||||
}
|
||||
@@ -790,7 +777,6 @@ namespace portal {
|
||||
runtime_t(const runtime_t &) = delete;
|
||||
runtime_t &operator=(const runtime_t &) = delete;
|
||||
|
||||
bool maxframerate_failed_ = false;
|
||||
bool is_portal_secured_ = false;
|
||||
};
|
||||
|
||||
@@ -800,511 +786,16 @@ namespace portal {
|
||||
return *instance_;
|
||||
}
|
||||
|
||||
class pipewire_t {
|
||||
class portal_t: public pipewire::pipewire_display_t {
|
||||
public:
|
||||
pipewire_t():
|
||||
loop(pw_thread_loop_new("Pipewire thread", nullptr)) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Start PW thread loop"sv;
|
||||
pw_thread_loop_start(loop);
|
||||
}
|
||||
|
||||
~pipewire_t() {
|
||||
BOOST_LOG(debug) << "[portalgrab] Destroying pipewire_t"sv;
|
||||
if (loop) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Stop PW thread loop"sv;
|
||||
pw_thread_loop_stop(loop);
|
||||
}
|
||||
try {
|
||||
cleanup_stream();
|
||||
} catch (const std::exception &e) {
|
||||
BOOST_LOG(error) << "[portalgrab] Standard exception caught in ~pipewire_t: "sv << e.what();
|
||||
} catch (...) {
|
||||
BOOST_LOG(error) << "[portalgrab] Unknown exception caught in ~pipewire_t"sv;
|
||||
}
|
||||
|
||||
pw_thread_loop_lock(loop);
|
||||
|
||||
if (core) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Disconnect PW core"sv;
|
||||
pw_core_disconnect(core);
|
||||
core = nullptr;
|
||||
}
|
||||
if (context) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Destroy PW context"sv;
|
||||
pw_context_destroy(context);
|
||||
context = nullptr;
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(loop);
|
||||
|
||||
if (fd >= 0) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Close pipewire_fd"sv;
|
||||
close(fd);
|
||||
}
|
||||
BOOST_LOG(debug) << "[portalgrab] Stop PW thread loop"sv;
|
||||
pw_thread_loop_stop(loop);
|
||||
BOOST_LOG(debug) << "[portalgrab] Destroy PW thread loop"sv;
|
||||
pw_thread_loop_destroy(loop);
|
||||
}
|
||||
|
||||
std::mutex &frame_mutex() {
|
||||
return stream_data.frame_mutex;
|
||||
}
|
||||
|
||||
std::condition_variable &frame_cv() {
|
||||
return stream_data.frame_cv;
|
||||
}
|
||||
|
||||
bool is_frame_ready() const {
|
||||
return stream_data.frame_ready;
|
||||
}
|
||||
|
||||
void set_frame_ready(bool ready) {
|
||||
stream_data.frame_ready = ready;
|
||||
}
|
||||
|
||||
int init(int stream_fd, int stream_node, std::shared_ptr<shared_state_t> shared_state) {
|
||||
fd = stream_fd;
|
||||
node = stream_node;
|
||||
stream_data.shared = std::move(shared_state);
|
||||
|
||||
pw_thread_loop_lock(loop);
|
||||
BOOST_LOG(debug) << "[portalgrab] Setup PW context"sv;
|
||||
context = pw_context_new(pw_thread_loop_get_loop(loop), nullptr, 0);
|
||||
if (context) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Connect PW context to fd"sv;
|
||||
core = pw_context_connect_fd(context, fd, nullptr, 0);
|
||||
if (core) {
|
||||
pw_core_add_listener(core, &core_listener, &core_events, nullptr);
|
||||
} else {
|
||||
BOOST_LOG(debug) << "[portalgrab] Failed to connect to PW core. Error: "sv << errno << "(" << strerror(errno) << ")"sv;
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG(debug) << "[portalgrab] Failed to setup PW context. Error: "sv << errno << "(" << strerror(errno) << ")"sv;
|
||||
return -1;
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(loop);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cleanup_stream() {
|
||||
BOOST_LOG(debug) << "[portalgrab] Cleaning up stream"sv;
|
||||
if (loop && stream_data.stream) {
|
||||
pw_thread_loop_lock(loop);
|
||||
|
||||
// 1. Lock the frame mutex to stop fill_img
|
||||
BOOST_LOG(debug) << "[portalgrab] Stop fill_img"sv;
|
||||
{
|
||||
std::scoped_lock lock(stream_data.frame_mutex);
|
||||
stream_data.frame_ready = false;
|
||||
stream_data.current_buffer = nullptr;
|
||||
}
|
||||
|
||||
if (stream_data.stream) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Disconnect stream"sv;
|
||||
pw_stream_disconnect(stream_data.stream);
|
||||
BOOST_LOG(debug) << "[portalgrab] Destroy stream"sv;
|
||||
pw_stream_destroy(stream_data.stream);
|
||||
stream_data.stream = nullptr;
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(loop);
|
||||
}
|
||||
}
|
||||
|
||||
int ensure_stream(const platf::mem_type_e mem_type, const uint32_t width, const uint32_t height, const uint32_t refresh_rate, const struct dmabuf_format_info_t *dmabuf_infos, const int n_dmabuf_infos, const bool display_is_nvidia) {
|
||||
pw_thread_loop_lock(loop);
|
||||
if (!stream_data.stream) {
|
||||
if (!core) {
|
||||
BOOST_LOG(debug) << "[portalgrab] PW core not available. Cannot ensure stream."sv;
|
||||
pw_thread_loop_unlock(loop);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct pw_properties *props = pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr);
|
||||
|
||||
BOOST_LOG(debug) << "[portalgrab] Create PW stream"sv;
|
||||
stream_data.stream = pw_stream_new(core, "Sunshine Video Capture", props);
|
||||
pw_stream_add_listener(stream_data.stream, &stream_data.stream_listener, &stream_events, &stream_data);
|
||||
|
||||
std::array<uint8_t, SPA_POD_BUFFER_SIZE> buffer;
|
||||
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
|
||||
|
||||
int n_params = 0;
|
||||
std::array<const struct spa_pod *, MAX_PARAMS> params;
|
||||
|
||||
// Add preferred parameters for DMA-BUF with modifiers
|
||||
// Use DMA-BUF for VAAPI, or for CUDA when the display GPU is NVIDIA (pure NVIDIA system).
|
||||
// On hybrid GPU systems (Intel+NVIDIA), DMA-BUFs come from the Intel GPU and cannot
|
||||
// be imported into CUDA, so we fall back to memory buffers in that case.
|
||||
bool use_dmabuf = n_dmabuf_infos > 0 && (mem_type == platf::mem_type_e::vaapi ||
|
||||
mem_type == platf::mem_type_e::vulkan ||
|
||||
(mem_type == platf::mem_type_e::cuda && display_is_nvidia));
|
||||
if (use_dmabuf) {
|
||||
for (int i = 0; i < n_dmabuf_infos; i++) {
|
||||
auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, dmabuf_infos[i].format, dmabuf_infos[i].modifiers, dmabuf_infos[i].n_modifiers);
|
||||
params[n_params] = format_param;
|
||||
n_params++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add fallback for memptr
|
||||
for (const auto &fmt : format_map) {
|
||||
if (fmt.fourcc == 0) {
|
||||
break;
|
||||
}
|
||||
auto format_param = build_format_parameter(&pod_builder, width, height, refresh_rate, fmt.pw_format, nullptr, 0);
|
||||
params[n_params] = format_param;
|
||||
n_params++;
|
||||
}
|
||||
BOOST_LOG(debug) << "[portalgrab] Connect PW stream - fd "sv << fd << " node "sv << node;
|
||||
pw_stream_connect(stream_data.stream, PW_DIRECTION_INPUT, node, (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params.data(), n_params);
|
||||
}
|
||||
pw_thread_loop_unlock(loop);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void close_img_fds(egl::img_descriptor_t *img_descriptor) {
|
||||
for (int &fd : img_descriptor->sd.fds) {
|
||||
if (fd >= 0) {
|
||||
close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fill_img_metadata(egl::img_descriptor_t *img_descriptor, struct spa_buffer *buf) {
|
||||
img_descriptor->frame_timestamp = std::chrono::steady_clock::now();
|
||||
|
||||
struct spa_meta_header *h = static_cast<struct spa_meta_header *>(
|
||||
spa_buffer_find_meta_data(buf, SPA_META_Header, sizeof(*h))
|
||||
);
|
||||
if (h) {
|
||||
img_descriptor->seq = h->seq;
|
||||
img_descriptor->pts = h->pts;
|
||||
}
|
||||
|
||||
if (buf->n_datas > 0) {
|
||||
img_descriptor->pw_flags = buf->datas[0].chunk->flags;
|
||||
}
|
||||
|
||||
struct spa_meta_region *damage = static_cast<struct spa_meta_region *>(
|
||||
spa_buffer_find_meta_data(buf, SPA_META_VideoDamage, sizeof(*damage))
|
||||
);
|
||||
img_descriptor->pw_damage = (damage && damage->region.size.width > 0 && damage->region.size.height > 0) ? std::optional<bool>(true) : std::nullopt;
|
||||
}
|
||||
|
||||
static void fill_img_dmabuf(egl::img_descriptor_t *img_descriptor, struct spa_buffer *buf, const stream_data_t &d) {
|
||||
img_descriptor->sd.width = d.format.info.raw.size.width;
|
||||
img_descriptor->sd.height = d.format.info.raw.size.height;
|
||||
img_descriptor->sd.modifier = d.format.info.raw.modifier;
|
||||
img_descriptor->sd.fourcc = d.drm_format;
|
||||
for (int i = 0; i < MIN(buf->n_datas, 4); i++) {
|
||||
img_descriptor->sd.fds[i] = dup(buf->datas[i].fd);
|
||||
img_descriptor->sd.pitches[i] = buf->datas[i].chunk->stride;
|
||||
img_descriptor->sd.offsets[i] = buf->datas[i].chunk->offset;
|
||||
}
|
||||
}
|
||||
|
||||
void fill_img(platf::img_t *img) {
|
||||
pw_thread_loop_lock(loop);
|
||||
std::scoped_lock lock(stream_data.frame_mutex);
|
||||
|
||||
if (stream_data.shared && stream_data.shared->stream_dead.load()) {
|
||||
img->data = nullptr;
|
||||
close_img_fds(static_cast<egl::img_descriptor_t *>(img));
|
||||
pw_thread_loop_unlock(loop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stream_data.current_buffer) {
|
||||
img->data = nullptr;
|
||||
pw_thread_loop_unlock(loop);
|
||||
return;
|
||||
}
|
||||
|
||||
struct spa_buffer *buf = stream_data.current_buffer->buffer;
|
||||
if (buf->datas[0].chunk->size != 0) {
|
||||
auto *img_descriptor = static_cast<egl::img_descriptor_t *>(img);
|
||||
fill_img_metadata(img_descriptor, buf);
|
||||
if (buf->datas[0].type == SPA_DATA_DmaBuf) {
|
||||
fill_img_dmabuf(img_descriptor, buf, stream_data);
|
||||
} else {
|
||||
img->data = stream_data.front_buffer->data();
|
||||
img->row_pitch = stream_data.local_stride;
|
||||
}
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(loop);
|
||||
}
|
||||
|
||||
private:
|
||||
struct pw_thread_loop *loop;
|
||||
struct pw_context *context;
|
||||
struct pw_core *core;
|
||||
struct spa_hook core_listener;
|
||||
struct stream_data_t stream_data;
|
||||
int fd;
|
||||
int node;
|
||||
|
||||
static struct spa_pod *build_format_parameter(struct spa_pod_builder *b, uint32_t width, uint32_t height, uint32_t refresh_rate, int32_t format, uint64_t *modifiers, int n_modifiers) {
|
||||
struct spa_pod_frame object_frame;
|
||||
struct spa_pod_frame modifier_frame;
|
||||
std::array<struct spa_rectangle, 3> sizes;
|
||||
std::array<struct spa_fraction, 3> framerates;
|
||||
|
||||
sizes[0] = SPA_RECTANGLE(width, height); // Preferred
|
||||
sizes[1] = SPA_RECTANGLE(1, 1);
|
||||
sizes[2] = SPA_RECTANGLE(8192, 4096);
|
||||
|
||||
framerates[0] = SPA_FRACTION(0, 1); // default; we only want variable rate, thus bypassing compositor pacing
|
||||
framerates[1] = SPA_FRACTION(0, 1); // min
|
||||
framerates[2] = SPA_FRACTION(0, 1); // max
|
||||
|
||||
spa_pod_builder_push_object(b, &object_frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat);
|
||||
spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0);
|
||||
spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0);
|
||||
spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0);
|
||||
spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&sizes[0], &sizes[1], &sizes[2]), 0);
|
||||
spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(&framerates[0]), 0);
|
||||
if (!runtime_t::instance().is_maxframerate_failed()) {
|
||||
spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_CHOICE_RANGE_Fraction(&framerates[0], &framerates[1], &framerates[2]), 0);
|
||||
}
|
||||
|
||||
if (n_modifiers) {
|
||||
spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE);
|
||||
spa_pod_builder_push_choice(b, &modifier_frame, SPA_CHOICE_Enum, 0);
|
||||
|
||||
// Preferred value, we pick the first modifier be the preferred one
|
||||
spa_pod_builder_long(b, modifiers[0]);
|
||||
for (uint32_t i = 0; i < n_modifiers; i++) {
|
||||
spa_pod_builder_long(b, modifiers[i]);
|
||||
}
|
||||
|
||||
spa_pod_builder_pop(b, &modifier_frame);
|
||||
}
|
||||
|
||||
return static_cast<struct spa_pod *>(spa_pod_builder_pop(b, &object_frame));
|
||||
}
|
||||
|
||||
static void on_core_info_cb([[maybe_unused]] void *user_data, const struct pw_core_info *pw_info) {
|
||||
BOOST_LOG(info) << "[portalgrab] Connected to pipewire version "sv << pw_info->version;
|
||||
}
|
||||
|
||||
static void on_core_error_cb([[maybe_unused]] void *user_data, const uint32_t id, const int seq, [[maybe_unused]] int res, const char *message) {
|
||||
BOOST_LOG(info) << "[portalgrab] Pipewire Error, id:"sv << id << " seq:"sv << seq << " message: "sv << message;
|
||||
}
|
||||
|
||||
constexpr static const struct pw_core_events core_events = {
|
||||
.version = PW_VERSION_CORE_EVENTS,
|
||||
.info = on_core_info_cb,
|
||||
.error = on_core_error_cb,
|
||||
};
|
||||
|
||||
static void on_stream_state_changed(void *user_data, enum pw_stream_state old, enum pw_stream_state state, const char *err_msg) {
|
||||
BOOST_LOG(debug) << "[portalgrab] PipeWire stream state: " << pw_stream_state_as_string(old)
|
||||
<< " -> " << pw_stream_state_as_string(state);
|
||||
|
||||
auto *d = static_cast<stream_data_t *>(user_data);
|
||||
|
||||
switch (state) {
|
||||
case PW_STREAM_STATE_PAUSED:
|
||||
if (d->shared && old == PW_STREAM_STATE_STREAMING) {
|
||||
{
|
||||
std::scoped_lock lock(d->frame_mutex);
|
||||
d->frame_ready = false;
|
||||
d->current_buffer = nullptr;
|
||||
d->shared->stream_dead.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
d->frame_cv.notify_all();
|
||||
}
|
||||
break;
|
||||
case PW_STREAM_STATE_ERROR:
|
||||
if (old != PW_STREAM_STATE_STREAMING && !runtime_t::instance().is_maxframerate_failed()) {
|
||||
BOOST_LOG(warning) << "[portalgrab] Negotiation failed, will retry without maxFramerate"sv;
|
||||
runtime_t::instance().set_maxframerate_failed();
|
||||
}
|
||||
[[fallthrough]];
|
||||
case PW_STREAM_STATE_UNCONNECTED:
|
||||
if (d->shared) {
|
||||
d->shared->stream_dead.store(true, std::memory_order_relaxed);
|
||||
d->frame_cv.notify_all();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_process(void *user_data) {
|
||||
const auto d = static_cast<struct stream_data_t *>(user_data);
|
||||
struct pw_buffer *b = nullptr;
|
||||
|
||||
// 1. Drain the queue: Always grab the most recent buffer
|
||||
while (struct pw_buffer *aux = pw_stream_dequeue_buffer(d->stream)) {
|
||||
if (b) {
|
||||
pw_stream_queue_buffer(d->stream, b); // Return the older, unused buffer
|
||||
}
|
||||
b = aux;
|
||||
}
|
||||
|
||||
if (!b) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Fast Path: DMA-BUF
|
||||
if (b->buffer->datas[0].type == SPA_DATA_DmaBuf) {
|
||||
std::scoped_lock lock(d->frame_mutex);
|
||||
if (d->current_buffer) {
|
||||
pw_stream_queue_buffer(d->stream, d->current_buffer);
|
||||
}
|
||||
d->current_buffer = b;
|
||||
d->frame_ready = true;
|
||||
}
|
||||
// 3. Optimized Path: Software/MemPtr
|
||||
else if (b->buffer->datas[0].data != nullptr) {
|
||||
size_t size = b->buffer->datas[0].chunk->size;
|
||||
|
||||
// Perform the copy to the BACK buffer while NOT holding the lock
|
||||
if (d->back_buffer->size() < size) {
|
||||
d->back_buffer->resize(size);
|
||||
}
|
||||
std::memcpy(d->back_buffer->data(), b->buffer->datas[0].data, size);
|
||||
|
||||
{
|
||||
// Lock only for the pointer swap and state update
|
||||
std::scoped_lock lock(d->frame_mutex);
|
||||
std::swap(d->front_buffer, d->back_buffer);
|
||||
|
||||
d->local_stride = b->buffer->datas[0].chunk->stride;
|
||||
d->frame_ready = true;
|
||||
d->current_buffer = b;
|
||||
}
|
||||
|
||||
// Release the PW buffer immediately after copy
|
||||
pw_stream_queue_buffer(d->stream, b);
|
||||
}
|
||||
|
||||
d->frame_cv.notify_one();
|
||||
}
|
||||
|
||||
static void on_param_changed(void *user_data, uint32_t id, const struct spa_pod *param) {
|
||||
const auto d = static_cast<struct stream_data_t *>(user_data);
|
||||
|
||||
d->current_buffer = nullptr;
|
||||
|
||||
if (param == nullptr || id != SPA_PARAM_Format) {
|
||||
return;
|
||||
}
|
||||
if (spa_format_parse(param, &d->format.media_type, &d->format.media_subtype) < 0) {
|
||||
return;
|
||||
}
|
||||
if (d->format.media_type != SPA_MEDIA_TYPE_video || d->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) {
|
||||
return;
|
||||
}
|
||||
if (spa_format_video_raw_parse(param, &d->format.info.raw) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOST_LOG(info) << "[portalgrab] Video format: "sv << d->format.info.raw.format;
|
||||
BOOST_LOG(info) << "[portalgrab] Size: "sv << d->format.info.raw.size.width << "x"sv << d->format.info.raw.size.height;
|
||||
if (d->format.info.raw.max_framerate.num == 0 && d->format.info.raw.max_framerate.denom == 1) {
|
||||
BOOST_LOG(info) << "[portalgrab] Framerate (from compositor): 0/1 (variable rate capture)";
|
||||
} else {
|
||||
BOOST_LOG(info) << "[portalgrab] Framerate (from compositor): "sv << d->format.info.raw.framerate.num << "/"sv << d->format.info.raw.framerate.denom;
|
||||
BOOST_LOG(info) << "[portalgrab] Framerate (from compositor, max): "sv << d->format.info.raw.max_framerate.num << "/"sv << d->format.info.raw.max_framerate.denom;
|
||||
}
|
||||
|
||||
int physical_w = d->format.info.raw.size.width;
|
||||
int physical_h = d->format.info.raw.size.height;
|
||||
|
||||
if (d->shared) {
|
||||
int old_w = d->shared->negotiated_width.load(std::memory_order_relaxed);
|
||||
int old_h = d->shared->negotiated_height.load(std::memory_order_relaxed);
|
||||
|
||||
if (physical_w != old_w || physical_h != old_h) {
|
||||
d->shared->negotiated_width.store(physical_w, std::memory_order_relaxed);
|
||||
d->shared->negotiated_height.store(physical_h, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t drm_format = 0;
|
||||
for (const auto &fmt : format_map) {
|
||||
if (fmt.fourcc == 0) {
|
||||
break;
|
||||
}
|
||||
if (fmt.pw_format == d->format.info.raw.format) {
|
||||
drm_format = fmt.fourcc;
|
||||
}
|
||||
}
|
||||
d->drm_format = drm_format;
|
||||
|
||||
uint32_t buffer_types = 0;
|
||||
if (spa_pod_find_prop(param, nullptr, SPA_FORMAT_VIDEO_modifier) != nullptr && d->drm_format) {
|
||||
BOOST_LOG(info) << "[portalgrab] using DMA-BUF buffers"sv;
|
||||
buffer_types |= 1 << SPA_DATA_DmaBuf;
|
||||
} else {
|
||||
BOOST_LOG(info) << "[portalgrab] using memory buffers"sv;
|
||||
buffer_types |= 1 << SPA_DATA_MemPtr;
|
||||
}
|
||||
|
||||
// Ack the buffer type and metadata
|
||||
std::array<uint8_t, SPA_POD_BUFFER_SIZE> buffer;
|
||||
std::array<const struct spa_pod *, 3> params;
|
||||
int n_params = 0;
|
||||
struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
|
||||
auto buffer_param = static_cast<const struct spa_pod *>(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(buffer_types)));
|
||||
params[n_params] = buffer_param;
|
||||
n_params++;
|
||||
auto meta_param = static_cast<const struct spa_pod *>(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header))));
|
||||
params[n_params] = meta_param;
|
||||
n_params++;
|
||||
int videoDamageRegionCount = 16;
|
||||
auto damage_param = static_cast<const struct spa_pod *>(spa_pod_builder_add_object(&pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(sizeof(struct spa_meta_region) * videoDamageRegionCount, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * videoDamageRegionCount)));
|
||||
params[n_params] = damage_param;
|
||||
n_params++;
|
||||
|
||||
pw_stream_update_params(d->stream, params.data(), n_params);
|
||||
}
|
||||
|
||||
constexpr static const struct pw_stream_events stream_events = {
|
||||
.version = PW_VERSION_STREAM_EVENTS,
|
||||
.state_changed = on_stream_state_changed,
|
||||
.param_changed = on_param_changed,
|
||||
.process = on_process,
|
||||
};
|
||||
};
|
||||
|
||||
class portal_t: public platf::display_t {
|
||||
public:
|
||||
int init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
|
||||
// calculate frame interval we should capture at
|
||||
framerate = config.framerate;
|
||||
if (config.framerateX100 > 0) {
|
||||
AVRational fps_strict = ::video::framerateX100_to_rational(config.framerateX100);
|
||||
delay = std::chrono::nanoseconds(
|
||||
(static_cast<int64_t>(fps_strict.den) * 1'000'000'000LL) / fps_strict.num
|
||||
);
|
||||
BOOST_LOG(info) << "[portalgrab] Requested frame rate [" << fps_strict.num << "/" << fps_strict.den << ", approx. " << av_q2d(fps_strict) << " fps]";
|
||||
} else {
|
||||
delay = std::chrono::nanoseconds {1s} / framerate;
|
||||
BOOST_LOG(info) << "[portalgrab] Requested frame rate [" << framerate << "fps]";
|
||||
}
|
||||
mem_type = hwdevice_type;
|
||||
|
||||
if (get_dmabuf_modifiers() < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int configure_stream(const std::string &display_name, int &out_pipewire_fd, int &out_pipewire_node, int &out_pos_x, int &out_pos_y, int &out_width, int &out_height) override {
|
||||
// Connect DBus portal session
|
||||
if (dbus.init() < 0) {
|
||||
BOOST_LOG(error) << "[portalgrab] Failed to connect to dbus. portal_t::init() failed.";
|
||||
BOOST_LOG(error) << "[portalgrab] Failed to connect to dbus. portal_t setup failed.";
|
||||
return -1;
|
||||
}
|
||||
if (dbus.connect_to_portal() < 0) {
|
||||
BOOST_LOG(error) << "[portalgrab] Failed to connect to portal. portal_t::init() failed.";
|
||||
BOOST_LOG(error) << "[portalgrab] Failed to connect to portal. portal_t setup failed.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1313,7 +804,7 @@ namespace portal {
|
||||
pipewire_streaminfo_t stream;
|
||||
auto streams = dbus.pipewire_streams;
|
||||
if (streams.empty()) {
|
||||
BOOST_LOG(error) << "[portalgrab] No streams found on portal. portal_t::init() failed.";
|
||||
BOOST_LOG(error) << "[portalgrab] No streams found on portal. portal_t setup failed.";
|
||||
return -1;
|
||||
}
|
||||
for (const auto &stream_ : streams) {
|
||||
@@ -1328,384 +819,59 @@ namespace portal {
|
||||
BOOST_LOG(info) << "[portalgrab] Using first available stream as no matching stream was found for: '"sv << display_name << "'";
|
||||
stream = dbus.pipewire_streams.at(0);
|
||||
}
|
||||
// Set values inherited from display_t
|
||||
width = stream.width;
|
||||
height = stream.height;
|
||||
BOOST_LOG(info) << "[portalgrab] Streaming display '"sv << stream.monitor_name << "' from position: "sv << stream.pos_x << "x"sv << stream.pos_y << " resolution: "sv << width << "x"sv << height;
|
||||
|
||||
framerate = config.framerate;
|
||||
|
||||
if (!shared_state) {
|
||||
shared_state = std::make_shared<shared_state_t>();
|
||||
} else {
|
||||
shared_state->stream_dead.store(false);
|
||||
shared_state->negotiated_width.store(0);
|
||||
shared_state->negotiated_height.store(0);
|
||||
}
|
||||
if (pipewire.init(dbus.pipewire_fd, stream.pipewire_node, shared_state) < 0) {
|
||||
BOOST_LOG(error) << "[portalgrab] Failed to init pipewire. portal_t::init() failed.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Start PipeWire now so format negotiation can proceed before capture start
|
||||
if (pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia) < 0) {
|
||||
BOOST_LOG(error) << "[portalgrab] Failed to ensure pipewire stream. portal_t::init() failed.";
|
||||
return -1;
|
||||
}
|
||||
|
||||
int timeout_ms = 1500;
|
||||
int negotiated_w = 0;
|
||||
int negotiated_h = 0;
|
||||
|
||||
while (timeout_ms > 0) {
|
||||
negotiated_w = shared_state->negotiated_width.load();
|
||||
negotiated_h = shared_state->negotiated_height.load();
|
||||
if (negotiated_w > 0 && negotiated_h > 0) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
timeout_ms -= 10;
|
||||
}
|
||||
|
||||
// Set width and height to the values negotiated by pipewire
|
||||
if (negotiated_w > 0 && negotiated_h > 0 && (negotiated_w != width || negotiated_h != height)) {
|
||||
BOOST_LOG(info) << "[portalgrab] Using negotiated resolution "sv
|
||||
<< negotiated_w << "x" << negotiated_h;
|
||||
|
||||
width = negotiated_w;
|
||||
height = negotiated_h;
|
||||
}
|
||||
|
||||
// Set env dimensions to match the captured display.
|
||||
// Portal captures a single display, so the environment size equals the capture size.
|
||||
// Without this, touch input is silently dropped because touch_port_t::operator bool()
|
||||
// checks env_width and env_height are non-zero.
|
||||
env_width = width;
|
||||
env_height = height;
|
||||
// Restore global maxframerate negotiation state
|
||||
pipewire.set_negotiate_maxframerate(negotiate_maxframerate.load());
|
||||
|
||||
// Return values
|
||||
out_pipewire_fd = dbus.pipewire_fd;
|
||||
out_pipewire_node = stream.pipewire_node;
|
||||
out_pos_x = stream.pos_x;
|
||||
out_pos_y = stream.pos_y;
|
||||
out_width = stream.width;
|
||||
out_height = stream.height;
|
||||
// Flag successful setup
|
||||
return 0;
|
||||
}
|
||||
|
||||
platf::capture_e snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool show_cursor) {
|
||||
// FIXME: show_cursor is ignored
|
||||
auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
int retries = 0;
|
||||
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (!wait_for_frame(deadline)) {
|
||||
return platf::capture_e::timeout;
|
||||
}
|
||||
|
||||
if (!pull_free_image_cb(img_out)) {
|
||||
return platf::capture_e::interrupted;
|
||||
}
|
||||
|
||||
auto *img_egl = static_cast<egl::img_descriptor_t *>(img_out.get());
|
||||
img_egl->reset();
|
||||
pipewire.fill_img(img_egl);
|
||||
|
||||
// Check if we got valid data (either DMA-BUF fd or memory pointer), then filter duplicates
|
||||
if ((img_egl->sd.fds[0] >= 0 || img_egl->data != nullptr) && !is_buffer_redundant(img_egl)) {
|
||||
// Update frame metadata
|
||||
update_metadata(img_egl, retries);
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
// No valid frame yet, or it was a duplicate
|
||||
retries++;
|
||||
bool check_stream_dead(platf::capture_e &out_status) override {
|
||||
// If the pipewire stream stopped due to closed portal session stop the capture with an error
|
||||
if (dbus.is_session_closed()) {
|
||||
BOOST_LOG(warning) << "[portalgrab] PipeWire stream stopped by closed portal session."sv;
|
||||
pipewire.frame_cv().notify_all();
|
||||
out_status = platf::capture_e::error;
|
||||
return true; // Stop capture with error (due to out_status)
|
||||
}
|
||||
return platf::capture_e::timeout;
|
||||
// Disable maxframerate negotiation if the stream died without having ever started (e.g. GNOME mutter does not support it)
|
||||
if (shared_state->previous_state != PW_STREAM_STATE_STREAMING && negotiate_maxframerate.load()) {
|
||||
BOOST_LOG(warning) << "[portalgrab] Negotiation failed, will retry without maxFramerate"sv;
|
||||
negotiate_maxframerate.store(false);
|
||||
pipewire.set_negotiate_maxframerate(false);
|
||||
out_status = platf::capture_e::reinit;
|
||||
return true; // Stop capture with reinit (due to out_status)
|
||||
}
|
||||
return false; // Return to default stream dead handling
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> alloc_img() override {
|
||||
// Note: this img_t type is also used for memory buffers
|
||||
auto img = std::make_shared<egl::img_descriptor_t>();
|
||||
|
||||
img->width = width;
|
||||
img->height = height;
|
||||
img->pixel_pitch = 4;
|
||||
img->row_pitch = img->pixel_pitch * width;
|
||||
img->sequence = 0;
|
||||
img->serial = std::numeric_limits<decltype(img->serial)>::max();
|
||||
img->data = nullptr;
|
||||
std::fill_n(img->sd.fds, 4, -1);
|
||||
|
||||
return img;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (pipewire.ensure_stream(mem_type, width, height, framerate, dmabuf_infos.data(), n_dmabuf_infos, display_is_nvidia) < 0) {
|
||||
BOOST_LOG(error) << "[portalgrab] Failed to ensure pipewire stream. capture() failed with error.";
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
sleep_overshoot_logger.reset();
|
||||
|
||||
while (true) {
|
||||
// Check if PipeWire signaled a dead stream
|
||||
if (shared_state->stream_dead.exchange(false)) {
|
||||
// If the pipewire stream stopped due to closed portal session stop the capture with an error
|
||||
if (dbus.is_session_closed()) {
|
||||
BOOST_LOG(warning) << "[portalgrab] PipeWire stream stopped by closed portal session."sv;
|
||||
pipewire.frame_cv().notify_all();
|
||||
return platf::capture_e::error;
|
||||
}
|
||||
// Re-init the capture if the stream is dead for any other reason
|
||||
BOOST_LOG(warning) << "[portalgrab] PipeWire stream disconnected. Forcing session reset."sv;
|
||||
return platf::capture_e::reinit;
|
||||
}
|
||||
|
||||
// Advance to (or catch up with) next delay interval
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
while (next_frame < now) {
|
||||
next_frame += delay;
|
||||
}
|
||||
|
||||
if (next_frame > now) {
|
||||
std::this_thread::sleep_until(next_frame);
|
||||
sleep_overshoot_logger.first_point(next_frame);
|
||||
sleep_overshoot_logger.second_point_now_and_log();
|
||||
}
|
||||
|
||||
std::shared_ptr<platf::img_t> img_out;
|
||||
switch (const auto status = snapshot(pull_free_image_cb, img_out, 1000ms, *cursor)) {
|
||||
case platf::capture_e::reinit:
|
||||
case platf::capture_e::error:
|
||||
case platf::capture_e::interrupted:
|
||||
pipewire.frame_cv().notify_all();
|
||||
return status;
|
||||
case platf::capture_e::timeout:
|
||||
if (!pull_free_image_cb(img_out)) {
|
||||
// Detect if shutdown is pending
|
||||
BOOST_LOG(debug) << "[portalgrab] PipeWire: timeout -> interrupt nudge";
|
||||
pipewire.frame_cv().notify_all();
|
||||
return platf::capture_e::interrupted;
|
||||
}
|
||||
if (!push_captured_image_cb(std::move(img_out), false)) {
|
||||
BOOST_LOG(debug) << "[portalgrab] PipeWire: !push_captured_image_cb -> ok";
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
break;
|
||||
case platf::capture_e::ok:
|
||||
if (!push_captured_image_cb(std::move(img_out), true)) {
|
||||
BOOST_LOG(debug) << "[portalgrab] PipeWire: !push_captured_image_cb -> ok";
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
BOOST_LOG(error) << "[portalgrab] Unrecognized capture status ["sv << std::to_underlying(status) << ']';
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
return platf::capture_e::ok;
|
||||
}
|
||||
|
||||
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, n_dmabuf_infos > 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SUNSHINE_BUILD_VULKAN
|
||||
if (mem_type == platf::mem_type_e::vulkan && n_dmabuf_infos > 0) {
|
||||
return vk::make_avcodec_encode_device_vram(width, height, 0, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SUNSHINE_BUILD_CUDA
|
||||
if (mem_type == platf::mem_type_e::cuda) {
|
||||
if (display_is_nvidia && n_dmabuf_infos > 0) {
|
||||
// Display GPU is NVIDIA - can use DMA-BUF directly
|
||||
return cuda::make_avcodec_gl_encode_device(width, height, 0, 0);
|
||||
} else {
|
||||
// Hybrid system (Intel display + NVIDIA encode) - use memory buffer path
|
||||
// DMA-BUFs from Intel GPU cannot be imported into CUDA
|
||||
return cuda::make_avcodec_encode_device(width, height, false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return std::make_unique<platf::avcodec_encode_device_t>();
|
||||
}
|
||||
|
||||
int dummy_img(platf::img_t *img) override {
|
||||
if (!img) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
img->data = new std::uint8_t[img->height * img->row_pitch];
|
||||
std::fill_n(img->data, img->height * img->row_pitch, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// This capture method is event driven; don't insert duplicate frames
|
||||
bool is_event_driven() override {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool is_buffer_redundant(const egl::img_descriptor_t *img) {
|
||||
// Check for corrupted frame
|
||||
if (img->pw_flags.has_value() && (img->pw_flags.value() & SPA_CHUNK_FLAG_CORRUPTED)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If PTS is identical, only drop if damage metadata confirms no change
|
||||
if (img->pts.has_value() && last_pts.has_value() && img->pts.value() == last_pts.value()) {
|
||||
return img->pw_damage.has_value() && !img->pw_damage.value();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void update_metadata(egl::img_descriptor_t *img, int retries) {
|
||||
last_seq = img->seq;
|
||||
last_pts = img->pts;
|
||||
img->sequence = ++sequence;
|
||||
|
||||
if (retries > 0) {
|
||||
BOOST_LOG(debug) << "[portalgrab] Processed frame after " << retries << " redundant events."sv;
|
||||
}
|
||||
}
|
||||
|
||||
bool wait_for_frame(std::chrono::steady_clock::time_point deadline) {
|
||||
std::unique_lock<std::mutex> lock(pipewire.frame_mutex());
|
||||
|
||||
bool success = pipewire.frame_cv().wait_until(lock, deadline, [&] {
|
||||
return pipewire.is_frame_ready() || shared_state->stream_dead.load();
|
||||
});
|
||||
|
||||
if (success) {
|
||||
pipewire.set_frame_ready(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static uint32_t lookup_pw_format(uint64_t fourcc) {
|
||||
for (const auto &fmt : format_map) {
|
||||
if (fmt.fourcc == 0) {
|
||||
break;
|
||||
}
|
||||
if (fmt.fourcc == fourcc) {
|
||||
return fmt.pw_format;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void query_dmabuf_formats(EGLDisplay egl_display) {
|
||||
EGLint num_dmabuf_formats = 0;
|
||||
std::array<EGLint, MAX_DMABUF_FORMATS> dmabuf_formats = {0};
|
||||
eglQueryDmaBufFormatsEXT(egl_display, MAX_DMABUF_FORMATS, dmabuf_formats.data(), &num_dmabuf_formats);
|
||||
|
||||
if (num_dmabuf_formats > MAX_DMABUF_FORMATS) {
|
||||
BOOST_LOG(warning) << "[portalgrab] Some DMA-BUF formats are being ignored"sv;
|
||||
}
|
||||
|
||||
for (EGLint i = 0; i < MIN(num_dmabuf_formats, MAX_DMABUF_FORMATS); i++) {
|
||||
uint32_t pw_format = lookup_pw_format(dmabuf_formats[i]);
|
||||
if (pw_format == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EGLint num_modifiers = 0;
|
||||
std::array<EGLuint64KHR, MAX_DMABUF_MODIFIERS> mods = {0};
|
||||
eglQueryDmaBufModifiersEXT(egl_display, dmabuf_formats[i], MAX_DMABUF_MODIFIERS, mods.data(), nullptr, &num_modifiers);
|
||||
|
||||
if (num_modifiers > MAX_DMABUF_MODIFIERS) {
|
||||
BOOST_LOG(warning) << "[portalgrab] Some DMA-BUF modifiers are being ignored"sv;
|
||||
}
|
||||
|
||||
dmabuf_infos[n_dmabuf_infos].format = pw_format;
|
||||
dmabuf_infos[n_dmabuf_infos].n_modifiers = MIN(num_modifiers, MAX_DMABUF_MODIFIERS);
|
||||
dmabuf_infos[n_dmabuf_infos].modifiers =
|
||||
static_cast<uint64_t *>(g_memdup2(mods.data(), sizeof(uint64_t) * dmabuf_infos[n_dmabuf_infos].n_modifiers));
|
||||
++n_dmabuf_infos;
|
||||
}
|
||||
}
|
||||
|
||||
int get_dmabuf_modifiers() {
|
||||
if (wl_display.init() < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto egl_display = egl::make_display(wl_display.get());
|
||||
if (!egl_display) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Detect if this is a pure NVIDIA system (not hybrid Intel+NVIDIA)
|
||||
// On hybrid systems, the wayland compositor typically runs on Intel,
|
||||
// so DMA-BUFs from portal will come from Intel and cannot be imported into CUDA.
|
||||
// Check if Intel GPU exists - if so, assume hybrid system and disable CUDA DMA-BUF.
|
||||
bool has_intel_gpu = std::ifstream("/sys/class/drm/card0/device/vendor").good() ||
|
||||
std::ifstream("/sys/class/drm/card1/device/vendor").good();
|
||||
if (has_intel_gpu) {
|
||||
// Read vendor IDs to check for Intel (0x8086)
|
||||
auto check_intel = [](const std::string &path) {
|
||||
if (std::ifstream f(path); f.good()) {
|
||||
std::string vendor;
|
||||
f >> vendor;
|
||||
return vendor == "0x8086";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
bool intel_present = check_intel("/sys/class/drm/card0/device/vendor") ||
|
||||
check_intel("/sys/class/drm/card1/device/vendor");
|
||||
if (intel_present) {
|
||||
BOOST_LOG(info) << "[portalgrab] Hybrid GPU system detected (Intel + discrete) - CUDA will use memory buffers"sv;
|
||||
display_is_nvidia = false;
|
||||
} else {
|
||||
// No Intel GPU found, check if NVIDIA is present
|
||||
const char *vendor = eglQueryString(egl_display.get(), EGL_VENDOR);
|
||||
if (vendor && std::string_view(vendor).contains("NVIDIA")) {
|
||||
BOOST_LOG(info) << "[portalgrab] Pure NVIDIA system - DMA-BUF will be enabled for CUDA"sv;
|
||||
display_is_nvidia = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eglQueryDmaBufFormatsEXT && eglQueryDmaBufModifiersEXT) {
|
||||
query_dmabuf_formats(egl_display.get());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
platf::mem_type_e mem_type;
|
||||
wl::display_t wl_display;
|
||||
// DBus portal connection
|
||||
dbus_t dbus;
|
||||
pipewire_t pipewire;
|
||||
std::array<struct dmabuf_format_info_t, MAX_DMABUF_FORMATS> dmabuf_infos;
|
||||
int n_dmabuf_infos;
|
||||
bool display_is_nvidia = false; // Track if display GPU is NVIDIA
|
||||
std::chrono::nanoseconds delay;
|
||||
std::optional<std::uint64_t> last_pts {};
|
||||
std::optional<std::uint64_t> last_seq {};
|
||||
std::uint64_t sequence {};
|
||||
uint32_t framerate;
|
||||
std::shared_ptr<shared_state_t> shared_state;
|
||||
|
||||
// Class variable to store runtime state of maxFramerate negotiation
|
||||
static inline std::atomic<bool> negotiate_maxframerate {true};
|
||||
};
|
||||
} // namespace portal
|
||||
|
||||
namespace platf {
|
||||
std::shared_ptr<display_t> portal_display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) {
|
||||
using enum platf::mem_type_e;
|
||||
if (hwdevice_type != system && hwdevice_type != vaapi && hwdevice_type != cuda && hwdevice_type != vulkan) {
|
||||
BOOST_LOG(error) << "[portalgrab] Could not initialize display with the given hw device type."sv;
|
||||
if (!pipewire::pipewire_display_t::init_pipewire_and_check_hwdevice_type(hwdevice_type)) {
|
||||
BOOST_LOG(error) << "[portalgrab] Could not initialize pipewire-based display with the given hw device type."sv;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Drop CAP_SYS_ADMIN and set DUMPABLE flag to allow XDG /root access
|
||||
portal::runtime_t::instance().finalize_portal_security();
|
||||
|
||||
// Ensure pipewire is initialized and modules are loaded
|
||||
pw_init(nullptr, nullptr);
|
||||
|
||||
auto portal = std::make_shared<portal::portal_t>();
|
||||
if (portal->init(hwdevice_type, display_name, config)) {
|
||||
return nullptr;
|
||||
|
||||
Reference in New Issue
Block a user