Skip to content

Commit 3d2b7f2

Browse files
T-Ganderclaude
andcommitted
feat(linux/kms): scale captured plane to output size when hardware-scaled
Replaces the clamp-and-crop fix from 14a6f2a 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>
1 parent 14a6f2a commit 3d2b7f2

1 file changed

Lines changed: 53 additions & 15 deletions

File tree

src/platform/linux/kmsgrab.cpp

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1703,25 +1703,54 @@ namespace platf {
17031703
// The plane's backing texture can be smaller than the configured capture
17041704
// resolution when the display controller hardware-scales it up at scanout time
17051705
// (e.g. a game's native-resolution exclusive-fullscreen swapchain stretched to
1706-
// fill the output). Reading past the texture's real bounds triggers
1707-
// GL_INVALID_VALUE, so clamp the read to what's actually there. GL_PACK_ROW_LENGTH
1708-
// keeps the destination stride matching the full-size image buffer so the read
1709-
// lands correctly in its top-left corner instead of shearing across rows.
1710-
// Note this does not reproduce the hardware scaling itself: the rest of the frame
1711-
// is left as whatever the buffer previously contained.
1712-
int read_width = std::max(0, std::min(width, w - img_offset_x));
1713-
int read_height = std::max(0, std::min(height, h - img_offset_y));
1714-
bool clamped = read_width != width || read_height != height;
1715-
if (clamped) {
1716-
gl::ctx.PixelStorei(GL_PACK_ROW_LENGTH, width);
1717-
}
1706+
// fill the output) -- kmsgrab imports the plane's raw pre-scale buffer via
1707+
// DMA-BUF, bypassing that hardware scaler entirely. Reproduce the same upscale
1708+
// with a linear-filtered GL blit into a full-resolution scratch texture before
1709+
// reading it back, so the captured frame matches what the display actually shows
1710+
// instead of being cropped to the plane's native corner.
1711+
GLuint read_tex = rgb->tex[0];
1712+
int read_offset_x = img_offset_x;
1713+
int read_offset_y = img_offset_y;
1714+
1715+
if (w != width || h != height) {
1716+
if (!scale_tex.size()) {
1717+
scale_tex = gl::tex_t::make(1);
1718+
gl::ctx.BindTexture(GL_TEXTURE_2D, scale_tex[0]);
1719+
gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height);
1720+
gl::ctx.BindTexture(GL_TEXTURE_2D, 0);
1721+
1722+
scale_dst_fb = gl::frame_buf_t::make(1);
1723+
scale_dst_fb.bind(&scale_tex[0], &scale_tex[0] + 1);
1724+
1725+
scale_src_fb = gl::frame_buf_t::make(1);
1726+
}
1727+
1728+
gl::ctx.BindFramebuffer(GL_READ_FRAMEBUFFER, scale_src_fb[0]);
1729+
gl::ctx.FramebufferTexture(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, rgb->tex[0], 0);
1730+
gl::ctx.ReadBuffer(GL_COLOR_ATTACHMENT0);
1731+
1732+
gl::ctx.BindFramebuffer(GL_DRAW_FRAMEBUFFER, scale_dst_fb[0]);
1733+
GLenum draw_buf = GL_COLOR_ATTACHMENT0;
1734+
gl::ctx.DrawBuffers(1, &draw_buf);
1735+
1736+
#ifndef NDEBUG
1737+
auto status = gl::ctx.CheckFramebufferStatus(GL_READ_FRAMEBUFFER);
1738+
if (status != GL_FRAMEBUFFER_COMPLETE) {
1739+
BOOST_LOG(error) << "Scale blit: source CheckFramebufferStatus() --> [0x"sv << util::hex(status).to_string_view() << ']';
1740+
}
1741+
#endif
1742+
1743+
gl::ctx.BlitFramebuffer(0, 0, w, h, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
17181744

1719-
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);
1745+
gl::ctx.BindFramebuffer(GL_FRAMEBUFFER, 0);
17201746

1721-
if (clamped) {
1722-
gl::ctx.PixelStorei(GL_PACK_ROW_LENGTH, 0);
1747+
read_tex = scale_tex[0];
1748+
read_offset_x = 0;
1749+
read_offset_y = 0;
17231750
}
17241751

1752+
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);
1753+
17251754
img_out->frame_timestamp = frame_timestamp;
17261755

17271756
if (cursor && captured_cursor.visible) {
@@ -1761,6 +1790,15 @@ namespace platf {
17611790
egl::display_t display; ///< EGL display created from the GBM device.
17621791
egl::ctx_t ctx; ///< EGL context used to copy KMS frames into RAM.
17631792
bool import_failed_last_frame = false; ///< Whether the previous frame's plane import failed, to avoid log spam.
1793+
1794+
// Lazily created the first time a captured plane's native size differs from the
1795+
// configured capture resolution (i.e. the display controller is hardware-scaling
1796+
// it). scale_tex is sized to the full capture resolution; scale_src_fb/scale_dst_fb
1797+
// wrap the per-frame imported texture and scale_tex respectively so BlitFramebuffer
1798+
// can scale between them.
1799+
gl::tex_t scale_tex; ///< Scratch texture holding the upscaled frame, when needed.
1800+
gl::frame_buf_t scale_src_fb; ///< FBO used to bind the per-frame imported texture as the blit source.
1801+
gl::frame_buf_t scale_dst_fb; ///< FBO wrapping scale_tex as the blit destination.
17641802
};
17651803

17661804
/**

0 commit comments

Comments
 (0)