mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[Fix, Test] (MG_Util, MG_Backend/DirectGLES, MG_Backend/DirectVulkan): the two image target kinds a compute dispatch could not read - 1D-array on ES, imageBuffer on Vulkan
This commit is contained in:
@@ -69,6 +69,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/SwizzleAccessRoutineScenario.cpp
|
||||
Scenarios/ProgramPipelineScenario.cpp
|
||||
Scenarios/ImageLoadStoreSsoScenario.cpp
|
||||
Scenarios/ImageTargetKindScenario.cpp
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageTargetKindScenario.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 - ONE IMAGE TARGET KIND AT A TIME, THROUGH A COMPUTE DISPATCH.
|
||||
//
|
||||
// KHR-GL44.multi_bind.dispatch_bind_image_textures decomposed. That conformance case declares
|
||||
// ELEVEN image uniforms of eleven different target kinds in one compute shader, binds a texture
|
||||
// of the matching kind to each unit, sums one texel from every one of them and compares the sum
|
||||
// against N*(N-1)/2. It is a single pass/fail bit over eleven independent mechanisms: if any one
|
||||
// of them is wrong - or merely fails to compile - the case fails and says nothing about which.
|
||||
// That is what it did here, on both backends, for two waves.
|
||||
//
|
||||
// So the eleven are pulled apart into one case each. Each case declares ONE image uniform, binds
|
||||
// ONE texture and checks the value that comes back, so a failure names the target kind and the
|
||||
// direction. What the conformance case does with eleven at once, AllKindsInOneProgram at the
|
||||
// bottom still does - a defect that only appears when several kinds share a program is invisible
|
||||
// to the single-kind cases by construction.
|
||||
//
|
||||
// The shape is deliberately the conformance case's own, not a cleaner equivalent:
|
||||
//
|
||||
// * r32ui / GL_R32UI throughout, 6x6x6 storage, one level, texel (0,0,0) read;
|
||||
// * `layout (location = N, r32ui) readonly uniform` - an explicit uniform LOCATION, not a
|
||||
// binding, with the image unit then assigned by glUniform1i. That combination is the one ES
|
||||
// cannot express directly, because ES forbids glUniform1i on an image uniform and the unit
|
||||
// has to be baked into the generated ESSL (RebindImageUniformsToFrontendUnits);
|
||||
// * `layout (std140, ...) buffer` for the result block - legal, but unusual enough that a
|
||||
// frontend could plausibly mishandle it. Mirroring it means a green scenario cannot be green
|
||||
// for a reason the conformance case excludes;
|
||||
// * glBindImageTexture with layered = GL_TRUE, which is what glBindImageTextures is specified
|
||||
// to pass, and which is where a target kind whose layeredness a backend does not recognise
|
||||
// goes wrong.
|
||||
//
|
||||
// MULTISAMPLE is the one kind that is not merely an emulation problem, and the conformance case
|
||||
// already knows it: it reads GL_MAX_IMAGE_SAMPLES and, when that is zero, substitutes a plain 2D
|
||||
// texture and a plain uimage2D for both multisample entries. MobileGL reports zero, so the
|
||||
// conformance case never asks it for a multisample image at all. The two cases below are kept
|
||||
// and skip on that same query, so the coverage is already written the day a backend advertises
|
||||
// them - and so the skip is a standing record of WHY the conformance case passes without them.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#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 {
|
||||
|
||||
// The conformance case's own dimensions: one level, 6 on every axis (which is also
|
||||
// exactly one cube's worth for a cube array), and a single texel read at the origin.
|
||||
constexpr int kExtent = 6;
|
||||
constexpr GLuint kFilledValue = 7u;
|
||||
constexpr GLuint kStoredValue = 13u;
|
||||
|
||||
// Everything that differs between the eleven kinds, in one row.
|
||||
struct TargetKind {
|
||||
const char* name; // this scenario's name for it, which failure messages carry
|
||||
GLenum target; // the GL texture target
|
||||
const char* imageType; // the GLSL image uniform type
|
||||
const char* coord; // the coordinate expression imageLoad/imageStore takes
|
||||
bool multisample; // needs GL_MAX_IMAGE_SAMPLES > 0
|
||||
bool buffer; // storage comes from a buffer object, not TexStorage
|
||||
};
|
||||
|
||||
constexpr TargetKind kKind1D{"1D", GL_TEXTURE_1D, "uimage1D", "0", false, false};
|
||||
constexpr TargetKind kKind1DArray{"1DArray", GL_TEXTURE_1D_ARRAY, "uimage1DArray", "ivec2(0, 0)", false,
|
||||
false};
|
||||
constexpr TargetKind kKind2D{"2D", GL_TEXTURE_2D, "uimage2D", "ivec2(0, 0)", false, false};
|
||||
constexpr TargetKind kKind2DArray{"2DArray", GL_TEXTURE_2D_ARRAY, "uimage2DArray", "ivec3(0, 0, 0)", false,
|
||||
false};
|
||||
constexpr TargetKind kKind3D{"3D", GL_TEXTURE_3D, "uimage3D", "ivec3(0, 0, 0)", false, false};
|
||||
constexpr TargetKind kKindBuffer{"Buffer", GL_TEXTURE_BUFFER, "uimageBuffer", "0", false, true};
|
||||
constexpr TargetKind kKindCube{"Cube", GL_TEXTURE_CUBE_MAP, "uimageCube", "ivec3(0, 0, 0)", false, false};
|
||||
constexpr TargetKind kKindCubeArray{"CubeArray", GL_TEXTURE_CUBE_MAP_ARRAY, "uimageCubeArray",
|
||||
"ivec3(0, 0, 0)", false, false};
|
||||
constexpr TargetKind kKindRect{"Rect", GL_TEXTURE_RECTANGLE, "uimage2DRect", "ivec2(0, 0)", false, false};
|
||||
constexpr TargetKind kKind2DMS{"2DMS", GL_TEXTURE_2D_MULTISAMPLE, "uimage2DMS", "ivec2(0, 0)", true, false};
|
||||
constexpr TargetKind kKind2DMSArray{"2DMSArray", GL_TEXTURE_2D_MULTISAMPLE_ARRAY, "uimage2DMSArray",
|
||||
"ivec3(0, 0, 0)", true, false};
|
||||
|
||||
// A multisample image load/store takes the sample index as an extra argument; no other
|
||||
// kind does. Keeping that in one place stops the two spellings drifting apart.
|
||||
std::string LoadExpression(const TargetKind& kind, const std::string& name) {
|
||||
return "imageLoad(" + name + ", " + kind.coord + (kind.multisample ? ", 0)" : ")");
|
||||
}
|
||||
|
||||
std::string StoreStatement(const TargetKind& kind, const std::string& name, const char* value) {
|
||||
return "imageStore(" + name + ", " + kind.coord + (kind.multisample ? ", 0, uvec4(" : ", uvec4(") +
|
||||
value + ", 0, 0, 0));";
|
||||
}
|
||||
|
||||
const char* kComputePrologue = "#version 440 core\n"
|
||||
"\n"
|
||||
"layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n"
|
||||
"\n";
|
||||
|
||||
const char* kResultBlock = "layout (std140, binding = 0) buffer SSB {\n"
|
||||
" uint sum;\n"
|
||||
"} ssb;\n"
|
||||
"\n";
|
||||
|
||||
// The conformance case's shader, narrowed to a single image.
|
||||
std::string SingleLoadSource(const TargetKind& kind) {
|
||||
return std::string(kComputePrologue) + "layout (location = 0, r32ui) readonly uniform " + kind.imageType +
|
||||
" i0;\n" + kResultBlock + "void main()\n{\n uvec4 v = " + LoadExpression(kind, "i0") +
|
||||
";\n ssb.sum = v.r;\n}\n";
|
||||
}
|
||||
|
||||
// The other direction. Written as its own program rather than a read-write one so that a
|
||||
// backend which gets the store right and the load wrong (or the reverse) is not able to
|
||||
// cancel its own defect out.
|
||||
std::string SingleStoreSource(const TargetKind& kind) {
|
||||
return std::string(kComputePrologue) + "layout (location = 0, r32ui) writeonly uniform " +
|
||||
kind.imageType + " i0;\n\nvoid main()\n{\n " + StoreStatement(kind, "i0", "13u") + "\n}\n";
|
||||
}
|
||||
|
||||
class ImageTargetKindScenario : 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);
|
||||
for (GLuint b : m_buffers) glDeleteBuffers(1, &b);
|
||||
m_programs.clear();
|
||||
m_textures.clear();
|
||||
m_buffers.clear();
|
||||
// Leave no image unit bound. These scenarios share one context, and a stale image
|
||||
// binding is exactly the kind of state that makes the NEXT scenario's failure
|
||||
// impossible to reproduce on its own.
|
||||
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);
|
||||
}
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
||||
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 >= 1 && maxComputeImageUniforms >= 1;
|
||||
}
|
||||
|
||||
// The conformance case's own multisample gate, asked the same way it asks it.
|
||||
bool MultisampleImagesAreUsable() const {
|
||||
GLint maxImageSamples = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_SAMPLES, &maxImageSamples);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
return maxImageSamples > 0;
|
||||
}
|
||||
|
||||
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 << "\nsource:\n" << source;
|
||||
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 << "\nsource:\n" << source;
|
||||
return 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
// Storage plus a full fill with `value`, in the spelling each target kind needs.
|
||||
// Returns 0 - having already reported - when the target could not be created.
|
||||
GLuint MakeTexture(const TargetKind& kind, bool fill) {
|
||||
const std::vector<GLuint> texels(static_cast<std::size_t>(kExtent) * kExtent * kExtent, kFilledValue);
|
||||
|
||||
if (kind.buffer) {
|
||||
GLuint buffer = 0;
|
||||
glGenBuffers(1, &buffer);
|
||||
m_buffers.push_back(buffer);
|
||||
glBindBuffer(GL_TEXTURE_BUFFER, buffer);
|
||||
glBufferData(GL_TEXTURE_BUFFER, static_cast<GLsizeiptr>(texels.size() * sizeof(GLuint)),
|
||||
fill ? texels.data() : nullptr, GL_DYNAMIC_COPY);
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
||||
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, buffer);
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << kind.name << ": creating the texture buffer errored with "
|
||||
<< GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
m_textures.push_back(texture);
|
||||
glBindTexture(kind.target, texture);
|
||||
|
||||
switch (kind.target) {
|
||||
case GL_TEXTURE_1D:
|
||||
glTexStorage1D(kind.target, 1, GL_R32UI, kExtent);
|
||||
break;
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
glTexStorage2D(kind.target, 1, GL_R32UI, kExtent, kExtent);
|
||||
break;
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
glTexStorage3D(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent);
|
||||
break;
|
||||
case GL_TEXTURE_2D_MULTISAMPLE:
|
||||
glTexStorage2DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, GL_FALSE);
|
||||
break;
|
||||
case GL_TEXTURE_2D_MULTISAMPLE_ARRAY:
|
||||
glTexStorage3DMultisample(kind.target, 1, GL_R32UI, kExtent, kExtent, kExtent, GL_FALSE);
|
||||
break;
|
||||
default:
|
||||
ADD_FAILURE() << kind.name << ": no storage spelling for target 0x" << std::hex << kind.target;
|
||||
return 0;
|
||||
}
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << kind.name << ": allocating storage errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A multisample texture has no TexSubImage - the conformance case fills it with a
|
||||
// compute pass, which is what the store cases below do.
|
||||
if (!fill || kind.multisample) return texture;
|
||||
|
||||
switch (kind.target) {
|
||||
case GL_TEXTURE_1D:
|
||||
glTexSubImage1D(kind.target, 0, 0, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||
break;
|
||||
case GL_TEXTURE_2D:
|
||||
case GL_TEXTURE_RECTANGLE:
|
||||
case GL_TEXTURE_1D_ARRAY:
|
||||
glTexSubImage2D(kind.target, 0, 0, 0, kExtent, kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT,
|
||||
texels.data());
|
||||
break;
|
||||
case GL_TEXTURE_CUBE_MAP:
|
||||
for (int face = 0; face < 6; ++face) {
|
||||
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
||||
kExtent, GL_RED_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||
}
|
||||
break;
|
||||
case GL_TEXTURE_2D_ARRAY:
|
||||
case GL_TEXTURE_3D:
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY:
|
||||
glTexSubImage3D(kind.target, 0, 0, 0, 0, kExtent, kExtent, kExtent, GL_RED_INTEGER,
|
||||
GL_UNSIGNED_INT, texels.data());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (const GLenum error = FirstGLError()) {
|
||||
ADD_FAILURE() << kind.name << ": uploading texels errored with " << GLErrorName(error);
|
||||
return 0;
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A 4-byte `buffer` block bound to base 0, which is where every case puts its answer.
|
||||
GLuint MakeResultBuffer() {
|
||||
GLuint ssbo = 0;
|
||||
glGenBuffers(1, &ssbo);
|
||||
m_buffers.push_back(ssbo);
|
||||
const GLuint zero = 0u;
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(GLuint), &zero, GL_DYNAMIC_COPY);
|
||||
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, ssbo);
|
||||
return ssbo;
|
||||
}
|
||||
|
||||
GLuint ReadResult(GLuint ssbo) {
|
||||
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
|
||||
GLuint value = 0xFFFFFFFFu;
|
||||
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(GLuint), &value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Fill a texture of `kind`, read texel (0,0,0) of it through an image uniform in a
|
||||
// compute dispatch, and require the value back.
|
||||
void RunLoadCase(const TargetKind& kind) {
|
||||
const GLuint program = MakeComputeProgram(SingleLoadSource(kind));
|
||||
if (program == 0) return;
|
||||
const GLuint texture = MakeTexture(kind, true);
|
||||
if (texture == 0) return;
|
||||
const GLuint ssbo = MakeResultBuffer();
|
||||
|
||||
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
|
||||
|
||||
glUseProgram(program);
|
||||
// The unit, by LOCATION - the conformance case's own redundant-but-legal
|
||||
// assignment, and the one ES cannot take at the API level.
|
||||
glUniform1i(0, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": assigning the image unit errored";
|
||||
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the dispatch leaked a GL error";
|
||||
|
||||
EXPECT_EQ(ReadResult(ssbo), kFilledValue)
|
||||
<< kind.name << ": the compute dispatch did not read the value the texture was filled with";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
// The other direction: store through an image uniform, then read the same texel back
|
||||
// through a SECOND program, so a defect cannot cancel itself out.
|
||||
void RunStoreCase(const TargetKind& kind) {
|
||||
const GLuint storeProgram = MakeComputeProgram(SingleStoreSource(kind));
|
||||
const GLuint loadProgram = MakeComputeProgram(SingleLoadSource(kind));
|
||||
if (storeProgram == 0 || loadProgram == 0) return;
|
||||
const GLuint texture = MakeTexture(kind, false);
|
||||
if (texture == 0) return;
|
||||
const GLuint ssbo = MakeResultBuffer();
|
||||
|
||||
glBindImageTexture(0, texture, 0, GL_TRUE, 0, GL_READ_WRITE, GL_R32UI);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kind.name << ": glBindImageTexture errored";
|
||||
|
||||
glUseProgram(storeProgram);
|
||||
glUniform1i(0, 0);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the storing dispatch leaked a GL error";
|
||||
|
||||
glUseProgram(loadProgram);
|
||||
glUniform1i(0, 0);
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << kind.name << ": the loading dispatch leaked a GL error";
|
||||
|
||||
EXPECT_EQ(ReadResult(ssbo), kStoredValue)
|
||||
<< kind.name << ": the value stored through the image did not come back";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
std::vector<GLuint> m_buffers;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- the load direction, one target kind per case -----------------------
|
||||
//
|
||||
// Exactly what the conformance case does with each of its eleven uniforms, but alone, so a
|
||||
// failure names the kind.
|
||||
|
||||
#define MGL_DEFINE_LOAD_CASE(CaseName, Kind) \
|
||||
TEST_F(ImageTargetKindScenario, Loads##CaseName) { \
|
||||
if (!Ready()) return; \
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
||||
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
|
||||
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
|
||||
"here and never asks for a multisample one"; \
|
||||
} \
|
||||
RunLoadCase(Kind); \
|
||||
}
|
||||
|
||||
#define MGL_DEFINE_STORE_CASE(CaseName, Kind) \
|
||||
TEST_F(ImageTargetKindScenario, Stores##CaseName) { \
|
||||
if (!Ready()) return; \
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms"; \
|
||||
if ((Kind).multisample && !MultisampleImagesAreUsable()) { \
|
||||
GTEST_SKIP() << "GL_MAX_IMAGE_SAMPLES is 0, so the conformance case substitutes a plain 2D image " \
|
||||
"here and never asks for a multisample one"; \
|
||||
} \
|
||||
RunStoreCase(Kind); \
|
||||
}
|
||||
|
||||
MGL_DEFINE_LOAD_CASE(Texture1D, kKind1D)
|
||||
MGL_DEFINE_LOAD_CASE(Texture1DArray, kKind1DArray)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2D, kKind2D)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2DArray, kKind2DArray)
|
||||
MGL_DEFINE_LOAD_CASE(Texture3D, kKind3D)
|
||||
MGL_DEFINE_LOAD_CASE(TextureBuffer, kKindBuffer)
|
||||
MGL_DEFINE_LOAD_CASE(TextureCube, kKindCube)
|
||||
MGL_DEFINE_LOAD_CASE(TextureCubeArray, kKindCubeArray)
|
||||
MGL_DEFINE_LOAD_CASE(TextureRectangle, kKindRect)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2DMultisample, kKind2DMS)
|
||||
MGL_DEFINE_LOAD_CASE(Texture2DMultisampleArray, kKind2DMSArray)
|
||||
|
||||
MGL_DEFINE_STORE_CASE(Texture1D, kKind1D)
|
||||
MGL_DEFINE_STORE_CASE(Texture1DArray, kKind1DArray)
|
||||
MGL_DEFINE_STORE_CASE(Texture2D, kKind2D)
|
||||
MGL_DEFINE_STORE_CASE(Texture2DArray, kKind2DArray)
|
||||
MGL_DEFINE_STORE_CASE(Texture3D, kKind3D)
|
||||
MGL_DEFINE_STORE_CASE(TextureBuffer, kKindBuffer)
|
||||
MGL_DEFINE_STORE_CASE(TextureCube, kKindCube)
|
||||
MGL_DEFINE_STORE_CASE(TextureCubeArray, kKindCubeArray)
|
||||
MGL_DEFINE_STORE_CASE(TextureRectangle, kKindRect)
|
||||
MGL_DEFINE_STORE_CASE(Texture2DMultisample, kKind2DMS)
|
||||
MGL_DEFINE_STORE_CASE(Texture2DMultisampleArray, kKind2DMSArray)
|
||||
|
||||
#undef MGL_DEFINE_LOAD_CASE
|
||||
#undef MGL_DEFINE_STORE_CASE
|
||||
|
||||
// ---- and all of them at once -------------------------------------------
|
||||
//
|
||||
// The conformance case's actual shape. The single-kind cases above cannot see a defect that
|
||||
// needs several kinds in one program - a binding remap that only collides when two image
|
||||
// types share a descriptor set, a per-kind rewrite that is not idempotent across declarations
|
||||
// - and that class of defect is precisely what "each kind passes alone but the case still
|
||||
// fails" would mean. Each unit is filled with its own INDEX rather than a constant, so the
|
||||
// sum names how many units contributed and a single mis-bound unit does not cancel out.
|
||||
TEST_F(ImageTargetKindScenario, AllKindsInOneProgram) {
|
||||
if (!Ready()) return;
|
||||
if (!ImagesAreUsable()) GTEST_SKIP() << "no compute image uniforms";
|
||||
|
||||
const bool multisample = MultisampleImagesAreUsable();
|
||||
std::vector<TargetKind> kinds{kKind1D, kKind1DArray, kKind2D, kKind2DArray,
|
||||
kKind3D, kKindBuffer, kKindCube, kKindCubeArray,
|
||||
kKindRect};
|
||||
if (multisample) {
|
||||
kinds.push_back(kKind2DMS);
|
||||
kinds.push_back(kKind2DMSArray);
|
||||
}
|
||||
|
||||
GLint maxComputeImageUniforms = 0;
|
||||
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &maxComputeImageUniforms);
|
||||
GLint maxImageUnits = 0;
|
||||
glGetIntegerv(GL_MAX_IMAGE_UNITS, &maxImageUnits);
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
const std::size_t count =
|
||||
std::min<std::size_t>(kinds.size(), static_cast<std::size_t>(std::max(0, std::min(maxComputeImageUniforms,
|
||||
maxImageUnits))));
|
||||
if (count == 0) GTEST_SKIP() << "no image units";
|
||||
kinds.resize(count);
|
||||
|
||||
std::string declarations;
|
||||
std::string sum;
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||
const std::string name = "i" + std::to_string(i);
|
||||
declarations += "layout (location = " + std::to_string(i) + ", r32ui) readonly uniform " +
|
||||
kinds[i].imageType + " " + name + ";\n";
|
||||
if (!sum.empty()) sum += " + ";
|
||||
sum += LoadExpression(kinds[i], name);
|
||||
}
|
||||
const std::string source = std::string(kComputePrologue) + declarations + kResultBlock +
|
||||
"void main()\n{\n uvec4 v = " + sum + ";\n ssb.sum = v.r;\n}\n";
|
||||
|
||||
const GLuint program = MakeComputeProgram(source);
|
||||
if (program == 0) return;
|
||||
|
||||
// Each unit gets its own value, so the sum says how many units contributed.
|
||||
GLuint expected = 0;
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||
const GLuint texture = MakeTexture(kinds[i], true);
|
||||
if (texture == 0) return;
|
||||
expected += kFilledValue;
|
||||
glBindImageTexture(static_cast<GLuint>(i), texture, 0, GL_TRUE, 0, GL_READ_ONLY, GL_R32UI);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << kinds[i].name << ": glBindImageTexture errored";
|
||||
}
|
||||
const GLuint ssbo = MakeResultBuffer();
|
||||
|
||||
glUseProgram(program);
|
||||
for (std::size_t i = 0; i < kinds.size(); ++i) {
|
||||
glUniform1i(static_cast<GLint>(i), static_cast<GLint>(i));
|
||||
}
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "assigning the image units errored";
|
||||
|
||||
glDispatchCompute(1, 1, 1);
|
||||
glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch leaked a GL error";
|
||||
|
||||
EXPECT_EQ(ReadResult(ssbo), expected)
|
||||
<< "the sum over " << kinds.size()
|
||||
<< " image target kinds is wrong; each kind contributes " << kFilledValue
|
||||
<< ", so the shortfall is a whole number of kinds that read zero";
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user