mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
507 lines
24 KiB
C++
507 lines
24 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnboundImageDescriptorScenario.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 - A PROGRAM DECLARES AN IMAGE-BACKED RESOURCE AND THE APPLICATION BINDS NOTHING.
|
|
//
|
|
// The sibling of GuiBatchScenario's MeshesBlockLeftUnbound, one descriptor kind further out.
|
|
// That one pinned an unbound shader storage BLOCK; the same "nothing is bound, so lose the
|
|
// whole draw" shape survived in the three image-backed kinds:
|
|
//
|
|
// * `samplerBuffer` - a texture unit with no buffer texture on it, and a buffer texture with
|
|
// no GL buffer attached to it. Both make the sampler INCOMPLETE (GL 4.6
|
|
// core 8.9, 8.24), and sampling an incomplete texture returns undefined
|
|
// VALUES. It is not an error and it is not a lost draw.
|
|
// * `imageBuffer` - an image unit with nothing on it. GL 4.6 core 8.26 is explicit: loads
|
|
// return zero and stores are discarded.
|
|
// * `image2D` - the same rule, through a VkImageView rather than a VkBufferView.
|
|
//
|
|
// Vulkan has no such thing as an unwritten descriptor, so DirectVulkan's descriptor resolution
|
|
// used to answer "no valid descriptor" and both SetupDraw and DispatchCompute skip everything on
|
|
// that answer - the draw or dispatch simply never happened, silently. Every test below asserts
|
|
// on the OTHER work in the same shader: the pixels the fragment stage painted, or the buffer the
|
|
// dispatch filled. All of it is unrelated to the unbound resource and all of it disappeared.
|
|
//
|
|
// The unbound resource is STATICALLY USED in every case, because an unreferenced one is
|
|
// optimised out before it ever reaches a descriptor and would prove nothing. Where the use is a
|
|
// read it sits behind a uniform-controlled branch that is false at runtime - the descriptor is
|
|
// declared and must be written, but no undefined value reaches an assertion. Where it is a write
|
|
// (the `writeonly` cases, which is how the real workloads spell it) it is unconditional: GL says
|
|
// the store is discarded, so there is nothing to guard against.
|
|
//
|
|
// Reproduces on DirectVulkan only. DirectGLES forwards the unbound unit to the GLES driver,
|
|
// which does what GL says, so it is the control - every test here must stay green on both.
|
|
|
|
#include <cstdint>
|
|
#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 kFboSize = 32;
|
|
constexpr int kElements = 4;
|
|
|
|
// No vertex attributes: the quad's corners come from gl_VertexID, so nothing about the
|
|
// vertex fetch can be confused with the descriptor question under test.
|
|
constexpr const char* kQuadVertexSource = R"(#version 430 core
|
|
void main() {
|
|
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
|
|
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
|
|
gl_Position = vec4(corner, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
// The assertion in every draw case: opaque green everywhere. The unbound resource
|
|
// contributes nothing to it - u_readUnbound is 0, so the fetch never runs - but the
|
|
// descriptor for it still has to exist, which is the point.
|
|
constexpr const char* kSamplerBufferFragmentSource = R"(#version 430 core
|
|
uniform samplerBuffer u_unbound;
|
|
uniform int u_readUnbound;
|
|
out vec4 o_color;
|
|
void main() {
|
|
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
|
if (u_readUnbound != 0) {
|
|
color = texelFetch(u_unbound, 0);
|
|
}
|
|
o_color = color;
|
|
}
|
|
)";
|
|
|
|
constexpr const char* kSamplerBufferComputeSource = R"(#version 430 core
|
|
layout(local_size_x = 1) in;
|
|
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
|
uniform samplerBuffer u_unbound;
|
|
uniform int u_readUnbound;
|
|
void main() {
|
|
uint index = gl_GlobalInvocationID.x;
|
|
uint value = index + 1u;
|
|
if (u_readUnbound != 0) {
|
|
value += uint(texelFetch(u_unbound, 0).r);
|
|
}
|
|
g_data[index] = value;
|
|
}
|
|
)";
|
|
|
|
// writeonly, and the store is unconditional: this is how AcceleratedRendering and the
|
|
// conformance cases spell an image the shader only produces into. GL discards the store
|
|
// when the unit is empty; nothing here reads it back.
|
|
constexpr const char* kImageBufferFragmentSource = R"(#version 430 core
|
|
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
|
out vec4 o_color;
|
|
void main() {
|
|
imageStore(u_unbound, 0, uvec4(7u));
|
|
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
constexpr const char* kImageBufferComputeSource = R"(#version 430 core
|
|
layout(local_size_x = 1) in;
|
|
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
|
layout(binding = 0, r32ui) uniform writeonly uimageBuffer u_unbound;
|
|
void main() {
|
|
uint index = gl_GlobalInvocationID.x;
|
|
imageStore(u_unbound, int(index), uvec4(7u));
|
|
g_data[index] = index + 1u;
|
|
}
|
|
)";
|
|
|
|
// A plain sampler2D on a unit the test leaves alone. Two cases point at it: a unit with
|
|
// nothing bound at all, and a unit whose DEFAULT texture (name 0) has been given a base
|
|
// level and no mip chain - GL calls the second one incomplete for the initial
|
|
// NEAREST_MIPMAP_LINEAR filter, and both must resolve to the fallback rather than to a
|
|
// texture the backend then fails to back.
|
|
constexpr const char* kSampler2DFragmentSource = R"(#version 430 core
|
|
uniform sampler2D u_unbound;
|
|
uniform int u_readUnbound;
|
|
out vec4 o_color;
|
|
void main() {
|
|
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
|
if (u_readUnbound != 0) {
|
|
color = texture(u_unbound, vec2(0.0));
|
|
}
|
|
o_color = color;
|
|
}
|
|
)";
|
|
|
|
// The multisample spelling of the same thing. GL_ARB_sample_variables' own conformance
|
|
// cases declare a sampler2D and a sampler2DMS side by side and deliberately point the
|
|
// unused one at an empty unit, so whichever of the two is unused has to have a
|
|
// placeholder - a multisample descriptor demands a multisample view, so the 2D fallback
|
|
// cannot stand in for it.
|
|
constexpr const char* kSampler2DMSFragmentSource = R"(#version 430 core
|
|
uniform sampler2DMS u_unbound;
|
|
uniform int u_readUnbound;
|
|
out vec4 o_color;
|
|
void main() {
|
|
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
|
if (u_readUnbound != 0) {
|
|
color = texelFetch(u_unbound, ivec2(0), 0);
|
|
}
|
|
o_color = color;
|
|
}
|
|
)";
|
|
|
|
// The integer spellings of the same thing. These are the ones a plain RGBA8 multisample
|
|
// placeholder cannot serve: a multisample image can never carry MUTABLE_FORMAT, so the
|
|
// reinterpreting view an integer sampler would need over UNORM texels is unbuildable and
|
|
// the descriptor resolve used to fail, losing the draw after the placeholder had already
|
|
// been created.
|
|
constexpr const char* kUsampler2DMSFragmentSource = R"(#version 430 core
|
|
uniform usampler2DMS u_unbound;
|
|
uniform int u_readUnbound;
|
|
out vec4 o_color;
|
|
void main() {
|
|
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
|
if (u_readUnbound != 0) {
|
|
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
|
}
|
|
o_color = color;
|
|
}
|
|
)";
|
|
|
|
constexpr const char* kIsampler2DMSFragmentSource = R"(#version 430 core
|
|
uniform isampler2DMS u_unbound;
|
|
uniform int u_readUnbound;
|
|
out vec4 o_color;
|
|
void main() {
|
|
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
|
|
if (u_readUnbound != 0) {
|
|
color = vec4(texelFetch(u_unbound, ivec2(0), 0));
|
|
}
|
|
o_color = color;
|
|
}
|
|
)";
|
|
|
|
constexpr const char* kImage2DFragmentSource = R"(#version 430 core
|
|
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
|
out vec4 o_color;
|
|
void main() {
|
|
imageStore(u_unbound, ivec2(0, 0), vec4(1.0));
|
|
o_color = vec4(0.0, 1.0, 0.0, 1.0);
|
|
}
|
|
)";
|
|
|
|
constexpr const char* kImage2DComputeSource = R"(#version 430 core
|
|
layout(local_size_x = 1) in;
|
|
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
|
layout(binding = 0, rgba8) uniform writeonly image2D u_unbound;
|
|
void main() {
|
|
uint index = gl_GlobalInvocationID.x;
|
|
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
|
g_data[index] = index + 1u;
|
|
}
|
|
)";
|
|
|
|
// No layout format at all, which GLSL 4.20 allows for a write-only image. The reflection
|
|
// then carries NO format for the binding, so the placeholder descriptor can only be
|
|
// constrained by the declaration's numeric class - a different route through the fix than
|
|
// every typed case above.
|
|
constexpr const char* kFormatlessImage2DComputeSource = R"(#version 430 core
|
|
layout(local_size_x = 1) in;
|
|
layout(std430, binding = 0) buffer Output { uint g_data[]; };
|
|
layout(binding = 0) uniform writeonly image2D u_unbound;
|
|
void main() {
|
|
uint index = gl_GlobalInvocationID.x;
|
|
imageStore(u_unbound, ivec2(int(index), 0), vec4(1.0));
|
|
g_data[index] = index + 1u;
|
|
}
|
|
)";
|
|
|
|
class UnboundImageDescriptorScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
if (!Ready()) return;
|
|
m_target = MakeColorFbo(kFboSize, kFboSize);
|
|
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
|
|
glGenVertexArrays(1, &m_vao);
|
|
glGenBuffers(1, &m_storage);
|
|
// The harness shares one context across every scenario in the process, so an
|
|
// earlier one may well have left a texture on unit 0 or an image on unit 0. The
|
|
// whole subject here is that nothing is bound, so say so rather than assume it.
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
|
|
FirstGLError();
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (!Ready()) return;
|
|
glUseProgram(0);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
|
|
if (m_program != 0) glDeleteProgram(m_program);
|
|
if (m_storage != 0) glDeleteBuffers(1, &m_storage);
|
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
BindDefaultFramebuffer();
|
|
DestroyColorFbo(m_target);
|
|
glViewport(0, 0, Gl().Width(), Gl().Height());
|
|
}
|
|
|
|
// Each case needs exactly one kind of opaque uniform in one stage, and a host with
|
|
// none of that kind there would report a failure that is about the host, not the fix.
|
|
// Asked for by the limit that governs the kind under test and no other: a guard that
|
|
// over-asks turns into a silent skip of the very thing the case exists for.
|
|
static bool LimitIsAtLeastOne(GLenum limit) {
|
|
GLint value = 0;
|
|
glGetIntegerv(limit, &value);
|
|
while (glGetError() != GL_NO_ERROR) {
|
|
}
|
|
return value >= 1;
|
|
}
|
|
|
|
unsigned int MakeComputeProgram(const char* source) {
|
|
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
|
|
glShaderSource(shader, 1, &source, 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();
|
|
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;
|
|
glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
return program;
|
|
}
|
|
|
|
// Fills a four-element SSBO with 1..4 while the unbound resource is declared and
|
|
// statically used. Zeros everywhere mean the dispatch never ran.
|
|
void ExpectDispatchStillRuns(const char* source, const char* what) {
|
|
m_program = MakeComputeProgram(source);
|
|
ASSERT_NE(m_program, 0u);
|
|
|
|
const std::vector<unsigned int> zeros(static_cast<std::size_t>(kElements), 0u);
|
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
|
glBufferData(GL_SHADER_STORAGE_BUFFER,
|
|
static_cast<GLsizeiptr>(zeros.size() * sizeof(unsigned int)), zeros.data(),
|
|
GL_DYNAMIC_COPY);
|
|
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_storage);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "setting up the output buffer raised a GL error";
|
|
|
|
glUseProgram(m_program);
|
|
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
|
if (readUnbound != -1) {
|
|
glUniform1i(readUnbound, 0);
|
|
}
|
|
glDispatchCompute(kElements, 1, 1);
|
|
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error (" << what << ")";
|
|
|
|
std::vector<unsigned int> values(static_cast<std::size_t>(kElements), 0xDEADBEEFu);
|
|
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_storage);
|
|
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
|
|
static_cast<GLsizeiptr>(values.size() * sizeof(unsigned int)), values.data());
|
|
for (int i = 0; i < kElements; ++i) {
|
|
EXPECT_EQ(values[static_cast<std::size_t>(i)], static_cast<unsigned int>(i + 1))
|
|
<< "element " << i << " came back as " << values[static_cast<std::size_t>(i)]
|
|
<< "; zero everywhere means the whole dispatch was dropped over the unbound " << what;
|
|
}
|
|
}
|
|
|
|
// Paints the whole render target green while the unbound resource is declared and
|
|
// statically used. A black target means the draw never happened.
|
|
void ExpectDrawStillRuns(const char* fragmentSource, const char* what) {
|
|
std::string error;
|
|
m_program = CompileProgram(kQuadVertexSource, fragmentSource, &error);
|
|
ASSERT_NE(m_program, 0u) << error;
|
|
|
|
BindFbo(m_target);
|
|
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glBindVertexArray(m_vao);
|
|
glUseProgram(m_program);
|
|
const GLint readUnbound = glGetUniformLocation(m_program, "u_readUnbound");
|
|
if (readUnbound != -1) {
|
|
glUniform1i(readUnbound, 0);
|
|
}
|
|
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
|
glBindVertexArray(0);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the draw raised a GL error (" << what << ")";
|
|
|
|
const Image image = ReadPixels(kFboSize, kFboSize);
|
|
ASSERT_FALSE(image.Empty()) << "the readback came back empty";
|
|
// Whole-region, not a centre pixel: the quad covers the target exactly, so
|
|
// anything short of all of it is a failure worth naming.
|
|
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, "green", 0.0,
|
|
std::string("the quad drawn with an unbound ") + what))
|
|
<< "an all-black target means the draw was dropped over the unbound " << what;
|
|
}
|
|
|
|
ColorFbo m_target{};
|
|
GLuint m_vao = 0;
|
|
GLuint m_storage = 0;
|
|
unsigned int m_program = 0;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
// ---- uniform samplerBuffer (VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER) --------------------
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDispatch) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS)) {
|
|
GTEST_SKIP() << "the compute stage has no texture image units";
|
|
}
|
|
ExpectDispatchStillRuns(kSamplerBufferComputeSource, "samplerBuffer");
|
|
}
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSamplerBufferDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "samplerBuffer");
|
|
}
|
|
|
|
// The other way a texel-buffer descriptor comes out empty: the unit HAS a buffer texture, but
|
|
// no glTexBuffer ever attached a buffer object to it. GL calls that texture incomplete, which
|
|
// is undefined data and not a lost draw - a separate site in the resolve from the one above,
|
|
// and it used to return false too.
|
|
TEST_F(UnboundImageDescriptorScenario, ABufferTextureWithNoAttachedBufferDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
|
|
GLuint texture = 0;
|
|
glGenTextures(1, &texture);
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_BUFFER, texture);
|
|
// Deliberately no glTexBuffer: the texture exists and is bound, and has no store.
|
|
ASSERT_EQ(FirstGLError(), 0u) << "binding an empty buffer texture raised a GL error";
|
|
|
|
ExpectDrawStillRuns(kSamplerBufferFragmentSource, "buffer texture with no attached buffer");
|
|
|
|
glBindTexture(GL_TEXTURE_BUFFER, 0);
|
|
glDeleteTextures(1, &texture);
|
|
}
|
|
|
|
// ---- writeonly imageBuffer (VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER) --------------------
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDispatch) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
|
GTEST_SKIP() << "the compute stage has no image uniforms";
|
|
}
|
|
ExpectDispatchStillRuns(kImageBufferComputeSource, "imageBuffer");
|
|
}
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImageBufferLeftUnboundDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
|
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
|
}
|
|
ExpectDrawStillRuns(kImageBufferFragmentSource, "imageBuffer");
|
|
}
|
|
|
|
// ---- writeonly image2D (VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) -------------------------------
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
|
GTEST_SKIP() << "the compute stage has no image uniforms";
|
|
}
|
|
ExpectDispatchStillRuns(kImage2DComputeSource, "image2D");
|
|
}
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, AWriteonlyImage2DLeftUnboundDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
if (!LimitIsAtLeastOne(GL_MAX_FRAGMENT_IMAGE_UNIFORMS)) {
|
|
GTEST_SKIP() << "the fragment stage has no image uniforms";
|
|
}
|
|
ExpectDrawStillRuns(kImage2DFragmentSource, "image2D");
|
|
}
|
|
|
|
// ---- sampler2D / sampler2DMS (VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) ----------------
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D");
|
|
}
|
|
|
|
// The regression this file exists for, in its sharpest form: a sampler pointing at a texture
|
|
// unit whose DEFAULT texture object has an image but no mip chain.
|
|
//
|
|
// DirectVulkan resolved such a binding twice, through two different predicates that
|
|
// disagreed. The collect pass (CollectSampledTextures -> ResolveSampledBinding), which
|
|
// pre-syncs and transitions every texture the draw will sample, asked only whether the
|
|
// default texture was UNDEFINED - texture 0 with an image is not - and kept it. The
|
|
// descriptor pass (ResolveSamplerDescriptor) asked the real GL question, whether it
|
|
// SAMPLES AS INCOMPLETE for the filter in effect, and swapped it for the fallback. So the
|
|
// collect pass synced a texture no descriptor would ever hold, VkTextureManager declined it
|
|
// ("mipmap not complete") and returned nullptr, and SetupDraw dereferenced that nullptr -
|
|
// a SIGSEGV inside the draw, not a degraded picture.
|
|
//
|
|
// The GL-CTS reaches this on its own: its between-case state reset gives the default 2D
|
|
// texture a base level, so the FIRST case in a process survived and every later one with an
|
|
// unbound sampler2D died. That is the whole of the 380-record sample_variables crash family
|
|
// on Mali-G1-Ultra. Any application that uploads to texture 0 has the same shape.
|
|
TEST_F(UnboundImageDescriptorScenario, ASamplerOnAUnitWhoseDefaultTextureIsIncompleteDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
|
|
// Unit 0 is where the sampler's default uniform value points. Give the DEFAULT texture
|
|
// object bound there a FORMAT and a zero-sized level - which is what a bare
|
|
// glTexImage2D(..., 0, 0, ...) with no data does, and what the GL-CTS's between-case
|
|
// state reset issues for every texture target. That combination is the whole point:
|
|
// * it is DEFINED, so IsUndefinedDefaultTexture (the collect path's old test) is false
|
|
// and the texture stays in the sampled set;
|
|
// * it is INCOMPLETE, so SamplesAsIncompleteTexture (the descriptor path's test) is
|
|
// true and the descriptor holds the fallback instead;
|
|
// * and it has no valid mip level, so the sync declines and hands back nullptr.
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "defining a zero-sized level 0 on the default texture raised a GL error";
|
|
|
|
ExpectDrawStillRuns(kSampler2DFragmentSource, "sampler2D on an incomplete default texture");
|
|
}
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSampler2DMSDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
ExpectDrawStillRuns(kSampler2DMSFragmentSource, "sampler2DMS");
|
|
}
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundUnsignedSampler2DMSDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
ExpectDrawStillRuns(kUsampler2DMSFragmentSource, "usampler2DMS");
|
|
}
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, ADeclaredButUnboundSignedSampler2DMSDoesNotLoseTheDraw) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
ExpectDrawStillRuns(kIsampler2DMSFragmentSource, "isampler2DMS");
|
|
}
|
|
|
|
TEST_F(UnboundImageDescriptorScenario, AFormatlessWriteonlyImage2DLeftUnboundDoesNotLoseTheDispatch) {
|
|
if (!Ready() || IsSkipped()) return;
|
|
if (!LimitIsAtLeastOne(GL_MAX_COMPUTE_IMAGE_UNIFORMS)) {
|
|
GTEST_SKIP() << "the compute stage has no image uniforms";
|
|
}
|
|
ExpectDispatchStillRuns(kFormatlessImage2DComputeSource, "format-less image2D");
|
|
}
|
|
|
|
} // namespace MGITest
|