From 14a6f2a9b58a23457e404a053299be3627109692 Mon Sep 17 00:00:00 2001 From: T-Gander Date: Wed, 19 Aug 2026 17:23:39 -0600 Subject: [PATCH] fix(linux/kms): clamp texture readback to actual size, not CRTC viewport display_ram_t::snapshot() read GetTextureSubImage() using the CRTC's output viewport dimensions, not the imported plane texture's actual size. These differ when the display controller hardware-scales a plane at scanout (e.g. a game's native-resolution exclusive-fullscreen swapchain stretched to fill a higher-resolution output) -- kmsgrab imports the plane's raw pre-scale buffer via DMA-BUF, bypassing the CRTC scaler entirely, so the texture is the smaller native size while the destination buffer is sized for the full output. Reading past the texture's real bounds triggered GL_INVALID_VALUE every frame, which manifested as a black screen for the whole scaled-plane session via the existing import-failure fallback (the "Failed to bind EGLImage" log line was a stray error from this call, not the import itself). Clamps the read to the texture's real dimensions and uses GL_PACK_ROW_LENGTH so a smaller read still lands correctly in the destination buffer's top-left corner instead of shearing across rows. This does not reproduce the hardware scaling itself -- the rest of the frame is left as whatever the buffer previously contained -- but it resolves the crash/black-screen and captures the correct content at its native resolution. Co-Authored-By: Claude Sonnet 5 --- src/platform/linux/kmsgrab.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index 56b12666..e51d44cb 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -1700,7 +1700,27 @@ 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). Reading past the texture's real bounds triggers + // GL_INVALID_VALUE, so clamp the read to what's actually there. GL_PACK_ROW_LENGTH + // keeps the destination stride matching the full-size image buffer so the read + // lands correctly in its top-left corner instead of shearing across rows. + // Note this does not reproduce the hardware scaling itself: the rest of the frame + // is left as whatever the buffer previously contained. + int read_width = std::max(0, std::min(width, w - img_offset_x)); + int read_height = std::max(0, std::min(height, h - img_offset_y)); + bool clamped = read_width != width || read_height != height; + if (clamped) { + gl::ctx.PixelStorei(GL_PACK_ROW_LENGTH, width); + } + + 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); + + if (clamped) { + gl::ctx.PixelStorei(GL_PACK_ROW_LENGTH, 0); + } img_out->frame_timestamp = frame_timestamp;