From acad001b34788af3b3379228702b1c3ed9088e7b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 08:04:24 -0400 Subject: [PATCH] [Fix] (Pipe): bind incomplete image units as no image --- MobileGL/MG_Impl/Pipe/ImageEmit.h | 36 ++++++++++++++++++++++--- MobileGL/MG_Test/Pipe/ImageEmitTest.cpp | 28 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/MobileGL/MG_Impl/Pipe/ImageEmit.h b/MobileGL/MG_Impl/Pipe/ImageEmit.h index 55f53e63..7faf7c22 100644 --- a/MobileGL/MG_Impl/Pipe/ImageEmit.h +++ b/MobileGL/MG_Impl/Pipe/ImageEmit.h @@ -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(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 diff --git a/MobileGL/MG_Test/Pipe/ImageEmitTest.cpp b/MobileGL/MG_Test/Pipe/ImageEmitTest.cpp index a1d50c9c..8eac3859 100644 --- a/MobileGL/MG_Test/Pipe/ImageEmitTest.cpp +++ b/MobileGL/MG_Test/Pipe/ImageEmitTest.cpp @@ -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