Files
MobileGL/MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp
T

376 lines
22 KiB
C++

// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImagePacked16Scenario.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
//
// Scenario - glCopyImageSubData PRESERVES 16-BIT PACKED WORDS ACROSS AN ARRAY MIP LEVEL.
//
// The shape is lifted verbatim from the 18 Espryt bodies of KHR-GL4x.copy_image.functional
// that survived every earlier wave: the three internal formats MobileGL can keep as 16-bit
// packed ES storage - GL_RGB5 (stored GL_RGB565), GL_RGB5_A1, GL_RGBA4 - crossed with the
// target pairs that put a GL_TEXTURE_2D_ARRAY's MIP LEVEL 1 on one side of the copy. On the
// affected Mali the mirrored *_REV field order is a property of WHOLE ALLOCATIONS (shape-
// and context-dependent; the failing 30x30x12 arrays carry it at every level, the small
// arrays of the suite's passing iterations do not), and glCopyImageSubData - a raw
// texel-block move - between a mirrored allocation and a plain one lands the fields
// reversed: src word 0x0047 arrives as 0x8C20 (its 5_5_5_1 -> 1_5_5_5_REV re-encoding),
// 0x0007 as 0x3800, byte-exact on every failing body. Uploads and readbacks of the same
// image are clean (the driver decodes its own layout consistently), which is why only the
// copy path ever crossed the two layouts and why the CTS's "source image was not modified"
// checks always passed.
//
// The array is 30x30x12 with THREE levels and the flat endpoint is 7x7 with three levels
// (7/3/1) because that is the allocation the failures pin - the CTS builds every functional
// texture with FUNCTIONAL_TEST_N_LEVELS = 3 (makeTextureComplete(0, 2)) - and any deviation
// from the measured shape might sit on the clean side of whatever allocation heuristic picks
// the driver's layout.
//
// The repair under test is the packed16 storage widening
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the POST probe
// (SelfTest::CopyImageMirrorsPacked16FieldOrder) measures the mirror - or
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it - the three formats are stored as
// GL_RGB8/GL_RGBA8, leaving no 16-bit packed image for a copy to disagree about. The client
// word still round-trips exactly: the canonical shadow is already UNorm8, and an n-bit field
// encodes to UNorm8 and back losslessly for every n <= 8.
//
// This scenario runs in BOTH configurations, and both must hand back identical client words:
// * the ambient registrations take the narrow path on a clean driver (llvmpipe has no
// mirror, so Auto keeps the native 16-bit storage - the pre-existing behaviour stays
// covered);
// * the DirectGLES.WidenedPacked16. registration pins MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1,
// which is the storage every affected device will actually run - without it the repair
// is unfalsifiable off-device, because no CI driver has the bug that arms it.
// The Mali mirror itself CANNOT be reproduced here; only the on-device CTS run can show the
// widening killing the 18 bodies. What this scenario pins is that the widened storage is
// client-invisible: same words in, same words out, on every leg the failing bodies used.
//
// DirectVulkan is the control - Magma has always resolved these formats to RGBA8 - so a
// failure on both backends means the scenario is wrong, and a failure on DirectGLES alone
// means the widening (or the narrow path it replaces) is.
#include <algorithm>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kBaseSize = 30; // array level 0; level 1 is 15x15
constexpr int kLevel1Size = kBaseSize / 2;
constexpr int kLayers = 12;
constexpr int kFlatSize = 7; // the plain-2D / renderbuffer endpoint, level 0
// Copies cover the whole flat endpoint and land at (8, 8) inside the 15x15 level so
// that offsets are honoured, not just texel (0, 0): 8 + 7 == 15 reaches the far edge.
constexpr int kRegion = kFlatSize;
constexpr int kArrayOffset = 8;
struct PackedFormatCase {
GLenum internalFormat; // the spelling the CTS uses
GLenum transferFormat;
GLenum transferType;
const char* name;
};
// Per-texel varying words, every field inside its width, so a swapped field order (or
// a mis-addressed row) cannot cancel out the way a uniform fill would let it.
GLushort MakeWord(GLenum type, int i) {
switch (type) {
case GL_UNSIGNED_SHORT_5_6_5: {
const int r = i % 32, g = (i * 7 + 3) % 64, b = (i * 5 + 11) % 32;
return static_cast<GLushort>((r << 11) | (g << 5) | b);
}
case GL_UNSIGNED_SHORT_4_4_4_4: {
const int r = i % 16, g = (i * 3 + 1) % 16, b = (i * 7 + 5) % 16, a = (i * 5 + 2) % 16;
return static_cast<GLushort>((r << 12) | (g << 8) | (b << 4) | a);
}
case GL_UNSIGNED_SHORT_5_5_5_1: {
const int r = i % 32, g = (i * 7 + 3) % 32, b = (i * 3 + 11) % 32, a = i % 2;
return static_cast<GLushort>((r << 11) | (g << 6) | (b << 1) | a);
}
default:
return 0;
}
}
std::vector<GLushort> MakeWords(GLenum type, int count, int seed) {
std::vector<GLushort> words(static_cast<size_t>(count));
for (int i = 0; i < count; ++i) {
words[static_cast<size_t>(i)] = MakeWord(type, i + seed);
}
return words;
}
class CopyImagePacked16Scenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
// 16-bit rows are 2-byte aligned; the default 4-byte row alignment would pad
// every odd-width row of the 15x15 level and shear the comparisons.
glPixelStorei(GL_UNPACK_ALIGNMENT, 2);
glPixelStorei(GL_PACK_ALIGNMENT, 2);
if (!CopyImageSubDataUsable()) {
GTEST_SKIP() << "glCopyImageSubData is unavailable on backend " << Gl().BackendName();
}
}
void TearDown() override {
if (!Ready()) return;
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
if (m_renderbuffer != 0) {
glDeleteRenderbuffers(1, &m_renderbuffer);
m_renderbuffer = 0;
}
if (m_fbo != 0) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
}
bool CopyImageSubDataUsable() {
GLuint probe[2] = {0, 0};
glGenTextures(2, probe);
for (const GLuint texture : probe) {
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 1, 1, 1);
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
while (glGetError() != GL_NO_ERROR) {
}
glCopyImageSubData(probe[0], GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, probe[1], GL_TEXTURE_2D_ARRAY, 0, 0, 0,
0, 1, 1, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(2, probe);
return usable;
}
// The CTS's own mutable shape: glTexImage3D per level, filter NEAREST, THREE levels
// (30/15/7) with the chain clamped to them. Level 2 carries its own fill so nothing
// below can pass by reading a level that was never written.
GLuint MakeArrayTexture(const PackedFormatCase& format, const std::vector<GLushort>& level0,
const std::vector<GLushort>& level1) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, static_cast<GLint>(format.internalFormat), kBaseSize, kBaseSize,
kLayers, 0, format.transferFormat, format.transferType, level0.data());
glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, static_cast<GLint>(format.internalFormat), kLevel1Size,
kLevel1Size, kLayers, 0, format.transferFormat, format.transferType, level1.data());
const int level2Size = kLevel1Size / 2;
const auto level2 = MakeWords(format.transferType, level2Size * level2Size * kLayers, 211);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, static_cast<GLint>(format.internalFormat), level2Size,
level2Size, kLayers, 0, format.transferFormat, format.transferType, level2.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
return texture;
}
// Three levels (7/3/1) like the CTS's plain endpoints; `texels` is level 0, the one
// every assertion reads.
GLuint MakeFlatTexture(const PackedFormatCase& format, const std::vector<GLushort>& texels) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), kFlatSize, kFlatSize, 0,
format.transferFormat, format.transferType, texels.data());
for (int level = 1; level <= 2; ++level) {
const int size = std::max(kFlatSize >> level, 1);
const auto fill = MakeWords(format.transferType, size * size, 97 + level);
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), size, size, 0,
format.transferFormat, format.transferType, fill.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
std::vector<GLushort> ReadTexImage(GLenum target, GLuint texture, int level,
const PackedFormatCase& format, size_t texelCount) {
std::vector<GLushort> words(texelCount, 0);
glBindTexture(target, texture);
glGetTexImage(target, level, format.transferFormat, format.transferType, words.data());
glBindTexture(target, 0);
return words;
}
// Every word of `got` inside the kRegion-square at (x0, y0) of a width-wide layer-0
// image equals the corresponding source word, and every word outside it still holds
// `fill`'s. Failures name the texel and both words, which is what turns a field-order
// regression into a one-line diagnosis.
void ExpectRegion(const std::vector<GLushort>& got, int width, int x0, int y0,
const std::vector<GLushort>& source, int sourceWidth, int sourceX0, int sourceY0,
const std::vector<GLushort>& fill, const char* what) {
for (int y = 0; y < width; ++y) {
for (int x = 0; x < width && static_cast<size_t>(y * width + x) < got.size(); ++x) {
const bool inRegion =
x >= x0 && x < x0 + kRegion && y >= y0 && y < y0 + kRegion;
const GLushort actual = got[static_cast<size_t>(y * width + x)];
const GLushort expected =
inRegion ? source[static_cast<size_t>((sourceY0 + y - y0) * sourceWidth + sourceX0 +
(x - x0))]
: fill[static_cast<size_t>(y * width + x)];
EXPECT_EQ(actual, expected)
<< what << ": texel (" << x << ", " << y << ")"
<< (inRegion ? " (copied)" : " (untouched)") << " holds 0x" << std::hex << actual
<< ", expected 0x" << expected;
if (actual != expected) return; // one texel names the defect; 224 more would bury it
}
}
}
std::vector<GLuint> m_textures;
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
};
const PackedFormatCase kFormats[] = {
{GL_RGB5, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, "rgb5"},
{GL_RGB5_A1, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, "rgb5_a1"},
{GL_RGBA4, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4, "rgba4"},
};
// texture_2d (the ES image behind GL_TEXTURE_RECTANGLE too) -> the array's level 1:
// the array-as-destination direction of 12 of the 18 failing bodies.
TEST_F(CopyImagePacked16Scenario, FlatImageLandsInArrayMipLevelIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const auto flat = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
const GLuint array = MakeArrayTexture(format, level0, level1);
const GLuint source = MakeFlatTexture(format, flat);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset,
kArrayOffset, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, flat, kFlatSize, 0, 0, level1,
(std::string("2d->2d_array level 1, ") + format.name).c_str());
// The source must not have moved - the CTS asserts this before it ever looks at
// the destination, and it is what pins the corruption to the copy itself.
const auto sourceAfter =
ReadTexImage(GL_TEXTURE_2D, source, 0, format, static_cast<size_t>(kFlatSize) * kFlatSize);
ExpectRegion(sourceAfter, kFlatSize, 0, 0, flat, kFlatSize, 0, 0, flat,
(std::string("source after 2d->2d_array, ") + format.name).c_str());
}
}
// The array's level 1 -> texture_2d: the array-as-source direction of the other 6
// bodies (2d_array -> 3d and 2d_array -> rectangle both read the level-1 array).
TEST_F(CopyImagePacked16Scenario, ArrayMipLevelLandsInFlatImageIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const auto fill = MakeWords(format.transferType, kFlatSize * kFlatSize, 131);
const GLuint array = MakeArrayTexture(format, level0, level1);
const GLuint destination = MakeFlatTexture(format, fill);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, 1, kArrayOffset, kArrayOffset, 0, destination,
GL_TEXTURE_2D, 0, 0, 0, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
const auto got = ReadTexImage(GL_TEXTURE_2D, destination, 0, format,
static_cast<size_t>(kFlatSize) * kFlatSize);
ExpectRegion(got, kFlatSize, 0, 0, level1, kLevel1Size, kArrayOffset, kArrayOffset, fill,
(std::string("2d_array level 1 -> 2d, ") + format.name).c_str());
}
}
// renderbuffer -> the array's level 1: the leg the remaining 3 bodies use, and the one
// that requires the renderbuffer's ES storage to move together with the textures' -
// glCopyImageSubData needs both endpoints in the same driver format, so a widening that
// reached textures alone would break exactly here.
TEST_F(CopyImagePacked16Scenario, RenderbufferLandsInArrayMipLevelIntact) {
if (!Ready() || IsSkipped()) return;
for (const PackedFormatCase& format : kFormats) {
const auto level0 = MakeWords(format.transferType, kBaseSize * kBaseSize * kLayers, 1);
const auto level1 = MakeWords(format.transferType, kLevel1Size * kLevel1Size * kLayers, 7);
const GLuint array = MakeArrayTexture(format, level0, level1);
if (m_renderbuffer == 0) glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, format.internalFormat, kFlatSize, kFlatSize);
if (m_fbo == 0) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< format.name << ": the renderbuffer is not attachable";
// Field values picked to encode exactly in the narrow fields AND in their
// UNorm8 expansions, so the expected word is the same whichever storage the
// configuration picked - which is the point of the whole scenario.
const int maxG = format.transferType == GL_UNSIGNED_SHORT_5_6_5 ? 63 : 31;
const int max = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : 31;
const int maxGreen = format.transferType == GL_UNSIGNED_SHORT_4_4_4_4 ? 15 : maxG;
const GLfloat clearColor[4] = {static_cast<GLfloat>(8 % (max + 1)) / max,
static_cast<GLfloat>(maxGreen / 2) / maxGreen,
static_cast<GLfloat>(max - 2) / max, 1.0f};
// The context is shared with every scenario in this process; a scissor left on
// would clip the clear and hand the copy undefined renderbuffer texels.
glDisable(GL_SCISSOR_TEST);
glClearBufferfv(GL_COLOR, 0, clearColor);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << format.name << ": setup failed";
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, 0, 0, 0, array, GL_TEXTURE_2D_ARRAY, 1,
kArrayOffset, kArrayOffset, 0, kRegion, kRegion, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR))
<< format.name << ": glCopyImageSubData raised an error";
GLushort clearedWord = 0;
switch (format.transferType) {
case GL_UNSIGNED_SHORT_5_6_5:
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 5) | (max - 2));
break;
case GL_UNSIGNED_SHORT_5_5_5_1:
clearedWord = static_cast<GLushort>((8 << 11) | ((maxGreen / 2) << 6) | ((max - 2) << 1) | 1);
break;
case GL_UNSIGNED_SHORT_4_4_4_4:
clearedWord = static_cast<GLushort>((8 << 12) | ((maxGreen / 2) << 8) | ((max - 2) << 4) | 15);
break;
default:
break;
}
std::vector<GLushort> expectedRegion(static_cast<size_t>(kRegion) * kRegion, clearedWord);
const auto got = ReadTexImage(GL_TEXTURE_2D_ARRAY, array, 1, format,
static_cast<size_t>(kLevel1Size) * kLevel1Size * kLayers);
ExpectRegion(got, kLevel1Size, kArrayOffset, kArrayOffset, expectedRegion, kRegion, 0, 0, level1,
(std::string("renderbuffer -> 2d_array level 1, ") + format.name).c_str());
}
}
} // namespace
} // namespace MGITest