feat(linux/kms): scale captured plane to output size when hardware-scaled
Some checks failed
Top issues / Top issues (push) Has been cancelled
CodeQL / CodeQL (push) Has been cancelled

Replaces the clamp-and-crop fix from 14a6f2a9 with the real fix: when
the display controller is hardware-scaling a plane at scanout (its
CRTC destination rect differs from the plane's native buffer size --
e.g. a game's native-resolution exclusive-fullscreen swapchain
stretched to fill a higher-resolution output), kmsgrab's raw DMA-BUF
import bypasses that scaler entirely and only ever sees the pre-scale
buffer. Reproduce the same upscale with a linear-filtered
glBlitFramebuffer into a scratch texture sized to the configured
capture resolution, before the existing GetTextureSubImage readback,
so the captured frame matches what the display actually shows instead
of being cropped to the plane's native corner.

The scratch texture/FBOs are lazily created on first use and reused
across frames; the common case (plane already fills the output, no
scaling needed) is unaffected and takes the same path as before.

Untested against real hardware in this session -- built by
cross-referencing this project's own glad config (gl:compatibility=4.6,
confirming BlitFramebuffer/GL_READ_FRAMEBUFFER/GL_DRAW_FRAMEBUFFER are
generated) and the existing FBO helper patterns already used elsewhere
in this file, but needs a live test to confirm the blit against an
EGLImage-backed source texture behaves as expected on this driver.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 17:32:16 -06:00
parent 14a6f2a9b5
commit 3d2b7f2770

View File

@@ -1703,24 +1703,53 @@ namespace platf {
// The plane's backing texture can be smaller than the configured capture // The plane's backing texture can be smaller than the configured capture
// resolution when the display controller hardware-scales it up at scanout time // resolution when the display controller hardware-scales it up at scanout time
// (e.g. a game's native-resolution exclusive-fullscreen swapchain stretched to // (e.g. a game's native-resolution exclusive-fullscreen swapchain stretched to
// fill the output). Reading past the texture's real bounds triggers // fill the output) -- kmsgrab imports the plane's raw pre-scale buffer via
// GL_INVALID_VALUE, so clamp the read to what's actually there. GL_PACK_ROW_LENGTH // DMA-BUF, bypassing that hardware scaler entirely. Reproduce the same upscale
// keeps the destination stride matching the full-size image buffer so the read // with a linear-filtered GL blit into a full-resolution scratch texture before
// lands correctly in its top-left corner instead of shearing across rows. // reading it back, so the captured frame matches what the display actually shows
// Note this does not reproduce the hardware scaling itself: the rest of the frame // instead of being cropped to the plane's native corner.
// is left as whatever the buffer previously contained. GLuint read_tex = rgb->tex[0];
int read_width = std::max(0, std::min(width, w - img_offset_x)); int read_offset_x = img_offset_x;
int read_height = std::max(0, std::min(height, h - img_offset_y)); int read_offset_y = img_offset_y;
bool clamped = read_width != width || read_height != height;
if (clamped) { if (w != width || h != height) {
gl::ctx.PixelStorei(GL_PACK_ROW_LENGTH, width); 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(rgb->tex[0], 0, img_offset_x, img_offset_y, 0, read_width, read_height, 1, GL_BGRA, GL_UNSIGNED_BYTE, img_out->height * img_out->row_pitch, img_out->data); 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);
if (clamped) {
gl::ctx.PixelStorei(GL_PACK_ROW_LENGTH, 0);
}
img_out->frame_timestamp = frame_timestamp; img_out->frame_timestamp = frame_timestamp;
@@ -1761,6 +1790,15 @@ namespace platf {
egl::display_t display; ///< EGL display created from the GBM device. egl::display_t display; ///< EGL display created from the GBM device.
egl::ctx_t ctx; ///< EGL context used to copy KMS frames into RAM. 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. 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.
}; };
/** /**