Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions src/platform/linux/cuda.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -538,15 +538,28 @@ namespace cuda {
} else if (descriptor.sequence > sequence) {
sequence = descriptor.sequence;

rgb = egl::rgb_t {};

auto rgb_opt = egl::import_source(display.get(), descriptor.sd);

if (!rgb_opt) {
return -1;
}
// The plane's current format/modifier can't be imported (e.g. a game switched to a
// 10bpc swapchain in exclusive fullscreen that this driver won't bind to a GL texture).
// Encode a blank frame instead of dropping the client, and only log once per failure
// streak to avoid flooding the log every frame.
if (!import_failed_last_frame) {
BOOST_LOG(warning) << "Skipping frame(s): plane format (fourcc: "sv << util::hex(descriptor.sd.fourcc).to_string_view()
<< ") failed to import; will resume automatically if the format changes back"sv;
import_failed_last_frame = true;
}

rgb = std::move(*rgb_opt);
rgb = egl::create_blank(img);
} else {
if (import_failed_last_frame) {
BOOST_LOG(info) << "Resumed capture after plane format import failure"sv;
import_failed_last_frame = false;
}

rgb = std::move(*rgb_opt);
}
}

auto fmt_desc = av_pix_fmt_desc_get(sw_format);
Expand Down Expand Up @@ -640,6 +653,8 @@ namespace cuda {
int offset_y; ///< Vertical offset in physical pixels.

bool is_yuv444; ///< Whether the CUDA converter outputs YUV 4:4:4.

bool import_failed_last_frame = false; ///< Whether the previous frame's plane import failed, to avoid log spam.
};

/**
Expand Down
44 changes: 43 additions & 1 deletion src/platform/linux/graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,25 @@ namespace egl {
return attribs;
}

/**
* @brief Log the surface descriptor an import attempt was built from, for diagnosing
* driver-specific import failures (e.g. a particular DRM format/modifier/pitch combo).
* Only meant to be called on the (rate-limited, caller-side) failure path, hence debug level.
*/
static void log_surface_descriptor(const surface_descriptor_t &xrgb) {
BOOST_LOG(debug) << "Surface descriptor: "sv << xrgb.width << 'x' << xrgb.height
<< " fourcc="sv << util::hex(xrgb.fourcc).to_string_view()
<< " modifier="sv << util::hex(xrgb.modifier).to_string_view();
for (auto x = 0; x < 4; ++x) {
if (xrgb.fds[x] < 0) {
continue;
}
BOOST_LOG(debug) << " plane["sv << x << "]: fd="sv << xrgb.fds[x]
<< " offset="sv << xrgb.offsets[x]
<< " pitch="sv << xrgb.pitches[x];
}
}

/**
* @brief Import the source frame texture for EGL/OpenGL conversion.
*
Expand All @@ -647,18 +666,41 @@ namespace egl {
};

if (!rgb->xrgb8) {
BOOST_LOG(error) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view();
BOOST_LOG(debug) << "Couldn't import RGB Image: "sv << util::hex(eglGetError()).to_string_view();
log_surface_descriptor(xrgb);

return std::nullopt;
}

// Some drivers can return a non-null EGLImage from eglCreateImage() while still leaving
// an error queued (validation deferred until first use). Catch that here too rather than
// only checking eglGetError() on the outright-null-image path above.
if (auto egl_err = eglGetError(); egl_err != EGL_SUCCESS) {
BOOST_LOG(debug) << "eglCreateImage() left a pending EGL error despite returning an image: "sv << util::hex(egl_err).to_string_view();
log_surface_descriptor(xrgb);
}

gl::ctx.BindTexture(GL_TEXTURE_2D, rgb->tex[0]);
if (!gl::egl_image_target_texture_2d()) {
BOOST_LOG(error) << "glEGLImageTargetTexture2DOES is not available; cannot import RGB DMA-BUF"sv;
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
return std::nullopt;
}
gl::egl_image_target_texture_2d()(GL_TEXTURE_2D, rgb->xrgb8);

// Some drivers accept an EGLImage of a given DRM format/modifier from eglCreateImage()
// but then reject binding it to a GL texture, e.g. Mesa/RADV rejecting a 10bpc format
// like DRM_FORMAT_XBGR2101010. When that happens, the texture is left with stale or
// incomplete contents, so this must be treated as an import failure rather than
// silently streaming whatever ends up in the texture. Logged at debug level: callers
// already surface a rate-limited, user-facing warning instead of flooding at error level.
if (auto err = gl::ctx.GetError(); err != GL_NO_ERROR) {
BOOST_LOG(debug) << "Failed to bind EGLImage to GL texture: "sv << util::hex(err).to_string_view();
log_surface_descriptor(xrgb);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
return std::nullopt;
}

gl::ctx.BindTexture(GL_TEXTURE_2D, 0);

gl_drain_errors;
Expand Down
94 changes: 92 additions & 2 deletions src/platform/linux/kmsgrab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <fcntl.h>
#include <filesystem>
#include <ranges>
#include <set>
#include <thread>
#include <unistd.h>

Expand Down Expand Up @@ -932,6 +933,7 @@
continue;
}

std::set<std::uint32_t> counted_crtcs;
auto end = std::end(card);
for (auto plane = std::begin(card); plane != end; ++plane) {
// Skip unused planes
Expand All @@ -943,8 +945,15 @@
continue;
}

// A CRTC can have more than one simultaneously-active plane (e.g. gamescope's
// base + overlay layers). Count each CRTC once so this matches kms_display_names().
if (counted_crtcs.count(plane->crtc_id)) {

Check warning on line 950 in src/platform/linux/kmsgrab.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "contains" member function.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AaAc04BkF3SknD5Zx-tP&open=AaAc04BkF3SknD5Zx-tP&pullRequest=5532
continue;
}

if (monitor != monitor_index) {
++monitor;
counted_crtcs.insert(plane->crtc_id);
continue;
}

Expand Down Expand Up @@ -1659,7 +1668,21 @@
auto rgb_opt = egl::import_source(display.get(), sd);

if (!rgb_opt) {
return capture_e::error;
// The plane's current format/modifier can't be imported (e.g. a game switched to a
// 10bpc swapchain in exclusive fullscreen that this driver won't bind to a GL texture).
// Skip this frame rather than tearing down the whole capture thread for every client,
// and only log once per failure streak to avoid flooding the log every frame.
if (!import_failed_last_frame) {
BOOST_LOG(warning) << "Skipping frame(s): plane format (fourcc: "sv << util::hex(sd.fourcc).to_string_view()
<< ") failed to import; will resume automatically if the format changes back"sv;
import_failed_last_frame = true;
}
return capture_e::timeout;
}

if (import_failed_last_frame) {
BOOST_LOG(info) << "Resumed capture after plane format import failure"sv;
import_failed_last_frame = false;
}

auto &rgb = *rgb_opt;
Expand All @@ -1677,7 +1700,56 @@
return platf::capture_e::interrupted;
}

gl::ctx.GetTextureSubImage(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data);
// The plane's backing texture can be smaller than the configured capture
// resolution when the display controller hardware-scales it up at scanout time
// (e.g. a game's native-resolution exclusive-fullscreen swapchain stretched to
// fill the output) -- kmsgrab imports the plane's raw pre-scale buffer via
// DMA-BUF, bypassing that hardware scaler entirely. Reproduce the same upscale
// with a linear-filtered GL blit into a full-resolution scratch texture before
// reading it back, so the captured frame matches what the display actually shows
// instead of being cropped to the plane's native corner.
GLuint read_tex = rgb->tex[0];
int read_offset_x = img_offset_x;
int read_offset_y = img_offset_y;

if (w != width || h != height) {
if (!scale_tex.size()) {
scale_tex = gl::tex_t::make(1);
gl::ctx.BindTexture(GL_TEXTURE_2D, scale_tex[0]);
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height);
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);

scale_dst_fb = gl::frame_buf_t::make(1);
scale_dst_fb.bind(&scale_tex[0], &scale_tex[0] + 1);

scale_src_fb = gl::frame_buf_t::make(1);
}

gl::ctx.BindFramebuffer(GL_READ_FRAMEBUFFER, scale_src_fb[0]);
gl::ctx.FramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, rgb->tex[0], 0);
gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0);

gl::ctx.BindFramebuffer(GL_DRAW_FRAMEBUFFER, scale_dst_fb[0]);
GLenum draw_buf = GL_COLOR_ATTACHMENT0;
gl::ctx.DrawBuffers(1, &draw_buf);

#ifndef NDEBUG
auto status = gl::ctx.CheckFramebufferStatus(GL_READ_FRAMEBUFFER);

Check warning on line 1737 in src/platform/linux/kmsgrab.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declaration shadows a local variable "status" in the outer scope.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AaAc04BkF3SknD5Zx-tQ&open=AaAc04BkF3SknD5Zx-tQ&pullRequest=5532
if (status != GL_FRAMEBUFFER_COMPLETE) {
BOOST_LOG(error) << "Scale blit: source CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
}
#endif

gl::ctx.BlitFramebuffer(0, 0, w, h, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR);

gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);

read_tex = scale_tex[0];
read_offset_x = 0;
read_offset_y = 0;
}

gl::ctx.GetTextureSubImage(read_tex, 0, read_offset_x, read_offset_y, 0, width, height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data);

img_out->frame_timestamp = frame_timestamp;

Expand Down Expand Up @@ -1717,6 +1789,16 @@
gbm::gbm_t gbm; ///< GBM device used for buffer allocation.
egl::display_t display; ///< EGL display created from the GBM device.
egl::ctx_t ctx; ///< EGL context used to copy KMS frames into RAM.
bool import_failed_last_frame = false; ///< Whether the previous frame's plane import failed, to avoid log spam.

// Lazily created the first time a captured plane's native size differs from the
// configured capture resolution (i.e. the display controller is hardware-scaling
// it). scale_tex is sized to the full capture resolution; scale_src_fb/scale_dst_fb
// wrap the per-frame imported texture and scale_tex respectively so BlitFramebuffer
// can scale between them.
gl::tex_t scale_tex; ///< Scratch texture holding the upscaled frame, when needed.
gl::frame_buf_t scale_src_fb; ///< FBO used to bind the per-frame imported texture as the blit source.
gl::frame_buf_t scale_dst_fb; ///< FBO wrapping scale_tex as the blit destination.
};

/**
Expand Down Expand Up @@ -2075,6 +2157,7 @@
}

auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count));
std::set<std::uint32_t> counted_crtcs;

auto end = std::end(card);
for (auto plane = std::begin(card); plane != end; ++plane) {
Expand All @@ -2087,6 +2170,12 @@
continue;
}

// A CRTC can have more than one simultaneously-active plane (e.g. gamescope's
// base + overlay layers). Count each CRTC once, not once per active plane.
if (counted_crtcs.count(plane->crtc_id)) {

Check warning on line 2175 in src/platform/linux/kmsgrab.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "contains" member function.

See more on https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&issues=AaAc04BkF3SknD5Zx-tR&open=AaAc04BkF3SknD5Zx-tR&pullRequest=5532
continue;
}

auto fb = card.fb(plane.get());
if (!fb) {
BOOST_LOG(error) << "Couldn't get drm fb for plane ["sv << plane->fb_id << "]: "sv << strerror(errno);
Expand Down Expand Up @@ -2126,6 +2215,7 @@
kms::print(plane.get(), fb.get(), crtc.get());
display_names.emplace_back(std::format("{}-{}", drmModeGetConnectorTypeName(it->second.type), it->second.index));
count++;
counted_crtcs.insert(plane->crtc_id);
}

cds.emplace_back(kms::card_descriptor_t {
Expand Down
25 changes: 20 additions & 5 deletions src/platform/linux/vaapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -558,15 +558,28 @@ namespace va {
} else if (descriptor.sequence > sequence) {
sequence = descriptor.sequence;

rgb = egl::rgb_t {};

auto rgb_opt = egl::import_source(display.get(), descriptor.sd);

if (!rgb_opt) {
return -1;
// The plane's current format/modifier can't be imported (e.g. a game switched to a
// 10bpc swapchain in exclusive fullscreen that this driver won't bind to a GL texture).
// Encode a blank frame instead of dropping the client, and only log once per failure
// streak to avoid flooding the log every frame.
if (!import_failed_last_frame) {
BOOST_LOG(warning) << "Skipping frame(s): plane format (fourcc: "sv << util::hex(descriptor.sd.fourcc).to_string_view()
<< ") failed to import; will resume automatically if the format changes back"sv;
import_failed_last_frame = true;
}

rgb = egl::create_blank(img);
} else {
if (import_failed_last_frame) {
BOOST_LOG(info) << "Resumed capture after plane format import failure"sv;
import_failed_last_frame = false;
}

rgb = std::move(*rgb_opt);
}

rgb = std::move(*rgb_opt);
}

sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0], false);
Expand Down Expand Up @@ -603,6 +616,8 @@ namespace va {

int offset_x; ///< Horizontal offset in physical pixels.
int offset_y; ///< Vertical offset in physical pixels.

bool import_failed_last_frame = false; ///< Whether the previous frame's plane import failed, to avoid log spam.
};

/**
Expand Down