diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 614a87fe..071a27d0 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1613,6 +1613,20 @@ namespace MobileGL::MG_Backend::DirectGLES { } const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + // A texture whose mip chain does not satisfy the filter's completeness + // rules samples as (0, 0, 0, 1). The ES driver cannot work that out for + // itself here: the backend texture is immutable storage, so a level the + // application redefined at the wrong size never reached it. Leaving the + // native target unbound produces exactly the incomplete-texture result. + const auto& effectiveSampler = textureUnit.GetSamplerObject() + ? textureUnit.GetSamplerObject() + : textureObject->GetSamplerObject(); + const Bool mipmappedFilter = + effectiveSampler && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None; + if (!MG_State::GLState::IsMipmapCompleteForFilter(textureObject.get(), mipmappedFilter)) { + continue; + } + // Bind texture object const auto& backendTextureIt = TextureImpl::g_backendTextureObjects.find(textureObject.get()); if (backendTextureIt == TextureImpl::g_backendTextureObjects.end()) continue; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 6eee7f76..1f0566cf 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -348,6 +348,58 @@ namespace MobileGL { // TODO: add other texture types as needed + Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) { + if (texture == nullptr) return true; + if (!texture->IsComplete()) return false; + if (!mipmapped) return true; + + const auto* mipmapTexture = AsMipmapTexture(texture); + if (mipmapTexture == nullptr) return true; // no mip chain to be incomplete about + + const UintVec2& levelRange = texture->GetLevelRange(); + const Uint baseLevel = levelRange.x(); + const Uint storedLevels = mipmapTexture->GetMipmapLevelCount(); + if (baseLevel >= storedLevels) return false; + + // An array texture's layer count is not a dimension of the image: it stays put all + // the way down the chain (GL 4.6 core 8.14.3). GetMipmapTexelSize reports it in the + // slot after the image's own dimensions. + const TextureTarget target = texture->GetTarget(); + Int shrinkingComponents = 3; + if (target == TextureTarget::Texture1DArray) { + shrinkingComponents = 1; + } else if (target == TextureTarget::Texture2DArray || target == TextureTarget::TextureCubeMapArray) { + shrinkingComponents = 2; + } + + for (const auto uploadTarget : texture->GetUploadTargets()) { + const IntVec3 baseSize = mipmapTexture->GetMipmapTexelSize(uploadTarget, baseLevel); + Int largest = 0; + for (Int component = 0; component < shrinkingComponents; ++component) { + largest = std::max(largest, baseSize[component]); + } + if (largest <= 0) return false; + + // p = log2 of the largest base dimension: the last level the chain needs + // before every dimension has reached 1. TEXTURE_MAX_LEVEL can cut it short. + Uint p = 0; + for (Int extent = largest; extent > 1; extent >>= 1) ++p; + const Uint lastLevel = std::min(baseLevel + p, levelRange.y()); + + for (Uint level = baseLevel; level <= lastLevel; ++level) { + if (level >= storedLevels) return false; + const IntVec3 actual = mipmapTexture->GetMipmapTexelSize(uploadTarget, level); + for (Int component = 0; component < 3; ++component) { + const Int expected = component < shrinkingComponents + ? std::max(1, baseSize[component] >> (level - baseLevel)) + : baseSize[component]; + if (actual[component] != expected) return false; + } + } + } + return true; + } + } // namespace GLState } // namespace MG_State } // namespace MobileGL diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 652f9689..f1196ad0 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -156,6 +156,14 @@ namespace MobileGL::MG_State::GLState { ? static_cast(texture) : nullptr; } + // Whether the texture satisfies the mipmap-completeness rules a minification filter + // that samples the mip chain imposes (GL 4.6 core 8.17): every level from the base to + // the effective max must exist at exactly half the previous one's size. `mipmapped` is + // the effective sampler's answer to "does this filter read more than the base level" - + // when it is false only base-level completeness matters, which the ordinary + // IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1). + Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped); + inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) { return (texture && texture->GetStorageType() == TextureStorageType::Mipmap) ? static_cast(texture)