From 4b5c315ea59797d74966f0485302735f2101f59f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 08:01:30 -0400 Subject: [PATCH] [Fix] (GL): report FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS and treat an attached layer past the last one as an incomplete attachment --- .../GLImpl/Framebuffer/GL_Framebuffer.cpp | 25 ++++--- .../FramebufferState/FramebufferObject.cpp | 55 +++++++++++++++- .../FramebufferState/FramebufferObject.h | 4 ++ .../MG_Test/Framebuffer/FramebufferTest.cpp | 66 +++++++++++++++++++ 4 files changed, 134 insertions(+), 16 deletions(-) diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index 4510ccab..b9690fb4 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -106,6 +106,15 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } + // Which status an incomplete framebuffer reports. GL 4.6 core 9.4.2 leaves the choice open + // when several rules fail; the layer rule is checked first because it is a whole-framebuffer + // property no single attachment's state explains. + GLenum IncompleteFramebufferStatus(const MG_State::GLState::FramebufferObject& framebufferObject) { + if (framebufferObject.HasLayerTargetMismatch()) return GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS; + return HasDefinedAttachment(framebufferObject) ? GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT + : GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; + } + // Whether the backend can actually attach this color format to a framebuffer. Preferred // source of truth is the backend's probed format-capability cache (real glCheckFramebufferStatus // probes, so extensions like EXT_render_snorm are respected). Formats a probe-less backend @@ -2329,15 +2338,9 @@ namespace MobileGL::MG_Impl::GLImpl { return GL_FRAMEBUFFER_UNDEFINED; } - // TODO: distinguish GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT and GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT // TODO: GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER, GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER, - // additional GL_FRAMEBUFFER_UNSUPPORTED cases, GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE, - // GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS - if (!framebufferObject->CheckCompleteness()) { - return HasDefinedAttachment(*framebufferObject) ? - GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : - GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; - } + // additional GL_FRAMEBUFFER_UNSUPPORTED cases, GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE + if (!framebufferObject->CheckCompleteness()) return IncompleteFramebufferStatus(*framebufferObject); if (HasNonRenderableColorAttachment(*framebufferObject)) { return GL_FRAMEBUFFER_UNSUPPORTED; } @@ -2361,11 +2364,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto framebufferObject = GetNamedFramebufferObject_State(framebuffer, "CheckNamedFramebufferStatus_State"); if (!framebufferObject) return GL_FRAMEBUFFER_UNDEFINED; - if (!framebufferObject->CheckCompleteness()) { - return HasDefinedAttachment(*framebufferObject) ? - GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT : - GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT; - } + if (!framebufferObject->CheckCompleteness()) return IncompleteFramebufferStatus(*framebufferObject); if (HasNonRenderableColorAttachment(*framebufferObject)) { return GL_FRAMEBUFFER_UNSUPPORTED; } diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp b/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp index 024811de..4a9f7bf5 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp @@ -93,8 +93,31 @@ namespace MobileGL::MG_State::GLState { Bool FramebufferAttachmentObject::IsComplete() const { if (IsTexture()) { - Bool complete = m_texture->IsComplete(); - return complete; + if (!m_texture->IsComplete()) return false; + // GL 4.6 core 9.4.1: a single layer of a three-dimensional, array or cube map array + // texture must exist, i.e. FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER has to be smaller than + // the level's depth / layer count. Attaching it is legal up to the implementation's + // layer limit (9.2.8), so this is where an out-of-range layer is caught. A cube map + // array counts layer-faces, which is the depth it was specified with. + if (!m_layered) { + const IntVec3 size = GetSize(); + Int layerCount = -1; + switch (m_texture->GetTarget()) { + case TextureTarget::Texture1DArray: + layerCount = size.y(); + break; + case TextureTarget::Texture3D: + case TextureTarget::Texture2DArray: + case TextureTarget::Texture2DMultisampleArray: + case TextureTarget::TextureCubeMapArray: + layerCount = size.z(); + break; + default: + break; + } + if (layerCount >= 0 && m_textureLayer >= layerCount) return false; + } + return true; } else if (IsRenderbuffer()) { Bool complete = m_renderbuffer->IsAllocated(); return complete; @@ -194,7 +217,33 @@ namespace MobileGL::MG_State::GLState { } if (validAttachmentCount == 0) return false; - return true; + return !HasLayerTargetMismatch(); + } + + Bool FramebufferObject::HasLayerTargetMismatch() const { + Bool anyLayered = false; + Bool anyNonLayered = false; + TextureTarget layeredColorTarget = TextureTarget::Unknown; + Bool colorTargetsDiffer = false; + for (SizeT index = 0; index < m_attachmentObjects.size(); ++index) { + const auto& attachment = m_attachmentObjects[index]; + if (!attachment.IsValid() || attachment.IsEmpty()) continue; + // A renderbuffer is never layered. + const Bool layered = attachment.IsTexture() && attachment.IsLayered(); + (layered ? anyLayered : anyNonLayered) = true; + + const auto type = static_cast(index); + if (!layered || type < FramebufferAttachmentType::Color0 || type > FramebufferAttachmentType::ColorMax) { + continue; + } + const TextureTarget target = attachment.GetTexture()->GetTarget(); + if (layeredColorTarget == TextureTarget::Unknown) { + layeredColorTarget = target; + } else if (layeredColorTarget != target) { + colorTargetsDiffer = true; + } + } + return anyLayered && (anyNonLayered || colorTargetsDiffer); } void FramebufferObject::SetDrawBuffer(Uint index, FramebufferAttachmentType buffer) { diff --git a/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.h b/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.h index 3f173380..689cd61b 100644 --- a/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.h +++ b/MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.h @@ -128,6 +128,10 @@ namespace MobileGL { const FramebufferAttachmentObject& GetAttachment(FramebufferAttachmentType type) const; const FramebufferAttachmentObjectArray& GetAllAttachmentObjects() const; Bool CheckCompleteness() const; + // GL 4.6 core 9.4.2 FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: some populated attachment is + // layered while another is not, or the layered color attachments come from textures of + // different targets. CheckCompleteness() includes it; this tells the status apart. + Bool HasLayerTargetMismatch() const; // aka. `buffer` as in glDrawBuffers/glReadBuffers void SetDrawBuffer(Uint index, FramebufferAttachmentType buffer); const FramebufferAttachmentArray& GetDrawBuffers() const; diff --git a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp index ef7db9f9..2379ed6a 100644 --- a/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp +++ b/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp @@ -1634,3 +1634,69 @@ TEST_F(FramebufferTest, NamedFramebufferTextureRejectsALevelTheTextureDoesNotHav EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_VALUE); DrainPendingGlErrors(); } + +// GL 4.6 core 9.4.2: if any populated attachment is layered, all of them must be, and the layered +// color attachments must come from textures of one target - otherwise the framebuffer is +// FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS (KHR-GL4x geometry_shader.layered_fbo, +// texture_cube_map_array.fbo_incompleteness). +TEST_F(FramebufferTest, MixedLayeredAttachmentsAreIncompleteLayerTargets) { + GLuint textures[3] = {}; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &textures[0]); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D_ARRAY, 1, &textures[1]); + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &textures[2]); + MG_Impl::GLImpl::TextureStorage2D(textures[0], 1, GL_RGBA8, 8, 8); + MG_Impl::GLImpl::TextureStorage3D(textures[1], 1, GL_RGBA8, 8, 8, 4); + MG_Impl::GLImpl::TextureStorage3D(textures[2], 1, GL_RGBA8, 8, 8, 4); + GLuint framebuffer = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + // One layered (the whole 2D array) beside one that is not. + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, textures[1], 0); + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT1, textures[0], 0); + EXPECT_EQ(MG_Impl::GLImpl::CheckNamedFramebufferStatus(framebuffer, GL_DRAW_FRAMEBUFFER), + static_cast(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS)); + + // Both layered, but from a 2D array and a 3D texture. + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT1, textures[2], 0); + EXPECT_EQ(MG_Impl::GLImpl::CheckNamedFramebufferStatus(framebuffer, GL_DRAW_FRAMEBUFFER), + static_cast(GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS)); + + // The positive control: one layered target throughout. + MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT1, 0, 0); + EXPECT_EQ(MG_Impl::GLImpl::CheckNamedFramebufferStatus(framebuffer, GL_DRAW_FRAMEBUFFER), + static_cast(GL_FRAMEBUFFER_COMPLETE)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::DeleteTextures(3, textures); + DrainPendingGlErrors(); +} + +// GL 4.6 core 9.4.1: a single layer of a 3D / array texture has to exist. Attaching layer == depth +// is legal (only the implementation's layer limit is checked at attach time); the attachment is +// what is incomplete. +TEST_F(FramebufferTest, AttachedLayerPastTheLastLayerIsIncompleteAttachment) { + GLuint texture = 0; + MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture); + MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 8, 8, 2); + GLuint framebuffer = 0; + MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer); + ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 2); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER), + static_cast(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT)); + + MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 1); + EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_DRAW_FRAMEBUFFER), + static_cast(GL_FRAMEBUFFER_COMPLETE)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer); + MG_Impl::GLImpl::DeleteTextures(1, &texture); + DrainPendingGlErrors(); +}