[Fix] (GL): report FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS and treat an attached layer past the last one as an incomplete attachment

This commit is contained in:
2026-10-12 08:22:15 -04:00
parent d4501e0c23
commit 4b5c315ea5
4 changed files with 134 additions and 16 deletions
@@ -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;
}
@@ -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<FramebufferAttachmentType>(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) {
@@ -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;
@@ -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<GLenum>(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<GLenum>(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<GLenum>(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<GLenum>(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<GLenum>(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();
}