[Fix] (MG_Impl/GLImpl, MG_IntegrationTest, MG_IntegrationTest/Scenarios, MG_Remote/Client, MG_Test/Texture, scripts/data): make by-name texture calls act on the named texture

- The 19 calls that bound their texture first now share object cores with the target forms
- GL 4.6 rules the binding hid: cube maps in TextureSubImage3D and level queries, named target errors
This commit is contained in:
2026-10-16 14:47:50 -04:00
parent 28bf0c1653
commit 230a90a439
10 changed files with 1498 additions and 2154 deletions
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2438,7 +2438,7 @@ endif()
# f1: split-only source keeps the monolith registry unchanged.
if (MOBILEGL_BUILD_DISAGGREGATED)
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/F1WireScenario.cpp)
foreach(f1Slot ClearBufferfi ClearBufferfv ClearBufferiv ClearBufferuiv ClearNamedFramebufferfi ClearNamedFramebufferfv ClearNamedFramebufferiv ClearNamedFramebufferuiv CopyTexImage2D CopyTexSubImage2D GenerateMipmap GenerateMipmapPackedFloat GenerateMipmapDepth)
foreach(f1Slot ClearBufferfi ClearBufferfv ClearBufferiv ClearBufferuiv ClearNamedFramebufferfi ClearNamedFramebufferfv ClearNamedFramebufferiv ClearNamedFramebufferuiv CopyTexImage2D CopyTexSubImage2D CopyTextureSubImage2D GenerateMipmap GenerateTextureMipmap GenerateMipmapPackedFloat GenerateMipmapDepth)
mgl_itest_register_split_arms("F1WireScenario.${f1Slot}Pixels" "F1.")
endforeach()
# THE TWO PACK-STATE CASES OF F1, which are not ${slot}Pixels-shaped (P3b/P4b espryt D1
@@ -2467,7 +2467,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
mgl_itest_magma_arm_shape(${mglItestArm})
mgl_itest_join_environment(mglItestMagmaWireEnvironment
${MGL_ITEST_MAGMA_WIRE_FLAVOUR_ENV} ${mglItestArmTransportEnv})
foreach(fmSlot ComputeImageStoreThroughViewPreservesOtherRootLayer EnabledVertexBufferDrawKeepsNamedP7Fatal SrgbDrawTracksFramebufferConversion Texture1DFramebufferClearPixels VertexIdSamplerAndScalarUniformPixels ComputeImageStoreFramebufferPixels PartialTextureUploadPreservesGpuClearPixels TextureViewClearTargetsItsRootMipAndLayer ClearBufferfiPixels ClearBufferfvPixels ClearBufferivPixels ClearBufferuivPixels ClearNamedFramebufferfiPixels ClearNamedFramebufferfvPixels ClearNamedFramebufferivPixels ClearNamedFramebufferuivPixels CopyTexImage2DPixels CopyTexSubImage2DPixels GenerateMipmapPixels GenerateMipmapPackedFloatPixels GenerateMipmapDepthPixels)
foreach(fmSlot ComputeImageStoreThroughViewPreservesOtherRootLayer EnabledVertexBufferDrawKeepsNamedP7Fatal SrgbDrawTracksFramebufferConversion Texture1DFramebufferClearPixels VertexIdSamplerAndScalarUniformPixels ComputeImageStoreFramebufferPixels PartialTextureUploadPreservesGpuClearPixels TextureViewClearTargetsItsRootMipAndLayer ClearBufferfiPixels ClearBufferfvPixels ClearBufferivPixels ClearBufferuivPixels ClearNamedFramebufferfiPixels ClearNamedFramebufferfvPixels ClearNamedFramebufferivPixels ClearNamedFramebufferuivPixels CopyTexImage2DPixels CopyTexSubImage2DPixels CopyTextureSubImage2DPixels GenerateMipmapPixels GenerateTextureMipmapPixels GenerateMipmapPackedFloatPixels GenerateMipmapDepthPixels)
mgl_itest_lane("DirectVulkan.${mglItestArm}.Fm." "F1WireScenario.${fmSlot}"
LABELS "${mglItestMagmaLabels}" PROPERTIES ${mglItestArmProperties} ENV "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5f-fm${mglItestArmLogTag}-${fmSlot}.log")
endforeach()
@@ -468,6 +468,64 @@ void main() { color = vec4(value, 0.25, 0.75, 1.0); }
for (int i = 0; i < 4; ++i)
EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.GenerateMipmap.pixels";
}
// The by-name copy and mipmap verbs name their texture in the record. Another texture stays
// bound on the unit throughout: it is neither written nor unbound, and the named one is.
TEST_F(F1WireScenario, CopyTextureSubImage2DPixels) {
if (!Ready()) return;
Attach(GL_RGBA8);
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
GLuint textures[2] = {}; // destination, decoy
glCreateTextures(GL_TEXTURE_2D, 2, textures);
const std::array<GLubyte, 4 * 4 * 4> zeros{};
for (GLuint name : textures) {
glTextureStorage2D(name, 1, GL_RGBA8, 4, 4);
glTextureSubImage2D(name, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, zeros.data());
}
glBindTexture(GL_TEXTURE_2D, textures[1]);
const auto before = PeekSplitRuntime().emitSeq;
glCopyTextureSubImage2D(textures[0], 0, 1, 1, 2, 3, 2, 2);
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.CopyTextureSubImage2D.wire";
GLint bound = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound);
EXPECT_EQ(GLuint(bound), textures[1]) << "F1.CopyTextureSubImage2D.binding";
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[0], 0);
const auto copied = ReadOnePixel(1, 1);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[1], 0);
const auto decoy = ReadOnePixel(1, 1);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.CopyTextureSubImage2D.error";
const int expected[4] = {64, 128, 191, 255};
for (int i = 0; i < 4; ++i) {
EXPECT_NEAR(copied[i], expected[i], 1) << "F1.CopyTextureSubImage2D.pixels";
EXPECT_EQ(decoy[i], 0) << "F1.CopyTextureSubImage2D.decoy";
}
glDeleteTextures(2, textures);
}
TEST_F(F1WireScenario, GenerateTextureMipmapPixels) {
if (!Ready()) return;
Attach(GL_RGBA8, GL_COLOR_ATTACHMENT0, 4);
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
GLuint decoy = 0;
glGenTextures(1, &decoy);
glBindTexture(GL_TEXTURE_2D, decoy);
glTexStorage2D(GL_TEXTURE_2D, 4, GL_RGBA8, 8, 8);
const auto before = PeekSplitRuntime().emitSeq;
glGenerateTextureMipmap(texture);
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.GenerateTextureMipmap.wire";
GLint bound = 0;
glGetIntegerv(GL_TEXTURE_BINDING_2D, &bound);
EXPECT_EQ(GLuint(bound), decoy) << "F1.GenerateTextureMipmap.binding";
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 2);
const auto pixel = ReadOnePixel(1, 1);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.GenerateTextureMipmap.error";
const int expected[4] = {64, 128, 191, 255};
for (int i = 0; i < 4; ++i)
EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.GenerateTextureMipmap.pixels";
glDeleteTextures(1, &decoy);
}
TEST_F(F1WireScenario, GenerateMipmapPackedFloatPixels) {
if (!Ready()) return;
// Only level zero exists initially. Generating the special-format chain must use the
+2 -2
View File
@@ -43,8 +43,8 @@ namespace MobileGL::MG_Remote::Client {
const MG_Backend::FormatCapabilityCache& Formats() const;
const String& ApiVersion() const;
// The SERVER's backend type, never a new "Remote" enumerator: frontend branches
// switch on this (GL_Framebuffer.cpp:47, GL_Texture.cpp:6536, CompileEnv.cpp:122) and
// a value they do not know silently takes the wrong arm.
// switch on this (ActiveBackendRejectsDistinctDepthStencil, CaptureCompileEnv) and a
// value they do not know silently takes the wrong arm.
BackendType Backend() const;
Uint64 CallMask() const;
+2 -3
View File
@@ -632,9 +632,8 @@ namespace MobileGL::MG_Remote::Client {
snapshot->apiVersion() == nullptr ? String{} : String{snapshot->apiVersion()->c_str()};
// THE SERVER'S BACKEND TYPE, NEVER A NEW "Remote" ENUMERATOR and never guessed from
// the renderer string: GL_Framebuffer.cpp:47, GL_Texture.cpp:6536 and
// CompileEnv.cpp:122 SWITCH on it, and a value they do not know takes a wrong arm
// rather than failing.
// the renderer string: ActiveBackendRejectsDistinctDepthStencil and CaptureCompileEnv
// switch on it, and a value they do not know takes a wrong arm rather than failing.
const Uint32 rawBackend = snapshot->backendType();
if (rawBackend >= static_cast<Uint32>(BackendType::BackendTypeCount)) {
MGLOG_E("MG_Remote client: CapsSnapshot names backend type %u, which this build "
+18
View File
@@ -45,3 +45,21 @@ target_link_libraries(
gtest_discover_tests(TextureViewTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
add_executable(
TextureByNameTest
TextureByNameTest.cpp
)
target_include_directories(TextureByNameTest PRIVATE
${MGL_ROOT}/include
${MGL_ROOT}/MobileGL
)
target_link_libraries(
TextureByNameTest PRIVATE
MobileGLTestMain
${LINK_LIBRARIES}
)
gtest_discover_tests(TextureByNameTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
@@ -0,0 +1,394 @@
// MobileGL - MobileGL/MG_Test/Texture/TextureByNameTest.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
// The by-name (DSA) texture entry points edit the named texture directly: no unit binding and no
// bind generation moves, and each checks the texture's effective target the way GL 4.6 core asks of
// the by-name spelling - which is not always what the bound-target spelling accepts.
#include <gtest/gtest.h>
#include <cstring>
#include "Includes.h"
#include "Init.h"
#include <MG_Backend/BackendObjects.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>
#include <MG_State/GLState/TextureState/TextureObject.h>
using namespace MobileGL;
class TextureByNameTest : public ::testing::Test {
protected:
static void DrainPendingGlErrors() {
for (Int drained = 0; drained < 16 && MG_Impl::GLImpl::GetError() != GL_NO_ERROR; ++drained) {}
}
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 SetUp() override {
MobileGL::Initialize();
DrainPendingGlErrors();
}
void TearDown() override {
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << "test left an unconsumed GL error behind";
}
};
namespace {
// This binary has no GL context behind its backend table, so the two calls that reach the backend
// are stubbed for the duration of a case; what is under test is what the frontend does first.
class ScopedBackendStubs {
public:
ScopedBackendStubs() : m_snapshot(MG_Backend::gBackendFunctionsTable) {
auto& gl = MG_Backend::gBackendFunctionsTable.GL;
gl.GenerateTextureMipmap = [](const SharedPtr<MG_State::GLState::ITextureObject>&, GLenum) {};
gl.CopyTextureSubImage2D = [](const SharedPtr<MG_State::GLState::ITextureObject>&, GLenum, GLint, GLint,
GLint, GLint, GLint, GLsizei, GLsizei) {};
}
~ScopedBackendStubs() { MG_Backend::gBackendFunctionsTable = m_snapshot; }
ScopedBackendStubs(const ScopedBackendStubs&) = delete;
ScopedBackendStubs& operator=(const ScopedBackendStubs&) = delete;
private:
MG_Backend::GlobalBackendFunctionsTable m_snapshot;
};
// What a by-name call must leave as it found it: every slot of the active unit and the bind
// generation backends key their per-unit memos on.
struct UnitState {
Uint64 generation = 0;
Vector<MG_State::GLState::ITextureObject*> slots;
static UnitState Capture() {
UnitState state;
state.generation = MG_State::pGLContext->GetTextureBindGeneration();
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
for (TextureTarget target :
{TextureTarget::Texture1D, TextureTarget::Texture2D, TextureTarget::Texture3D,
TextureTarget::Texture1DArray, TextureTarget::Texture2DArray, TextureTarget::TextureCubeMap,
TextureTarget::Texture2DMultisample, TextureTarget::Texture2DMultisampleArray}) {
state.slots.push_back(unit.GetBindingSlot(target).GetBoundObject().get());
}
return state;
}
Bool operator==(const UnitState& other) const { return generation == other.generation && slots == other.slots; }
};
GLuint CreateTexture(GLenum target) {
GLuint texture = 0;
MG_Impl::GLImpl::CreateTextures(target, 1, &texture);
return texture;
}
// The CPU texel shadow of one image of a named texture.
const Uint8* ImageBytes(GLuint texture, TextureUploadTarget image, Uint level = 0) {
auto* mipmap = MG_State::GLState::AsMipmapTexture(MG_State::pGLContext->GetTextureObject(texture).get());
return mipmap ? static_cast<const Uint8*>(mipmap->MapMipmapData(image, level)) : nullptr;
}
TextureUploadTarget CubeFace(Int face) {
return static_cast<TextureUploadTarget>(static_cast<SizeT>(TextureUploadTarget::CubeMapPositiveX) +
static_cast<SizeT>(face));
}
constexpr GLsizei kRgtc1Block = 8; // one 4x4 block of GL_COMPRESSED_RED_RGTC1
} // namespace
TEST_F(TextureByNameTest, EveryByNameCallLeavesTheUnitAndTheBindGenerationAlone) {
const ScopedBackendStubs stubs;
// Decoys on every target of the active unit, so a call that bound its texture would show.
for (GLenum target : {GL_TEXTURE_1D, GL_TEXTURE_2D, GL_TEXTURE_3D, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_MAP}) {
GLuint decoy = 0;
MG_Impl::GLImpl::GenTextures(1, &decoy);
MG_Impl::GLImpl::BindTexture(target, decoy);
}
const GLuint tex1D = CreateTexture(GL_TEXTURE_1D);
MG_Impl::GLImpl::TextureStorage1D(tex1D, 1, GL_RGBA8, 4);
const GLuint tex2D = CreateTexture(GL_TEXTURE_2D);
MG_Impl::GLImpl::TextureStorage2D(tex2D, 3, GL_RGBA8, 4, 4);
const GLuint texArray = CreateTexture(GL_TEXTURE_2D_ARRAY);
MG_Impl::GLImpl::TextureStorage3D(texArray, 1, GL_RGBA8, 4, 4, 2);
const GLuint texMs = CreateTexture(GL_TEXTURE_2D_MULTISAMPLE);
const GLuint texMsArray = CreateTexture(GL_TEXTURE_2D_MULTISAMPLE_ARRAY);
GLuint readFramebuffer = 0;
MG_Impl::GLImpl::CreateFramebuffers(1, &readFramebuffer);
const GLuint readTexture = CreateTexture(GL_TEXTURE_2D);
MG_Impl::GLImpl::TextureStorage2D(readTexture, 1, GL_RGBA8, 4, 4);
MG_Impl::GLImpl::NamedFramebufferTexture(readFramebuffer, GL_COLOR_ATTACHMENT0, readTexture, 0);
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, readFramebuffer);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
const Uint8 pixels[4 * 4 * 4 * 2] = {};
const GLfloat border[4] = {0, 0, 0, 1};
const GLint swizzle[4] = {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA};
const GLuint swizzleU[4] = {GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA};
GLint ints[4] = {};
GLuint uints[4] = {};
GLfloat floats[4] = {};
const auto expectUntouched = [](const char* name, const auto& call) {
const UnitState before = UnitState::Capture();
call();
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) << name;
EXPECT_TRUE(UnitState::Capture() == before) << name << " moved a binding or the bind generation";
};
expectUntouched("TextureParameterfv",
[&] { MG_Impl::GLImpl::TextureParameterfv(tex2D, GL_TEXTURE_BORDER_COLOR, border); });
expectUntouched("TextureParameteriv",
[&] { MG_Impl::GLImpl::TextureParameteriv(tex2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle); });
expectUntouched("TextureParameterIiv",
[&] { MG_Impl::GLImpl::TextureParameterIiv(tex2D, GL_TEXTURE_SWIZZLE_RGBA, swizzle); });
expectUntouched("TextureParameterIuiv",
[&] { MG_Impl::GLImpl::TextureParameterIuiv(tex2D, GL_TEXTURE_SWIZZLE_RGBA, swizzleU); });
expectUntouched("GetTextureParameteriv",
[&] { MG_Impl::GLImpl::GetTextureParameteriv(tex2D, GL_TEXTURE_MAX_LEVEL, ints); });
expectUntouched("GetTextureParameterfv",
[&] { MG_Impl::GLImpl::GetTextureParameterfv(tex2D, GL_TEXTURE_MIN_LOD, floats); });
expectUntouched("GetTextureParameterIiv",
[&] { MG_Impl::GLImpl::GetTextureParameterIiv(tex2D, GL_TEXTURE_BORDER_COLOR, ints); });
expectUntouched("GetTextureParameterIuiv",
[&] { MG_Impl::GLImpl::GetTextureParameterIuiv(tex2D, GL_TEXTURE_BORDER_COLOR, uints); });
expectUntouched("GetTextureLevelParameteriv",
[&] { MG_Impl::GLImpl::GetTextureLevelParameteriv(tex2D, 1, GL_TEXTURE_WIDTH, ints); });
expectUntouched("GetTextureLevelParameterfv",
[&] { MG_Impl::GLImpl::GetTextureLevelParameterfv(tex2D, 1, GL_TEXTURE_HEIGHT, floats); });
expectUntouched("TextureSubImage1D",
[&] { MG_Impl::GLImpl::TextureSubImage1D(tex1D, 0, 0, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixels); });
expectUntouched("TextureSubImage2D", [&] {
MG_Impl::GLImpl::TextureSubImage2D(tex2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
});
expectUntouched("TextureSubImage3D", [&] {
MG_Impl::GLImpl::TextureSubImage3D(texArray, 0, 0, 0, 0, 4, 4, 2, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
});
expectUntouched("TextureStorage2DMultisample",
[&] { MG_Impl::GLImpl::TextureStorage2DMultisample(texMs, 1, GL_RGBA8, 4, 4, GL_TRUE); });
expectUntouched("TextureStorage3DMultisample",
[&] { MG_Impl::GLImpl::TextureStorage3DMultisample(texMsArray, 1, GL_RGBA8, 4, 4, 2, GL_TRUE); });
expectUntouched("GenerateTextureMipmap", [&] { MG_Impl::GLImpl::GenerateTextureMipmap(tex2D); });
expectUntouched("CopyTextureSubImage2D",
[&] { MG_Impl::GLImpl::CopyTextureSubImage2D(tex2D, 0, 0, 0, 0, 0, 2, 2); });
MG_Impl::GLImpl::BindFramebuffer(GL_READ_FRAMEBUFFER, 0);
DrainPendingGlErrors();
}
// The compressed by-name uploads, with textures whose images are stored compressed.
TEST_F(TextureByNameTest, CompressedByNameUploadsLeaveTheUnitAndTheBindGenerationAlone) {
const GLuint tex2D = CreateTexture(GL_TEXTURE_2D);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, tex2D);
const Uint8 blocks[kRgtc1Block * 2] = {};
MG_Impl::GLImpl::CompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RED_RGTC1, 4, 4, 0, kRgtc1Block, blocks);
const GLuint texArray = CreateTexture(GL_TEXTURE_2D_ARRAY);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, texArray);
MG_Impl::GLImpl::CompressedTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_COMPRESSED_RED_RGTC1, 4, 4, 2, 0, kRgtc1Block * 2,
blocks);
GLuint decoys[2] = {};
MG_Impl::GLImpl::GenTextures(2, decoys);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, decoys[0]);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D_ARRAY, decoys[1]);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
UnitState before = UnitState::Capture();
MG_Impl::GLImpl::CompressedTextureSubImage2D(tex2D, 0, 0, 0, 4, 4, GL_COMPRESSED_RED_RGTC1, kRgtc1Block, blocks);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_TRUE(UnitState::Capture() == before);
before = UnitState::Capture();
MG_Impl::GLImpl::CompressedTextureSubImage3D(texArray, 0, 0, 0, 0, 4, 4, 2, GL_COMPRESSED_RED_RGTC1,
kRgtc1Block * 2, blocks);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_TRUE(UnitState::Capture() == before);
}
// GL 4.6 core 8.6: the by-name 3D upload takes a cube map, zoffset naming the first face and depth
// the number of faces. The bound-target path it used to borrow has no target for that.
TEST_F(TextureByNameTest, TextureSubImage3DWritesTheCubeFacesZOffsetSelects) {
const GLuint cube = CreateTexture(GL_TEXTURE_CUBE_MAP);
MG_Impl::GLImpl::TextureStorage2D(cube, 1, GL_RGBA8, 2, 2);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 faces[2][2 * 2 * 4];
std::memset(faces[0], 0x40, sizeof(faces[0]));
std::memset(faces[1], 0x80, sizeof(faces[1]));
MG_Impl::GLImpl::TextureSubImage3D(cube, 0, 0, 0, 2, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, faces);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
for (Int face = 0; face < 6; ++face) {
const Uint8* bytes = ImageBytes(cube, CubeFace(face));
ASSERT_NE(bytes, nullptr) << "face " << face;
const Uint8 expected = face == 2 ? 0x40 : (face == 3 ? 0x80 : 0);
for (SizeT i = 0; i < sizeof(faces[0]); ++i) {
ASSERT_EQ(bytes[i], expected) << "face " << face << " byte " << i;
}
}
// Faces past the sixth do not exist.
MG_Impl::GLImpl::TextureSubImage3D(cube, 0, 0, 0, 5, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, faces);
ExpectSingleGlError(GL_INVALID_VALUE);
}
TEST_F(TextureByNameTest, TextureSubImage3DIntoACubeMapThatIsNotCubeCompleteIsInvalidOperation) {
const GLuint cube = CreateTexture(GL_TEXTURE_CUBE_MAP);
const Uint8 pixels[2 * 2 * 4] = {};
MG_Impl::GLImpl::TextureSubImage3D(cube, 0, 0, 0, 0, 2, 2, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_OPERATION);
}
TEST_F(TextureByNameTest, CompressedTextureSubImage3DWritesTheCubeFacesZOffsetSelects) {
const GLuint cube = CreateTexture(GL_TEXTURE_CUBE_MAP);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, cube);
const Uint8 zero[kRgtc1Block] = {};
for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; ++face) {
MG_Impl::GLImpl::CompressedTexImage2D(face, 0, GL_COMPRESSED_RED_RGTC1, 4, 4, 0, kRgtc1Block, zero);
}
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
Uint8 blocks[2][kRgtc1Block];
std::memset(blocks[0], 0x11, sizeof(blocks[0]));
std::memset(blocks[1], 0x22, sizeof(blocks[1]));
MG_Impl::GLImpl::CompressedTextureSubImage3D(cube, 0, 0, 0, 1, 4, 4, 2, GL_COMPRESSED_RED_RGTC1, kRgtc1Block * 2,
blocks);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
for (Int face = 0; face < 6; ++face) {
Uint8 stored[kRgtc1Block] = {};
MG_Impl::GLImpl::GetCompressedTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, stored);
const Uint8 expected = face == 1 ? 0x11 : (face == 2 ? 0x22 : 0);
for (SizeT i = 0; i < sizeof(stored); ++i) {
ASSERT_EQ(stored[i], expected) << "face " << face << " byte " << i;
}
}
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core table 8.15. A target the command does not take is INVALID_ENUM through a target
// token and INVALID_OPERATION through a named texture's effective target.
TEST_F(TextureByNameTest, SubImageCommandsCheckTheirTargetTable) {
const Uint8 pixels[4 * 4 * 4] = {};
const GLuint cube = CreateTexture(GL_TEXTURE_CUBE_MAP);
MG_Impl::GLImpl::TextureStorage2D(cube, 1, GL_RGBA8, 4, 4);
const GLuint tex2D = CreateTexture(GL_TEXTURE_2D);
MG_Impl::GLImpl::TextureStorage2D(tex2D, 1, GL_RGBA8, 4, 4);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::TextureSubImage2D(cube, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::TextureSubImage3D(tex2D, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::TextureSubImage1D(tex2D, 0, 0, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, tex2D);
MG_Impl::GLImpl::TexSubImage3D(GL_TEXTURE_2D, 0, 0, 0, 0, 4, 4, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, cube);
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_CUBE_MAP, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_ENUM);
MG_Impl::GLImpl::TexSubImage2D(GL_PROXY_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_ENUM);
// A face takes any rectangle inside it: the square-face rule is image specification's.
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, 0, 0, 4, 2, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// The box is bounded by the level it writes, not by level 0: at level 1 of a 4x4 texture a 2x2
// box at (1, 1) runs off the image, which is INVALID_VALUE rather than a silently dropped upload.
TEST_F(TextureByNameTest, SubImageOutsideTheLevelItWritesIsInvalidValue) {
const Uint8 pixels[2 * 2 * 4] = {};
const GLuint texture = CreateTexture(GL_TEXTURE_2D);
MG_Impl::GLImpl::TextureStorage2D(texture, 2, GL_RGBA8, 4, 4);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
MG_Impl::GLImpl::TextureSubImage2D(texture, 1, 1, 1, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 1, 1, 1, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
ExpectSingleGlError(GL_INVALID_VALUE);
MG_Impl::GLImpl::TexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core 8.11.3: by name a cube map answers level queries for face zero; through a target
// token GL_TEXTURE_CUBE_MAP is not a level-query target at all.
TEST_F(TextureByNameTest, LevelQueriesOfACubeMapByNameAnswerForFaceZero) {
const GLuint cube = CreateTexture(GL_TEXTURE_CUBE_MAP);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_CUBE_MAP, cube);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
MG_Impl::GLImpl::TexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint width = 0;
MG_Impl::GLImpl::GetTextureLevelParameteriv(cube, 0, GL_TEXTURE_WIDTH, &width);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(width, 8);
GLfloat height = 0;
MG_Impl::GLImpl::GetTextureLevelParameterfv(cube, 0, GL_TEXTURE_HEIGHT, &height);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(height, 8.0f);
MG_Impl::GLImpl::GetTexLevelParameteriv(GL_TEXTURE_CUBE_MAP, 0, GL_TEXTURE_WIDTH, &width);
ExpectSingleGlError(GL_INVALID_ENUM);
}
// GL 4.6 core 8.11.2: TEXTURE_TARGET answers the effective target by name and the target itself
// through a token.
TEST_F(TextureByNameTest, ParameterQueriesAnswerTheTextureTarget) {
const GLuint array = CreateTexture(GL_TEXTURE_2D_ARRAY);
GLint target = 0;
MG_Impl::GLImpl::GetTextureParameteriv(array, GL_TEXTURE_TARGET, &target);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(target, GL_TEXTURE_2D_ARRAY);
GLuint bound = 0;
MG_Impl::GLImpl::GenTextures(1, &bound);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_3D, bound);
GLfloat floatTarget = 0;
MG_Impl::GLImpl::GetTexParameterfv(GL_TEXTURE_3D, GL_TEXTURE_TARGET, &floatTarget);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
EXPECT_EQ(floatTarget, static_cast<GLfloat>(GL_TEXTURE_3D));
}
// The float setters of both spellings take every scalar pname the integer ones take: the bound
// form once refused DEPTH_STENCIL_TEXTURE_MODE and the by-name form the single-channel swizzles.
TEST_F(TextureByNameTest, FloatSettersTakeEveryScalarPnameOfBothForms) {
const GLuint texture = CreateTexture(GL_TEXTURE_2D);
MG_Impl::GLImpl::TextureParameterf(texture, GL_TEXTURE_SWIZZLE_G, static_cast<GLfloat>(GL_RED));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint swizzle = 0;
MG_Impl::GLImpl::GetTextureParameteriv(texture, GL_TEXTURE_SWIZZLE_G, &swizzle);
EXPECT_EQ(swizzle, GL_RED);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, texture);
MG_Impl::GLImpl::TexParameterf(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE,
static_cast<GLfloat>(GL_STENCIL_INDEX));
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
GLint mode = 0;
MG_Impl::GLImpl::GetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &mode);
EXPECT_EQ(mode, GL_STENCIL_INDEX);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
}
// GL 4.6 core 8.14.4: mipmaps are generated for seven targets; a rectangle texture is not one.
TEST_F(TextureByNameTest, MipmapGenerationChecksTheTargetOfBothForms) {
const ScopedBackendStubs stubs;
const GLuint rectangle = CreateTexture(GL_TEXTURE_RECTANGLE);
MG_Impl::GLImpl::GenerateTextureMipmap(rectangle);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_RECTANGLE, rectangle);
MG_Impl::GLImpl::GenerateMipmap(GL_TEXTURE_RECTANGLE);
ExpectSingleGlError(GL_INVALID_ENUM);
}
+20 -48
View File
@@ -181,23 +181,11 @@ namespace {
GLuint BoundTexture = 0;
} g_copyTexSubImage2DCall;
void RecordCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
g_copyTexSubImage2DCall = {
true,
target,
level,
xoffset,
yoffset,
x,
y,
width,
height,
MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit())
.GetBindingSlot(TextureTarget::Texture2D)
.GetBoundObject()
->GetExternalIndex(),
};
void RecordCopyTextureSubImage2D(const SharedPtr<MG_State::GLState::ITextureObject>& texture, GLenum target,
GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height) {
g_copyTexSubImage2DCall = {true, target, level, xoffset, yoffset,
x, y, width, height, texture->GetExternalIndex()};
}
} // namespace
@@ -285,7 +273,7 @@ TEST_F(TextureTest, ClearTexSubImageClearsOnlyRequestedRectangle) {
TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D = RecordCopyTexSubImage2D;
MG_Backend::gBackendFunctionsTable.GL.CopyTextureSubImage2D = RecordCopyTextureSubImage2D;
g_copyTexSubImage2DCall = {};
GLuint namedTexture = 0;
@@ -319,6 +307,7 @@ TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
EXPECT_EQ(g_copyTexSubImage2DCall.Y, 6);
EXPECT_EQ(g_copyTexSubImage2DCall.Width, 7);
EXPECT_EQ(g_copyTexSubImage2DCall.Height, 8);
// The backend is handed the named texture itself; nothing was bound to reach it.
EXPECT_EQ(g_copyTexSubImage2DCall.BoundTexture, namedTexture);
EXPECT_EQ(MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).GetBoundObject(),
boundBefore);
@@ -327,7 +316,7 @@ TEST_F(TextureTest, CopyTextureSubImage2DUsesNamedObjectAndRestoresBinding) {
TEST_F(TextureTest, CopyTextureSubImage2DRejectsCubeMapTargets) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyTexSubImage2D = RecordCopyTexSubImage2D;
MG_Backend::gBackendFunctionsTable.GL.CopyTextureSubImage2D = RecordCopyTextureSubImage2D;
g_copyTexSubImage2DCall = {};
GLuint cubeTexture = 0;
@@ -1125,8 +1114,7 @@ TEST_F(TextureTest, EverySamplerScalarPnameConvertsToTheQueriedType) {
}
// glTextureParameter* has no target token, so GL 4.6 core 8.10 applies the ten-target list to the
// texture's EFFECTIVE target. The four vector DSA forms reached that gate for free by re-entering
// through the bound-target path; the two scalar forms called the per-object setter directly and
// texture's EFFECTIVE target, and a texture outside it is INVALID_OPERATION. The scalar forms once
// reached no gate at all, so one DSA family disagreed with itself about the same texture.
TEST_F(TextureTest, ScalarDsaTextureParameterAppliesTheSameTargetRuleAsItsVectorTwins) {
GLuint bufferTexture = 0;
@@ -1134,16 +1122,13 @@ TEST_F(TextureTest, ScalarDsaTextureParameterAppliesTheSameTargetRuleAsItsVector
ASSERT_NE(bufferTexture, 0u);
ASSERT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The vector form has rejected this since the target gate landed...
const GLint baseLevel = 1;
MG_Impl::GLImpl::TextureParameteriv(bufferTexture, GL_TEXTURE_BASE_LEVEL, &baseLevel);
ExpectSingleGlError(GL_INVALID_ENUM);
// ...and the scalar forms must agree rather than silently applying the parameter.
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::TextureParameteri(bufferTexture, GL_TEXTURE_BASE_LEVEL, 1);
ExpectSingleGlError(GL_INVALID_ENUM);
ExpectSingleGlError(GL_INVALID_OPERATION);
MG_Impl::GLImpl::TextureParameterf(bufferTexture, GL_TEXTURE_MIN_LOD, 1.0f);
ExpectSingleGlError(GL_INVALID_ENUM);
ExpectSingleGlError(GL_INVALID_OPERATION);
// A texture whose target IS legal still goes through, so the gate is about the target and not
// about the by-name spelling.
@@ -4875,19 +4860,10 @@ TEST_F(TextureTest, BindSamplerRejectsUnitsBeyondMaxCombinedTextureImageUnits) {
DrainPendingGlErrors();
}
// The DSA by-name entry points are emulated by temporarily binding the named texture onto the
// active unit's slot for its target, running the classic bound-texture code, then putting the
// previous binding back. For as long as the emulated call runs, that swap is a REAL change to
// which texture is bound at that unit, so both transitions have to move the texture bind
// generation.
//
// They used to move nothing. Backends memoise per-unit work keyed on the bind generation and
// BORROW the binding slot (they hold a pointer to the slot's shared_ptr, not a copy), so a memo
// built while texture A sat in the slot stayed "valid" while B was temporarily in it - and the
// backend then drove A's backend twin from B's frontend state, re-specifying A's backend storage
// with B's shape. Any content A only ever had on the GPU was gone. That is what blanked
// Minecraft's lightmap when Iris uploaded to a BSL shadow map: the text shader multiplies by the
// lightmap, so `if (color.a < 0.1) discard` then threw away every glyph in the process.
// The by-name entry points edit the named texture directly. They used to be emulated by binding it
// onto the active unit and running the bound-texture code, and a swap the bind generation did not
// see let a backend memo drive one texture's twin from another's state - which blanked Minecraft's
// lightmap under Iris. Neither the binding nor the bind generation may move now.
TEST_F(TextureTest, NamedTextureCallKeepsUnitBindingAccountingCoherent) {
GLuint names[2] = {};
MG_Impl::GLImpl::GenTextures(2, names);
@@ -4905,19 +4881,13 @@ TEST_F(TextureTest, NamedTextureCallKeepsUnitBindingAccountingCoherent) {
ASSERT_NE(boundObject, nullptr);
ASSERT_EQ(boundObject->GetExternalIndex(), boundName);
// TextureParameteriv is one of the by-name calls that is emulated by binding: it reaches
// WithTemporarilyBoundNamedTexture, unlike the scalar TextureParameteri, which edits the
// object directly and never touches a unit.
const Uint64 base = MG_State::pGLContext->GetTextureBindGeneration();
const GLint maxLevel = 0;
MG_Impl::GLImpl::TextureParameteriv(namedName, GL_TEXTURE_MAX_LEVEL, &maxLevel);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// The emulation put `namedName` on the unit and took it off again. A generation-keyed memo
// must be able to see that the slot it borrows was not stable across the call.
EXPECT_GT(MG_State::pGLContext->GetTextureBindGeneration(), base)
<< "a by-name texture call swapped a live unit binding without moving the bind generation";
// ...and the application-visible binding is exactly what it was before the call.
EXPECT_EQ(MG_State::pGLContext->GetTextureBindGeneration(), base)
<< "a by-name texture call moved the bind generation";
EXPECT_EQ(slot.GetBoundObject(), boundObject);
MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0);
@@ -6839,6 +6809,8 @@ namespace {
struct ScopedNoOpGenerateMipmap {
ScopedNoOpGenerateMipmap() : m_snapshot(MobileGL::MG_Backend::gBackendFunctionsTable) {
MobileGL::MG_Backend::gBackendFunctionsTable.GL.GenerateMipmap = [](GLenum) {};
MobileGL::MG_Backend::gBackendFunctionsTable.GL.GenerateTextureMipmap =
[](const SharedPtr<MG_State::GLState::ITextureObject>&, GLenum) {};
}
~ScopedNoOpGenerateMipmap() { MobileGL::MG_Backend::gBackendFunctionsTable = m_snapshot; }
ScopedNoOpGenerateMipmap(const ScopedNoOpGenerateMipmap&) = delete;
+1 -1
View File
@@ -1,5 +1,5 @@
{
"file_excess_lines": 22926,
"file_excess_lines": 21859,
"function_excess_lines": 15516,
"layering_violations": 63
}
+4 -4
View File
@@ -1,7 +1,7 @@
{
"allcaps_comment_lines": 2256,
"line_citations": 165,
"long_comment_excess_lines": 8911,
"allcaps_comment_lines": 2253,
"line_citations": 159,
"long_comment_excess_lines": 8895,
"non_english_comment_lines": 20,
"planning_code_comment_lines": 3190
"planning_code_comment_lines": 3188
}