The OpenGL backend sets GL_PACK_ROW_LENGTH before glReadPixels but never sets GL_PACK_ALIGNMENT, leaving it at its default of 4, so each row is padded up to a 4-byte boundary in the destination buffer. The Vulkan backend uses bufferRowLength = width, producing tightly-packed rows with no padding. The consumer (Screenshot) allocates width × height × blockSize bytes and indexes with (x + y·width)·blockSize, i.e. it assumes the tightly-packed Vulkan layout. For RGBA8_UNORM, the only format currently read back, width × 4 is always a multiple of 4, so the padding is a no-op and both backends agree; this is why it isn't observable in vanilla. Any format where width × blockSize isn't a multiple of 4 (e.g. R8_UNORM at an odd width) diverges: OpenGL pads rows while the tightly-sized buffer and reader expect none, giving corrupted rows or a PBO overrun on OpenGL only. Fix: set GL_PACK_ALIGNMENT to the format's byteAlignment() before glReadPixels, matching the tight packing Vulkan produces.
Can confirm.