diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 55f0695a..1d326d3c 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -434,8 +434,28 @@ namespace MobileGL { } Bool SamplesAsIncompleteTexture(const ITextureObject* texture, const SamplerObject* effectiveSampler) { - const Bool mipmapped = - effectiveSampler != nullptr && effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None; + // A multisample texture is fetched, never filtered. GL 4.6 core 8.17 gives it exactly + // one level and says its sampler state is not used at all - texelFetch is the only way + // a shader can read it - so 8.14's filter-completeness rules, which is what the + // `mipmapped` branch below asks about, never apply to it. + // + // Deriving `mipmapped` from that unused sampler is what made EVERY multisample texture + // look incomplete: MIN_FILTER's initial value is NEAREST_MIPMAP_LINEAR, and a texture + // that can only ever have one level never satisfies the mip-chain check. Both backends + // treat "samples as incomplete" as "do not bind it" (DirectGLES's per-unit walk in + // ResolveAndBindUnitTextures, DirectVulkan's UniformManager), so the sampler2DMS the + // shader declared was left pointing at nothing and every texelFetch read zero. That is + // the sampler2DMS/sampler2DMSArray half of KHR-GL43.compute_shader.resource-texture, + // which fails at the first data7 element with the multisample texture correctly + // cleared and simply never bound. + // + // IsCopyImageEndpointComplete already spells the same guard as + // CopyImageTargetHasMipmapChain; this was the one place that asked without it. + const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown; + const Bool filtered = target != TextureTarget::Texture2DMultisample && + target != TextureTarget::Texture2DMultisampleArray; + const Bool mipmapped = filtered && effectiveSampler != nullptr && + effectiveSampler->GetMipmapMode() != SamplerMipmapMode::None; return !IsMipmapCompleteForFilter(texture, mipmapped); } diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 3c6d100c..eeb07457 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -263,6 +263,63 @@ TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) { extensions.end()); } +// A multisample texture is fetched, never filtered, so the mip-chain completeness rules never +// apply to it (GL 4.6 core 8.17). It has exactly one level and MIN_FILTER's initial value is +// NEAREST_MIPMAP_LINEAR, so asking those rules anyway calls EVERY multisample texture incomplete +// - and both backends express "incomplete" as "leave the native target unbound", which makes the +// shader's sampler2DMS read zero from a texture that was written correctly. +// +// That is KHR-GL43.compute_shader.resource-texture: it clears its 2DMS texture to 123.0 through +// an FBO (which succeeds - the ES clear is issued on a COMPLETE 4-sample framebuffer with no +// error) and then fails at the first sampler2DMS element because the texture was never bound. +TEST(DirectGLESSanity, BindsAMultisampleTextureDespiteTheDefaultMipmapFilter) { + using namespace MobileGL; + namespace DirectGLES = MG_Backend::DirectGLES; + + ScopedDirectGLESTextureBindings state; + + GLuint frontendTexture = 0; + MG_Impl::GLImpl::GenTextures(1, &frontendTexture); + ASSERT_NE(frontendTexture, 0u); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, frontendTexture); + const auto& textureObject = MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2DMultisample) + .GetBoundObject(); + ASSERT_NE(textureObject, nullptr); + + textureObject->SetInternalFormat(TextureInternalFormat::RGBA8); + textureObject->SetSamples(4); + textureObject->SetFixedSampleLocations(false); + // One level, 4x4 - the shape glTexImage2DMultisample produces, and a size whose mip chain + // would need three levels if the filter rules were (wrongly) applied. + MG_State::GLState::AsMipmapTexture(textureObject.get()) + ->AllocateStorage(TextureUploadTarget::Texture2DMultisample, 0, {{4, 4, 1}, 4}); + + // The precondition that used to poison it, asserted rather than assumed: the texture's own + // sampler still reports a mipmapping filter, because GL's initial MIN_FILTER is + // NEAREST_MIPMAP_LINEAR and a multisample texture has no way (and no reason) to change it. + // If a future default made this None the test would pass without covering anything. + const auto& sampler = textureObject->GetSamplerObject(); + ASSERT_NE(sampler, nullptr); + ASSERT_NE(sampler->GetMipmapMode(), SamplerMipmapMode::None) + << "fixture is stale: the default sampler no longer asks for mipmapping, so this test " + "would not exercise the multisample guard"; + + EXPECT_FALSE(MG_State::GLState::SamplesAsIncompleteTexture(textureObject.get(), sampler.get())) + << "a multisample texture is never filter-incomplete"; + + auto& backendTexture = DirectGLES::TextureImpl::g_backendTextureObjects.GetOrCreate(textureObject); + backendTexture = MakeShared(); + const GLuint backendTextureId = backendTexture->GetBackendTextureId(); + + // The symptom itself: the per-unit walk has to actually bind it. + DirectGLES::BindCurrentTextures(); + ASSERT_EQ(state.bindCalls.size(), 1u) + << "the multisample texture was not bound; every texelFetch against it reads zero"; + EXPECT_EQ(state.bindCalls[0].target, GL_TEXTURE_2D_MULTISAMPLE); + EXPECT_EQ(state.bindCalls[0].texture, backendTextureId); +} + TEST(DirectGLESSanity, BindingZeroClearsPreviousNativeTextureBinding) { using namespace MobileGL; namespace DirectGLES = MG_Backend::DirectGLES;