diff --git a/src/platform/linux/cuda.cpp b/src/platform/linux/cuda.cpp index 6623c9e95a0..23534a741f2 100644 --- a/src/platform/linux/cuda.cpp +++ b/src/platform/linux/cuda.cpp @@ -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); @@ -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. }; /** diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 1ef27c7166e..da42c49d6c9 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -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. * @@ -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; diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index fdf10a92fc5..c65269f7a0d 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -932,6 +933,7 @@ namespace platf { continue; } + std::set counted_crtcs; auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { // Skip unused planes @@ -943,8 +945,15 @@ namespace platf { 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)) { + continue; + } + if (monitor != monitor_index) { ++monitor; + counted_crtcs.insert(plane->crtc_id); continue; } @@ -1659,7 +1668,21 @@ namespace platf { 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; @@ -1677,7 +1700,56 @@ namespace platf { 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); + 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; @@ -1717,6 +1789,16 @@ namespace platf { 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. }; /** @@ -2075,6 +2157,7 @@ namespace platf { } auto crtc_to_monitor = kms::map_crtc_to_monitor(card.monitors(conn_type_count)); + std::set counted_crtcs; auto end = std::end(card); for (auto plane = std::begin(card); plane != end; ++plane) { @@ -2087,6 +2170,12 @@ namespace platf { 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)) { + 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); @@ -2126,6 +2215,7 @@ namespace platf { 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 { diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index a9190a7bb55..4cb30378691 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -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); @@ -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. }; /**