diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 35bcc6bb..a8669cd5 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -430,7 +430,9 @@ namespace MobileGL::MG_Backend::DirectGLES { auto& unit = MG_State::pGLContext->GetTextureUnitObject(index); for (const auto& bindingSlot : unit.GetAllBindingSlots()) { auto& textureObject = bindingSlot.GetBoundObject(); - if (textureObject) { + // An image-less default texture (name 0) is the slot's initial / "unbound" + // state; it has nothing to sync, so skip it as cheaply as the old null slot. + if (textureObject && !MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) { SyncTextureObjectToBackend(textureObject); } } @@ -1025,6 +1027,10 @@ namespace MobileGL::MG_Backend::DirectGLES { for (const auto& bindingSlot : textureUnit.GetAllBindingSlots()) { const auto& textureObject = bindingSlot.GetBoundObject(); if (!textureObject) continue; + // A default texture (name 0) that was never given an image is the initial / + // "unbound" state of the slot; skip it exactly like the old null slot so bind-0 + // heavy apps pay nothing per draw for the always-populated slots. + if (MG_State::GLState::IsUndefinedDefaultTexture(textureObject.get())) continue; // Bind texture object auto target = textureObject->GetTarget(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index eb41fcd3..4fa257c9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -360,6 +360,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit); const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; outTexture = textureUnit.GetBindingSlot(preferredTarget).GetBoundObject(); + // The slot always holds at least the target's default texture (name 0). While that + // default has no image it is unsampleable; report it as "unbound" so callers keep + // taking their fallback paths instead of trying to sync a storage-less texture. + if (MG_State::GLState::IsUndefinedDefaultTexture(outTexture.get())) { + outTexture.reset(); + } return true; } @@ -380,7 +386,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding]; // GetBoundObject() returns the SharedPtr by const ref; .get() reads the pointer without // touching the refcount (no atomic inc/dec per binding per draw). - return textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get(); + MG_State::GLState::ITextureObject* texture = + textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get(); + // The slot always holds at least the target's default texture (name 0). While that + // default has no image it is unsampleable; report it as "unbound" so the caller + // substitutes its fallback texture exactly like it did for the old null slot. + if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) { + return nullptr; + } + return texture; } Bool UniformManager::ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 4654a311..5007ad88 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -422,13 +422,10 @@ namespace MobileGL::MG_Impl::GLImpl { "2D multisample textures must use depth 1.")); return false; } - if (textureTarget == TextureTarget::Texture2DMultisampleArray && depth == 0) { - MG_State::pGLContext->RecordError( - ErrorCode::InvalidValue, - MakeUnique("MG_Impl/GLImpl", caller, - "2D multisample array textures must have at least one layer.")); - return false; - } + // Zero layers is NOT an error for multisample arrays: GL 4.5 8.8 only raises + // INVALID_VALUE for negative dimensions, and GL CTS's per-case state reset + // (gluStateReset) clears the default GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with + // glTexImage3DMultisample(..., depth = 0) after every case. const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat); if (samples > maxSamples) { @@ -1718,8 +1715,11 @@ namespace MobileGL::MG_Impl::GLImpl { if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return; if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return; // TODO: make sure `internalformat` is in one of supported format for TexBuffer + // GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a + // nonzero name that is not an existing buffer object is an error. This is reachable on + // the default buffer texture now that binding texture 0 binds a real object. auto& bufferObject = MG_State::pGLContext->GetBufferObject(buffer); - if (!bufferObject) { + if (buffer != 0 && !bufferObject) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, MakeUnique("MG_Impl/GLImpl", __func__, @@ -2578,11 +2578,13 @@ namespace MobileGL::MG_Impl::GLImpl { // ===================== Error Checking ============================== if (!TextureImpl::ValidateTextureTarget(textureTarget)) return; - // Name 0 unbinds the current target from the active texture unit. + // GL 3.3 core 3.8: name 0 is the target's default texture object - a real texture that + // glTexImage*/glTexParameter*/glGetTex* must operate on - not "nothing bound". Binding it + // restores the unit/target slot to its initial state. if (texture == 0) { auto& currentUnit = MG_State::pGLContext->GetTextureUnitObject(activeUnit); auto& bindingSlot = currentUnit.GetBindingSlot(textureTarget); - bindingSlot.Bind(nullptr); + bindingSlot.Bind(MG_State::pGLContext->GetDefaultTextureObject(textureTarget)); MG_State::pGLContext->NoteTextureUnitTouched(activeUnit); return; } @@ -2623,14 +2625,27 @@ namespace MobileGL::MG_Impl::GLImpl { void ActiveTexture_State(GLenum texture) { // ===================== Error Checking ============================== - if (texture < GL_TEXTURE0 || texture > GL_TEXTURE31) { + // GL 3.3 core 3.8: ActiveTexture accepts TEXTUREi for i in + // [0, MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1] - NOT a fixed 0..31 range. GL CTS's per-case + // state reset walks every advertised combined unit, so rejecting units the getter + // advertises aborts whole test batches. The backend already clamps its advertised value + // to the state layer's MAX_TEXTURE_IMAGE_UNITS capacity. + GLenum maxCombinedUnits = MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS; + if (MG_Backend::pActiveBackendObject != nullptr) { + maxCombinedUnits = std::min( + maxCombinedUnits, + static_cast( + std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxCombinedTextureImageUnits, + 1))); + } + if (texture < GL_TEXTURE0 || texture >= GL_TEXTURE0 + maxCombinedUnits) { MG_State::pGLContext->RecordError( ErrorCode::InvalidEnum, MakeUnique( "MG_Impl/GLImpl", "ActiveTexture_State", - std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to 31, but got " + std::format("Texture must be one of GL_TEXTUREi, where i is in the range 0 to {}, but got " "invalid enum: 0x{:X}, which may stand for unit {}.", - texture, texture - GL_TEXTURE0))); + maxCombinedUnits - 1, texture, texture - GL_TEXTURE0))); return; } @@ -3032,6 +3047,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& textureObject = bindingSlot.GetBoundObject(); if (!TextureImpl::ValidateTextureObject(textureObject)) return; + if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return; TextureStorage1D(textureObject->GetExternalIndex(), levels, internalformat, width); } @@ -3046,6 +3062,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& textureObject = bindingSlot.GetBoundObject(); if (!TextureImpl::ValidateTextureObject(textureObject)) return; + if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return; TextureStorage2D(textureObject->GetExternalIndex(), levels, internalformat, width, height); } @@ -3061,6 +3078,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto& bindingSlot = activeUnit.GetBindingSlot(textureTarget); auto& textureObject = bindingSlot.GetBoundObject(); if (!TextureImpl::ValidateTextureObject(textureObject)) return; + if (!TextureImpl::ValidateTextureNotDefault(textureObject, __func__)) return; TextureStorage3D(textureObject->GetExternalIndex(), levels, internalformat, width, height, depth); } @@ -3216,8 +3234,10 @@ namespace MobileGL::MG_Impl::GLImpl { auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(static_cast(unit)); MG_State::pGLContext->NoteTextureUnitTouched(static_cast(unit)); if (texture == 0) { + // GL 4.5 8.1: texture zero unbinds every target of the unit, i.e. rebinds each + // target's default texture object (the unit's initial state). for (auto& slot : textureUnit.GetAllBindingSlots()) { - slot.Bind(nullptr); + slot.Bind(MG_State::pGLContext->GetDefaultTextureObject(slot.GetTarget())); } return; } diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp index f9713609..ff1fc635 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp @@ -360,6 +360,19 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { return true; } + Bool ValidateTextureNotDefault(const SharedPtr& textureObject, + const char* caller) { + if (textureObject && textureObject->GetExternalIndex() == 0) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("MG_Impl/GLImpl", caller, + "This operation is not allowed on the default texture (zero is " + "bound to the target).")); + return false; + } + return true; + } + Bool ValidateTextureTargetUniformity(SharedPtr textureObject, TextureTarget target) { if (!textureObject) return true; // should be created later diff --git a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h index 32291304..431e798d 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/Validators.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/Validators.h @@ -29,6 +29,11 @@ namespace MobileGL::MG_Impl::GLImpl::TextureImpl { TexturePixelDataType type); Bool ValidateTextureLevelWithUploadTarget(TextureUploadTarget target, Int level); Bool ValidateTextureObject(SharedPtr textureObject); + // Rejects the per-target default texture objects (name 0) with GL_INVALID_OPERATION for entry + // points that require a GenTextures-created texture, e.g. TexStorage* ("An INVALID_OPERATION + // error is generated if zero is bound to target", ARB_texture_storage). + Bool ValidateTextureNotDefault(const SharedPtr& textureObject, + const char* caller); Bool ValidateTextureTargetUniformity(SharedPtr textureObject, TextureTarget target); Bool ValidateTextureSubImageOffsets(SharedPtr textureObject, Int xoffset, diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index fecbec3e..9fad6cce 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -226,6 +226,10 @@ namespace MobileGL::MG_State { return m_textureState.GetTextureObject(index); } + const SharedPtr& GLContext::GetDefaultTextureObject(TextureTarget target) const { + return m_textureState.GetDefaultTextureObject(target); + } + const SharedPtr& GLContext::CreateTextureObject(Uint index, TextureTarget target) { return m_textureState.CreateTextureObject(index, target); } diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index cb0a3c86..d3f20539 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -95,6 +95,8 @@ namespace MobileGL { // Texture void GenTextureNames(Uint number, Vector& textures); const SharedPtr& GetTextureObject(Uint index); + // Per-target default texture object (name 0); see TextureState::GetDefaultTextureObject. + const SharedPtr& GetDefaultTextureObject(TextureTarget target) const; const SharedPtr& CreateTextureObject(Uint index, TextureTarget target); void MarkTextureObjectForDeletion(Uint index); TextureUnit& GetTextureUnitObject(Int unit); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index a9dc8535..a5f3a21f 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -154,6 +154,17 @@ namespace MobileGL::MG_State::GLState { : nullptr; } + // The per-target default texture objects (name 0) sit permanently in every texture unit's + // binding slots, so "nothing useful bound" is no longer a null slot. While a default texture + // has never been given an image (its internal format is still Unknown) it can contribute + // nothing to sampling; backends treat such a binding exactly like the old empty slot and + // skip per-draw sync/bind work for it. Once an application defines an image on a default + // texture it loses this shortcut and is synced like any other texture. + inline Bool IsUndefinedDefaultTexture(const ITextureObject* texture) { + return texture != nullptr && texture->GetExternalIndex() == 0 && + texture->GetFormat() == TextureInternalFormat::Unknown; + } + class TextureObjectWithOneMipmap : public TextureObjectMipmap { public: TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex) diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index b2bfc78f..76a5b290 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -18,9 +18,51 @@ #include "TextureObjectStubs.h" namespace MobileGL::MG_State::GLState { + static SharedPtr MakeTextureObjectForTarget(Uint index, TextureTarget target) { + switch (target) { + case TextureTarget::Texture1D: + return MakeShared(index); + case TextureTarget::TextureCubeMap: + return MakeShared(index); + case TextureTarget::Texture2D: + return MakeShared(index); + case TextureTarget::Texture3D: + return MakeShared(index); + case TextureTarget::TextureBuffer: + return MakeShared(index); + + // These texture types are still stubbed: + case TextureTarget::TextureRectangle: + return MakeShared(index); + case TextureTarget::Texture2DMultisample: + return MakeShared(index); + case TextureTarget::Texture1DArray: + return MakeShared(index); + case TextureTarget::Texture2DArray: + return MakeShared(index); + case TextureTarget::TextureCubeMapArray: + return MakeShared(index); + case TextureTarget::Texture2DMultisampleArray: + return MakeShared(index); + default: + MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target); + return nullptr; + } + } + TextureState::TextureState() : m_indexGenerator(1024, 1) { + // GL 3.3 core 3.8: each target owns one default texture object (name 0) per context, + // shared across all texture units, and it is the initial binding of every unit/target + // slot. It is created outside m_textureObjects so name-based paths (glIsTexture, + // GenTextures/DeleteTextures, by-name DSA lookups) never see it. + for (int i = 0; i < (int)TextureTarget::TextureTargetCount; ++i) { + m_defaultTextureObjects[i] = MakeTextureObjectForTarget(0, static_cast(i)); + } for (int i = 0; i < MAX_TEXTURE_IMAGE_UNITS; ++i) { m_textureUnits[i] = TextureUnit(); + for (auto& bindingSlot : m_textureUnits[i].GetAllBindingSlots()) { + bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]); + } } } @@ -33,6 +75,12 @@ namespace MobileGL::MG_State::GLState { return nullTextureObject; } + const SharedPtr& TextureState::GetDefaultTextureObject(TextureTarget target) const { + MOBILEGL_ASSERT(target > TextureTarget::Unknown && target < TextureTarget::TextureTargetCount, + "GetDefaultTextureObject: invalid texture target %d", (int)target); + return m_defaultTextureObjects[(int)target]; + } + void TextureState::GenerateNames(Uint number, Vector& textures) { textures.resize(number); m_indexGenerator.Generate(number, textures.data()); @@ -40,48 +88,11 @@ namespace MobileGL::MG_State::GLState { const SharedPtr& TextureState::CreateTextureObject(Uint index, TextureTarget target) { auto& textureObject = m_textureObjects[index]; - switch (target) { - case TextureTarget::Texture1D: - textureObject = MakeShared(index); - break; - case TextureTarget::TextureCubeMap: - textureObject = MakeShared(index); - break; - case TextureTarget::Texture2D: - textureObject = MakeShared(index); - break; - case TextureTarget::Texture3D: - textureObject = MakeShared(index); - break; - case TextureTarget::TextureBuffer: - textureObject = MakeShared(index); - break; - - // These texture types are still stubbed: - case TextureTarget::TextureRectangle: - textureObject = MakeShared(index); - break; - case TextureTarget::Texture2DMultisample: - textureObject = MakeShared(index); - break; - case TextureTarget::Texture1DArray: - textureObject = MakeShared(index); - break; - case TextureTarget::Texture2DArray: - textureObject = MakeShared(index); - break; - case TextureTarget::TextureCubeMapArray: - textureObject = MakeShared(index); - break; - case TextureTarget::Texture2DMultisampleArray: - textureObject = MakeShared(index); - break; - default: - MOBILEGL_ASSERT(false, "Unimplemented texture type when creating texture object!: %d", (int)target); + textureObject = MakeTextureObjectForTarget(index, target); + if (!textureObject) { static SharedPtr nullTextureObject = nullptr; return nullTextureObject; } - return textureObject; } @@ -94,7 +105,9 @@ namespace MobileGL::MG_State::GLState { auto& bindingSlots = m_textureUnits[unit].GetAllBindingSlots(); for (auto& bindingSlot : bindingSlots) { if (bindingSlot.GetBoundObject() == it->second) { - bindingSlot.Bind(nullptr); + // GL 3.3 core 3.8.1: deleting a bound texture rebinds zero, i.e. the + // target's default texture object, on every unit it was bound to. + bindingSlot.Bind(m_defaultTextureObjects[(int)bindingSlot.GetTarget()]); } } } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index 7653d7bd..6f6af3a2 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -49,6 +49,12 @@ namespace MobileGL::MG_State::GLState { void GenerateNames(Uint number, Vector& textures); const SharedPtr& CreateTextureObject(Uint index, TextureTarget target); const SharedPtr& GetTextureObject(Uint index); + // The context's default texture object (name 0) for `target`. GL 3.3 core 3.8: texture + // zero names a real, per-target texture object shared by every texture unit; binding 0 + // binds it, and image/parameter calls on it must work like on any texture. It is not a + // GenTextures name: it lives outside m_textureObjects (so glIsTexture(0) stays GL_FALSE + // and by-name lookups keep failing for 0) and can never be deleted. + const SharedPtr& GetDefaultTextureObject(TextureTarget target) const; TextureUnit& GetUnitObject(Int unit); ImageTextureBinding& GetImageTextureBinding(Int unit); const ImageTextureBinding& GetImageTextureBinding(Int unit) const; @@ -83,5 +89,8 @@ namespace MobileGL::MG_State::GLState { Array m_imageTextureBindings; IndexGenerator m_indexGenerator; UnorderedMap> m_textureObjects; + // One default texture object (external name 0) per target, created with the context and + // immortal for its lifetime; the initial binding of every unit/target slot. + Array, (int)TextureTarget::TextureTargetCount> m_defaultTextureObjects; }; } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 1e6cd5b2..55257e36 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -151,14 +151,20 @@ namespace { } // namespace TEST_F(TextureTest, CreateTexturesCreatesObjectsWithoutBinding) { + auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); + const auto boundBefore = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(); + GLuint texture = 0; MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture); EXPECT_NE(texture, 0u); EXPECT_TRUE(MG_State::pGLContext->ValidateTextureObject(texture)); - auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); - EXPECT_EQ(unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(), nullptr); + // CreateTextures must not disturb the unit's binding (which is never empty anymore: at + // minimum it holds the target's default texture object). + EXPECT_EQ(unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(), boundBefore); + EXPECT_NE(unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(), + MG_State::pGLContext->GetTextureObject(texture)); EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); } @@ -441,14 +447,17 @@ TEST_F(TextureTest, BindTextureUnitEnumAsNameIsSilentNoOp) { EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(textureUnitEnum)); } -TEST_F(TextureTest, TexSubImage2DWithoutBoundTextureReportsErrorInsteadOfDereferencingNull) { +TEST_F(TextureTest, TexSubImage2DOnImagelessDefaultTextureReportsErrorInsteadOfDereferencingNull) { MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + // Texture zero is a real (default) texture object now, so TexSubImage2D no longer fails for + // want of a bound object - it fails because the region exceeds the default texture's (empty + // or zero-sized) level 0, which is GL_INVALID_VALUE per GL 3.3 core 3.8.2. const Uint8 pixel[] = {1, 2, 3, 4}; MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); - EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); } TEST_F(TextureTest, TextureStorageAndSubImageModifyNamedObjectOnly) { @@ -1629,3 +1638,272 @@ TEST_F(TextureTest, NormalizeDepth24Stencil8UsesPackedDepthStencilType) { EXPECT_EQ(format, GL_DEPTH_STENCIL); EXPECT_EQ(type, GL_UNSIGNED_INT_24_8); } + +// ==================== Default texture objects (name 0), GL 3.3 core 3.8 ==================== + +TEST_F(TextureTest, DefaultTextureIsBoundInitiallyAndIsPerTarget) { + // The initial binding of every unit/target slot is the target's default texture object. + const auto& default2D = MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D); + const auto& default3D = MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture3D); + ASSERT_NE(default2D, nullptr); + ASSERT_NE(default3D, nullptr); + EXPECT_NE(default2D, default3D); + EXPECT_EQ(default2D->GetExternalIndex(), 0u); + EXPECT_EQ(default2D->GetTarget(), TextureTarget::Texture2D); + EXPECT_EQ(default3D->GetTarget(), TextureTarget::Texture3D); + + // Binding 0 restores the default object, and the binding query reports name 0. + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + default2D); + GLint binding = -1; + MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &binding); + EXPECT_EQ(binding, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // One default per target per context, shared across all texture units. + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(5) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + default2D); +} + +TEST_F(TextureTest, DefaultTextureAcceptsImageAndParameterCalls) { + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The exact shape of GL CTS's per-case state reset (gluStateReset resetStateGLCore): a + // zero-sized TexImage2D plus parameter resets on the default texture, all of which must + // succeed without recording anything. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + GLint minFilter = 0; + MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, &minFilter); + EXPECT_EQ(minFilter, GL_NEAREST); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // A real upload works like on any texture: data lands in the default object's shadow store. + const Uint8 pixels[] = { + 1, 2, 3, 4, + 5, 6, 7, 8, + }; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + const auto& default2D = MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D); + auto* mipmapObject = static_cast(default2D.get()); + EXPECT_EQ(mipmapObject->GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0), IntVec3(2, 1, 1)); + const auto* stored = + static_cast(mipmapObject->MapMipmapData(TextureUploadTarget::Texture2D, 0)); + ASSERT_NE(stored, nullptr); + EXPECT_EQ(std::memcmp(stored, pixels, sizeof(pixels)), 0); + + // Restore the CTS-reset shape (zero-sized level 0, default parameters) so later tests see + // the default texture in its usual post-reset state regardless of execution order. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, DefaultTextureParametersAreSharedAcrossUnits) { + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The same default object is bound on every unit, so the parameter shows up on unit 1 too. + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE1); + GLint wrapS = 0; + MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, &wrapS); + EXPECT_EQ(wrapS, GL_CLAMP_TO_EDGE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // The 3D default is a distinct object and keeps its own (initial) wrap mode. + GLint wrapS3D = 0; + MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, &wrapS3D); + EXPECT_EQ(wrapS3D, GL_REPEAT); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, DefaultTextureIsNotAnObjectNameAndSurvivesDeleteCalls) { + // glIsTexture(0) is GL_FALSE (name 0 is never a GenTextures name), with no error. + EXPECT_EQ(MG_Impl::GLImpl::IsTexture(0), GL_FALSE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureObject(0)); + EXPECT_FALSE(MG_State::pGLContext->ValidateTextureName(0)); + + // Deleting name 0 is silently ignored and leaves the default object fully usable. + constexpr GLuint zero = 0; + MG_Impl::GLImpl::DeleteTextures(1, &zero); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_NE(MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D), nullptr); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, DeletingBoundTextureRebindsDefaultTexture) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // GL 3.3 core 3.8.1: deleting the bound texture is as if BindTexture(target, 0) had run. + EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0) + .GetBindingSlot(TextureTarget::Texture2D) + .GetBoundObject(), + MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D)); + GLint binding = -1; + MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &binding); + EXPECT_EQ(binding, 0); + + // Image and parameter calls keep working against the (now bound) default texture. + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, NamedTextureRebindsAndWorksAfterUsingDefault) { + GLuint texture = 0; + MG_Impl::GLImpl::GenTextures(1, &texture); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + const Uint8 pixels[] = {9, 8, 7, 6}; + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // Detour through the default texture, then rebind the named one. + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture); + + GLint binding = -1; + MG_Impl::GLImpl::GetIntegerv(GL_TEXTURE_BINDING_2D, &binding); + EXPECT_EQ(binding, static_cast(texture)); + + // The named texture's contents were not disturbed by the operations on the default. + const auto* stored = GetBoundTexture2DLevelBytes(texture); + ASSERT_NE(stored, nullptr); + EXPECT_EQ(std::memcmp(stored, pixels, sizeof(pixels)), 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteTextures(1, &texture); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); +} + +TEST_F(TextureTest, TexStorageOnDefaultTextureIsInvalidOperation) { + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // ARB_texture_storage: "An INVALID_OPERATION error is generated if zero is bound to target" + // - immutable storage can never be established on a default texture. + MG_Impl::GLImpl::TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2); + ExpectSingleGlError(GL_INVALID_OPERATION); + EXPECT_FALSE(MG_State::pGLContext->GetDefaultTextureObject(TextureTarget::Texture2D)->IsImmutable()); +} + +TEST_F(TextureTest, CtsStyleStateResetOnDefaultTexturesLeavesNoError) { + // Mirrors the texture section of VK-GL-CTS gluStateReset resetStateGLCore, which runs after + // EVERY case: bind 0 on each target, clear the default texture's image with a zero-sized + // TexImage*, and reset sampler-ish parameters. Any leftover error aborts the whole batch + // ("Texture state reset failed"), so this exact sequence must stay clean end to end. + const GLfloat borderColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + const auto resetCommonTexParams = [&borderColor](GLenum target) { + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + MG_Impl::GLImpl::TexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColor); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_WRAP_S, GL_REPEAT); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_WRAP_T, GL_REPEAT); + MG_Impl::GLImpl::TexParameterf(target, GL_TEXTURE_MIN_LOD, -1000.0f); + MG_Impl::GLImpl::TexParameterf(target, GL_TEXTURE_MAX_LOD, 1000.0f); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_BASE_LEVEL, 0); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_MAX_LEVEL, 1000); + MG_Impl::GLImpl::TexParameterf(target, GL_TEXTURE_LOD_BIAS, 0.0f); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_COMPARE_MODE, GL_NONE); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_R, GL_RED); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_G, GL_GREEN); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_B, GL_BLUE); + MG_Impl::GLImpl::TexParameteri(target, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); + }; + + MG_Impl::GLImpl::ActiveTexture(GL_TEXTURE0); + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D, 0); + MG_Impl::GLImpl::TexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + resetCommonTexParams(GL_TEXTURE_1D); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_1D reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + resetCommonTexParams(GL_TEXTURE_2D); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, 0); + for (int face = 0; face < 6; ++face) { + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_RGBA, 0, 0, 0, GL_RGBA, + GL_UNSIGNED_BYTE, nullptr); + } + resetCommonTexParams(GL_TEXTURE_CUBE_MAP); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_CUBE_MAP reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, 0); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + resetCommonTexParams(GL_TEXTURE_2D_ARRAY); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_ARRAY reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, 0); + MG_Impl::GLImpl::TexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT); + resetCommonTexParams(GL_TEXTURE_3D); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_3D reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_1D_ARRAY, 0); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + resetCommonTexParams(GL_TEXTURE_1D_ARRAY); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_1D_ARRAY reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_RECTANGLE, 0); + MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, + nullptr); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_RECTANGLE reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_BUFFER, 0); + MG_Impl::GLImpl::TexBuffer(GL_TEXTURE_BUFFER, GL_R8, 0); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_BUFFER reset failed"; + + // 3.2-core section: multisample defaults are cleared with ZERO-sized (and, for the array + // target, zero-layer) TexImage*Multisample calls - GL only rejects negative dimensions. + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_R, GL_RED); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_G, GL_GREEN); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_B, GL_BLUE); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_BASE_LEVEL, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_MAX_LEVEL, 1000); + MG_Impl::GLImpl::TexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 1, GL_RGBA8, 0, 0, GL_TRUE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE reset failed"; + + MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_R, GL_RED); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_G, GL_GREEN); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_B, GL_BLUE); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_SWIZZLE_A, GL_ALPHA); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_BASE_LEVEL, 0); + MG_Impl::GLImpl::TexParameteri(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_MAX_LEVEL, 1000); + MG_Impl::GLImpl::TexImage3DMultisample(GL_TEXTURE_2D_MULTISAMPLE_ARRAY, 1, GL_RGBA8, 0, 0, 0, GL_TRUE); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "GL_TEXTURE_2D_MULTISAMPLE_ARRAY reset failed"; +}