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 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 17:23:39 -06:00
parent 220e4f3916
commit 14a6f2a9b5

View File

@@ -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;