Files
MobileGL/MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp
T

861 lines
42 KiB
C++

// MobileGL - MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#include <limits>
#include "Includes.h"
#include "Init.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectGLES/Utils.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/Texture/GL_Texture.h>
#include <MG_State/GLState/Core.h>
using namespace MobileGL;
namespace {
SharedPtr<MG_State::GLState::FramebufferObject> g_lastBlitReadFramebuffer;
SharedPtr<MG_State::GLState::FramebufferObject> g_lastBlitDrawFramebuffer;
Int g_blitNamedFramebufferCallCount = 0;
SharedPtr<MG_State::GLState::FramebufferObject> g_lastClearFramebuffer;
GLenum g_lastClearBuffer = GL_NONE;
GLint g_lastClearDrawbuffer = -1;
GLfloat g_lastClearDepth = -1.0f;
GLint g_lastClearStencil = -1;
FloatVec4 g_lastClearColor = {};
Int g_clearNamedFramebufferfvCallCount = 0;
Int g_clearNamedFramebufferfiCallCount = 0;
Int g_readPixelsCallCount = 0;
GLenum g_lastReadPixelsFormat = GL_NONE;
GLenum g_lastReadPixelsType = GL_NONE;
void RecordBlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield, GLenum) {
g_lastBlitReadFramebuffer = readFramebuffer;
g_lastBlitDrawFramebuffer = drawFramebuffer;
++g_blitNamedFramebufferCallCount;
}
void RecordClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLfloat* value) {
g_lastClearFramebuffer = framebuffer;
g_lastClearBuffer = buffer;
g_lastClearDrawbuffer = drawbuffer;
if (value) {
if (buffer == GL_COLOR) {
g_lastClearColor = FloatVec4(value[0], value[1], value[2], value[3]);
} else if (buffer == GL_DEPTH) {
g_lastClearDepth = value[0];
}
}
++g_clearNamedFramebufferfvCallCount;
}
void RecordClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
g_lastClearFramebuffer = framebuffer;
g_lastClearBuffer = buffer;
g_lastClearDrawbuffer = drawbuffer;
g_lastClearDepth = depth;
g_lastClearStencil = stencil;
++g_clearNamedFramebufferfiCallCount;
}
void RecordReadPixels(GLint, GLint, GLsizei, GLsizei, GLenum format, GLenum type, void*) {
++g_readPixelsCallCount;
g_lastReadPixelsFormat = format;
g_lastReadPixelsType = type;
}
} // namespace
class FramebufferTest : public ::testing::Test {
protected:
// GL error flags are sticky per error code and the context outlives an individual test in this
// binary, so drain whatever an earlier test left pending - otherwise an error-code assertion
// here reads someone else's error. Bounded: one flag per code, so this cannot hang the suite.
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {
}
}
// The call under test must raise exactly the expected error and nothing more: a second pending
// error means one entry point queued several, which GetError() would hand out at an unrelated
// call site later on.
static void ExpectSingleGlError(GLenum expected) {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), expected);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "the call recorded more than one error";
}
void TearDown() override {
// Attribute a leaked error to the test that caused it instead of to whoever runs next.
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
void SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
ASSERT_NE(defaultFramebuffer, nullptr);
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).Bind(defaultFramebuffer);
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).Bind(defaultFramebuffer);
defaultFramebuffer->SetDrawBuffer(0, FramebufferAttachmentType::BackLeft);
for (Uint i = 1; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
defaultFramebuffer->SetDrawBuffer(i, FramebufferAttachmentType::None);
}
defaultFramebuffer->SetReadBuffer(FramebufferAttachmentType::BackLeft);
g_lastBlitReadFramebuffer = nullptr;
g_lastBlitDrawFramebuffer = nullptr;
g_blitNamedFramebufferCallCount = 0;
g_lastClearFramebuffer = nullptr;
g_lastClearBuffer = GL_NONE;
g_lastClearDrawbuffer = -1;
g_lastClearDepth = -1.0f;
g_lastClearStencil = -1;
g_lastClearColor = {};
g_clearNamedFramebufferfvCallCount = 0;
g_clearNamedFramebufferfiCallCount = 0;
g_readPixelsCallCount = 0;
g_lastReadPixelsFormat = GL_NONE;
g_lastReadPixelsType = GL_NONE;
MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer = nullptr;
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv = nullptr;
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi = nullptr;
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = nullptr;
}
};
TEST_F(FramebufferTest, CreateFramebuffersCreatesObjectsImmediately) {
GLuint framebuffers[2] = {};
MG_Impl::GLImpl::CreateFramebuffers(2, framebuffers);
EXPECT_NE(framebuffers[0], 0u);
EXPECT_NE(framebuffers[1], 0u);
EXPECT_TRUE(MG_State::pGLContext->ValidateFramebufferObject(framebuffers[0]));
EXPECT_TRUE(MG_State::pGLContext->ValidateFramebufferObject(framebuffers[1]));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 3.3 core 4.4.1/4.4.2 name lifecycle - mirrors the rules asserted for the other object
// families: deleting an unknown name is silent, a released reservation is recycled, and binding
// a dead name is INVALID_OPERATION.
TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedFramebufferNameIsSilent) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer);
ASSERT_NE(framebuffer, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Not a small literal: other tests in this binary share the context and generate names in
// bulk, so a low number may well be a legitimately reserved name here.
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
MG_Impl::GLImpl::DeleteFramebuffers(2, unknownNames);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DeleteGeneratedButUnboundFramebufferNameReleasesReservationAndBindFails) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::GenFramebuffers(1, &framebuffer);
ASSERT_NE(framebuffer, 0u);
ASSERT_TRUE(MG_State::pGLContext->ValidateFramebufferName(framebuffer));
MG_Impl::GLImpl::DeleteFramebuffers(1, &framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateFramebufferName(framebuffer));
MG_Impl::GLImpl::BindFramebuffer(GL_FRAMEBUFFER, framebuffer);
ExpectSingleGlError(GL_INVALID_OPERATION);
GLuint recycled = 0;
MG_Impl::GLImpl::GenFramebuffers(1, &recycled);
EXPECT_EQ(recycled, framebuffer);
}
TEST_F(FramebufferTest, DeleteOfUnknownOrAlreadyDeletedRenderbufferNameIsSilent) {
GLuint renderbuffer = 0;
MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer);
ASSERT_NE(renderbuffer, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// Not a small literal: other tests in this binary share the context and generate names in
// bulk, so a low number may well be a legitimately reserved name here.
const GLuint unknownNames[] = {0u, std::numeric_limits<GLuint>::max()};
MG_Impl::GLImpl::DeleteRenderbuffers(2, unknownNames);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DeleteGeneratedButUnboundRenderbufferNameReleasesReservationAndBindFails) {
GLuint renderbuffer = 0;
MG_Impl::GLImpl::GenRenderbuffers(1, &renderbuffer);
ASSERT_NE(renderbuffer, 0u);
ASSERT_TRUE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer));
MG_Impl::GLImpl::DeleteRenderbuffers(1, &renderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_FALSE(MG_State::pGLContext->ValidateRenderbufferName(renderbuffer));
MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
ExpectSingleGlError(GL_INVALID_OPERATION);
GLuint recycled = 0;
MG_Impl::GLImpl::GenRenderbuffers(1, &recycled);
EXPECT_EQ(recycled, renderbuffer);
}
TEST_F(FramebufferTest, DefaultFramebufferIdentityTracksFramebufferNameZero) {
const auto defaultFramebuffer = MG_State::pGLContext->GetFramebufferObject(0);
ASSERT_NE(defaultFramebuffer, nullptr);
EXPECT_TRUE(defaultFramebuffer->IsDefaultFramebuffer());
const auto defaultFramebufferCopy = *defaultFramebuffer;
EXPECT_TRUE(defaultFramebufferCopy.IsDefaultFramebuffer());
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
const auto userFramebuffer = MG_State::pGLContext->GetFramebufferObject(framebuffer);
ASSERT_NE(userFramebuffer, nullptr);
EXPECT_FALSE(userFramebuffer->IsDefaultFramebuffer());
}
TEST_F(FramebufferTest, NamedFramebufferTextureAttachesWithoutChangingBindings) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
Vector<Uint> textureNames;
MG_State::pGLContext->GenTextureNames(1, textureNames);
MG_State::pGLContext->CreateTextureObject(textureNames[0], TextureTarget::Texture2D);
const auto originalDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const auto originalRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, textureNames[0], 3);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0);
EXPECT_TRUE(attachment.IsTexture());
EXPECT_EQ(attachment.GetTexture()->GetExternalIndex(), textureNames[0]);
EXPECT_EQ(attachment.GetTextureLevel(), 3);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), originalDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), originalRead);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, NamedDepthFramebufferTextureStorageIsCompleteWithoutBinding) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
const auto originalDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const auto originalRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_DEPTH_COMPONENT24, 64, 32);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_DEPTH_ATTACHMENT, texture, 0);
EXPECT_EQ(MG_Impl::GLImpl::CheckNamedFramebufferStatus(framebuffer, GL_FRAMEBUFFER), GL_FRAMEBUFFER_COMPLETE);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), originalDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), originalRead);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, FramebufferTextureBumpsAttachmentVersionOnlyOnce) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 64, 32);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
const auto beforeVersions = framebufferObject->GetAllFramebufferAttachmentVersions();
const auto beforeObjectVersion = framebufferObject->GetObjectVersion();
MG_Impl::GLImpl::FramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0);
const auto afterVersions = framebufferObject->GetAllFramebufferAttachmentVersions();
EXPECT_EQ(afterVersions[static_cast<SizeT>(FramebufferAttachmentType::Color0)],
beforeVersions[static_cast<SizeT>(FramebufferAttachmentType::Color0)] + 1);
EXPECT_EQ(framebufferObject->GetObjectVersion(), beforeObjectVersion + 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ReadPixelsAllowsPersistentMappedPixelPackBuffer) {
GLuint framebuffer = 0;
GLuint texture = 0;
GLuint pixelPackBuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::CreateBuffers(1, &pixelPackBuffer);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Impl::GLImpl::BindBuffer(GL_PIXEL_PACK_BUFFER, pixelPackBuffer);
MG_Impl::GLImpl::BufferStorage(GL_PIXEL_PACK_BUFFER, 4 * 4 * 4, nullptr,
GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT);
ASSERT_NE(MG_Impl::GLImpl::MapBufferRange(GL_PIXEL_PACK_BUFFER, 0, 4 * 4 * 4,
GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT),
nullptr);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ReadPixelsRejectsMismatchedPackedTypeFormatPairs) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
Uint8 pixelStorage[4 * 4 * 4] = {};
// Packed RGB type with a non-RGB format must never reach the backend (GL CTS packed_pixels
// reads GL_RED with GL_UNSIGNED_SHORT_5_6_5 and expects an error).
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RED, GL_UNSIGNED_SHORT_5_6_5, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// Packed RGBA type with a non-RGBA/BGRA format is rejected as well.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGB, GL_UNSIGNED_INT_8_8_8_8, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// Packed depth-stencil type requires the DEPTH_STENCIL format.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_INT_24_8, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// A plain RGBA/UNSIGNED_BYTE readback keeps working.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ReadPixelsForwardsSingleChannelDesktopClientFormats) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
Uint8 pixelStorage[4 * 4 * 4] = {};
// Desktop GL treats GL_GREEN/GL_BLUE/GL_ALPHA as valid ReadPixels client formats (GL CTS
// packed_pixels rgba8_format_green failed with GL_INVALID_ENUM before). The state layer must
// validate them and forward the raw enum to the backend, which extracts the source channel
// from a wide RGBA read.
const GLenum singleChannelFormats[] = {GL_GREEN, GL_BLUE, GL_ALPHA};
Int expectedCallCount = 0;
for (const GLenum format : singleChannelFormats) {
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, format, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, ++expectedCallCount);
EXPECT_EQ(g_lastReadPixelsFormat, format);
EXPECT_EQ(g_lastReadPixelsType, static_cast<GLenum>(GL_UNSIGNED_BYTE));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// Packed-type pairing still applies: packed RGB/RGBA types never pair with single-channel formats.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN, GL_UNSIGNED_SHORT_5_6_5, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, expectedCallCount);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// Integer client formats reject floating-point types.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_GREEN_INTEGER, GL_FLOAT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, expectedCallCount);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
TEST_F(FramebufferTest, NamedRenderbufferStorageAndFramebufferAttachDoNotChangeBindings) {
GLuint framebuffer = 0;
GLuint renderbuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateRenderbuffers(1, &renderbuffer);
const auto originalDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const auto originalRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
const auto originalRenderbuffer =
MG_State::pGLContext->GetRenderbufferBindingSlot(RenderbufferTarget::Renderbuffer).GetBoundObject();
MG_Impl::GLImpl::NamedRenderbufferStorage(renderbuffer, GL_RGBA8, 64, 32);
GLint width = 0;
GLint height = 0;
GLint format = 0;
MG_Impl::GLImpl::GetNamedRenderbufferParameteriv(renderbuffer, GL_RENDERBUFFER_WIDTH, &width);
MG_Impl::GLImpl::GetNamedRenderbufferParameteriv(renderbuffer, GL_RENDERBUFFER_HEIGHT, &height);
MG_Impl::GLImpl::GetNamedRenderbufferParameteriv(renderbuffer, GL_RENDERBUFFER_INTERNAL_FORMAT, &format);
EXPECT_EQ(width, 64);
EXPECT_EQ(height, 32);
EXPECT_EQ(format, GL_RGBA8);
MG_Impl::GLImpl::NamedFramebufferRenderbuffer(framebuffer, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0);
EXPECT_TRUE(attachment.IsRenderbuffer());
EXPECT_EQ(attachment.GetRenderbuffer()->GetExternalIndex(), renderbuffer);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), originalDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), originalRead);
EXPECT_EQ(MG_State::pGLContext->GetRenderbufferBindingSlot(RenderbufferTarget::Renderbuffer).GetBoundObject(),
originalRenderbuffer);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, NamedFramebufferDrawBuffersDoNotModifyDefaultFramebuffer) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
const auto defaultDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const auto defaultRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
const auto defaultDrawBuffer = defaultDraw->GetDrawBuffers()[0];
const auto defaultReadBuffer = defaultRead->GetReadBuffer();
GLenum bufs[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
MG_Impl::GLImpl::NamedFramebufferDrawBuffers(framebuffer, 2, bufs);
MG_Impl::GLImpl::NamedFramebufferReadBuffer(framebuffer, GL_COLOR_ATTACHMENT1);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
EXPECT_EQ(framebufferObject->GetDrawBuffers()[0], FramebufferAttachmentType::Color0);
EXPECT_EQ(framebufferObject->GetDrawBuffers()[1], FramebufferAttachmentType::Color1);
EXPECT_EQ(framebufferObject->GetReadBuffer(), FramebufferAttachmentType::Color1);
EXPECT_EQ(defaultDraw->GetDrawBuffers()[0], defaultDrawBuffer);
EXPECT_EQ(defaultRead->GetReadBuffer(), defaultReadBuffer);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), defaultDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DefaultFramebufferReadBufferAcceptsGLBackAlias) {
const auto defaultRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
MG_Impl::GLImpl::ReadBuffer(GL_BACK);
EXPECT_EQ(defaultRead->GetReadBuffer(), FramebufferAttachmentType::BackLeft);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DefaultFramebufferDrawBufferAcceptsGLFrontAlias) {
const auto defaultDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
MG_Impl::GLImpl::DrawBuffer(GL_FRONT);
EXPECT_EQ(defaultDraw->GetDrawBuffers()[0], FramebufferAttachmentType::FrontLeft);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, DefaultFramebufferProvidesTextureAttachmentsForFrontAndBackAliases) {
const auto defaultFramebuffer =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
ASSERT_NE(defaultFramebuffer, nullptr);
const auto& frontLeft = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::FrontLeft);
const auto& frontRight = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::FrontRight);
const auto& backLeft = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::BackLeft);
const auto& backRight = defaultFramebuffer->GetAttachment(FramebufferAttachmentType::BackRight);
EXPECT_TRUE(frontLeft.IsTexture());
EXPECT_TRUE(frontRight.IsTexture());
EXPECT_TRUE(backLeft.IsTexture());
EXPECT_TRUE(backRight.IsTexture());
EXPECT_TRUE(frontLeft.IsComplete());
EXPECT_TRUE(frontRight.IsComplete());
EXPECT_TRUE(backLeft.IsComplete());
EXPECT_TRUE(backRight.IsComplete());
}
TEST_F(FramebufferTest, ClearNamedFramebufferfvUsesNamedObjectWithoutChangingBindings) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
const auto defaultDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const auto defaultRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
const GLfloat depth[] = {0.25f};
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfv = RecordClearNamedFramebufferfv;
MG_Impl::GLImpl::ClearNamedFramebufferfv(framebuffer, GL_DEPTH, 0, depth);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
EXPECT_EQ(g_clearNamedFramebufferfvCallCount, 1);
EXPECT_EQ(g_lastClearFramebuffer, framebufferObject);
EXPECT_EQ(g_lastClearBuffer, GL_DEPTH);
EXPECT_EQ(g_lastClearDrawbuffer, 0);
EXPECT_EQ(g_lastClearDepth, 0.25f);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), defaultDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, ClearNamedFramebufferfiAllowsDefaultFramebufferZero) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
const auto defaultDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const auto defaultRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
MG_Backend::gBackendFunctionsTable.GL.ClearNamedFramebufferfi = RecordClearNamedFramebufferfi;
MG_Impl::GLImpl::ClearNamedFramebufferfi(0, GL_DEPTH_STENCIL, 0, 0.5f, 7);
EXPECT_EQ(g_clearNamedFramebufferfiCallCount, 1);
EXPECT_EQ(g_lastClearFramebuffer, defaultDraw);
EXPECT_EQ(g_lastClearBuffer, GL_DEPTH_STENCIL);
EXPECT_EQ(g_lastClearDrawbuffer, 0);
EXPECT_EQ(g_lastClearDepth, 0.5f);
EXPECT_EQ(g_lastClearStencil, 7);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), defaultDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, GetNamedFramebufferAttachmentParameterivReadsTargetObjectDirectly) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
Vector<Uint> textureNames;
MG_State::pGLContext->GenTextureNames(1, textureNames);
MG_State::pGLContext->CreateTextureObject(textureNames[0], TextureTarget::Texture2D);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, textureNames[0], 2);
GLint objectType = 0;
GLint objectName = 0;
GLint textureLevel = 0;
MG_Impl::GLImpl::GetNamedFramebufferAttachmentParameteriv(
framebuffer, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &objectType);
MG_Impl::GLImpl::GetNamedFramebufferAttachmentParameteriv(
framebuffer, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &objectName);
MG_Impl::GLImpl::GetNamedFramebufferAttachmentParameteriv(
framebuffer, GL_COLOR_ATTACHMENT0, GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, &textureLevel);
EXPECT_EQ(objectType, GL_TEXTURE);
EXPECT_EQ(objectName, static_cast<GLint>(textureNames[0]));
EXPECT_EQ(textureLevel, 2);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, BlitNamedFramebufferAllowsDefaultFramebufferZero) {
GLuint framebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
const auto defaultDraw =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const auto defaultRead =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject();
MG_Backend::gBackendFunctionsTable.GL.BlitNamedFramebuffer = RecordBlitNamedFramebuffer;
MG_Impl::GLImpl::BlitNamedFramebuffer(framebuffer, 0, 0, 0, 16, 16, 0, 0, 16, 16, GL_COLOR_BUFFER_BIT,
GL_NEAREST);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
EXPECT_EQ(g_blitNamedFramebufferCallCount, 1);
EXPECT_EQ(g_lastBlitReadFramebuffer, framebufferObject);
EXPECT_EQ(g_lastBlitDrawFramebuffer, defaultDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), defaultDraw);
EXPECT_EQ(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(), defaultRead);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// ---- Packed-type readback encoding ------------------------------------------------------------------
// Oracle-independent guard for the DirectGLES client-format readback conversion: feeds known wide RGBA
// rows through ReadbackImpl::ConvertWideReadbackRow and asserts the exact packed words. Field positions
// were hand-computed from GL 3.3 table 3.6 and match the GL CTS packed_pixels comparison functions
// (glcPackedPixelsTests.cpp pack_UNSIGNED_*): non-REV types pack the first format component from the
// most significant bit, *_REV types from the least significant bit.
namespace {
namespace ReadbackImpl = MG_Backend::DirectGLES::ReadbackImpl;
// Converts a row of wide pixels (4 components of wideType each) into `format`/`type` words.
template <typename WordT, typename SrcT>
Vector<WordT> ConvertWideRowToPackedWords(const Vector<SrcT>& wide, GLenum wideType, GLenum format,
GLenum type) {
ReadbackImpl::ReadbackChannelMapping mapping{};
EXPECT_TRUE(ReadbackImpl::GetReadbackChannelMapping(format, mapping));
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(mapping, type), sizeof(WordT));
const SizeT width = wide.size() / 4;
Vector<WordT> out(width, static_cast<WordT>(0));
ReadbackImpl::ConvertWideReadbackRow(reinterpret_cast<const Uint8*>(wide.data()),
reinterpret_cast<Uint8*>(out.data()), width, wideType, mapping,
type);
return out;
}
// Normalized encodes read the wide row as RGBA8 (values are v / 255).
template <typename WordT>
Vector<WordT> ConvertRGBA8Row(const Vector<Uint8>& rgba, GLenum format, GLenum type) {
return ConvertWideRowToPackedWords<WordT>(rgba, GL_UNSIGNED_BYTE, format, type);
}
// Wide RGBA8 pattern shared by the normalized-encode tests. Expected fields below are
// round(v / 255 * (2^bits - 1)), computed by hand per pixel.
// R G B A
const Vector<Uint8> kRGBA8Row{255, 0, 128, 64, // P0
10, 250, 33, 200, // P1
85, 170, 255, 0}; // P2
} // namespace
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565) {
// P0: R=31 G=0 B=round(128*31/255)=16 -> 31<<11 | 0<<5 | 16 = 0xF810
// P1: R=round(10*31/255)=1 G=round(250*63/255)=62 B=round(33*31/255)=4 -> 1<<11|62<<5|4 = 0x0FC4
// P2: R=round(85*31/255)=10 G=round(170*63/255)=42 B=31 -> 10<<11|42<<5|31 = 0x555F
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5);
EXPECT_EQ(words[0], 0xF810u);
EXPECT_EQ(words[1], 0x0FC4u);
EXPECT_EQ(words[2], 0x555Fu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort565Rev) {
// REV packs R from the LSB: P2 -> 10 | 42<<5 | 31<<11 = 0xFD4A
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGB, GL_UNSIGNED_SHORT_5_6_5_REV);
EXPECT_EQ(words[2], 0xFD4Au);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444) {
// P0: R=15 G=0 B=round(128*15/255)=8 A=round(64*15/255)=4 -> 0xF084
// P2: R=round(85*15/255)=5 G=round(170*15/255)=10 B=15 A=0 -> 0x5AF0
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4);
EXPECT_EQ(words[0], 0xF084u);
EXPECT_EQ(words[2], 0x5AF0u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort4444Rev) {
// P0 fields R=15 G=0 B=8 A=4 packed from the LSB -> 15 | 0<<4 | 8<<8 | 4<<12 = 0x480F
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_4_4_4_4_REV);
EXPECT_EQ(words[0], 0x480Fu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort5551) {
// P0: R=31 G=0 B=16 A=round(64/255)=0 -> 31<<11 | 16<<1 = 0xF820
// P1: R=1 G=round(250*31/255)=30 B=4 A=round(200/255)=1 -> 1<<11|30<<6|4<<1|1 = 0x0F89
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1);
EXPECT_EQ(words[0], 0xF820u);
EXPECT_EQ(words[1], 0x0F89u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedShort1555Rev) {
// P1 fields R=1 G=30 B=4 A=1 packed from the LSB -> 1 | 30<<5 | 4<<10 | 1<<15 = 0x93C1
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV);
EXPECT_EQ(words[1], 0x93C1u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedInt2101010Rev) {
// P0: R=1023 G=0 B=round(128*1023/255)=514 A=round(64*3/255)=1 -> 1023|514<<20|1<<30 = 0x602003FF
// P2: R=round(85*1023/255)=341 G=round(170*1023/255)=682 B=1023 A=0 -> 0x3FFAA955
const auto words = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV);
EXPECT_EQ(words[0], 0x602003FFu);
EXPECT_EQ(words[2], 0x3FFAA955u);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedInt1010102) {
// P2 fields R=341 G=682 B=1023 A=0 packed from the MSB -> 341<<22 | 682<<12 | 1023<<2 = 0x556AAFFC
const auto words = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_10_10_10_2);
EXPECT_EQ(words[2], 0x556AAFFCu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedByte332) {
// P0: R=7 G=0 B=round(128*3/255)=2 -> 7<<5 | 2 = 0xE2
// P1: R=round(10*7/255)=0 G=round(250*7/255)=7 B=round(33*3/255)=0 -> 7<<2 = 0x1C
const auto words = ConvertRGBA8Row<Uint8>(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_3_3_2);
EXPECT_EQ(words[0], 0xE2u);
EXPECT_EQ(words[1], 0x1Cu);
}
TEST(PackedReadbackEncodeTest, EncodesUnsignedByte233Rev) {
// P0 fields R=7 G=0 B=2 packed from the LSB -> 7 | 0<<3 | 2<<6 = 0x87
const auto words = ConvertRGBA8Row<Uint8>(kRGBA8Row, GL_RGB, GL_UNSIGNED_BYTE_2_3_3_REV);
EXPECT_EQ(words[0], 0x87u);
}
TEST(PackedReadbackEncodeTest, Encodes8888KeepsLegacyByteOrder) {
// Regression for the previously supported types: P0 = (255, 0, 128, 64).
const auto msbFirst = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8);
EXPECT_EQ(msbFirst[0], 0xFF008040u);
const auto lsbFirst = ConvertRGBA8Row<Uint32>(kRGBA8Row, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV);
EXPECT_EQ(lsbFirst[0], 0x408000FFu);
}
TEST(PackedReadbackEncodeTest, EncodesBGRAWithChannelMapping) {
// BGRA's first format component is Blue: P0 fields B=8 G=0 R=15 A=4 -> 8<<12 | 15<<4 | 4 = 0x80F4
const auto words = ConvertRGBA8Row<Uint16>(kRGBA8Row, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4);
EXPECT_EQ(words[0], 0x80F4u);
}
TEST(PackedReadbackEncodeTest, EncodesIntegerRGBA2101010RevWithFieldClamp) {
// Integer sources clamp to each field's unsigned range (10/10/10/2 bits).
const Vector<Uint32> wide{1023u, 1024u, 5u, 4u};
const auto words =
ConvertWideRowToPackedWords<Uint32>(wide, GL_UNSIGNED_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV);
EXPECT_EQ(words[0], 0xC05FFFFFu); // 1023 | 1023<<10 | 5<<20 | 3<<30
}
TEST(PackedReadbackEncodeTest, EncodesIntegerNegativeValuesClampToZero) {
const Vector<Int32> wide{-5, 2, 100000, 1};
const auto words =
ConvertWideRowToPackedWords<Uint32>(wide, GL_INT, GL_RGBA_INTEGER, GL_UNSIGNED_INT_2_10_10_10_REV);
EXPECT_EQ(words[0], 0x7FF00800u); // 0 | 2<<10 | 1023<<20 | 1<<30
}
TEST(PackedReadbackEncodeTest, EncodesIntegerRGB565) {
const Vector<Uint32> wide{31u, 64u, 2u, 0u};
const auto words =
ConvertWideRowToPackedWords<Uint16>(wide, GL_UNSIGNED_INT, GL_RGB_INTEGER, GL_UNSIGNED_SHORT_5_6_5);
EXPECT_EQ(words[0], 0xFFE2u); // 31<<11 | 63<<5 | 2 (G clamps 64 -> 63)
}
TEST(PackedReadbackEncodeTest, EncodesPackedFloat10F11F11FRev) {
// F11(1.0)=0x3C0 F11(0.5)=0x380 F10(0.25)=0x1A0 -> 0x3C0 | 0x380<<11 | 0x1A0<<22 = 0x681C03C0.
// Second pixel: values above 65024 clamp to the max finite F11 (0x7BF), negatives go to zero.
const Vector<Float> wide{1.0f, 0.5f, 0.25f, 1.0f, 100000.0f, -1.0f, 0.25f, 1.0f};
const auto words = ConvertWideRowToPackedWords<Uint32>(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_10F_11F_11F_REV);
EXPECT_EQ(words[0], 0x681C03C0u);
EXPECT_EQ(words[1], 0x680007BFu);
}
TEST(PackedReadbackEncodeTest, EncodesSharedExponent5999Rev) {
// (1.0, 0.5, 0.25): shared exponent 16, fields 256/128/64 -> 256 | 128<<9 | 64<<18 | 16<<27
const Vector<Float> wide{1.0f, 0.5f, 0.25f, 1.0f};
const auto words = ConvertWideRowToPackedWords<Uint32>(wide, GL_FLOAT, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV);
EXPECT_EQ(words[0], 0x81010100u);
}
TEST(PackedReadbackEncodeTest, RejectsMismatchedPackedFieldCounts) {
ReadbackImpl::ReadbackChannelMapping rgba{};
ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGBA, rgba));
ReadbackImpl::ReadbackChannelMapping rgbInteger{};
ASSERT_TRUE(ReadbackImpl::GetReadbackChannelMapping(GL_RGB_INTEGER, rgbInteger));
// 3-field packed types never pair with 4-component formats and vice versa.
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgba, GL_UNSIGNED_SHORT_5_6_5), 0u);
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_SHORT_4_4_4_4), 0u);
// Packed-float RGB types never pair with integer formats.
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_5_9_9_9_REV), 0u);
EXPECT_EQ(ReadbackImpl::GetReadbackDstPixelSize(rgbInteger, GL_UNSIGNED_INT_10F_11F_11F_REV), 0u);
}
// ---- GL CTS packed_pixels readback root-cause regressions --------------------------------------
TEST_F(FramebufferTest, ReadPixelsRejectsIntegerFormatMismatchWithReadBuffer) {
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGBA8UI, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
MG_Backend::gBackendFunctionsTable.GL.ReadPixels = RecordReadPixels;
Uint8 pixelStorage[4 * 4 * 4] = {};
// GL 3.3 section 4.3.1: normalized format on an integer read buffer -> GL_INVALID_OPERATION
// (GL CTS packed_pixels expects the error for every mismatched combination).
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
// The matching integer readback stays valid.
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// And the inverse mismatch: integer format on a normalized attachment.
GLuint normalizedTexture = 0;
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &normalizedTexture);
MG_Impl::GLImpl::TextureStorage2D(normalizedTexture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, normalizedTexture, 0);
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixelStorage);
EXPECT_EQ(g_readPixelsCallCount, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_OPERATION);
}
TEST_F(FramebufferTest, BindRenderbufferZeroUnbindsWithoutError) {
// The GL CTS state reset calls glBindRenderbuffer(GL_RENDERBUFFER, 0) and expects no error;
// name 0 used to be reported as an invalid renderbuffer name.
MG_Impl::GLImpl::BindRenderbuffer(GL_RENDERBUFFER, 0);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
TEST_F(FramebufferTest, FramebufferTexture3DAttachesSliceWithLayerTracking) {
// glFramebufferTexture3D with zoffset used to be rejected outright, leaving a sticky
// GL_INVALID_OPERATION behind (GL CTS packed_pixels varied_rectangle runs on GL_TEXTURE_3D).
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_3D, 1, &texture);
MG_Impl::GLImpl::TextureStorage3D(texture, 1, GL_RGBA8, 4, 4, 2);
MG_Impl::GLImpl::BindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);
MG_Impl::GLImpl::FramebufferTexture3D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_3D, texture, 0, 1);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const auto framebufferObject = MG_State::pGLContext->GetFramebufferObject(framebuffer);
ASSERT_NE(framebufferObject, nullptr);
const auto& attachment = framebufferObject->GetAttachment(FramebufferAttachmentType::Color0);
ASSERT_TRUE(attachment.IsTexture());
EXPECT_EQ(attachment.GetTextureLayer(), 1);
EXPECT_FALSE(attachment.IsLayered());
}
TEST_F(FramebufferTest, NonRenderableColorFormatsReportUnsupportedFramebuffer) {
// Without a probing backend the conservative list applies: RGB9_E5 is texture-only, so
// attaching it must not report GL_FRAMEBUFFER_COMPLETE (GL CTS packed_pixels rgb9_e5 expects
// read errors instead of silent unwritten readbacks).
GLuint framebuffer = 0;
GLuint texture = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &framebuffer);
MG_Impl::GLImpl::CreateTextures(GL_TEXTURE_2D, 1, &texture);
MG_Impl::GLImpl::TextureStorage2D(texture, 1, GL_RGB9_E5, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(framebuffer, GL_COLOR_ATTACHMENT0, texture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
EXPECT_EQ(MG_Impl::GLImpl::CheckFramebufferStatus(GL_READ_FRAMEBUFFER),
static_cast<GLenum>(GL_FRAMEBUFFER_UNSUPPORTED));
Uint8 pixelStorage[4 * 4 * 4] = {};
MG_Impl::GLImpl::ReadPixels(0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixelStorage);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_INVALID_FRAMEBUFFER_OPERATION);
}