Skip to content

Instantly share code, notes, and snippets.

@luxus
Created July 11, 2026 19:28
Show Gist options
  • Select an option

  • Save luxus/e2cf68243c3f23b9934cea8ade4339bb to your computer and use it in GitHub Desktop.

Select an option

Save luxus/e2cf68243c3f23b9934cea8ade4339bb to your computer and use it in GitHub Desktop.
polaris-stream portal HDR/DmaBuf patches for gamescope (luxusAi): portal capture, EGL TexStorageEXT, CUDA GL import
From: luxusAi
Subject: cuda: GL encode path — EGL import + mmap fallback, renderD prefer
Drop CUDA extmem (INVALID_VALUE on gamescope). Prefer render node.
EGL import with fd cache; mmap fallback is logged visibly.
--- a/src/platform/linux/cuda.cpp
+++ b/src/platform/linux/cuda.cpp
@@ -3,9 +3,12 @@
* @brief Definitions for CUDA encoding.
*/
// standard includes
+#include <algorithm>
#include <array>
#include <bitset>
#include <fcntl.h>
+#include <sys/mman.h>
+#include <unistd.h>
#include <filesystem>
#include <thread>
@@ -38,6 +41,11 @@
#define CU_CHECK_IGNORE(x, y) \
check((x), POLARIS_STRINGVIEW(y ": "))
+// CUDA 11.2+; not always present in bundled nv-codec-headers.
+#ifndef CU_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_FD
+ #define CU_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_FD ((CUexternalMemoryHandleType) 7)
+#endif
+
namespace fs = std::filesystem;
using namespace std::literals;
@@ -249,23 +257,40 @@
return std::tolower(c);
});
- // Look for the name of the primary node in sysfs
+ // Prefer render node for GBM/EGL import of client DMA-BUFs (gamescope/Vulkan
+ // export via renderD*). Primary card node is fallback.
try {
char sysfs_path[PATH_MAX];
std::snprintf(sysfs_path, sizeof(sysfs_path), "/sys/bus/pci/devices/%s/drm", pci_bus_id.data());
fs::path sysfs_dir {sysfs_path};
+ fs::path dri_path {"/dev/dri"sv};
+ std::string render_name;
+ std::string card_name;
for (auto &entry : fs::directory_iterator {sysfs_dir}) {
- auto file = entry.path().filename();
- auto filestring = file.generic_string();
- if (std::string_view {filestring}.substr(0, 4) != "card"sv) {
- continue;
+ auto filestring = entry.path().filename().generic_string();
+ if (filestring.rfind("renderD", 0) == 0) {
+ render_name = filestring;
}
-
- BOOST_LOG(debug) << "Found DRM primary node: "sv << filestring;
-
- fs::path dri_path {"/dev/dri"sv};
- auto device_path = dri_path / file;
- return open(device_path.c_str(), O_RDWR);
+ else if (filestring.rfind("card", 0) == 0 && filestring.find("control") == std::string::npos) {
+ card_name = filestring;
+ }
+ }
+ auto try_open = [&](const std::string &name, const char *kind) -> file_t {
+ if (name.empty()) {
+ return -1;
+ }
+ auto device_path = dri_path / name;
+ int fd = open(device_path.c_str(), O_RDWR | O_CLOEXEC);
+ if (fd >= 0) {
+ BOOST_LOG(info) << "CUDA/EGL DRM "sv << kind << " node: "sv << device_path.string();
+ }
+ return fd;
+ };
+ if (auto fd = try_open(render_name, "render"); fd.el >= 0) {
+ return fd;
+ }
+ if (auto fd = try_open(card_name, "primary"); fd.el >= 0) {
+ return fd;
}
} catch (const std::filesystem::filesystem_error &err) {
BOOST_LOG(error) << "Failed to read sysfs: "sv << err.what();
@@ -382,14 +407,15 @@
this->sws = std::move(*sws_opt);
this->nv12 = std::move(*nv12_opt);
- auto cuda_ctx = (AVCUDADeviceContext *) hw_frames_ctx->device_ctx->hwctx;
+ auto cuda_dev_ctx = (AVCUDADeviceContext *) hw_frames_ctx->device_ctx->hwctx;
+ cu_context = cuda_dev_ctx->cuda_ctx;
stream = make_stream();
if (!stream) {
return -1;
}
- cuda_ctx->stream = stream.get();
+ cuda_dev_ctx->stream = stream.get();
CU_CHECK(cdf->cuGraphicsGLRegisterImage(&y_res, nv12->tex[0], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register Y plane texture");
CU_CHECK(cdf->cuGraphicsGLRegisterImage(&uv_res, nv12->tex[1], GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY), "Couldn't register UV plane texture");
@@ -407,7 +433,6 @@
egl::rgb_t *active_rgb = nullptr;
if (descriptor.sequence == 0) {
- // For dummy images, use a blank RGB texture instead of importing a DMA-BUF
blank_rgb = egl::create_blank(img);
active_rgb = &blank_rgb;
active_buffer_key = 0;
@@ -416,7 +441,7 @@
if (descriptor.dmabuf_buffer_key != 0) {
for (auto &entry : rgb_cache) {
- if (entry.buffer_key == descriptor.dmabuf_buffer_key && entry.rgb.el.xrgb8 != EGL_NO_IMAGE) {
+ if (entry.buffer_key == descriptor.dmabuf_buffer_key && entry.rgb->tex[0] != 0) {
active_rgb = &entry.rgb;
break;
}
@@ -424,16 +449,66 @@
}
if (!active_rgb) {
- auto rgb_opt = egl::import_source(display.get(), descriptor.sd);
- if (!rgb_opt) {
- return -1;
+ const auto &sd = descriptor.sd;
+ bool filled = false;
+
+ // EGL TexStorageEXT import (zero-copy). gamescope dmabufs reject CUDA extmem.
+ if (dmabuf_path != dmabuf_path_e::mmap_cpu && !egl_import_disabled) {
+ auto rgb_opt = egl::import_source(display.get(), sd);
+ if (rgb_opt) {
+ if (dmabuf_path != dmabuf_path_e::egl_ok) {
+ BOOST_LOG(info) << "CUDA DMABUF: EGL import OK"sv;
+ }
+ dmabuf_path = dmabuf_path_e::egl_ok;
+ auto &entry = rgb_cache[rgb_cache_slot];
+ entry.buffer_key = descriptor.dmabuf_buffer_key;
+ entry.rgb = std::move(*rgb_opt);
+ active_rgb = &entry.rgb;
+ rgb_cache_slot = (rgb_cache_slot + 1) % rgb_cache.size();
+ filled = true;
+ } else {
+ egl_import_disabled = true;
+ BOOST_LOG(warning) << "CUDA DMABUF: EGL import failed; mmap fallback for session"sv;
+ }
}
- auto &entry = rgb_cache[rgb_cache_slot];
- entry.buffer_key = descriptor.dmabuf_buffer_key;
- entry.rgb = std::move(*rgb_opt);
- active_rgb = &entry.rgb;
- rgb_cache_slot = (rgb_cache_slot + 1) % rgb_cache.size();
+ // Fallback: mmap → TexSubImage (CPU). Visible when zero-copy unsupported.
+ if (!filled &&
+ sd.fds[0] >= 0 && sd.width > 0 && sd.height > 0 &&
+ sd.pitches[0] >= (std::uint32_t) sd.width * 4) {
+ const size_t map_size = static_cast<size_t>(sd.offsets[0]) +
+ static_cast<size_t>(sd.pitches[0]) * static_cast<size_t>(sd.height);
+ void *map = mmap(nullptr, map_size, PROT_READ, MAP_SHARED, sd.fds[0], 0);
+ if (map != MAP_FAILED) {
+ auto rgb = egl::create_blank(img);
+ gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
+ gl::ctx.PixelStorei(GL_UNPACK_ROW_LENGTH, sd.pitches[0] / 4);
+ gl::ctx.TexSubImage2D(
+ GL_TEXTURE_2D, 0, 0, 0, sd.width, sd.height,
+ GL_BGRA, GL_UNSIGNED_BYTE,
+ static_cast<uint8_t *>(map) + sd.offsets[0]
+ );
+ gl::ctx.PixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+ gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
+ munmap(map, map_size);
+ if (dmabuf_path != dmabuf_path_e::mmap_cpu) {
+ BOOST_LOG(warning) << "CUDA DMABUF: CPU mmap→GL fallback "sv
+ << sd.width << "x"sv << sd.height
+ << " pitch="sv << sd.pitches[0];
+ dmabuf_path = dmabuf_path_e::mmap_cpu;
+ }
+ auto &entry = rgb_cache[rgb_cache_slot];
+ entry.buffer_key = descriptor.dmabuf_buffer_key;
+ entry.rgb = std::move(rgb);
+ active_rgb = &entry.rgb;
+ rgb_cache_slot = (rgb_cache_slot + 1) % rgb_cache.size();
+ filled = true;
+ }
+ }
+
+ if (!filled) {
+ return -1;
+ }
}
active_buffer_key = descriptor.dmabuf_buffer_key;
@@ -443,7 +518,7 @@
active_rgb = &blank_rgb;
} else {
for (auto &entry : rgb_cache) {
- if (entry.buffer_key == active_buffer_key && entry.rgb.el.xrgb8 != EGL_NO_IMAGE) {
+ if (entry.buffer_key == active_buffer_key && entry.rgb->tex[0] != 0) {
active_rgb = &entry.rgb;
break;
}
@@ -462,17 +537,14 @@
return -1;
}
- // Perform the color conversion and scaling in GL
sws.load_vram(descriptor, offset_x, offset_y, (*active_rgb)->tex[0]);
sws.convert(nv12->buf);
auto fmt_desc = av_pix_fmt_desc_get(sw_format);
- // Map the GL textures to read for CUDA
CUgraphicsResource resources[2] = {y_res.get(), uv_res.get()};
CU_CHECK(cdf->cuGraphicsMapResources(2, resources, stream.get()), "Couldn't map GL textures in CUDA");
- // Copy from the GL textures to the target CUDA frame
for (int i = 0; i < 2; i++) {
CUDA_MEMCPY2D cpy = {};
cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY;
@@ -487,7 +559,6 @@
CU_CHECK_IGNORE(cdf->cuMemcpy2DAsync(&cpy, stream.get()), "Couldn't copy texture to CUDA frame");
}
- // Unmap the textures to allow modification from GL again
CU_CHECK(cdf->cuGraphicsUnmapResources(2, resources, stream.get()), "Couldn't unmap GL textures from CUDA");
return 0;
}
@@ -520,6 +591,11 @@
std::size_t rgb_cache_slot {0};
std::uint64_t active_buffer_key {0};
+ enum class dmabuf_path_e { unknown, egl_ok, mmap_cpu };
+ dmabuf_path_e dmabuf_path {dmabuf_path_e::unknown};
+ bool egl_import_disabled {false};
+ CUcontext cu_context {nullptr};
+
registered_resource_t y_res;
registered_resource_t uv_res;
From: luxusAi
Subject: egl: bare-tex + TexStorageEXT for NVIDIA gamescope dmabuf
Import path that works on NVIDIA+gamescope. TexStorageEXT only; LINEAR retry.
--- a/src/platform/linux/graphics.cpp
+++ b/src/platform/linux/graphics.cpp
@@ -27,6 +27,8 @@
#define fourcc_code(a, b, c, d) ((std::uint32_t) (a) | ((std::uint32_t) (b) << 8) | ((std::uint32_t) (c) << 16) | ((std::uint32_t) (d) << 24))
#define fourcc_mod_code(vendor, val) ((((uint64_t) vendor) << 56) | ((val) & 0x00ffffffffffffffULL))
#define DRM_FORMAT_MOD_INVALID fourcc_mod_code(0, ((1ULL << 56) - 1))
+#define DRM_FORMAT_MOD_LINEAR 0ULL
+#define DRM_FORMAT_XRGB8888 fourcc_code('X', 'R', '2', '4')
#if !defined(POLARIS_SHADERS_DIR) // for testing this needs to be defined in cmake as we don't do an install
#define POLARIS_SHADERS_DIR POLARIS_ASSETS_DIR "/shaders/opengl"
@@ -570,33 +572,119 @@
return attribs;
}
- std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
+ // Bare texture (no TexParameteri first) — NVIDIA returns GL 0x502 if the
+ // texture already has sampler state when targeting a DMA-BUF EGLImage.
+ static gl::tex_t make_bare_tex() {
+ gl::tex_t textures {1};
+ gl::ctx.GenTextures(textures.size(), textures.begin());
+ return textures;
+ }
+
+ static void finish_tex_params(GLuint tex) {
+ gl::ctx.BindTexture(GL_TEXTURE_2D, tex);
+ gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ gl::ctx.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
+ }
+
+ static bool gl_clear_errors() {
+ bool bad = false;
+ GLenum err;
+ while ((err = gl::ctx.GetError()) != GL_NO_ERROR) {
+ bad = true;
+ }
+ return !bad;
+ }
+
+ std::optional<rgb_t> import_source_once(display_t::pointer egl_display, const surface_descriptor_t &xrgb, const char *tag) {
auto attribs = surface_descriptor_to_egl_attribs(xrgb);
+#ifndef EGL_IMAGE_PRESERVED_KHR
+ #define EGL_IMAGE_PRESERVED_KHR 0x30D2
+#endif
+ if (!attribs.empty() && attribs.back() == EGL_NONE) {
+ attribs.pop_back();
+ attribs.emplace_back(EGL_IMAGE_PRESERVED_KHR);
+ attribs.emplace_back(EGL_TRUE);
+ attribs.emplace_back(EGL_NONE);
+ }
+
+ auto image = eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data());
+ if (!image) {
+ BOOST_LOG(warning) << "EGL import "sv << tag << " eglCreateImage failed egl="sv
+ << util::hex(eglGetError()).to_string_view()
+ << " "sv << xrgb.width << "x"sv << xrgb.height
+ << " fourcc=0x"sv << util::hex(xrgb.fourcc).to_string_view()
+ << " modifier=0x"sv << util::hex(xrgb.modifier).to_string_view()
+ << " pitch="sv << xrgb.pitches[0];
+ return std::nullopt;
+ }
rgb_t rgb {
egl_display,
- eglCreateImage(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()),
- gl::tex_t::make(1)
+ image,
+ make_bare_tex()
};
- if (!rgb->xrgb8) {
- BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view();
+ gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
+ gl_clear_errors();
- return std::nullopt;
+ using TexStorageFn = void (*)(GLenum, void *, const int *);
+ static TexStorageFn tex_storage = nullptr;
+ static bool tex_storage_resolved = false;
+ if (!tex_storage_resolved) {
+ tex_storage = (TexStorageFn) eglGetProcAddress("glEGLImageTargetTexStorageEXT");
+ tex_storage_resolved = true;
}
- gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
- gl::ctx.EGLImageTargetTexture2DOES(GL_TEXTURE_2D, rgb->xrgb8);
-
+ if (!tex_storage) {
+ BOOST_LOG(warning) << "EGL import "sv << tag << " glEGLImageTargetTexStorageEXT missing"sv;
+ return std::nullopt;
+ }
+ tex_storage(GL_TEXTURE_2D, rgb->xrgb8, nullptr);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
-
- if (gl_drain_errors) {
+ if (!gl_clear_errors()) {
+ BOOST_LOG(warning) << "EGL import "sv << tag << " TexStorageEXT failed "sv
+ << xrgb.width << "x"sv << xrgb.height
+ << " fourcc=0x"sv << util::hex(xrgb.fourcc).to_string_view()
+ << " pitch="sv << xrgb.pitches[0];
return std::nullopt;
}
+ finish_tex_params(rgb->tex[0]);
+ static bool logged_ok = false;
+ if (!logged_ok) {
+ BOOST_LOG(info) << "EGL import "sv << tag << " OK via TexStorageEXT "sv
+ << xrgb.width << "x"sv << xrgb.height
+ << " fourcc=0x"sv << util::hex(xrgb.fourcc).to_string_view()
+ << " pitch="sv << xrgb.pitches[0]
+ << " (once)"sv;
+ logged_ok = true;
+ }
return rgb;
}
+ std::optional<rgb_t> import_source(display_t::pointer egl_display, const surface_descriptor_t &xrgb) {
+ // NVIDIA+gamescope: bare tex + TexStorageEXT. LINEAR if as-is fails.
+ if (auto rgb = import_source_once(egl_display, xrgb, "as-is")) {
+ return rgb;
+ }
+ if (xrgb.modifier != DRM_FORMAT_MOD_LINEAR) {
+ surface_descriptor_t lin = xrgb;
+ lin.modifier = DRM_FORMAT_MOD_LINEAR;
+ if (auto rgb = import_source_once(egl_display, lin, "LINEAR")) {
+ return rgb;
+ }
+ }
+ BOOST_LOG(error) << "Couldn't import RGB DMA-BUF "sv
+ << xrgb.width << "x"sv << xrgb.height
+ << " fourcc=0x"sv << util::hex(xrgb.fourcc).to_string_view()
+ << " modifier=0x"sv << util::hex(xrgb.modifier).to_string_view()
+ << " pitch="sv << xrgb.pitches[0];
+ return std::nullopt;
+ }
+
/**
* @brief Create a black RGB texture of the specified image size.
* @param img The image to use for texture sizing.
From: luxusAi
Subject: portal: DmaBuf GPU capture, 10-bit prefer, no forced CPU mmap
PipeWire portal: prefer DmaBuf, xBGR_210LE first, LINEAR modifier,
skip 4K mmap on GPU path, fd-keyed buffer cache, first-frame diagnostics.
--- a/src/platform/linux/portal_grab.cpp
+++ b/src/platform/linux/portal_grab.cpp
@@ -7,7 +7,10 @@
* for the Polaris cage-as-window architecture.
*/
+#include <algorithm>
#include <atomic>
+#include <climits>
+#include <limits>
#include <chrono>
#include <cstdlib>
#include <cstring>
@@ -19,8 +22,11 @@
#include <thread>
#include <vector>
+#include <drm_fourcc.h>
+#include <fcntl.h>
#include <poll.h>
#include <sys/mman.h>
+#include <unistd.h>
#include <gio/gio.h>
#include <wayland-client.h>
@@ -31,14 +37,18 @@
#include <spa/param/video/type-info.h>
#include <spa/debug/types.h>
#include <spa/param/video/format.h>
+#include <spa/pod/builder.h>
#include <spa/utils/result.h>
#include "src/config.h"
#include "src/logging.h"
+#include "src/utility.h"
#include "src/platform/common.h"
+#include "src/stream_stats.h"
#include "src/video.h"
#include "src/platform/linux/cage_display_router.h"
+#include "src/platform/linux/graphics.h"
#ifdef POLARIS_BUILD_CUDA
#include "src/platform/linux/cuda.h"
@@ -431,8 +441,69 @@
// -----------------------------------------------------------------------
// PipeWire frame capture
+ //
+ // Prefer DMA-BUF (GPU-resident) when the portal/compositor exports it so the
+ // CUDA/VAAPI path can import via EGL without a system-memory copy. Fall back
+ // to MemPtr/SHM when DMA-BUF is unavailable (same behaviour as before).
// -----------------------------------------------------------------------
+ static uint32_t spa_format_to_drm_fourcc(uint32_t spa_fmt) {
+ switch (spa_fmt) {
+ case SPA_VIDEO_FORMAT_BGRx:
+ return DRM_FORMAT_XRGB8888;
+ case SPA_VIDEO_FORMAT_BGRA:
+ return DRM_FORMAT_ARGB8888;
+ case SPA_VIDEO_FORMAT_RGBx:
+ return DRM_FORMAT_XBGR8888;
+ case SPA_VIDEO_FORMAT_RGBA:
+ return DRM_FORMAT_ABGR8888;
+ case SPA_VIDEO_FORMAT_xBGR_210LE:
+ return DRM_FORMAT_XBGR2101010;
+ default:
+ return 0;
+ }
+ }
+
+ static platf::frame_format_e spa_format_to_frame_format(uint32_t spa_fmt) {
+ switch (spa_fmt) {
+ case SPA_VIDEO_FORMAT_BGRx:
+ case SPA_VIDEO_FORMAT_BGRA:
+ case SPA_VIDEO_FORMAT_RGBx:
+ case SPA_VIDEO_FORMAT_RGBA:
+ return platf::frame_format_e::bgra8;
+ case SPA_VIDEO_FORMAT_xBGR_210LE:
+ // 10-bit packed RGB (HDR path). Stats as p010-class source.
+ return platf::frame_format_e::p010;
+ default:
+ return platf::frame_format_e::unknown;
+ }
+ }
+
+ struct pw_frame_t {
+ bool is_dmabuf = false;
+ int width = 0;
+ int height = 0;
+ int stride = 0;
+ std::vector<uint8_t> cpu_data;
+ egl::surface_descriptor_t sd {};
+ std::uint64_t buffer_key = 0;
+ uint32_t spa_format = SPA_VIDEO_FORMAT_UNKNOWN;
+
+ void release() {
+ if (is_dmabuf) {
+ for (int i = 0; i < 4; ++i) {
+ if (sd.fds[i] >= 0) {
+ close(sd.fds[i]);
+ sd.fds[i] = -1;
+ }
+ }
+ }
+ cpu_data.clear();
+ is_dmabuf = false;
+ buffer_key = 0;
+ }
+ };
+
struct pw_capture_t {
struct pw_thread_loop *pw_loop = nullptr;
struct pw_stream *pw_stream_handle = nullptr;
@@ -440,38 +511,165 @@
int frame_width = 0;
int frame_height = 0;
uint32_t frame_stride = 0;
+ uint32_t spa_format = SPA_VIDEO_FORMAT_UNKNOWN;
+ uint64_t spa_modifier = DRM_FORMAT_MOD_INVALID;
+ bool spa_has_modifier = false;
std::mutex frame_mtx;
- std::vector<uint8_t> frame_data;
+ pw_frame_t latest;
bool frame_available = false;
std::condition_variable frame_cv;
+ std::uint64_t frame_seq = 0;
+ std::atomic<bool> saw_dmabuf {false};
+ std::atomic<bool> saw_memptr {false};
- std::atomic<bool> running{false};
- std::atomic<bool> negotiated{false};
+ // Legacy CPU buffer accessors used by MemPtr path helpers
+ std::vector<uint8_t> frame_data;
+
+ std::atomic<bool> running {false};
+ std::atomic<bool> negotiated {false};
~pw_capture_t() {
- if (pw_loop) pw_thread_loop_stop(pw_loop);
- if (pw_stream_handle) pw_stream_destroy(pw_stream_handle);
- if (pw_loop) pw_thread_loop_destroy(pw_loop);
+ // Stop the loop first so process/param callbacks cannot run during teardown.
+ running = false;
+ if (pw_loop) {
+ pw_thread_loop_stop(pw_loop);
+ }
+ if (pw_stream_handle) {
+ pw_stream_destroy(pw_stream_handle);
+ pw_stream_handle = nullptr;
+ }
+ {
+ std::lock_guard lk(frame_mtx);
+ latest.release();
+ }
+ if (pw_loop) {
+ pw_thread_loop_destroy(pw_loop);
+ pw_loop = nullptr;
+ }
}
};
static void on_process(void *userdata) {
auto *cap = static_cast<pw_capture_t *>(userdata);
struct pw_buffer *b = pw_stream_dequeue_buffer(cap->pw_stream_handle);
- if (!b) return;
+ if (!b || !b->buffer || b->buffer->n_datas < 1) {
+ if (b) {
+ pw_stream_queue_buffer(cap->pw_stream_handle, b);
+ }
+ return;
+ }
struct spa_buffer *buf = b->buffer;
- if (!buf->datas[0].data || buf->datas[0].chunk->size == 0) {
+ struct spa_data *d0 = &buf->datas[0];
+ if (!d0->chunk || d0->chunk->size == 0) {
+ pw_stream_queue_buffer(cap->pw_stream_handle, b);
+ return;
+ }
+
+ pw_frame_t next;
+ next.width = cap->frame_width;
+ next.height = cap->frame_height;
+ next.stride = d0->chunk->stride > 0 ? d0->chunk->stride : cap->frame_width * 4;
+ next.spa_format = cap->spa_format;
+
+ if (d0->type == SPA_DATA_DmaBuf && d0->fd >= 0) {
+ const uint32_t fourcc = spa_format_to_drm_fourcc(cap->spa_format);
+ // XBGR2101010 is imported via EGL TexStorageEXT as 10-bit UNORM (same path
+ // as BGRx). Reject only unknown fourccs.
+ if (fourcc == 0) {
+ static bool once = false;
+ if (!once) {
+ BOOST_LOG(warning) << "portal: DmaBuf unknown spa_format="sv << cap->spa_format
+ << " — dropping until format is mapped"sv;
+ once = true;
+ }
+ pw_stream_queue_buffer(cap->pw_stream_handle, b);
+ return;
+ }
+
+ int dup_fd = fcntl(d0->fd, F_DUPFD_CLOEXEC, 0);
+ if (dup_fd < 0) {
+ pw_stream_queue_buffer(cap->pw_stream_handle, b);
+ return;
+ }
+
+ next.is_dmabuf = true;
+ next.sd.width = cap->frame_width;
+ next.sd.height = cap->frame_height;
+ next.sd.fourcc = fourcc;
+ // gamescope always exports linear when DmaBuf; without a fixated modifier
+ // prop, INVALID skips EGL modifier attrs and NVIDIA fails
+ // EGLImageTargetTexture2DOES with GL 0x502 (INVALID_OPERATION).
+ next.sd.modifier = cap->spa_has_modifier ? cap->spa_modifier : DRM_FORMAT_MOD_LINEAR;
+ std::fill_n(next.sd.fds, 4, -1);
+ std::fill_n(next.sd.pitches, 4, 0);
+ std::fill_n(next.sd.offsets, 4, 0);
+ next.sd.fds[0] = dup_fd;
+ next.sd.pitches[0] = next.stride;
+ next.sd.offsets[0] = d0->chunk->offset;
+
+ // CPU mirror only when GPU path is forced off. Full 4K mmap+memcpy every
+ // frame (~2GB/s) was burning CPU while the GPU path never used cpu_data.
+ const char *dmabuf_env = std::getenv("POLARIS_PORTAL_DMABUF");
+ const bool force_cpu = dmabuf_env && dmabuf_env[0] == '0' && dmabuf_env[1] == '\0';
+ if (force_cpu &&
+ (next.sd.modifier == DRM_FORMAT_MOD_INVALID || next.sd.modifier == DRM_FORMAT_MOD_LINEAR)) {
+ const size_t map_size = static_cast<size_t>(d0->chunk->offset) + d0->chunk->size;
+ void *map = mmap(nullptr, map_size, PROT_READ, MAP_SHARED, d0->fd, 0);
+ if (map != MAP_FAILED) {
+ next.cpu_data.resize(d0->chunk->size);
+ std::memcpy(next.cpu_data.data(),
+ static_cast<uint8_t *>(map) + d0->chunk->offset, d0->chunk->size);
+ munmap(map, map_size);
+ }
+ }
+
+ // Stable key for EGL RGB cache: gamescope reuses a small PW buffer pool;
+ // keying by frame_seq forced a full eglCreateImage every frame.
+ next.buffer_key = (static_cast<std::uint64_t>(static_cast<std::uint32_t>(d0->fd)) << 1) | 1ull;
+
+ cap->saw_dmabuf.store(true, std::memory_order_relaxed);
+ }
+ else if ((d0->type == SPA_DATA_MemPtr || d0->type == SPA_DATA_MemFd) && d0->data) {
+ next.is_dmabuf = false;
+ next.cpu_data.resize(d0->chunk->size);
+ std::memcpy(next.cpu_data.data(), static_cast<uint8_t *>(d0->data) + d0->chunk->offset, d0->chunk->size);
+ cap->saw_memptr.store(true, std::memory_order_relaxed);
+ }
+ else {
pw_stream_queue_buffer(cap->pw_stream_handle, b);
return;
}
- uint32_t size = buf->datas[0].chunk->size;
{
std::lock_guard lk(cap->frame_mtx);
- cap->frame_data.resize(size);
- std::memcpy(cap->frame_data.data(), buf->datas[0].data, size);
+ const bool is_dmabuf = next.is_dmabuf;
+ const uint32_t spa_fmt = next.spa_format;
+ const int w = next.width;
+ const int h = next.height;
+ cap->latest.release();
+ // MemPtr/MemFd: sequence key. DmaBuf: already set from producer fd above.
+ if (!next.is_dmabuf || next.buffer_key == 0) {
+ next.buffer_key = ++cap->frame_seq;
+ } else {
+ ++cap->frame_seq; // still count frames for first-frame log
+ }
+ cap->latest = std::move(next);
+ if (!cap->latest.is_dmabuf) {
+ cap->frame_data = cap->latest.cpu_data;
+ }
+ if (cap->frame_seq == 1) {
+ const uint32_t fourcc = spa_format_to_drm_fourcc(spa_fmt);
+ const char *depth = (fourcc == DRM_FORMAT_XBGR2101010) ? "10-bit" : "8-bit";
+ const char *kind = is_dmabuf ? "DmaBuf" : "MemPtr";
+ BOOST_LOG(info) << "portal: first frame type="sv << kind
+ << " "sv << w << "x"sv << h
+ << " "sv << depth
+ << " fourcc=0x"sv << util::hex(fourcc).to_string_view()
+ << " pitch="sv << (is_dmabuf ? cap->latest.sd.pitches[0] : 0)
+ << " cpu_copy="sv << (is_dmabuf && !cap->latest.cpu_data.empty() ? "1" : "0");
+ }
cap->frame_available = true;
cap->frame_cv.notify_one();
}
@@ -481,32 +679,64 @@
static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) {
auto *cap = static_cast<pw_capture_t *>(userdata);
- if (!param || id != SPA_PARAM_Format) return;
+ if (!param || id != SPA_PARAM_Format) {
+ return;
+ }
- uint32_t media_type, media_subtype;
- if (spa_format_parse(param, &media_type, &media_subtype) < 0) return;
- if (media_type != SPA_MEDIA_TYPE_video || media_subtype != SPA_MEDIA_SUBTYPE_raw) return;
+ uint32_t media_type = 0, media_subtype = 0;
+ if (spa_format_parse(param, &media_type, &media_subtype) < 0) {
+ return;
+ }
+ if (media_type != SPA_MEDIA_TYPE_video || media_subtype != SPA_MEDIA_SUBTYPE_raw) {
+ return;
+ }
- struct spa_video_info_raw raw_info;
+ struct spa_video_info_raw raw_info {};
spa_format_video_raw_parse(param, &raw_info);
cap->frame_width = raw_info.size.width;
cap->frame_height = raw_info.size.height;
cap->frame_stride = cap->frame_width * 4;
+ cap->spa_format = raw_info.format;
+#ifdef SPA_VIDEO_FLAG_MODIFIER
+ if (raw_info.flags & SPA_VIDEO_FLAG_MODIFIER) {
+ cap->spa_modifier = raw_info.modifier;
+ cap->spa_has_modifier = true;
+ }
+ else
+#endif
+ {
+ cap->spa_modifier = DRM_FORMAT_MOD_INVALID;
+ cap->spa_has_modifier = false;
+ }
cap->negotiated = true;
BOOST_LOG(info) << "portal: PipeWire format negotiated: "sv
- << cap->frame_width << "x"sv << cap->frame_height;
+ << cap->frame_width << "x"sv << cap->frame_height
+ << " spa_format="sv << cap->spa_format
+ << " drm_fourcc=0x"sv << util::hex(spa_format_to_drm_fourcc(cap->spa_format)).to_string_view()
+ << (cap->spa_has_modifier ? " modifier=0x"sv : " no-modifier"sv)
+ << (cap->spa_has_modifier ? util::hex(cap->spa_modifier).to_string_view() : ""sv);
+
+ // Dual-GPU (AMD DmaBuf → NVIDIA GL/NVENC) fails import (GL 0x0502).
+ // POLARIS_PORTAL_DMABUF=0 forces MemFd/MemPtr only so the CPU path gets pixels.
+ // Default: prefer DmaBuf when the producer can export it.
+ const char *dmabuf_env = std::getenv("POLARIS_PORTAL_DMABUF");
+ const bool force_cpu = dmabuf_env && dmabuf_env[0] == '0' && dmabuf_env[1] == '\0';
+ const int data_type = force_cpu
+ ? ((1 << SPA_DATA_MemFd) | (1 << SPA_DATA_MemPtr))
+ : ((1 << SPA_DATA_DmaBuf) | (1 << SPA_DATA_MemFd) | (1 << SPA_DATA_MemPtr));
- // Set buffer parameters
uint8_t params_buffer[1024];
struct spa_pod_builder pb = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
const struct spa_pod *buf_param = (const struct spa_pod *) spa_pod_builder_add_object(
&pb,
SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(4, 2, 8),
- SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(1 << SPA_DATA_MemPtr));
+ SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(data_type));
+ BOOST_LOG(info) << "portal: requesting buffer dataType="sv
+ << (force_cpu ? "MemFd|MemPtr (CPU)"sv : "DmaBuf|MemFd|MemPtr"sv);
pw_stream_update_params(cap->pw_stream_handle, &buf_param, 1);
}
@@ -529,6 +759,8 @@
static std::unique_ptr<pw_capture_t> start_pw_capture(uint32_t node_id, int req_w, int req_h) {
auto cap = std::make_unique<pw_capture_t>();
+ (void) req_w;
+ (void) req_h;
pw_init(nullptr, nullptr);
@@ -556,17 +788,19 @@
return nullptr;
}
- // Accept any video format — let PipeWire/portal decide
+ // Prefer 10-bit when gamescope HDR offers it — avoids 8-bit DmaBuf → P010
+ // upconvert on the GL encode path (main cost at 4K/120). BGRx remains fallback.
+ // CHOICE_ENUM first value is the preferred default.
uint8_t params_buffer[1024];
struct spa_pod_builder pb = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer));
-
auto *fmt_param = (const struct spa_pod *) spa_pod_builder_add_object(
&pb,
SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
- SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(5,
- SPA_VIDEO_FORMAT_BGRx,
+ SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(6,
+ SPA_VIDEO_FORMAT_xBGR_210LE,
+ SPA_VIDEO_FORMAT_xBGR_210LE,
SPA_VIDEO_FORMAT_BGRx,
SPA_VIDEO_FORMAT_BGRA,
SPA_VIDEO_FORMAT_RGBx,
@@ -575,7 +809,7 @@
if (pw_stream_connect(cap->pw_stream_handle,
PW_DIRECTION_INPUT,
node_id,
- (enum pw_stream_flags)(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS),
+ (enum pw_stream_flags) (PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS),
&fmt_param, 1) < 0) {
BOOST_LOG(warning) << "portal: Failed to connect PipeWire stream to node "sv << node_id;
return nullptr;
@@ -587,7 +821,8 @@
}
cap->running = true;
- BOOST_LOG(info) << "portal: PipeWire capture started on node "sv << node_id;
+ BOOST_LOG(info) << "portal: PipeWire capture started on node "sv << node_id
+ << " (DmaBuf preferred, MemFd/MemPtr fallback)"sv;
return cap;
}
@@ -985,9 +1220,14 @@
int cfg_width = 0;
int cfg_height = 0;
platf::mem_type_e mem_type = platf::mem_type_e::system;
+ std::uint64_t img_sequence = 0;
bool probe_only = false; // true during encoder probe (skip portal session)
+ bool use_gpu_imgs() const {
+ return mem_type == platf::mem_type_e::cuda || mem_type == platf::mem_type_e::vaapi;
+ }
+
int
init(platf::mem_type_e hwdevice_type, const std::string &display_name, const ::video::config_t &config) {
cfg_width = config.width;
@@ -1112,21 +1352,72 @@
return platf::capture_e::interrupted;
}
- // Copy frame
- int copy_h = std::min(cap->frame_height, img_out->height);
- int copy_w = std::min(cap->frame_width, img_out->width);
- int src_stride = cap->frame_width * 4;
- int dst_stride = img_out->row_pitch;
+ auto frame_timestamp = std::chrono::steady_clock::now();
- for (int y = 0; y < copy_h; ++y) {
- std::memcpy(
- img_out->data + y * dst_stride,
- cap->frame_data.data() + y * src_stride,
- copy_w * 4);
+ // DMA-BUF → GPU descriptor only when we allocated a GPU img (data==nullptr).
+ // CPU img + DmaBuf without cpu_data: drop (request MemFd via POLARIS_PORTAL_DMABUF=0).
+ if (cap->latest.is_dmabuf && use_gpu_imgs() && img_out->data == nullptr) {
+ auto *img = static_cast<egl::img_descriptor_t *>(img_out.get());
+ img->reset();
+ img->width = cap->latest.width;
+ img->height = cap->latest.height;
+ img->sd = cap->latest.sd;
+ std::fill_n(cap->latest.sd.fds, 4, -1);
+ img->sequence = ++img_sequence;
+ img->dmabuf_buffer_key = cap->latest.buffer_key;
+ img->frame_timestamp = frame_timestamp;
+ img->frame_metadata = {
+ .transport = platf::frame_transport_e::dmabuf,
+ .residency = platf::frame_residency_e::gpu,
+ .format = spa_format_to_frame_format(cap->latest.spa_format),
+ };
+ stream_stats::update_capture_metadata(img->frame_metadata);
+ }
+ else {
+ // MemPtr / MemFd CPU path (known-good video). Also used when DMA-BUF
+ // was downloaded into cpu_data in on_process.
+ if (!img_out->data) {
+ // GPU descriptor with only MemPtr frames → cannot paint; skip.
+ static bool warned_memptr = false;
+ if (!warned_memptr) {
+ BOOST_LOG(warning)
+ << "portal: MemPtr frames with GPU image descriptors — "
+ "encoder should use the system-memory path; dropping until reinit"sv;
+ warned_memptr = true;
+ }
+ cap->frame_available = false;
+ lk.unlock();
+ continue;
+ }
+
+ if (cap->latest.cpu_data.empty()) {
+ cap->frame_available = false;
+ lk.unlock();
+ continue;
+ }
+
+ int copy_h = std::min(cap->latest.height, img_out->height);
+ int copy_w = std::min(cap->latest.width, img_out->width);
+ int src_stride = cap->latest.stride > 0 ? cap->latest.stride : cap->latest.width * 4;
+ int dst_stride = img_out->row_pitch;
+ const auto *src = cap->latest.cpu_data.data();
+ const int row_bytes = std::min(copy_w * 4, src_stride);
+ for (int y = 0; y < copy_h; ++y) {
+ std::memcpy(img_out->data + y * dst_stride, src + y * src_stride, row_bytes);
+ }
+ img_out->frame_timestamp = frame_timestamp;
+ img_out->frame_metadata = {
+ .transport = platf::frame_transport_e::shm,
+ .residency = platf::frame_residency_e::cpu,
+ .format = platf::frame_format_e::bgra8,
+ };
+ stream_stats::update_capture_metadata(img_out->frame_metadata);
}
- img_out->frame_timestamp = std::chrono::steady_clock::now();
cap->frame_available = false;
+ if (!cap->latest.is_dmabuf) {
+ cap->latest.cpu_data.clear();
+ }
lk.unlock();
if (!push_captured_image_cb(std::move(img_out), true)) {
@@ -1137,28 +1428,107 @@
return platf::capture_e::error;
}
+ // GPU descriptors when cuda/vaapi. Default on; POLARIS_PORTAL_DMABUF=0 forces CPU.
+ // Needs gamescope-hdr pipewire-prefer-dmabuf (or any DmaBuf producer).
+ bool want_dmabuf_gpu() const {
+ if (!use_gpu_imgs()) {
+ return false;
+ }
+ const char *e = std::getenv("POLARIS_PORTAL_DMABUF");
+ if (e && e[0] == '0' && e[1] == '\0') {
+ return false;
+ }
+ return true;
+ }
+
std::shared_ptr<platf::img_t>
alloc_img() override {
- auto img = std::make_shared<platf::img_t>();
+ if (want_dmabuf_gpu()) {
+ auto img = std::make_shared<egl::img_descriptor_t>();
+ img->width = cfg_width;
+ img->height = cfg_height;
+ img->pixel_pitch = 4;
+ img->row_pitch = cfg_width * 4;
+ img->data = nullptr;
+ img->sequence = 0;
+ img->serial = std::numeric_limits<decltype(img->serial)>::max();
+ img->dmabuf_buffer_key = 0;
+ std::fill_n(img->sd.fds, 4, -1);
+
+ BOOST_LOG(info) << "portal: alloc_img (GPU/DMA-BUF descriptor) "sv
+ << img->width << "x"sv << img->height
+ << " env="sv << this->env_width << "x"sv << this->env_height;
+ return img;
+ }
+ auto img = std::make_shared<platf::img_t>();
img->width = cfg_width;
img->height = cfg_height;
img->pixel_pitch = 4;
img->row_pitch = cfg_width * 4;
img->data = new uint8_t[cfg_height * img->row_pitch]();
- BOOST_LOG(info) << "portal: alloc_img "sv << img->width << "x"sv << img->height
+ BOOST_LOG(info) << "portal: alloc_img (CPU/MemPtr) "sv << img->width << "x"sv << img->height
<< " env="sv << this->env_width << "x"sv << this->env_height;
return img;
}
int
dummy_img(platf::img_t *img) override {
- if (!img || !img->data) return -1;
+ if (!img) {
+ return -1;
+ }
+ // GPU descriptors use sequence 0 + blank texture in the GL converter.
+ if (!img->data) {
+ return 0;
+ }
std::memset(img->data, 0, img->height * img->row_pitch);
return 0;
}
+ // Gamescope headless HDR (IceDOS / polaris#152): polaris runs with
+ // XDG_CURRENT_DESKTOP=gamescope so ScreenCast uses xdg-desktop-portal-gamescope
+ // against gamescope-0. Portal capture has no DRM HDR_OUTPUT_METADATA blob;
+ // synthesize usable static metadata matching gamescope's headless EDID.
+ static bool gamescope_hdr_session() {
+ if (const char *d = std::getenv("XDG_CURRENT_DESKTOP"); d && std::strcmp(d, "gamescope") == 0) {
+ return true;
+ }
+ if (const char *g = std::getenv("GAMESCOPE_WAYLAND_DISPLAY"); g && *g) {
+ return true;
+ }
+ return false;
+ }
+
+ bool is_hdr() override {
+ return gamescope_hdr_session();
+ }
+
+ bool get_hdr_metadata(SS_HDR_METADATA &metadata) override {
+ if (!gamescope_hdr_session()) {
+ std::memset(&metadata, 0, sizeof(metadata));
+ return false;
+ }
+
+ // Rec.2020 primaries + D65, units = CIE xy * 50000 (same as DXGI path).
+ // Mastering luminance matches gamescope headless HDR EDID (1000 nits).
+ std::memset(&metadata, 0, sizeof(metadata));
+ metadata.displayPrimaries[0].x = static_cast<std::uint16_t>(0.708f * 50000);
+ metadata.displayPrimaries[0].y = static_cast<std::uint16_t>(0.292f * 50000);
+ metadata.displayPrimaries[1].x = static_cast<std::uint16_t>(0.170f * 50000);
+ metadata.displayPrimaries[1].y = static_cast<std::uint16_t>(0.797f * 50000);
+ metadata.displayPrimaries[2].x = static_cast<std::uint16_t>(0.131f * 50000);
+ metadata.displayPrimaries[2].y = static_cast<std::uint16_t>(0.046f * 50000);
+ metadata.whitePoint.x = static_cast<std::uint16_t>(0.3127f * 50000);
+ metadata.whitePoint.y = static_cast<std::uint16_t>(0.3290f * 50000);
+ metadata.maxDisplayLuminance = 1000;
+ metadata.minDisplayLuminance = 1;
+ metadata.maxContentLightLevel = 1000;
+ metadata.maxFrameAverageLightLevel = 400;
+ metadata.maxFullFrameLuminance = 1000;
+ return true;
+ }
+
std::unique_ptr<platf::avcodec_encode_device_t>
make_avcodec_encode_device(platf::pix_fmt_e pix_fmt) override {
int w = cfg_width;
@@ -1166,15 +1536,22 @@
#ifdef POLARIS_BUILD_VAAPI
if (mem_type == platf::mem_type_e::vaapi) {
+ if (want_dmabuf_gpu()) {
+ return va::make_avcodec_encode_device(w, h, 0, 0, true);
+ }
return va::make_avcodec_encode_device(w, h, false);
}
#endif
#ifdef POLARIS_BUILD_CUDA
if (mem_type == platf::mem_type_e::cuda) {
+ if (want_dmabuf_gpu()) {
+ (void) pix_fmt;
+ return cuda::make_avcodec_gl_encode_device(w, h, 0, 0);
+ }
+ // NV12: RAM→CUDA. P010/HDR: empty device → software convert (cuda_t is NV12-only).
if (pix_fmt == platf::pix_fmt_e::nv12) {
return cuda::make_avcodec_encode_device(w, h, false);
}
-
return std::make_unique<platf::avcodec_encode_device_t>();
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment