diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 42c0a4e4..042e2445 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -940,6 +940,17 @@ namespace MobileGL::MG_Backend::DirectGLES { // picks a whole different shader for draw_buffers without // explicit_attrib_location. DirectVulkan advertises both. E_GL_ARB_explicit_attrib_location, E_GL_ARB_texture_multisample, E_GL_ARB_shader_image_size, + // Core since GL 3.1 and implemented for every version advertised here. The string + // matters because applications gate the ENTRY POINTS on it rather than on the + // version: a caller that finds the extension missing never resolves + // glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform + // blocks anyway calls through a null pointer. + E_GL_ARB_uniform_buffer_object, + // Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3, + // so on a 4.0 context the string is the only way to reach it. The host ES driver + // has had the same texture parameter since ES 3.1, which every device MobileGL + // runs on provides. + E_GL_ARB_stencil_texturing, // Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the // extension explicitly permits. It is also the only thing that // exposes glProgramParameteri before GL 4.1. diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index e6aa40a1..de01ba69 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -3315,6 +3315,29 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("%s(%s:%d) ES error %s", func, file, line, MG_Util::ConvertGLEnumToString(err).c_str()); }); } + + // GL_DEPTH_STENCIL_TEXTURE_MODE (GL_ARB_stencil_texturing / ES 3.1 core): which aspect + // of a packed depth/stencil image a sampler reads. Until this was forwarded the + // frontend kept the mode as a pure shadow - glGetTexParameter answered it, sampling + // ignored it - so a usampler2D bound to a D24S8 texture in STENCIL_INDEX mode read the + // depth aspect. Texture state rather than sampler state, so multisample targets take + // it too (ES 3.1 8.10 lists it among the three pnames they accept). It is only sent + // when it has moved, which for the overwhelming majority of textures is never. + const Bool supportsStencilTextureMode = + g_GLESCapabilities.GLESVersion.Major > 3 || + (g_GLESCapabilities.GLESVersion.Major == 3 && g_GLESCapabilities.GLESVersion.Minor >= 1); + if (supportsStencilTextureMode) { + const GLenum depthStencilTextureMode = stateTextureObject->GetDepthStencilTextureMode(); + if (m_cacheDepthStencilTextureMode != depthStencilTextureMode) { + g_GLESFuncs.glTexParameteri(target, GL_DEPTH_STENCIL_TEXTURE_MODE, + static_cast(depthStencilTextureMode)); + m_cacheDepthStencilTextureMode = depthStencilTextureMode; + DebugImpl::ErrorLopper::Loop([file = __FILE__, line = __LINE__, func = __func__](GLenum err) { + MGLOG_D("%s(%s:%d) ES error %s", func, file, line, + MG_Util::ConvertGLEnumToString(err).c_str()); + }); + } + } } void ActivateTextureUnit(Uint unit) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index bd7fc955..20c666b4 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -699,6 +699,12 @@ namespace MobileGL::MG_Backend::DirectGLES { FloatVec4 m_cacheBorderColor = {0.0f, 0.0f, 0.0f, 0.0f}; Vec4 m_cacheSwizzleParams = {TextureSwizzleParam::Red, TextureSwizzleParam::Green, TextureSwizzleParam::Blue, TextureSwizzleParam::Alpha}; + // GL_DEPTH_STENCIL_TEXTURE_MODE. GL_DEPTH_COMPONENT is the GL and ES default, so a + // texture that never asks for the stencil aspect never emits the call. The + // depth/stencil readback and replicate-blit emulations also write this parameter + // raw, but only ever on their own scratch textures (never on an application + // texture), so they cannot desynchronise this cache. + GLenum m_cacheDepthStencilTextureMode = GL_DEPTH_COMPONENT; Uint16 m_syncedSamplerVersion = 0; Uint16 m_syncedTextureParamsVersion = 0; }; diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index da2e639b..de4753d2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -517,6 +517,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind, E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size, E_GL_ARB_explicit_attrib_location, + // Core since GL 3.1 and implemented for every version advertised here. The string + // matters because applications gate the ENTRY POINTS on it rather than on the + // version: a caller that finds the extension missing never resolves + // glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform + // blocks anyway calls through a null pointer. + E_GL_ARB_uniform_buffer_object, + // Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3, + // so on a 4.0 context the string is the only way to reach it. + E_GL_ARB_stencil_texturing, // Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the // extension explicitly permits. It is also the only thing that // exposes glProgramParameteri before GL 4.1. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 60240d17..180fa530 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -950,7 +950,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(texture.GetFormat()); const VkComponentMapping sampledComponents = ResolveSampledViewComponents(texture, formatInfo); - const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource->aspect); + const VkImageAspectFlags sampledAspect = + ResolveSampledImageViewAspectMask(resource->aspect, texture.GetDepthStencilTextureMode()); perMipSampledView = CreateImageView(resource->image, resource->format, sampledAspect, resource->viewType, mipLevel, 1, 0, resource->arrayLayers, &sampledComponents); if (perMipSampledView == VK_NULL_HANDLE) { @@ -2238,7 +2239,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (resource.fullView == VK_NULL_HANDLE) { return false; } - const VkImageAspectFlags sampledAspect = ResolveSampledImageViewAspectMask(resource.aspect); + const VkImageAspectFlags sampledAspect = + ResolveSampledImageViewAspectMask(resource.aspect, texture.GetDepthStencilTextureMode()); resource.sampledView = CreateImageView(resource.image, resource.format, sampledAspect, resource.viewType, baseMipLevel, levelCount, 0, resource.arrayLayers, &sampledComponents); if (resource.sampledView == VK_NULL_HANDLE) { @@ -2860,10 +2862,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } - VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect) { + VkImageAspectFlags VkTextureManager::ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect, + GLenum depthStencilTextureMode) { if ((imageAspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) { return VK_IMAGE_ASPECT_COLOR_BIT; } + // A sampled view of a combined depth/stencil image may name exactly one aspect + // (VUID-VkDescriptorImageInfo-imageView-01976), and GL_DEPTH_STENCIL_TEXTURE_MODE is + // what picks it - the whole content of GL_ARB_stencil_texturing. Depth stays the + // default, so nothing that never sets the mode changes shape. The texture's params + // version moves with the mode, which is what makes the cached views be rebuilt. + if (depthStencilTextureMode == GL_STENCIL_INDEX && (imageAspect & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) { + return VK_IMAGE_ASPECT_STENCIL_BIT; + } if ((imageAspect & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) { return VK_IMAGE_ASPECT_DEPTH_BIT; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index a34157c8..be6b37cd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -379,7 +379,11 @@ public: // true - a false positive merely ends the render pass, a false negative would skip a barrier. Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const; - static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect); + // `depthStencilTextureMode` is the texture's GL_DEPTH_STENCIL_TEXTURE_MODE; it only decides + // anything for an image that carries both aspects. Defaulted so the call sites that have no + // texture in hand keep the depth-aspect answer they have always given. + static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect, + GLenum depthStencilTextureMode = GL_DEPTH_COMPONENT); static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain); static Bool AreSampledImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); static Bool AreStorageImageViewFormatsCompatible(VkFormat imageFormat, VkFormat viewFormat); diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 3ef42aff..080445a0 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -1259,7 +1259,15 @@ namespace MobileGL::MG_Impl::GLImpl { MakeUnique("MG_Impl/GLImpl", functionName, "instancecount must be non-negative.")); return; } - if (!MG_State::pGLContext->ValidateTransformFeedbackName(id)) { + // "id is not the name of a transform feedback object" has to mean the same thing here + // as it does to glIsTransformFeedback, and the two predicates are not interchangeable: + // a name glGenTransformFeedbacks handed out is only reserved until it is first bound, + // and only the bind turns it into an object (GL 4.6 core 13.2.1). ValidateTransformFeedbackName + // answers the reservation question - the right one for glBindTransformFeedback, which is + // what turns a reserved name into an object - so using it here let a generated-but-unbound + // name through to the completed-span check below and raised INVALID_OPERATION where the + // spec asks for INVALID_VALUE. Name 0 is the default object and always drawable. + if (id != 0 && !MG_State::pGLContext->IsTransformFeedbackObject(id)) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index 2bfd1655..9e346014 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -117,7 +117,15 @@ namespace MobileGL::MG_State::GLState { void SetFixedSampleLocations(Bool fixedSampleLocations) override; Uint64 GetLifetimeId() const override; GLenum GetDepthStencilTextureMode() const override { return m_depthStencilTextureMode; } - void SetDepthStencilTextureMode(GLenum mode) override { m_depthStencilTextureMode = mode; } + // Bumps the params version like every other backend-visible texture parameter: the mode + // decides which ASPECT of a packed depth/stencil image a sampler reads, which DirectGLES + // forwards as a texture parameter and DirectVulkan bakes into the sampled image view. A + // silent write here would leave both backends showing the aspect they last built. + void SetDepthStencilTextureMode(GLenum mode) override { + if (m_depthStencilTextureMode == mode) return; + m_depthStencilTextureMode = mode; + ++m_textureParamsVersion; + } protected: static Uint64 AllocateLifetimeId(); diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index ce08a706..8475f598 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -198,6 +198,38 @@ TEST(DirectGLESSanity, AdvertisesDepthTextureForGlmarkShadowScenes) { EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_depth_texture), extensions.end()); } +// Two strings that name capabilities MobileGL has always had, and that were missing from the +// advertised list for as long as it existed. +// +// GL_ARB_uniform_buffer_object is the one with teeth: applications gate the ENTRY POINTS on the +// string rather than on the context version. KHR-GL4x.transform_feedback.draw_xfb_instanced_test +// resolves glGetUniformBlockIndex / glUniformBlockBinding only inside `if (is_arb_ubo)`, then +// calls them unconditionally because the context claims >= 4.2 - so a missing string turned into +// a call through a null pointer and took the whole process down with SIGSEGV. Withdrawing it +// again would restore that crash on both backends. +// +// GL_ARB_stencil_texturing is what makes DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX reachable +// at all before GL 4.3, which is the whole of KHR-GL3x.packed_depth_stencil.stencil_texturing. +TEST(DirectGLESSanity, AdvertisesUniformBufferObjectAndStencilTexturing) { + MobileGL::MG_Backend::DirectGLES::BackendObject_DirectGLES backend; + const auto& extensions = backend.GetRendererInfo().RendererGLInfo.Extensions; + + EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_uniform_buffer_object), + extensions.end()); + EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_stencil_texturing), + extensions.end()); +} + +TEST(DirectVulkanSanity, AdvertisesUniformBufferObjectAndStencilTexturing) { + MobileGL::MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; + const auto& extensions = backend.GetRendererInfo().RendererGLInfo.Extensions; + + EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_uniform_buffer_object), + extensions.end()); + EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_stencil_texturing), + extensions.end()); +} + // Voxy only ever needed the extensions, which stay advertised whatever the version is; the version // assertion just pins what the backend really reports, now that V_OpenGL40 is in the list. TEST(DirectGLESSanity, AdvertisesVoxyRequiredRenderingExtensions) { @@ -435,6 +467,31 @@ TEST(DirectVulkanSanity, AdvertisesTextureStorageForDirectStateAccess) { EXPECT_NE(std::find(extensions.begin(), extensions.end(), MobileGL::E_GL_ARB_texture_storage), extensions.end()); } +// A sampled view of a combined depth/stencil image may name exactly one aspect, and +// GL_DEPTH_STENCIL_TEXTURE_MODE picks which - the whole of GL_ARB_stencil_texturing on this +// backend. Depth remains the answer for everything that does not ask for stencil, including +// depth-only images asked for the stencil aspect they do not have. +TEST(DirectVulkanSanity, SampledViewAspectFollowsDepthStencilTextureMode) { + using MobileGL::MG_Backend::DirectVulkan::VkTextureManager; + constexpr VkImageAspectFlags kPacked = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(kPacked, GL_DEPTH_COMPONENT), + static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(kPacked, GL_STENCIL_INDEX), + static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)); + // The default argument is the pre-existing behaviour, for the call sites with no texture. + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(kPacked), + static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)); + + // Single-aspect images ignore the mode: there is only one aspect to name. + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_DEPTH_BIT, GL_STENCIL_INDEX), + static_cast(VK_IMAGE_ASPECT_DEPTH_BIT)); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_STENCIL_BIT, GL_DEPTH_COMPONENT), + static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)); + EXPECT_EQ(VkTextureManager::ResolveSampledImageViewAspectMask(VK_IMAGE_ASPECT_COLOR_BIT, GL_STENCIL_INDEX), + static_cast(VK_IMAGE_ASPECT_COLOR_BIT)); +} + TEST(DirectVulkanSanity, RenderPassExtentUsesSwapchainSizeOnlyForDefaultFramebuffer) { using MobileGL::MG_Backend::DirectVulkan::ResolveRenderPassFramebufferExtent; diff --git a/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp b/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp index d1169f43..58faf612 100644 --- a/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp +++ b/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp @@ -323,6 +323,47 @@ namespace { static_cast(parameterBuffer); } + // A transform feedback name has two different truths and glDrawTransformFeedback used to ask + // for the wrong one. glGenTransformFeedbacks only RESERVES a name; the first + // glBindTransformFeedback is what creates the object (GL 4.6 core 13.2.1), and + // glIsTransformFeedback reports exactly that distinction. glDrawTransformFeedback's + // "id is not the name of a transform feedback object" INVALID_VALUE has to agree with + // glIsTransformFeedback, or a caller that picks an unused name the way + // KHR-GL4x.transform_feedback.api_errors_test does - increment until glIsTransformFeedback + // says false - gets a name the draw then accepts, and the draw falls through to a different + // error entirely (INVALID_OPERATION, "glEndTransformFeedback has never been called"). + // + // The draw path itself needs a backend and a linked program before it reaches the name, which + // this GPU-free suite has neither of, so what is pinned here is the predicate pair the fix + // turns on: the two must not collapse back into one. + TEST_F(NegativeApiErrorsTest, ReservedTransformFeedbackNameIsNotYetAnObject) { + GLuint name = 0; + GenTransformFeedbacks(1, &name); + ASSERT_NE(name, 0u); + DrainErrors(); + + // Reserved, so it is a legal argument to glBindTransformFeedback... + EXPECT_TRUE(MG_State::pGLContext->ValidateTransformFeedbackName(name)); + // ...but not an object yet, which is what a draw must key off. + EXPECT_FALSE(MG_State::pGLContext->IsTransformFeedbackObject(name)); + EXPECT_EQ(IsTransformFeedback(name), GL_FALSE); + + BindTransformFeedback(GL_TRANSFORM_FEEDBACK, name); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + EXPECT_TRUE(MG_State::pGLContext->ValidateTransformFeedbackName(name)); + EXPECT_TRUE(MG_State::pGLContext->IsTransformFeedbackObject(name)); + EXPECT_EQ(IsTransformFeedback(name), GL_TRUE); + + // The default object is never "an object" by this predicate and is always drawable, so + // the draw path has to special-case it rather than reuse the answer directly. + EXPECT_FALSE(MG_State::pGLContext->IsTransformFeedbackObject(0)); + EXPECT_TRUE(MG_State::pGLContext->ValidateTransformFeedbackName(0)); + + BindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); + DrainErrors(); + } + TEST_F(NegativeApiErrorsTest, TexStorage3DRejectsCompressedFormatsOnTexture3D) { GLuint texture = 0; GenTextures(1, &texture); diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index fc07caf3..934d760b 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -1651,6 +1651,43 @@ TEST_F(TextureTest, AnUncompressedRespecificationClearsTheCompressedTag) { EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } +// GL_DEPTH_STENCIL_TEXTURE_MODE used to be a pure frontend shadow: stored, answered by +// glGetTexParameter, and never shown to a backend. Sampling therefore always read the depth +// aspect however the mode was set, which is the whole of +// KHR-GL3x.packed_depth_stencil.stencil_texturing. Both backends pick the aspect up through the +// texture-params version - DirectGLES re-emits glTexParameteri when it moves, DirectVulkan +// rebuilds the sampled image view - so the version bump is the load-bearing part, and a +// no-op write must not spend one (every bump costs DirectVulkan a view recreation). +TEST_F(TextureTest, DepthStencilTextureModeIsBackendVisibleThroughTheParamsVersion) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, 8, 8); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + const auto textureObject = MG_State::pGLContext->GetTextureObject(texture); + ASSERT_NE(textureObject, nullptr); + EXPECT_EQ(textureObject->GetDepthStencilTextureMode(), static_cast(GL_DEPTH_COMPONENT)); + + const Uint16 initialVersion = textureObject->GetTextureParamsVersion(); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(textureObject->GetDepthStencilTextureMode(), static_cast(GL_STENCIL_INDEX)); + EXPECT_NE(textureObject->GetTextureParamsVersion(), initialVersion); + + // Re-writing the value already in force is not a change and must not invalidate anything. + const Uint16 settledVersion = textureObject->GetTextureParamsVersion(); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(textureObject->GetTextureParamsVersion(), settledVersion); + + // ...and going back to the depth aspect is a change again. + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(textureObject->GetDepthStencilTextureMode(), static_cast(GL_DEPTH_COMPONENT)); + EXPECT_NE(textureObject->GetTextureParamsVersion(), settledVersion); +} + namespace { // 8x8 RGTC1: 2x2 blocks of 8 bytes, so the stored image is 32 bytes and one block row is 16. constexpr GLsizei kRgtc1Size8x8 = 32;