mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
315 lines
16 KiB
C++
315 lines
16 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageFormatQualifierScenario.cpp
|
|
// Copyright (c) 2025-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 - AN IMAGE UNIFORM THAT DECLARES NO FORMAT.
|
|
//
|
|
// Desktop GLSL 4.2 lets a writeonly image declaration omit its format layout qualifier:
|
|
//
|
|
// writeonly uniform uimage2D uni_image; // legal desktop GLSL
|
|
//
|
|
// GLSL ES has no such relaxation; every image uniform must carry one, and Adreno says so as "all
|
|
// images have to define layout format", which fails the whole program. That is what took the
|
|
// compute half of KHR-GL4x.packed_depth_stencil.stencil_texturing.
|
|
//
|
|
// The only qualifier that is CORRECT to substitute is whatever glBindImageTexture named for the
|
|
// unit that uniform addresses - GL requires the qualifier, the bind format and the texture's
|
|
// internal format to belong to one format class - so the format is not knowable when the shader
|
|
// is compiled, only when it is drawn with. Espryt therefore BAKES it into the program it
|
|
// generates and keys that program on the (unit, format) pairs it baked
|
|
// (BackendProgramObjectImpl::ImageUnitFormatsStillMatch, MG_Backend/DirectGLES).
|
|
//
|
|
// Three separate things follow from "the program is built against live binding state", and each
|
|
// one is a case below:
|
|
//
|
|
// 1. the format reaches the shader at all, so the store lands where the texture is (Writes);
|
|
// 2. binding a DIFFERENT format to the same unit rebuilds the program, rather than reusing one
|
|
// compiled against the old format (RebindToADifferentFormatRebuilds);
|
|
// 3. an image bound for the FIRST time after the link works, i.e. the program built against
|
|
// "nothing bound yet" is not the one the dispatch runs (FirstBindAfterLinkRebuilds).
|
|
//
|
|
// Magma needs none of this - Vulkan takes an Unknown-format storage image given
|
|
// shaderStorageImageWriteWithoutFormat, and the view format is resolved from the same bind state
|
|
// at descriptor time - so every case here runs on both backends and must agree, which is what
|
|
// makes the ES-only machinery falsifiable rather than merely exercised.
|
|
|
|
#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 kExtent = 4;
|
|
// The image unit is deliberately NOT 0 and the uniform declares no binding, so the unit
|
|
// has to travel through glUniform1i and be baked into the ESSL alongside the format -
|
|
// the two bakes share a rebuild key and a bug in either shows up as the wrong texel.
|
|
constexpr GLint kImageUnit = 1;
|
|
|
|
// KHR-GL4x.packed_depth_stencil.stencil_texturing's own image declaration, verbatim.
|
|
const char* kStoreSource = R"(#version 430 core
|
|
|
|
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
|
|
|
writeonly uniform uimage2D uni_image;
|
|
|
|
void main()
|
|
{
|
|
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
|
}
|
|
)";
|
|
|
|
class ImageFormatQualifierScenario : public ScenarioTest {
|
|
protected:
|
|
void TearDown() override {
|
|
if (!Ready()) return;
|
|
glUseProgram(0);
|
|
for (GLuint p : m_programs) glDeleteProgram(p);
|
|
for (GLuint t : m_textures) glDeleteTextures(1, &t);
|
|
m_programs.clear();
|
|
m_textures.clear();
|
|
GLint maxImageUnits = 0;
|
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
|
for (GLint unit = 0; unit < maxImageUnits; ++unit) {
|
|
glBindImageTexture(static_cast<GLuint>(unit), 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
|
|
}
|
|
while (glGetError() != GL_NO_ERROR) {
|
|
}
|
|
}
|
|
|
|
bool ImagesAreUsable() const {
|
|
GLint maxImageUnits = 0;
|
|
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
|
GLint maxComputeImageUniforms = 0;
|
|
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
|
while (glGetError() != GL_NO_ERROR) {
|
|
}
|
|
return maxImageUnits > kImageUnit && maxComputeImageUniforms >= 1;
|
|
}
|
|
|
|
GLuint MakeComputeProgram(const std::string& source) {
|
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
const char* text = source.c_str();
|
|
glShaderSource(shader, 1, &text, nullptr);
|
|
glCompileShader(shader);
|
|
GLint compiled = GL_FALSE;
|
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
if (compiled == GL_FALSE) {
|
|
char log[4096] = {};
|
|
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
|
ADD_FAILURE() << "the compute shader did not compile: " << log;
|
|
glDeleteShader(shader);
|
|
return 0;
|
|
}
|
|
const GLuint program = glCreateProgram();
|
|
m_programs.push_back(program);
|
|
glAttachShader(program, shader);
|
|
glLinkProgram(program);
|
|
glDeleteShader(shader);
|
|
GLint linked = GL_FALSE;
|
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
if (linked == GL_FALSE) {
|
|
char log[4096] = {};
|
|
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
|
ADD_FAILURE() << "the compute program did not link: " << log;
|
|
return 0;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
GLuint MakeTexture(GLenum internalFormat) {
|
|
GLuint texture = 0;
|
|
glGenTextures(1, &texture);
|
|
m_textures.push_back(texture);
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kExtent, kExtent);
|
|
if (const GLenum error = FirstGLError()) {
|
|
ADD_FAILURE() << "allocating storage errored with " << GLErrorName(error);
|
|
return 0;
|
|
}
|
|
// Seeded to a value no dispatch writes, so "the store never happened" and "the
|
|
// store wrote the right thing" cannot be confused.
|
|
const std::vector<GLuint> zeros(static_cast<std::size_t>(kExtent) * kExtent * 4u, 0u);
|
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kExtent, kExtent,
|
|
internalFormat == GL_RGBA32UI ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
|
zeros.data());
|
|
while (glGetError() != GL_NO_ERROR) {
|
|
}
|
|
return texture;
|
|
}
|
|
|
|
// Texel (x, 0) of the texture's red channel, read back through the GL frontend rather
|
|
// than through a second image uniform: a defect in the format bake would be shared by
|
|
// a reader declared the same way and could cancel itself out.
|
|
GLuint ReadRedTexel(GLuint texture, GLenum internalFormat, int x) {
|
|
const bool rgba = internalFormat == GL_RGBA32UI;
|
|
std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * (rgba ? 4u : 1u),
|
|
0xFFFFFFFFu);
|
|
glBindTexture(GL_TEXTURE_2D, texture);
|
|
glGetTexImage(GL_TEXTURE_2D, 0, rgba ? GL_RGBA_INTEGER : GL_RED_INTEGER, GL_UNSIGNED_INT,
|
|
texels.data());
|
|
if (const GLenum error = FirstGLError()) {
|
|
ADD_FAILURE() << "reading the image back errored with " << GLErrorName(error);
|
|
return 0xFFFFFFFFu;
|
|
}
|
|
return texels[static_cast<std::size_t>(x) * (rgba ? 4u : 1u)];
|
|
}
|
|
|
|
void DispatchStore(GLuint program, GLuint texture, GLenum internalFormat) {
|
|
glBindImageTexture(static_cast<GLuint>(kImageUnit), texture, 0, GL_FALSE, 0, GL_WRITE_ONLY,
|
|
internalFormat);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "glBindImageTexture errored";
|
|
glUseProgram(program);
|
|
const GLint location = glGetUniformLocation(program, "uni_image");
|
|
ASSERT_GE(location, 0) << "the image uniform was not reflected";
|
|
glUniform1i(location, kImageUnit);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image unit errored";
|
|
glDispatchCompute(kExtent, 1, 1);
|
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
|
glUseProgram(0);
|
|
}
|
|
|
|
std::vector<GLuint> m_programs;
|
|
std::vector<GLuint> m_textures;
|
|
};
|
|
|
|
// The defect itself. Without the bake the ES driver refuses the program outright and the
|
|
// texture keeps its seed - which is also exactly what a silently no-op dispatch looks
|
|
// like, and why the seed is a value no store writes.
|
|
TEST_F(ImageFormatQualifierScenario, AFormatlessWriteonlyImageWrites) {
|
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
|
|
|
const GLuint program = MakeComputeProgram(kStoreSource);
|
|
const GLuint texture = MakeTexture(GL_R32UI);
|
|
if (program == 0 || texture == 0) return;
|
|
|
|
DispatchStore(program, texture, GL_R32UI);
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
|
<< "texel " << x << " of a format-less writeonly image did not take the store";
|
|
}
|
|
}
|
|
|
|
// The rebuild key. The SAME program is dispatched twice with a different format bound to
|
|
// its unit; a build keyed only on the link (or only on the image UNIT) would reuse the
|
|
// r32ui program for the rgba32ui texture, and the second half would come back seeded.
|
|
//
|
|
// What the SOFTWARE lanes cannot falsify: with the key disabled this case still passes on
|
|
// Mesa, because the reused r32ui declaration writes the red channel of an RGBA32UI image
|
|
// anyway - a format-class mismatch GL leaves undefined and that driver happens to absorb.
|
|
// FirstBindAfterLinkRebuilds below is the case that fails there, because the reused
|
|
// program was built with no format at all and never compiled. Both are kept: this one is
|
|
// the shape a strict driver is entitled to reject, and it is the shape the device runs.
|
|
TEST_F(ImageFormatQualifierScenario, RebindToADifferentFormatRebuilds) {
|
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
|
|
|
const GLuint program = MakeComputeProgram(kStoreSource);
|
|
const GLuint first = MakeTexture(GL_R32UI);
|
|
const GLuint second = MakeTexture(GL_RGBA32UI);
|
|
if (program == 0 || first == 0 || second == 0) return;
|
|
|
|
DispatchStore(program, first, GL_R32UI);
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
ASSERT_EQ(ReadRedTexel(first, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
|
<< "the first format must work before the rebind can be blamed for anything";
|
|
}
|
|
|
|
DispatchStore(program, second, GL_RGBA32UI);
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
EXPECT_EQ(ReadRedTexel(second, GL_RGBA32UI, x), static_cast<GLuint>(x) + 100u)
|
|
<< "texel " << x << ": the program was not rebuilt for the newly bound format";
|
|
}
|
|
|
|
// ...and back, so the rebuild is not a one-way door: returning to a format the
|
|
// program was once built against must build for it again, not resurrect a cache row.
|
|
const GLuint third = MakeTexture(GL_R32UI);
|
|
if (third == 0) return;
|
|
DispatchStore(program, third, GL_R32UI);
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
EXPECT_EQ(ReadRedTexel(third, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
|
<< "texel " << x << ": going back to the first format did not rebuild";
|
|
}
|
|
}
|
|
|
|
// Nothing is bound to the unit when the program links, so whatever the first build sees
|
|
// is not the format the dispatch needs. glBindImageTexture must not itself trigger a
|
|
// build - it is an entry point, and building there is the constraint
|
|
// glShaderStorageBlockBinding is held to as well - so the rebuild has to happen at the
|
|
// next dispatch preparation instead. This case fails either way round: no rebuild, or a
|
|
// build attempted from the entry point before the state settles.
|
|
TEST_F(ImageFormatQualifierScenario, FirstBindAfterLinkRebuilds) {
|
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
|
|
|
const GLuint program = MakeComputeProgram(kStoreSource);
|
|
if (program == 0) return;
|
|
|
|
// Use it once with NOTHING bound to the unit, which is what makes the backend build
|
|
// against an empty binding. The dispatch writes nowhere and must not error.
|
|
glUseProgram(program);
|
|
const GLint location = glGetUniformLocation(program, "uni_image");
|
|
ASSERT_GE(location, 0);
|
|
glUniform1i(location, kImageUnit);
|
|
glDispatchCompute(kExtent, 1, 1);
|
|
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "dispatching with an unbound image unit must not error";
|
|
glUseProgram(0);
|
|
|
|
const GLuint texture = MakeTexture(GL_R32UI);
|
|
if (texture == 0) return;
|
|
DispatchStore(program, texture, GL_R32UI);
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
|
<< "texel " << x << ": the first bind after the link did not reach the shader";
|
|
}
|
|
}
|
|
|
|
// A DECLARED format is authoritative and the bake must never touch it - including when
|
|
// the texture behind the unit has a different (but class-compatible) internal format,
|
|
// which GL explicitly allows. If the bake ever overrode a declaration, this is the case
|
|
// that would go wrong while every other one stayed green.
|
|
TEST_F(ImageFormatQualifierScenario, ADeclaredFormatStillWins) {
|
|
if (!Ready()) GTEST_SKIP() << "no GL context";
|
|
if (!ImagesAreUsable()) GTEST_SKIP() << "no image load/store on this driver";
|
|
|
|
const GLuint program = MakeComputeProgram(R"(#version 430 core
|
|
|
|
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
|
|
|
|
layout (r32ui) writeonly uniform uimage2D uni_image;
|
|
|
|
void main()
|
|
{
|
|
imageStore(uni_image, ivec2(gl_GlobalInvocationID.xy), uvec4(gl_GlobalInvocationID.x + 100u, 0u, 0u, 0u));
|
|
}
|
|
)");
|
|
const GLuint texture = MakeTexture(GL_R32UI);
|
|
if (program == 0 || texture == 0) return;
|
|
|
|
DispatchStore(program, texture, GL_R32UI);
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
EXPECT_EQ(ReadRedTexel(texture, GL_R32UI, x), static_cast<GLuint>(x) + 100u)
|
|
<< "texel " << x << ": a declared format stopped working";
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|