mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[Fix] (Espryt): store RGB565/RGB5_A1/RGBA4 as 8-bit channels where a POST probe measures the Mali packed16 array-mip field-order mirror
This commit is contained in:
@@ -253,6 +253,22 @@ namespace MobileGL::MG_Config {
|
||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||
// the negative control the emulation is measured against.
|
||||
QuirkOverride ViewportArrayEmulation = QuirkOverride::Auto;
|
||||
// MOBILEGL_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
|
||||
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
|
||||
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers keep a MIRRORED
|
||||
// field order for the 16-bit packed texels of a non-zero mip level of a
|
||||
// GL_TEXTURE_2D_ARRAY, so glCopyImageSubData - a raw texel-block move - lands
|
||||
// R/G/B/A reversed whenever exactly one endpoint is such a level
|
||||
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
|
||||
// With no 16-bit packed ES image left there is no field order to disagree about; the
|
||||
// client word still round-trips exactly, because the canonical shadow is already
|
||||
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
|
||||
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
|
||||
// path); ForceOff keeps the native narrow storage even where the probe fires - the
|
||||
// negative control that replays the corruption. Costs 2x the memory of the affected
|
||||
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
||||
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -201,6 +201,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||
features.ViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_WIDEN_PACKED16_STORAGE");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -3375,6 +3375,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (format != TextureInternalFormat::RGB5 && format != TextureInternalFormat::RGB5A1) {
|
||||
return data;
|
||||
}
|
||||
// With the storage widened to 8-bit-per-channel (the packed16 field-order quirk)
|
||||
// there is no driver requantization left for the repack to pre-empt - the shadow's
|
||||
// UNorm8 bytes ARE the stored bytes - and the packed 16-bit client type this leg
|
||||
// retargets to is not a legal upload for a GL_RGB8/GL_RGBA8 store at all.
|
||||
if (TextureImpl::UsesWidenedPacked16NormStorage(format)) {
|
||||
return data;
|
||||
}
|
||||
const Bool hasAlpha = format == TextureInternalFormat::RGB5A1;
|
||||
const GLenum packedType = hasAlpha ? GL_UNSIGNED_SHORT_5_5_5_1 : GL_UNSIGNED_SHORT_5_6_5;
|
||||
// Idempotent across a region's level loop: glType is shared, so later levels arrive with
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
#include "Managers.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
#include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/SelfTest/DriverBugProbes.h"
|
||||
#include "MG_Util/Texture/TextureFormatProcessor.h"
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include <Config.h>
|
||||
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
@@ -125,6 +127,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
requestedInternalFormat,
|
||||
TextureImpl::GetRenderTargetNormalizeOptions(g_GLESCapabilities, targetIndex));
|
||||
}
|
||||
// Outside the caveat branch on purpose: the driver CAN create the native narrow
|
||||
// storage - the capability probes say so - it just cannot be trusted as a raw-copy
|
||||
// endpoint. Texture and renderbuffer targets both come through here, which is what
|
||||
// keeps a renderbuffer -> texture copy of these formats same-ES-format when the
|
||||
// widening engages.
|
||||
if (TextureImpl::UsesWidenedPacked16NormStorage(internalFormat)) {
|
||||
options |= PixelFormatNormalizeOptionBit::WidenPacked16Norm;
|
||||
}
|
||||
NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType);
|
||||
}
|
||||
} // namespace
|
||||
@@ -182,6 +192,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return options;
|
||||
}
|
||||
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat) {
|
||||
switch (internalFormat) {
|
||||
// TextureInternalFormat::RGB5 is both GL_RGB5 and GL_RGB565 - the GL-to-MG
|
||||
// converter folds the two spellings onto one logical format.
|
||||
case TextureInternalFormat::RGB5:
|
||||
case TextureInternalFormat::RGB5A1:
|
||||
case TextureInternalFormat::RGBA4:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
switch (MG_Config::Features.EsprytWidenPacked16Storage) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
return true;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
return false;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
// Behind the backend gate on purpose: the memoized probe latches its first answer
|
||||
// for the whole process, and before the backend is up the GL function table may
|
||||
// not be resolved yet - a probe run then would latch "cannot tell" as "clean"
|
||||
// forever. Once the backend exists, the first narrow-format image this process
|
||||
// creates runs the probe on a live context.
|
||||
if (pActiveBackendObject == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return MG_Util::SelfTest::CopyImageMirrorsPacked16FieldOrder(g_GLESFuncs);
|
||||
}
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType, TextureTarget target) {
|
||||
#ifdef TRACY_ENABLE
|
||||
|
||||
@@ -46,6 +46,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
||||
|
||||
// Whether this format's ES storage is widened to 8-bit-per-channel because the
|
||||
// driver's 16-bit packed storage mirrors its field order at a non-zero array mip
|
||||
// level (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
|
||||
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
|
||||
// divergence (or MOBILEGL_WIDEN_PACKED16_STORAGE forces it). The transfer paths
|
||||
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
|
||||
// is no longer 16-bit packed.
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType,
|
||||
TextureTarget target = TextureTarget::Unknown);
|
||||
|
||||
@@ -106,6 +106,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/VertexArrayEnableDisableScenario.cpp
|
||||
Scenarios/CopyImageLevelRangeScenario.cpp
|
||||
Scenarios/CopyImageLayeredScenario.cpp
|
||||
Scenarios/CopyImagePacked16Scenario.cpp
|
||||
Scenarios/TextureViewScenario.cpp
|
||||
Scenarios/PackedWordReadbackScenario.cpp
|
||||
Scenarios/LayeredAttachmentBarrierScenario.cpp
|
||||
@@ -362,6 +363,8 @@ mgl_itest_join_environment(MGL_ITEST_GLES_UNLOCATED_IO_BLOCKS_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1"
|
||||
"MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/unlocated-io-blocks.log"
|
||||
${MGL_ITEST_COMMON_ENV})
|
||||
mgl_itest_join_environment(MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT
|
||||
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_WIDEN_PACKED16_STORAGE=1" ${MGL_ITEST_COMMON_ENV})
|
||||
|
||||
# TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it.
|
||||
set(MGL_ITEST_TIMEOUT 120)
|
||||
@@ -541,3 +544,21 @@ gtest_discover_tests(MobileGLIntegrationTest
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
# The packed16 copy scenarios again, with the 8-bit storage widening PINNED ON. The ambient
|
||||
# registrations above cover the narrow storage - on every CI driver the widening's POST
|
||||
# probe finds no field-order mirror, so Auto keeps the native 16-bit path - which means the
|
||||
# storage every AFFECTED device will actually run would otherwise execute nowhere at all:
|
||||
# no CI driver has the Mali bug that arms it. This lane is what proves the widened storage
|
||||
# is client-invisible (same packed words in and out on every leg the 18 failing CTS bodies
|
||||
# used, the renderbuffer one included). DirectGLES only - the flag steers nothing on
|
||||
# DirectVulkan, which has always stored these formats widened.
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectGLES.WidenedPacked16."
|
||||
TEST_FILTER "CopyImagePacked16Scenario.*"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES
|
||||
LABELS integration-gpu
|
||||
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
ENVIRONMENT "${MGL_ITEST_GLES_WIDENED_PACKED16_ENVIRONMENT}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
// 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 driver's physical field order for such a level is the *_REV mirror of
|
||||
// every other image's, and glCopyImageSubData - a raw texel-block move - 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
|
||||
// level 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 two levels because that is the shape the failures pin: the same
|
||||
// suite's level-0 copies and a 14x14 base's level 1 measured clean on the same driver, so a
|
||||
// smaller array might sit on the clean side of whatever allocation threshold 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_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_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 <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, the chain
|
||||
// clamped to the two levels it has.
|
||||
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, 1);
|
||||
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());
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
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, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), kFlatSize, kFlatSize, 0,
|
||||
format.transferFormat, format.transferType, texels.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};
|
||||
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
|
||||
@@ -26,6 +26,7 @@ using MobileGL::MG_Util::SelfTest::ProbeGeometryStageSsboWriteAfterEmitDropped;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageLocationPerNameBudget;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeImageWriteReadCoherencyResidual;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeBlitIgnoresDestinationArrayLayer;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeCopyImageMirrorsPacked16FieldOrder;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeExplicitVertexInputLocationCeiling;
|
||||
using MobileGL::MG_Util::SelfTest::ProbeR32FMultisampleSwizzleCorruption;
|
||||
|
||||
@@ -107,6 +108,21 @@ namespace {
|
||||
bool blitIgnoresDestinationLayer = false;
|
||||
bool blitIgnoresSourceLayer = false;
|
||||
|
||||
// Probe 7: the driver's PHYSICAL field order for a 16-bit packed texel at a non-zero
|
||||
// mip level of a 2D array is the *_REV mirror of the plain-image order. Modelled at
|
||||
// upload, which is where the real defect lives: the words such a level stores are the
|
||||
// mirrored re-encoding, the raw copy moves them verbatim, and the plain 2D readback
|
||||
// decodes them with the non-REV order - exactly the 0x0047 -> 0x8C20 arithmetic the
|
||||
// affected Mali hands back. Uploads and readbacks of the SAME image stay consistent,
|
||||
// which is why only the copy path can observe the knob.
|
||||
bool packed16ArrayMipFieldOrderMirrored = false;
|
||||
// The inconclusive path: EVERY array level stores mirrored words, level 0 included, so
|
||||
// the probe's level-0 control copy is dirty too and no verdict may be reached.
|
||||
bool packed16EveryArrayLevelMirrored = false;
|
||||
// Another inconclusive path: the copy silently lands nothing, so the destination keeps
|
||||
// its 0xFFFF fill - a value that is neither the word nor its mirror.
|
||||
bool packed16CopyDoesNothing = false;
|
||||
|
||||
// ---- object bookkeeping ---------------------------------------------
|
||||
GLenum pendingError = GL_NO_ERROR;
|
||||
GLuint nextShaderId = 1;
|
||||
@@ -136,7 +152,14 @@ namespace {
|
||||
std::map<GLuint, std::array<GLubyte, 2>> arrayLayerFill;
|
||||
// framebuffer id -> the (2D array texture, layer) glFramebufferTextureLayer attached.
|
||||
std::map<GLuint, std::pair<GLuint, GLint>> framebufferLayerAttachment;
|
||||
// (texture, level) -> the PHYSICAL 16-bit word every texel of that 5551 image holds.
|
||||
// One word per level is all the packed16 probe distinguishes: it uploads a uniform
|
||||
// fill and reads one texel.
|
||||
std::map<std::pair<GLuint, GLint>, GLushort> packedTexelWords;
|
||||
// framebuffer id -> the plain 2D texture glFramebufferTexture2D attached.
|
||||
std::map<GLuint, GLuint> framebuffer2DAttachment;
|
||||
GLuint boundArrayTexture = 0;
|
||||
GLuint boundTexture2D = 0;
|
||||
GLuint boundDrawFramebuffer = 0;
|
||||
GLuint boundReadFramebuffer = 0;
|
||||
|
||||
@@ -164,6 +187,17 @@ namespace {
|
||||
return haystack.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
// The 5_5_5_1 <-> 1_5_5_5_REV field-order mirror: the same fields, packed from the other
|
||||
// end of the word. 0x0047 (R,G,B,A = 0,1,3,1) becomes 0x8C20 - the exact pair every
|
||||
// failing KHR-GL4x.copy_image body printed on the affected Mali.
|
||||
GLushort MirrorPacked5551(GLushort word) {
|
||||
const GLushort r = (word >> 11) & 0x1F;
|
||||
const GLushort g = (word >> 6) & 0x1F;
|
||||
const GLushort b = (word >> 1) & 0x1F;
|
||||
const GLushort a = word & 0x1;
|
||||
return static_cast<GLushort>((a << 15) | (b << 10) | (g << 5) | r);
|
||||
}
|
||||
|
||||
// Every `image2D <name>` the program declares, across all its stages.
|
||||
std::vector<std::string> DeclaredImageNames(GLuint program) {
|
||||
std::vector<std::string> names;
|
||||
@@ -401,6 +435,7 @@ namespace {
|
||||
funcs.glBindTexture = [](GLenum target, GLuint texture) {
|
||||
if (target == GL_TEXTURE_2D_MULTISAMPLE) g_fake.boundMultisampleTexture = texture;
|
||||
if (target == GL_TEXTURE_2D_ARRAY) g_fake.boundArrayTexture = texture;
|
||||
if (target == GL_TEXTURE_2D) g_fake.boundTexture2D = texture;
|
||||
};
|
||||
funcs.glTexStorage3D = [](GLenum target, GLsizei, GLenum, GLsizei, GLsizei, GLsizei) {
|
||||
if (target == GL_TEXTURE_2D_ARRAY) g_fake.arrayLayerFill[g_fake.boundArrayTexture] = {0, 0};
|
||||
@@ -418,6 +453,13 @@ namespace {
|
||||
funcs.glDeleteTextures = [](GLsizei n, const GLuint* textures) {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (textures[i] != 0) --g_fake.aliveTextures;
|
||||
for (auto it = g_fake.packedTexelWords.begin(); it != g_fake.packedTexelWords.end();) {
|
||||
if (it->first.first == textures[i]) {
|
||||
it = g_fake.packedTexelWords.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
funcs.glTexParameteri = [](GLenum target, GLenum pname, GLint param) {
|
||||
@@ -426,8 +468,39 @@ namespace {
|
||||
static_cast<GLenum>(param);
|
||||
}
|
||||
};
|
||||
funcs.glTexImage2D = [](GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum,
|
||||
const void*) {};
|
||||
// The packed16 probe's endpoints. A plain 2D image stores its 5551 words in the
|
||||
// canonical (non-REV) order on every knob setting - the defect is confined to array
|
||||
// mip levels, and keeping the 2D side clean is what lets the readback below decode
|
||||
// with one order and still reproduce the mirror.
|
||||
funcs.glTexImage2D = [](GLenum target, GLint level, GLint, GLsizei, GLsizei, GLint, GLenum,
|
||||
GLenum type, const void* pixels) {
|
||||
if (target != GL_TEXTURE_2D || type != GL_UNSIGNED_SHORT_5_5_5_1 || pixels == nullptr) return;
|
||||
GLushort word = 0;
|
||||
std::memcpy(&word, pixels, sizeof(word));
|
||||
g_fake.packedTexelWords[{g_fake.boundTexture2D, level}] = word;
|
||||
};
|
||||
// The defect itself, at the upload where the physical layout is chosen: a mirrored
|
||||
// array level stores the re-encoded word. Its own readback would decode it back
|
||||
// consistently - only the raw copy below ever leaks the layout.
|
||||
funcs.glTexImage3D = [](GLenum target, GLint level, GLint, GLsizei, GLsizei, GLsizei, GLint,
|
||||
GLenum, GLenum type, const void* pixels) {
|
||||
if (target != GL_TEXTURE_2D_ARRAY || type != GL_UNSIGNED_SHORT_5_5_5_1 || pixels == nullptr) return;
|
||||
GLushort word = 0;
|
||||
std::memcpy(&word, pixels, sizeof(word));
|
||||
const bool mirrored = g_fake.packed16EveryArrayLevelMirrored ||
|
||||
(g_fake.packed16ArrayMipFieldOrderMirrored && level >= 1);
|
||||
g_fake.packedTexelWords[{g_fake.boundArrayTexture, level}] =
|
||||
mirrored ? MirrorPacked5551(word) : word;
|
||||
};
|
||||
// A raw texel-block move: the PHYSICAL word travels, whichever layout wrote it.
|
||||
funcs.glCopyImageSubData = [](GLuint srcName, GLenum, GLint srcLevel, GLint, GLint, GLint,
|
||||
GLuint dstName, GLenum, GLint dstLevel, GLint, GLint, GLint,
|
||||
GLsizei, GLsizei, GLsizei) {
|
||||
if (g_fake.packed16CopyDoesNothing) return;
|
||||
const auto source = g_fake.packedTexelWords.find({srcName, srcLevel});
|
||||
if (source == g_fake.packedTexelWords.end()) return;
|
||||
g_fake.packedTexelWords[{dstName, dstLevel}] = source->second;
|
||||
};
|
||||
funcs.glTexSubImage2D = [](GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum,
|
||||
const void*) {};
|
||||
funcs.glTexStorage2D = [](GLenum, GLsizei, GLenum, GLsizei, GLsizei) {};
|
||||
@@ -446,7 +519,11 @@ namespace {
|
||||
g_fake.boundReadFramebuffer = framebuffer;
|
||||
}
|
||||
};
|
||||
funcs.glFramebufferTexture2D = [](GLenum, GLenum, GLenum, GLuint, GLint) {};
|
||||
funcs.glFramebufferTexture2D = [](GLenum target, GLenum, GLenum, GLuint texture, GLint) {
|
||||
const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer
|
||||
: g_fake.boundDrawFramebuffer;
|
||||
g_fake.framebuffer2DAttachment[framebuffer] = texture;
|
||||
};
|
||||
funcs.glFramebufferTextureLayer = [](GLenum target, GLenum, GLuint texture, GLint, GLint layer) {
|
||||
const GLuint framebuffer = (target == GL_READ_FRAMEBUFFER) ? g_fake.boundReadFramebuffer
|
||||
: g_fake.boundDrawFramebuffer;
|
||||
@@ -482,6 +559,7 @@ namespace {
|
||||
for (GLsizei i = 0; i < n; ++i) {
|
||||
if (framebuffers[i] != 0) --g_fake.aliveFramebuffers;
|
||||
g_fake.framebufferLayerAttachment.erase(framebuffers[i]);
|
||||
g_fake.framebuffer2DAttachment.erase(framebuffers[i]);
|
||||
}
|
||||
};
|
||||
funcs.glGenVertexArrays = [](GLsizei n, GLuint* arrays) {
|
||||
@@ -561,9 +639,31 @@ namespace {
|
||||
funcs.glReadPixels = [](GLint, GLint, GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
void* pixels) {
|
||||
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
|
||||
// Answered before anything else: a read framebuffer that names an array LAYER is the
|
||||
// layered-blit probe asking what that layer holds, and its bytes have nothing to do
|
||||
// with the pass/fail texel encoding the image probes below share.
|
||||
// A read framebuffer naming a plain 2D texture that holds a 5551 word is the
|
||||
// packed16 probe reading its copy destination. The driver decodes its OWN storage
|
||||
// with the canonical non-REV order and expands each field by bit replication -
|
||||
// which is exactly how the mirrored word 0x8C20 becomes (140, 132, 132, 0).
|
||||
if (const auto attached = g_fake.framebuffer2DAttachment.find(g_fake.boundReadFramebuffer);
|
||||
attached != g_fake.framebuffer2DAttachment.end() &&
|
||||
g_fake.packedTexelWords.count({attached->second, 0}) != 0) {
|
||||
// Gated on the texture actually holding a 5551 word, so every OTHER probe that
|
||||
// attaches a plain 2D texture keeps the pass/fail readback paths below.
|
||||
const GLushort w = g_fake.packedTexelWords[{attached->second, 0}];
|
||||
const auto expand5 = [](GLushort v) {
|
||||
return static_cast<GLubyte>((v << 3) | (v >> 2));
|
||||
};
|
||||
GLubyte* out = static_cast<GLubyte*>(pixels);
|
||||
for (std::size_t i = 0; i < texels; ++i) {
|
||||
out[i * 4 + 0] = expand5((w >> 11) & 0x1F);
|
||||
out[i * 4 + 1] = expand5((w >> 6) & 0x1F);
|
||||
out[i * 4 + 2] = expand5((w >> 1) & 0x1F);
|
||||
out[i * 4 + 3] = (w & 0x1) ? 255 : 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Answered before anything else below: a read framebuffer that names an array LAYER
|
||||
// is the layered-blit probe asking what that layer holds, and its bytes have nothing
|
||||
// to do with the pass/fail texel encoding the image probes below share.
|
||||
if (const auto layered = g_fake.framebufferLayerAttachment.find(g_fake.boundReadFramebuffer);
|
||||
layered != g_fake.framebufferLayerAttachment.end()) {
|
||||
const auto& fill = g_fake.arrayLayerFill[layered->second.first];
|
||||
@@ -626,6 +726,8 @@ TEST(DriverBugProbes, AProbeThatCannotRunReportsNoBug) {
|
||||
EXPECT_FALSE(ProbeImageLocationPerNameBudget(gl).detected);
|
||||
EXPECT_FALSE(ProbeCrossStageImageQualifierMergeDropsWrites(gl));
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl))
|
||||
<< "a probe with no entry points has measured nothing";
|
||||
}
|
||||
|
||||
// The section lists only bugs the device HAS, so a driver nothing could be probed on renders
|
||||
@@ -945,3 +1047,52 @@ TEST(DriverBugProbes, ImageCoherencyNeedsBothHalvesOfTheSplitPairInOneStage) {
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeImageWriteReadCoherencyResidual(gl).detected);
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, Packed16FieldOrderIsCleanOnAConformingDriver) {
|
||||
ResetFakeDriver();
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
TEST(DriverBugProbes, Packed16FieldOrderIsDetectedWhenTheArrayMipLevelIsMirrored) {
|
||||
ResetFakeDriver();
|
||||
g_fake.packed16ArrayMipFieldOrderMirrored = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_TRUE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// THE CONTROL. A driver whose EVERY array level stores mirrored words fails the level-0
|
||||
// control copy too - a different (and larger) defect than the one this probe is entitled to
|
||||
// report, so it must reach no verdict rather than pin the mip-level shape.
|
||||
TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheControlIsMirroredToo) {
|
||||
ResetFakeDriver();
|
||||
g_fake.packed16ArrayMipFieldOrderMirrored = true;
|
||||
g_fake.packed16EveryArrayLevelMirrored = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// And the shape that is not this bug: a copy that lands nothing leaves the destination's
|
||||
// 0xFFFF fill, which matches neither the word nor its mirror - "reached no verdict".
|
||||
TEST(DriverBugProbes, Packed16FieldOrderReportsNothingWhenTheCopyLandsNothing) {
|
||||
ResetFakeDriver();
|
||||
g_fake.packed16ArrayMipFieldOrderMirrored = true;
|
||||
g_fake.packed16CopyDoesNothing = true;
|
||||
const MG_External::GLESFunctionsTable gl = MakeFakeGLESFunctions();
|
||||
EXPECT_FALSE(ProbeCopyImageMirrorsPacked16FieldOrder(gl));
|
||||
ExpectProbeReleasedEverything();
|
||||
}
|
||||
|
||||
// The byte arithmetic the fake's mirror encodes, pinned against the QPA evidence. The fake
|
||||
// models the ARRAY-AS-SOURCE direction (decode 5_5_5_1, re-encode 1_5_5_5_REV): 0x0047 must
|
||||
// deliver 0x8C20, the exact pair every failing array-as-source copy_image body printed. The
|
||||
// QPA's array-as-destination bodies show the INVERSE transform (enc_5551 of dec_REV: 0x0007
|
||||
// delivered as 0x3800), and enc_REV(dec_5551(x)) inverts enc_5551(dec_REV(x)), so feeding
|
||||
// the delivered word back through the fake's mirror must reproduce the original.
|
||||
TEST(DriverBugProbes, Packed16MirrorArithmeticMatchesTheDeviceEvidence) {
|
||||
EXPECT_EQ(MirrorPacked5551(0x0047), 0x8C20);
|
||||
EXPECT_EQ(MirrorPacked5551(0x3800), 0x0007);
|
||||
}
|
||||
|
||||
@@ -3388,6 +3388,46 @@ TEST_F(TextureTest, NormalizePixelFormatKeepsPackedTransferTypesForPackedSizedFo
|
||||
}
|
||||
}
|
||||
|
||||
// The packed16 field-order quirk (PixelFormatNormalizeOptionBit::WidenPacked16Norm): where the
|
||||
// driver's 16-bit packed storage mirrors its field order at a non-zero array mip level (the
|
||||
// Mali defect behind the KHR-GL4x.copy_image rgb5/rgb5_a1/rgba4 x *2d_array* failures), the
|
||||
// three ES narrow formats move to 8-bit-per-channel storage. The transfer pair must NOT move
|
||||
// with the bit - it is already the UNorm8 component layout the canonical shadow holds - and
|
||||
// no other format may move with it either.
|
||||
TEST_F(TextureTest, NormalizePixelFormatWidensThePacked16FormatsUnderTheQuirkBit) {
|
||||
using MG_Util::TextureFormatProcessor::NormalizePixelFormat;
|
||||
struct {
|
||||
GLenum requested;
|
||||
GLenum expectedNarrow;
|
||||
GLenum expectedWidened;
|
||||
GLenum expectedFormat;
|
||||
} cases[] = {
|
||||
{GL_RGB565, GL_RGB565, GL_RGB8, GL_RGB},
|
||||
{GL_RGB5_A1, GL_RGB5_A1, GL_RGBA8, GL_RGBA},
|
||||
{GL_RGBA4, GL_RGBA4, GL_RGBA8, GL_RGBA},
|
||||
// Negative controls: a 32-bit packed format and an already-8-bit one stay put with
|
||||
// the bit set - the quirk is about 16-bit packed normalized storage and nothing else.
|
||||
{GL_RGB10_A2, GL_RGB10_A2, GL_RGB10_A2, GL_RGBA},
|
||||
{GL_RGBA8, GL_RGBA8, GL_RGBA8, GL_RGBA},
|
||||
};
|
||||
for (const auto& c : cases) {
|
||||
GLenum narrowInternal = 0, narrowFormat = 0, narrowType = 0;
|
||||
NormalizePixelFormat(c.requested, PixelFormatNormalizeOptionBit::None, &narrowInternal, &narrowFormat,
|
||||
&narrowType);
|
||||
EXPECT_EQ(narrowInternal, c.expectedNarrow) << "internalformat 0x" << std::hex << c.requested;
|
||||
|
||||
GLenum widenedInternal = 0, widenedFormat = 0, widenedType = 0;
|
||||
NormalizePixelFormat(c.requested, PixelFormatNormalizeOptionBit::WidenPacked16Norm, &widenedInternal,
|
||||
&widenedFormat, &widenedType);
|
||||
EXPECT_EQ(widenedInternal, c.expectedWidened) << "internalformat 0x" << std::hex << c.requested;
|
||||
// The transfer pair is identical narrow and widened: the widening changes only the ES
|
||||
// storage, never how client data is described to it.
|
||||
EXPECT_EQ(widenedFormat, narrowFormat) << "internalformat 0x" << std::hex << c.requested;
|
||||
EXPECT_EQ(widenedType, narrowType) << "internalformat 0x" << std::hex << c.requested;
|
||||
EXPECT_EQ(widenedFormat, c.expectedFormat) << "internalformat 0x" << std::hex << c.requested;
|
||||
}
|
||||
}
|
||||
|
||||
// GL_RGB565 (ARB_ES2_compatibility / GL 4.1, used directly by the GL CTS) must round-trip
|
||||
// through the internal-format enums; it had no GLToMG mapping at all, so glTexImage* with
|
||||
// GL_RGB565 was rejected as an unknown internal format.
|
||||
|
||||
@@ -127,6 +127,21 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
GLfloat clearColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
GLint packAlignment = 4;
|
||||
GLint packRowLength = 0;
|
||||
// The rest of the pixel-transfer scope. The probes that upload or read back texels
|
||||
// run under whatever scope their caller left - the lazy ones run from live paths,
|
||||
// not just the POST screen - and a caller's skip/row-length/PBO would silently
|
||||
// shear a probe's own data. Saved so a probe can zero them and the caller gets
|
||||
// them back.
|
||||
GLint packSkipPixels = 0;
|
||||
GLint packSkipRows = 0;
|
||||
GLint unpackAlignment = 4;
|
||||
GLint unpackRowLength = 0;
|
||||
GLint unpackImageHeight = 0;
|
||||
GLint unpackSkipPixels = 0;
|
||||
GLint unpackSkipRows = 0;
|
||||
GLint unpackSkipImages = 0;
|
||||
GLint pixelPackBuffer = 0;
|
||||
GLint pixelUnpackBuffer = 0;
|
||||
GLint imageName = 0;
|
||||
GLint imageLevel = 0;
|
||||
GLint imageLayered = 0;
|
||||
@@ -166,6 +181,16 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
gl.glGetIntegerv(GL_TEXTURE_BINDING_2D_ARRAY, &state.texture2DArray);
|
||||
gl.glGetIntegerv(GL_PACK_ALIGNMENT, &state.packAlignment);
|
||||
gl.glGetIntegerv(GL_PACK_ROW_LENGTH, &state.packRowLength);
|
||||
gl.glGetIntegerv(GL_PACK_SKIP_PIXELS, &state.packSkipPixels);
|
||||
gl.glGetIntegerv(GL_PACK_SKIP_ROWS, &state.packSkipRows);
|
||||
gl.glGetIntegerv(GL_UNPACK_ALIGNMENT, &state.unpackAlignment);
|
||||
gl.glGetIntegerv(GL_UNPACK_ROW_LENGTH, &state.unpackRowLength);
|
||||
gl.glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, &state.unpackImageHeight);
|
||||
gl.glGetIntegerv(GL_UNPACK_SKIP_PIXELS, &state.unpackSkipPixels);
|
||||
gl.glGetIntegerv(GL_UNPACK_SKIP_ROWS, &state.unpackSkipRows);
|
||||
gl.glGetIntegerv(GL_UNPACK_SKIP_IMAGES, &state.unpackSkipImages);
|
||||
gl.glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &state.pixelPackBuffer);
|
||||
gl.glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &state.pixelUnpackBuffer);
|
||||
if (gl.glGetFloatv != nullptr) {
|
||||
gl.glGetFloatv(GL_COLOR_CLEAR_VALUE, state.clearColor);
|
||||
}
|
||||
@@ -221,6 +246,18 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
if (gl.glPixelStorei != nullptr) {
|
||||
gl.glPixelStorei(GL_PACK_ALIGNMENT, state.packAlignment);
|
||||
gl.glPixelStorei(GL_PACK_ROW_LENGTH, state.packRowLength);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_PIXELS, state.packSkipPixels);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_ROWS, state.packSkipRows);
|
||||
gl.glPixelStorei(GL_UNPACK_ALIGNMENT, state.unpackAlignment);
|
||||
gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, state.unpackRowLength);
|
||||
gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, state.unpackImageHeight);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, state.unpackSkipPixels);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, state.unpackSkipRows);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, state.unpackSkipImages);
|
||||
}
|
||||
if (gl.glBindBuffer != nullptr) {
|
||||
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(state.pixelPackBuffer));
|
||||
gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(state.pixelUnpackBuffer));
|
||||
}
|
||||
if (gl.glClearColor != nullptr) {
|
||||
gl.glClearColor(state.clearColor[0], state.clearColor[1], state.clearColor[2],
|
||||
@@ -1886,6 +1923,199 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ===================== PACKED16 COPY-IMAGE FIELD ORDER =====================
|
||||
|
||||
constexpr const char* kPacked16CopyProbeName = "packed16 copy-image field order";
|
||||
|
||||
// The shape the KHR-GL4x.copy_image failures pin, verbatim: on the affected Mali only a
|
||||
// 2D array whose base level is 30x30x12 showed the divergence at LEVEL 1 (the same
|
||||
// suite's level-0 copies and a 14x14 base's level 1 round-trip clean), so the probe
|
||||
// reproduces those dimensions rather than a minimal shape that might sit on the clean
|
||||
// side of whatever allocation threshold picks the driver's layout.
|
||||
constexpr GLsizei kPacked16BaseSize = 30;
|
||||
constexpr GLsizei kPacked16Layers = 12;
|
||||
constexpr GLsizei kPacked16DstSize = 7;
|
||||
|
||||
// One GL_RGB5_A1 texel, as the client word the probe uploads everywhere:
|
||||
// (R, G, B, A) = (0, 1, 3, 1) under GL_UNSIGNED_SHORT_5_5_5_1. Chosen because 5551 is
|
||||
// the one 16-bit packed layout whose field widths are not a palindrome - its mirror
|
||||
// fixes the DIRECTION of the swap - and because this word's mirror differs in every
|
||||
// channel including alpha, so no expansion rounding can confuse the two predictions.
|
||||
constexpr Uint16 kPacked16Word = 0x0047;
|
||||
// What an FBO readback answers for the word, as UNorm8: (0, 1, 3) / 31 and alpha 1.
|
||||
constexpr GLubyte kPacked16Expected[4] = {0, 8, 25, 255};
|
||||
// The same readback when the stored bits are the mirrored re-encoding: 0x0047 decoded
|
||||
// as 5_5_5_1 and re-encoded as 1_5_5_5_REV is 0x8C20, which the destination's non-REV
|
||||
// layout then decodes as (17, 16, 16) / 31 with alpha 0. This is byte-for-byte the
|
||||
// arithmetic behind every failing CTS body (src 0x0047 -> got 0x8C20).
|
||||
constexpr GLubyte kPacked16Mirrored[4] = {140, 132, 132, 0};
|
||||
// A 5-bit step is 255/31 ~ 8.2 UNorm8 codes; half a step accepts every 5-bit-to-8-bit
|
||||
// expansion a driver uses (floor, round, bit replication) while still telling two
|
||||
// adjacent 5-bit values apart.
|
||||
constexpr Int kPacked16Tolerance = 4;
|
||||
|
||||
// A 2-level GL_RGB5_A1 2D array allocated the way MobileGL's own mutable-texture path
|
||||
// allocates one (glTexImage3D per level), with every texel of both levels holding
|
||||
// kPacked16Word. MAX_LEVEL is clamped so the two-level chain is complete - some
|
||||
// drivers refuse glCopyImageSubData on an incomplete texture.
|
||||
GLuint MakePacked16ArrayTexture(const GLESFunctionsTable& gl) {
|
||||
GLuint texture = 0;
|
||||
gl.glGenTextures(1, &texture);
|
||||
if (texture == 0) return 0;
|
||||
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 1);
|
||||
for (GLint level = 0; level < 2; ++level) {
|
||||
const GLsizei size = kPacked16BaseSize >> level;
|
||||
const Vector<Uint16> words(
|
||||
static_cast<SizeT>(size) * static_cast<SizeT>(size) * kPacked16Layers, kPacked16Word);
|
||||
gl.glTexImage3D(GL_TEXTURE_2D_ARRAY, level, GL_RGB5_A1, size, size, kPacked16Layers, 0,
|
||||
GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, words.data());
|
||||
}
|
||||
gl.glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A one-level GL_RGB5_A1 2D destination filled with 0xFFFF - the CTS's own (1,1,1,1)
|
||||
// destination fill - so a copy that silently did nothing reads as "no verdict" rather
|
||||
// than as either prediction.
|
||||
GLuint MakePacked16DstTexture(const GLESFunctionsTable& gl) {
|
||||
GLuint texture = 0;
|
||||
gl.glGenTextures(1, &texture);
|
||||
if (texture == 0) return 0;
|
||||
gl.glBindTexture(GL_TEXTURE_2D, texture);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
const Vector<Uint16> fill(static_cast<SizeT>(kPacked16DstSize) * kPacked16DstSize, Uint16{0xFFFF});
|
||||
gl.glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5_A1, kPacked16DstSize, kPacked16DstSize, 0, GL_RGBA,
|
||||
GL_UNSIGNED_SHORT_5_5_5_1, fill.data());
|
||||
gl.glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// The destination's texel (0, 0), through a framebuffer of its own. False when the
|
||||
// attachment is incomplete or the read errors - both are declines, not verdicts.
|
||||
Bool ReadPacked16DstTexel(const GLESFunctionsTable& gl, GLuint texture, GLubyte out[4]) {
|
||||
GLuint framebuffer = 0;
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
if (framebuffer == 0) return false;
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
gl.glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||
Bool read = false;
|
||||
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE) {
|
||||
gl.glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
Drain(gl);
|
||||
gl.glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||
read = gl.glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
Drain(gl);
|
||||
return read;
|
||||
}
|
||||
|
||||
// Copies a kPacked16DstSize-square region out of layer 0 of the array's `sourceLevel`
|
||||
// onto a freshly filled 2D destination and hands back the destination's texel (0, 0).
|
||||
// False when the copy raised an error or the readback could not run.
|
||||
Bool Packed16CopyLandsTexel(const GLESFunctionsTable& gl, GLuint array, GLint sourceLevel,
|
||||
GLubyte out[4]) {
|
||||
const GLuint destination = MakePacked16DstTexture(gl);
|
||||
if (destination == 0) return false;
|
||||
Drain(gl);
|
||||
gl.glCopyImageSubData(array, GL_TEXTURE_2D_ARRAY, sourceLevel, 0, 0, 0, destination,
|
||||
GL_TEXTURE_2D, 0, 0, 0, 0, kPacked16DstSize, kPacked16DstSize, 1);
|
||||
const Bool copied = gl.glGetError() == GL_NO_ERROR;
|
||||
const Bool read = copied && ReadPacked16DstTexel(gl, destination, out);
|
||||
gl.glDeleteTextures(1, &destination);
|
||||
Drain(gl);
|
||||
return read;
|
||||
}
|
||||
|
||||
Bool Packed16TexelNear(const GLubyte got[4], const GLubyte want[4]) {
|
||||
for (Int i = 0; i < 4; ++i) {
|
||||
const Int delta = static_cast<Int>(got[i]) - static_cast<Int>(want[i]);
|
||||
if (delta > kPacked16Tolerance || delta < -kPacked16Tolerance) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool ProbeCopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) {
|
||||
if (!gl.glGenTextures || !gl.glBindTexture || !gl.glTexParameteri || !gl.glTexImage2D ||
|
||||
!gl.glTexImage3D || !gl.glDeleteTextures || !gl.glCopyImageSubData || !gl.glGenFramebuffers ||
|
||||
!gl.glBindFramebuffer || !gl.glFramebufferTexture2D || !gl.glCheckFramebufferStatus ||
|
||||
!gl.glDeleteFramebuffers || !gl.glReadBuffer || !gl.glReadPixels || !gl.glPixelStorei ||
|
||||
!gl.glGetError) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SavedState saved;
|
||||
Save(gl, saved);
|
||||
// The uploads and readbacks below run under the probe's own tight pixel-transfer
|
||||
// scope - a caller's skip/row-length/PBO would shear the probe's data into a false
|
||||
// verdict either way. Restore puts the caller's scope back with the rest.
|
||||
gl.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
gl.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
|
||||
gl.glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
|
||||
gl.glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
gl.glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
|
||||
gl.glPixelStorei(GL_PACK_SKIP_ROWS, 0);
|
||||
if (gl.glBindBuffer != nullptr) {
|
||||
gl.glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
gl.glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
}
|
||||
Drain(gl);
|
||||
|
||||
Bool detected = false;
|
||||
const GLuint array = MakePacked16ArrayTexture(gl);
|
||||
GLubyte control[4] = {0, 0, 0, 0};
|
||||
GLubyte subject[4] = {0, 0, 0, 0};
|
||||
// THE CONTROL: the identical copy out of the array's LEVEL 0, which is clean on the
|
||||
// affected driver too. It proves glCopyImageSubData works between a 5551 array and a
|
||||
// 5551 2D image at all, that the upload and the FBO readback round-trip the word, and
|
||||
// that only the mip level moves the answer - so a driver with no copy_image, or none
|
||||
// for these formats, reaches no verdict instead of being reported as this.
|
||||
if (array == 0 || !Packed16CopyLandsTexel(gl, array, 0, control)) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the level-0 control copy could not run)",
|
||||
kPacked16CopyProbeName);
|
||||
} else if (!Packed16TexelNear(control, kPacked16Expected)) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the level-0 control read back "
|
||||
"(%d, %d, %d, %d) instead of the uploaded word's (%d, %d, %d, %d))",
|
||||
kPacked16CopyProbeName, control[0], control[1], control[2], control[3],
|
||||
kPacked16Expected[0], kPacked16Expected[1], kPacked16Expected[2], kPacked16Expected[3]);
|
||||
} else if (!Packed16CopyLandsTexel(gl, array, 1, subject)) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the level-1 subject copy could not run)",
|
||||
kPacked16CopyProbeName);
|
||||
} else if (Packed16TexelNear(subject, kPacked16Mirrored)) {
|
||||
detected = true;
|
||||
MGLOG_I("[driver-bug] %s probe: a copy out of the array's level 1 delivered "
|
||||
"(%d, %d, %d, %d), the 1_5_5_5_REV re-encoding of the word - THE FIELD ORDER "
|
||||
"OF A NON-ZERO ARRAY MIP LEVEL IS MIRRORED",
|
||||
kPacked16CopyProbeName, subject[0], subject[1], subject[2], subject[3]);
|
||||
} else if (!Packed16TexelNear(subject, kPacked16Expected)) {
|
||||
MGLOG_I("[driver-bug] %s probe reached no verdict (the level-1 copy read back "
|
||||
"(%d, %d, %d, %d), which is neither the word nor its mirror)",
|
||||
kPacked16CopyProbeName, subject[0], subject[1], subject[2], subject[3]);
|
||||
}
|
||||
if (array != 0) gl.glDeleteTextures(1, &array);
|
||||
Restore(gl, saved);
|
||||
return detected;
|
||||
}
|
||||
|
||||
Bool CopyImageMirrorsPacked16FieldOrder(const GLESFunctionsTable& gl) {
|
||||
// One driver per process, and the answer is structural (the driver's storage layout
|
||||
// for a shape), not sampled.
|
||||
static const Bool mirrored = ProbeCopyImageMirrorsPacked16FieldOrder(gl);
|
||||
return mirrored;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Optional<DriverBugFinding> ProbeExplicitVertexInputLocationCeilingBug(const GLESFunctionsTable& gl) {
|
||||
const VertexInputLocationCeilingMeasurement& measurement = ExplicitVertexInputLocationCeiling(gl);
|
||||
@@ -2053,6 +2283,25 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
Move(detail)};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbeCopyImagePacked16FieldOrderBug(const GLESFunctionsTable& gl) {
|
||||
if (!CopyImageMirrorsPacked16FieldOrder(gl)) return std::nullopt;
|
||||
return DriverBugFinding{
|
||||
"glCopyImageSubData mirrors 16-bit packed texels at a non-zero array mip level",
|
||||
DriverBugVerdict::Fixed,
|
||||
"the driver's physical field order for a 16-bit packed texel (RGB565 / RGB5_A1 / "
|
||||
"RGBA4) at a non-zero mip level of a GL_TEXTURE_2D_ARRAY is the *_REV mirror of "
|
||||
"the order every other image uses, so a glCopyImageSubData - a raw texel-block "
|
||||
"move - between such a level and any other image lands the R/G/B/A fields "
|
||||
"reversed (a 5551 word 0x0047 arrives as 0x8C20). Uploads and readbacks of the "
|
||||
"same level are clean - the driver decodes its own layout consistently, which is "
|
||||
"this probe's control - so only the raw-copy path ever crosses the two layouts. "
|
||||
"MobileGL stores these three formats as 8-bit-per-channel ES storage on this "
|
||||
"driver instead (GL_RGB8 / GL_RGBA8, the storage their canonical shadow already "
|
||||
"holds and the client word round-trips through exactly), so no 16-bit packed "
|
||||
"image is left for a copy to disagree about, at twice the memory for images of "
|
||||
"those formats"};
|
||||
}
|
||||
|
||||
// The table. One row per known driver bug; see the header for how to add a sibling.
|
||||
using DriverBugProbeFn = Optional<DriverBugFinding> (*)(const GLESFunctionsTable&);
|
||||
constexpr DriverBugProbeFn kGlesDriverBugProbes[] = {
|
||||
@@ -2064,6 +2313,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
&ProbeExplicitVertexInputLocationCeilingBug,
|
||||
&ProbeLayeredBlitDestinationBug,
|
||||
&ProbeLocatedIoBlockPayloadBug,
|
||||
&ProbeCopyImagePacked16FieldOrderBug,
|
||||
};
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -299,6 +299,35 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
const ImageCoherencyResidualMeasurement& ImageWriteReadCoherencyResidual(
|
||||
const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Copies one known GL_UNSIGNED_SHORT_5_5_5_1 word out of a GL_RGB5_A1 2D array's mip
|
||||
// level 1 into a plain 2D image with glCopyImageSubData and reads the landed texel back.
|
||||
// Returns true only when the level-1 copy delivers the word's 5_5_5_1 <-> 1_5_5_5_REV
|
||||
// field-order mirror while the identical level-0 copy delivers the word itself.
|
||||
//
|
||||
// The affected Mali stores 16-bit packed texels (RGB565 / RGB5_A1 / RGBA4) at a non-zero
|
||||
// mip level of a 2D array in the *_REV field order every other image does NOT use.
|
||||
// Uploads and readbacks decode that layout consistently, so nothing but a raw texel-block
|
||||
// move can see it - which is exactly what glCopyImageSubData is defined to be, and why
|
||||
// the whole KHR-GL4x.copy_image rgb5/rgb5_a1/rgba4 x *2d_array* matrix fails there while
|
||||
// every other suite touching these formats passes. The texture reproduces the failing
|
||||
// shape verbatim (a 30x30x12 two-level array; a 14x14 base's level 1 measured clean on
|
||||
// the same driver, so a minimal shape might not manifest the layout).
|
||||
//
|
||||
// THE CONTROL is the identical copy out of mip level 0, which is clean on the affected
|
||||
// driver too: it proves copy_image works between these images at all and that the
|
||||
// upload/readback round trip is exact, so a driver that cannot host the shape reaches no
|
||||
// verdict instead of being reported as this. The subject must also match the mirror
|
||||
// PREDICTION, not merely differ from the expectation - a copy that delivered anything
|
||||
// else is a different defect and reaches no verdict either. Restores every piece of GL
|
||||
// state it touches.
|
||||
Bool ProbeCopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeCopyImageMirrorsPacked16FieldOrder(), evaluated at most once per process. The
|
||||
// DirectGLES format normalization consults this to decide whether the three 16-bit packed
|
||||
// normalized formats must be stored as 8-bit-per-channel ES storage (see
|
||||
// PixelFormatNormalizeOptionBit::WidenPacked16Norm).
|
||||
Bool CopyImageMirrorsPacked16FieldOrder(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Every known driver bug this GLES driver actually has. Bugs it does not have are absent,
|
||||
// so an unaffected device renders an empty section rather than a wall of "not affected".
|
||||
Vector<DriverBugFinding> CollectGlesKnownDriverBugs(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
@@ -340,17 +340,38 @@ namespace MobileGL::MG_Util::TextureFormatProcessor {
|
||||
// (VkTextureManager::ResolveTextureFormatInfo resolves all six legacy low-bit formats
|
||||
// to R8G8B8A8_UNORM), so the two backends now agree here.
|
||||
//
|
||||
// Only the DESKTOP-ONLY formats move. GL_RGBA4 and GL_RGB5_A1 are ES formats an
|
||||
// application can legitimately ask for - the same normalization picks the storage for
|
||||
// glRenderbufferStorage - so widening them would be a memory decision, not a
|
||||
// correctness one. Nothing about the REPORTED precision moves either way:
|
||||
// GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on the
|
||||
// requested format, not on the ES storage.
|
||||
// Only the DESKTOP-ONLY formats move UNCONDITIONALLY. GL_RGBA4, GL_RGB5_A1 and
|
||||
// GL_RGB565 are ES formats an application can legitimately ask for - the same
|
||||
// normalization picks the storage for glRenderbufferStorage - so widening them
|
||||
// used to be declined as "a memory decision, not a correctness one". The 18
|
||||
// KHR-GL4x.copy_image.functional bodies on Mali falsified that: the driver's own
|
||||
// 16-bit packed storage keeps a MIRRORED field order at a non-zero mip level of a
|
||||
// 2D array, so a raw glCopyImageSubData between such a level and any other image
|
||||
// delivers the channels reversed (0x0007 -> 0x3800 for a 5551 word: the 1_5_5_5_REV
|
||||
// re-encoding of the same fields). Where that is measured -
|
||||
// WidenPacked16Norm, set from the POST probe or its ForceOn override - the three
|
||||
// formats take the same 8-bit widening; everywhere else they stay narrow and the
|
||||
// memory argument stands. Nothing about the REPORTED precision moves either way:
|
||||
// GL_TEXTURE_*_SIZE and glGetInternalformativ answer from TextureMetrics, keyed on
|
||||
// the requested format, not on the ES storage.
|
||||
case GL_R3_G3_B2:
|
||||
case GL_RGB4:
|
||||
case GL_RGB5:
|
||||
*outInternalFormat = GL_RGB8;
|
||||
break;
|
||||
// GL_RGB5 above is nominally the same resolution, but a TEXTURE never arrives here
|
||||
// as GL_RGB5: ConvertGLEnumToTextureInternalFormat folds GL_RGB5 and GL_RGB565 onto
|
||||
// one logical format whose GL spelling is GL_RGB565, so this case is the one the
|
||||
// allocation path actually reaches for both spellings.
|
||||
case GL_RGB565:
|
||||
*outInternalFormat =
|
||||
(options & PixelFormatNormalizeOptionBit::WidenPacked16Norm) ? GL_RGB8 : internalFormat;
|
||||
break;
|
||||
case GL_RGB5_A1:
|
||||
case GL_RGBA4:
|
||||
*outInternalFormat =
|
||||
(options & PixelFormatNormalizeOptionBit::WidenPacked16Norm) ? GL_RGBA8 : internalFormat;
|
||||
break;
|
||||
case GL_RGB10:
|
||||
case GL_RGB12:
|
||||
*outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoNorm16) ||
|
||||
|
||||
@@ -44,6 +44,17 @@ namespace MobileGL {
|
||||
// IS exact - every value in [-127, 127] divided by 127 round-trips through a half - so the
|
||||
// substitute matches what the always-on GL_RGBA8_SNORM fallback already picks.
|
||||
NoSnorm8RenderTarget = 1 << 9,
|
||||
// Store the three 16-bit packed normalized formats (GL_RGB565, GL_RGB5_A1, GL_RGBA4)
|
||||
// as 8-bit-per-channel ES storage (GL_RGB8 / GL_RGBA8), the way the desktop-only
|
||||
// narrow formats already are. Set by DirectGLES when the driver's 16-bit packed
|
||||
// storage cannot be trusted as a raw-copy endpoint: some Mali drivers keep a
|
||||
// MIRRORED field order for these texels at a non-zero mip level of a 2D array, so
|
||||
// glCopyImageSubData (a raw texel-block move) delivers the channels reversed. The
|
||||
// (format, type) transfer pair does not move with the bit - it is already the
|
||||
// UNorm8 component layout the canonical shadow holds for all three formats.
|
||||
// Reported precision does not move either: GL_TEXTURE_*_SIZE and
|
||||
// glGetInternalformativ answer from TextureMetrics, keyed on the requested format.
|
||||
WidenPacked16Norm = 1 << 10,
|
||||
None = 0,
|
||||
};
|
||||
namespace MG_Util::TextureFormatProcessor {
|
||||
|
||||
Reference in New Issue
Block a user