[Fix] (Pipe): bind incomplete image units as no image

This commit is contained in:
2026-10-12 08:32:51 -04:00
parent 8fff9363f7
commit acad001b34
2 changed files with 61 additions and 3 deletions
+33 -3
View File
@@ -72,6 +72,36 @@ namespace MobileGL::MG_Pipe {
}
}
// GL 4.6 core 8.26: an image access is INVALID - loads return zero, stores are dropped - when
// the bound texture is incomplete or the bound level lies outside [level_base, q]. Image
// units ignore the sampler, so "complete" here is the level's own: the base level needs
// base-level completeness and any other level a complete mip chain up to it, the reading
// the conformance suite applies (a mutable texture with only level 0 defined, bound at
// level 2). Such a binding travels as an EMPTY unit, which both backends already answer
// with "no image" - an ES driver would otherwise happily address the undefined level.
inline Bool MGPipeImageBindingIsAccessible(const MG_State::GLState::ImageTextureBinding& binding) {
using namespace MG_State::GLState;
const ITextureObject* texture = binding.Texture.get();
if (texture == nullptr) return false;
const TextureObjectMipmap* mipmapTexture = AsMipmapTexture(texture);
// Buffer and multisample storage have exactly one level and no level range to leave.
if (mipmapTexture == nullptr) return true;
if (binding.Level < 0) return false;
const Uint level = static_cast<Uint>(binding.Level);
const UintVec2& range = texture->GetLevelRange();
if (level < range.x() || level > range.y()) return false;
if (texture->IsImmutable() || texture->IsTextureView()) {
// Every level of immutable storage exists, so the range against the storage is all.
return level < texture->GetAddressableLevelCount();
}
if (level >= mipmapTexture->GetMipmapLevelCount()) return false;
for (const auto uploadTarget : texture->GetUploadTargets()) {
const IntVec3 size = mipmapTexture->GetMipmapTexelSize(uploadTarget, level);
if (size.x() <= 0 || size.y() <= 0 || size.z() <= 0) return false;
}
return IsMipmapCompleteForFilter(texture, /*mipmapped=*/level != range.x());
}
// D-G3. Over the tail with Start and Count mixed in, the same shape the two sampler sets
// use - and it covers InternalFormat and Access because those are live glBindImageTexture
// state that the format-less image bake keys on, not decoration.
@@ -128,9 +158,9 @@ namespace MobileGL::MG_Pipe {
MGPImageView& entry = m_entries[unit];
entry = MGPImageView{};
entry.Unit = unit;
entry.Res = binding.Texture ? MGPipeSlots().Acquire(MGPipeKind::Texture,
binding.Texture->GetLifetimeId())
: kMGPipeNullHandle;
entry.Res = MGPipeImageBindingIsAccessible(binding)
? MGPipeSlots().Acquire(MGPipeKind::Texture, binding.Texture->GetLifetimeId())
: kMGPipeNullHandle;
// D-A4: a texture named in an emitted MGPImageView is SHADER-IMAGE-bound from
// then on - the bit ImageBindableHint is derived from. The bind itself noted it
// first (TextureState.h, so the hint precedes the first sync); this is the
+28
View File
@@ -607,6 +607,34 @@ void main() { imageStore(img, ivec2(0, 0), vec4(1.0)); }
GL::BindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
}
// GL 4.6 core 8.26: a level the texture does not have as a complete image - here a mutable
// texture with only level 0 defined and the default MAX_LEVEL, bound at level 2 - is an
// invalid access, so the unit travels EMPTY and the backend binds no image (loads read zero,
// stores are dropped). The same texture bound at its base level stays a real binding.
TEST(ImageEmit, AnIncompleteLevelTravelsAsAnEmptyUnit) {
EmitterScope scope;
const GLuint program = MakeComputeProgram(kImageCompute);
GL::UseProgram(program);
GLuint name = 0;
GL::GenTextures(1, &name);
GL::BindTexture(GL_TEXTURE_2D, name);
GL::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
GL::BindImageTexture(1, name, 2, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
ASSERT_TRUE(Ctx().GetImageTextureBinding(1).Texture != nullptr) << "the bind itself is legal";
Emitter().EmitShaderImages(Ctx());
ASSERT_GE(Emitter().Window(), 2u);
EXPECT_TRUE(MGPipeHandleIsNull(Emitter().LastImageViews()[1].Res))
<< "an undefined, mip-incomplete level must not reach the backend as an image";
GL::BindImageTexture(1, name, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA8);
Emitter().EmitShaderImages(Ctx());
EXPECT_FALSE(MGPipeHandleIsNull(Emitter().LastImageViews()[1].Res))
<< "the base level is base-complete and must stay bound";
GL::UseProgram(0);
GL::BindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
}
// P5d round 3, package C: glBindImageTexture IS THE PRODUCER OF THE IMAGE-UNIT HIGH-WATER
// MARK, and this case is the reason a reader may bound a walk with it. The split client's
// per-draw GPU-write sweep (MG_Remote/Client/GpuWritePending.cpp) stops at the mark instead