diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index e9bde988..00fb641f 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1621,9 +1621,8 @@ namespace MobileGL::MG_Backend::DirectGLES { 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)) { + if (MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(), + effectiveSampler.get())) { continue; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index b96aedab..2259577e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -266,6 +266,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto& samplerOverride = textureUnit.GetSamplerObject(); const auto preferredTarget = programObj.samplerTextureTargetByBinding[binding]; SharedPtr fallbackHolder; + // A texture that fails the completeness rules for the filter in effect reads + // (0, 0, 0, 1), which is exactly what the fallback texture holds - so it takes the + // same route as a sampler with nothing bound. + if (texture != nullptr && + MG_State::GLState::SamplesAsIncompleteTexture( + texture, samplerOverride ? samplerOverride.get() : texture->GetSamplerObject().get())) { + texture = nullptr; + } if (texture == nullptr) { fallbackHolder = GetFallbackTexture(preferredTarget); texture = fallbackHolder.get(); @@ -769,6 +777,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { fallbackTexture->SetInternalFormat(TextureInternalFormat::RGBA8); fallbackTexture->AllocateStorage(TextureUploadTarget::Texture2D, 0, {.texelSize = {1, 1, 1}, .byteSize = 4}); + // (0, 0, 0, 1): what GL reads from a texture that is not complete, and the only + // sensible answer for a sampler with nothing bound. + static Uint8 kOpaqueBlackTexel[4] = {0, 0, 0, 255}; + fallbackTexture->UpdateMipmapSubData(TextureUploadTarget::Texture2D, 0, + {kOpaqueBlackTexel, sizeof(kOpaqueBlackTexel)}); fallbackTexture->MarkStorageDirty(TextureUploadTarget::Texture2D, 0, true); m_fallbackTexture2D = fallbackTexture; } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 1f0566cf..1b41f8f2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -348,6 +348,12 @@ namespace MobileGL { // TODO: add other texture types as needed + Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) { + const Bool mipmapped = + effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None; + return !IsMipmapCompleteForFilter(texture, mipmapped); + } + Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped) { if (texture == nullptr) return true; if (!texture->IsComplete()) return false; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index f1196ad0..d3fea053 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -164,6 +164,12 @@ namespace MobileGL::MG_State::GLState { // IsComplete() already covers. Sampling an incomplete texture returns (0, 0, 0, 1). Bool IsMipmapCompleteForFilter(const ITextureObject* texture, Bool mipmapped); + // The rule above asked as the backends need it: does a lookup on this texture read + // (0, 0, 0, 1) instead of its contents? `effectiveSampler` is the sampler object bound + // to the unit when there is one, otherwise the texture's own. A backend answers yes by + // routing the texture to whatever it already uses for "nothing is bound there". + Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler); + inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) { return (texture && texture->GetStorageType() == TextureStorageType::Mipmap) ? static_cast(texture)