From 10c5168be2a042a0a2ebb8ba1f076c93d7391a5f Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 23 Sep 2026 10:52:25 -0400 Subject: [PATCH] [MG_Pipe, MG_Remote, MG_Test, MG_IntegrationTest] (P7): accept empty texture levels in the respecify extent carrier --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 21 +++ .../Scenarios/EmptyTextureLevelScenario.cpp | 149 ++++++++++++++++++ MobileGL/MG_Pipe/MGPipeTypes.h | 11 +- MobileGL/MG_Pipe/PipeApply.cpp | 2 +- .../MG_Remote/Server/StagedTextureStore.h | 18 ++- .../MG_Test/Wire/StagedTextureStoreTest.cpp | 59 +++++++ 6 files changed, 252 insertions(+), 8 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/EmptyTextureLevelScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index dc8d5bc4..1c334a2a 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -104,6 +104,7 @@ add_executable(MobileGLIntegrationTest Scenarios/ImageFormatQualifierScenario.cpp Scenarios/NonCoreImageFormatScenario.cpp Scenarios/ImageSizeAfterRespecScenario.cpp + Scenarios/EmptyTextureLevelScenario.cpp Scenarios/SsboDeclarationFormScenario.cpp Scenarios/Glsl420DeclarationScenario.cpp Scenarios/IoBlockNameCollisionScenario.cpp @@ -2862,6 +2863,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED) mgl_itest_register_split_arms("${mglItestD2Scenario}.*" "") endforeach() + # P7 gate 5 (g5-msrbo): AN EMPTY TEXTURE LEVEL. glTexImage*D with a zero extent is legal GL + # and dEQP's resetStateGLCore issues it for every target after every case; PH-4's extent + # carrier refused it as ProtocolCorruption on the apply thread, so no KHR-GL case could + # finish on a split arm. Backend-agnostic (the refusal was in MGPipeApplyResourceRespecify), + # so the DirectGLES gated arms carry it here and the Magma gated lanes below carry it too. + mgl_itest_register_split_arms("EmptyTextureLevelScenario.*" "") + # ProgramPipelineScenario BEYOND *StorageBlock*: the i1 block above already registers the two # storage-block cases. The other nine are pure name/state cases that draw nothing, and # ScenarioFixture's armed-lane rule (the client encoder's record ordinal must move) makes a @@ -3071,6 +3079,19 @@ if (MOBILEGL_BUILD_DISAGGREGATED) ${mglItestArmProperties} ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5f-fm${mglItestArmLogTag}-${fmCase}.log") endforeach() + # P7 gate 5 (g5-msrbo): the empty-level pair, on the gated Magma lanes as well - the CTS + # arm gate 5 measures is inproc x DirectVulkan, and every one of its cases ends in the + # empty-level reset this scenario reproduces. + foreach(emptyLevelCase TheCtsStateResetDefinesEveryDefaultTargetEmpty + ALevelDefinedEmptyThenRedefinedCarriesTheNewTexels) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.Fm." + TEST_FILTER "EmptyTextureLevelScenario.${emptyLevelCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-emptylevel${mglItestArmLogTag}-${emptyLevelCase}.log") + endforeach() # ---- P7 wave 2-B: THE TWO SHADER MIP ARMS, FORCED ------------------------------------ # # WHY A KNOB AND NOT A FORMAT. The refusal these two entries cover diff --git a/MobileGL/MG_IntegrationTest/Scenarios/EmptyTextureLevelScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/EmptyTextureLevelScenario.cpp new file mode 100644 index 00000000..66edee6a --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/EmptyTextureLevelScenario.cpp @@ -0,0 +1,149 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/EmptyTextureLevelScenario.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 +// +// Scenario - A TEXTURE LEVEL DEFINED EMPTY IS LEGAL GL, AND IT MUST NOT END THE SESSION. +// +// glTexImage1D(width 0), glTexImage2D(0 x 0), glTexImage3D(0 x 0 x 0) define a level with no +// texels. dEQP's glu::resetStateGLCore issues exactly that for EVERY texture target on EVERY unit +// after EVERY case (gluStateReset.cpp), so a split arm that cannot carry an empty level cannot run +// a single KHR-GL case to completion. +// +// P7 PH-4 (f90afcd1) put each per-level respecify's exact extent on the wire and made the server +// refuse any zero component as a noncanonical carrier - Fatal{ProtocolCorruption, +// "ResourceRespecify.ExtentCarrier"} on the apply thread, from the reset that follows the case +// rather than the case itself. The two cases below are the reset's own sequence, then the +// round trip that proves a level can go empty and come back with texels the server really staged. + +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + class EmptyTextureLevelScenario : public ScenarioTest { + protected: + void TearDown() override { + if (!Ready()) return; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo); + if (m_texture != 0) glDeleteTextures(1, &m_texture); + m_fbo = m_texture = 0; + while (glGetError() != GL_NO_ERROR) { + } + } + + // Every target resetStateGLCore empties, in its order, on the default textures. + static void EmptyEveryDefaultTarget() { + glBindTexture(GL_TEXTURE_1D, 0); + glTexImage1D(GL_TEXTURE_1D, 0, GL_RGBA, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_2D, 0); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); + for (GLenum face = GL_TEXTURE_CUBE_MAP_POSITIVE_X; face <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; ++face) + glTexImage2D(face, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_1D_ARRAY, 0); + glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_3D, 0); + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, 0, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glBindTexture(GL_TEXTURE_RECTANGLE, 0); + glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + } + + GLuint m_texture = 0; + GLuint m_fbo = 0; + }; + + } // namespace + + // dEQP's reset, verbatim in shape: every default texture target defined EMPTY at level 0, + // twice (the reset runs after every case), then an ordinary clear + readback. Before the fix + // the first glTexImage1D ended the session on every split arm; the readback is what proves + // the session is still the one the case started with. + TEST_F(EmptyTextureLevelScenario, TheCtsStateResetDefinesEveryDefaultTargetEmpty) { + if (!Ready()) return; + for (int pass = 0; pass < 2; ++pass) { + glActiveTexture(GL_TEXTURE0); + EmptyEveryDefaultTarget(); + glActiveTexture(GL_TEXTURE1); + EmptyEveryDefaultTarget(); + glActiveTexture(GL_TEXTURE0); + ASSERT_EQ(FirstGLError(), 0u) << "an empty level definition is legal GL (pass " << pass << ")"; + } + + HeadlessGL& gl = Gl(); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, gl.Width(), gl.Height()); + glDisable(GL_SCISSOR_TEST); + glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + std::array pixel{}; + glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + EXPECT_EQ(FirstGLError(), 0u); + // The default framebuffer's readback is not what this case is about (it reads zeros on some + // DirectVulkan hosts, tools/cts/README.md) - what matters is that the call RETURNED, i.e. + // the session that took the empty levels is still answering. + gl.EndFrame(); + } + + // A NAMED texture's level goes 4x4 (with texels) -> EMPTY -> 2x2 (with other texels), and the + // 2x2 content must be what a framebuffer readback of that level returns. This is the staged + // store's half: the empty definition must drop the 4x4 bytes and coverage (the coordinate + // system was replaced), and must not stand in the way of the next non-empty definition + // adopting its own bytes. + TEST_F(EmptyTextureLevelScenario, ALevelDefinedEmptyThenRedefinedCarriesTheNewTexels) { + if (!Ready()) return; + glGenTextures(1, &m_texture); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + std::vector redTexels(4 * 4 * 4, 0); + for (std::size_t i = 0; i < redTexels.size(); i += 4) { + redTexels[i] = 255; + redTexels[i + 3] = 255; + } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, redTexels.data()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); // empty + std::vector greenTexels(2 * 2 * 4, 0); + for (std::size_t i = 0; i < greenTexels.size(); i += 4) { + greenTexels[i + 1] = 255; + greenTexels[i + 3] = 255; + } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, greenTexels.data()); + ASSERT_EQ(FirstGLError(), 0u) << "4x4 -> empty -> 2x2 on one level is legal GL"; + + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + std::vector readBack(2 * 2 * 4, 7); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, readBack.data()); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_EQ(readBack, greenTexels) + << "the level that went empty and came back must hold the 2x2 texels it was redefined with"; + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + Gl().EndFrame(); + } + +} // namespace MGITest diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 55c5dea4..154f8a8e 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -428,10 +428,15 @@ namespace MobileGL::MG_Pipe { inline constexpr Uint32 MGPipeRespecifiedDepthOf(const MGPResourceDesc& desc) { return MGPipeRespecifyIsWholeResource(desc) ? 0u : static_cast(desc.BufSize); } + // A ZERO COMPONENT IS CANONICAL. glTexImage1D(width 0), glTexImage2D(0 x 0) and friends are + // legal GL that define an EMPTY level - dEQP's resetStateGLCore issues exactly that for every + // texture target after every case - and the frontend reports the level's exact extent as + // e.g. {0, 1, 1}. Requiring nonzero components made that legal call a session Fatal on every + // split arm. The carrier's only noncanonical shape is a depth word past 32 bits; whether an + // extent is USABLE is the staged store's question (an empty level has no byte bound, so any + // resource_subdata against it is refused there). inline constexpr Bool MGPipeRespecifiedExtentCarrierIsCanonical(const MGPResourceDesc& desc) { - return MGPipeRespecifyIsWholeResource(desc) || - ((desc.BufSize >> 32) == 0 && MGPipeRespecifiedWidthOf(desc) != 0 && - MGPipeRespecifiedHeightOf(desc) != 0 && MGPipeRespecifiedDepthOf(desc) != 0); + return MGPipeRespecifyIsWholeResource(desc) || (desc.BufSize >> 32) == 0; } inline constexpr void MGPipeSetRespecifiedLevel(MGPResourceDesc& desc, Uint16 uploadTarget, diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index df0e346a..f5ff8566 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -2081,7 +2081,7 @@ namespace MobileGL::MG_Pipe { if (!MGPipeRespecifyIsWholeResource(desc) && !MGPipeRespecifiedExtentCarrierIsCanonical(desc)) { MGP_TRIP_WIRE_REPORT("MGPipe: Fatal{ProtocolCorruption, \"ResourceRespecify.ExtentCarrier\"} - " - "a named image level must carry nonzero width/height/depth with no high BufSize bits"); + "a named image level's depth word must fit in 32 bits (no high BufSize bits)"); } if (level == nullptr && desc.Target != static_cast(MGPipeResourceTarget::TexBuffer) && (desc.BufOffset != 0 || desc.BufSize != 0)) { diff --git a/MobileGL/MG_Remote/Server/StagedTextureStore.h b/MobileGL/MG_Remote/Server/StagedTextureStore.h index 53adfab7..47bd983e 100644 --- a/MobileGL/MG_Remote/Server/StagedTextureStore.h +++ b/MobileGL/MG_Remote/Server/StagedTextureStore.h @@ -376,6 +376,15 @@ namespace MobileGL::MG_Remote::Server { // extent carried by that accepted operation (including noncanonical mutable mips and // null-data glTexImage*D). Its byte bound is established here, before any upload arrives. // An extent move replaces the coordinate system, so old bytes and coverage go with it. + // + // AN EMPTY LEVEL IS A DEFINITION TOO. A zero width, height or depth is legal GL + // (glTexImage1D(width 0) is how dEQP's resetStateGLCore resets every texture target after + // every case) and it replaces the level's coordinate system like any other: the old bytes + // go, the level is defined at its exact empty extent - which is what the frontend's + // GetMipmapTexelSize answers for it, so both roles agree - and it gets NO byte bound, so + // a resource_subdata against it is RequireDeclaredLevel's refusal rather than a write + // into storage that has no texels. Only a NEGATIVE component (a 32-bit carrier word past + // INT_MAX) is malformed. void NoteLevelDefined(Uint64 key, Uint16 uploadTarget, Uint16 level, const IntVec3& extent, Uint8 resourceTarget = static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D), Uint32 internalFormat = static_cast(TextureInternalFormat::RGBA8)) { @@ -383,10 +392,11 @@ namespace MobileGL::MG_Remote::Server { const std::lock_guard lock(m_mutex); Uint64 byteBound = 0; const Bool supportsSubData = StagedTextureTargetSupportsSubData(resourceTarget); - const Bool validExtent = extent.x() > 0 && extent.y() > 0 && extent.z() > 0; + const Bool validExtent = extent.x() >= 0 && extent.y() >= 0 && extent.z() >= 0; + const Bool emptyExtent = validExtent && (extent.x() == 0 || extent.y() == 0 || extent.z() == 0); if (MG_Util::GetSizedInternalFormatSizeInBytes( static_cast(internalFormat)) == 0 || !validExtent || - (supportsSubData && !StagedTextureDeclaredLevelByteBound( + (supportsSubData && !emptyExtent && !StagedTextureDeclaredLevelByteBound( resourceTarget, internalFormat, extent, m_deviceLimits, &byteBound))) { SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.NoteLevelDefined\"} - " @@ -407,7 +417,7 @@ namespace MobileGL::MG_Remote::Server { shadow.Extent = extent; shadow.InternalFormat = internalFormat; shadow.ResourceTarget = resourceTarget; - shadow.DeclaredByteBound = byteBound; // zero for targets without glTexSubImage + shadow.DeclaredByteBound = byteBound; // zero for targets without glTexSubImage and for an empty level shadow.Defined = true; m_any.store(true, std::memory_order_release); } @@ -591,7 +601,7 @@ namespace MobileGL::MG_Remote::Server { SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"StagedTextureStore.Target\"} - " "resource_subdata is not legal for the server-declared texture target " - "(key=%llu, uploadTarget=%u, level=%u, target=%u)", + "or empty level (key=%llu, uploadTarget=%u, level=%u, target=%u)", static_cast(key), uploadTarget, level, static_cast(shadow.ResourceTarget)); } diff --git a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp index b05374fa..0feacd79 100644 --- a/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp +++ b/MobileGL/MG_Test/Wire/StagedTextureStoreTest.cpp @@ -164,6 +164,65 @@ TEST(StagedTextureStoreTest, AMutableNoncanonicalLevelKeepsItsExactDeclaredExten EXPECT_FALSE(store.IsCovered(key, kTex2DTarget, 2)); } +// AN EMPTY LEVEL IS A DEFINITION (P7 gate 5). glTexImage1D(width 0) / glTexImage2D(0 x 0) are +// legal GL and dEQP's state reset issues them after every case; the frontend reports the level's +// exact extent as {0,1,1} / {0,0,1}. The store must take it as a definition that REPLACES the +// level (the old run and its coverage go), answer the exact empty extent, and give it no byte +// bound - never a session Fatal. A negative component (a carrier word past INT_MAX) stays one. +TEST(StagedTextureStoreTest, AnEmptyLevelIsADefinitionWithNoByteBound) { + Server::StagedTextureStore store(/*copies=*/true); + const Uint64 key = Server::StagedTextureStore::KeyForHandle(TestHandle(42, 1)); + constexpr Uint16 kTex1DTarget = static_cast(TextureUploadTarget::Texture1D); + const auto tex1D = static_cast(MG_Pipe::MGPipeResourceTarget::Tex1D); + const auto rgba8 = static_cast(TextureInternalFormat::RGBA8); + + store.NoteLevelDefined(key, kTex1DTarget, 0, IntVec3{4, 1, 1}, tex1D, rgba8); + Vector bytes(16, 0x5A); + store.Adopt(key, kTex1DTarget, 0, IntVec3{4, 1, 1}, bytes.data(), bytes.size()); + ASSERT_TRUE(store.IsCovered(key, kTex1DTarget, 0)); + + store.NoteLevelDefined(key, kTex1DTarget, 0, IntVec3{0, 1, 1}, tex1D, rgba8); + EXPECT_TRUE(store.IsLevelDefined(key, kTex1DTarget, 0)) << "an empty level still exists"; + EXPECT_EQ(store.LevelExtentOrUndefined(key, kTex1DTarget, 0), IntVec3(0, 1, 1)) + << "the store must answer the frontend's own exact extent for an empty level"; + EXPECT_EQ(store.LevelDeclaredByteBound(key, kTex1DTarget, 0), 0u) << "an empty level has no texels"; + EXPECT_EQ(store.LevelByteSize(key, kTex1DTarget, 0), 0u) << "the 4-texel run belongs to the old level"; + EXPECT_FALSE(store.IsCovered(key, kTex1DTarget, 0)); + + // And back: the next non-empty definition takes its own bytes as usual. + store.NoteLevelDefined(key, kTex1DTarget, 0, IntVec3{2, 1, 1}, tex1D, rgba8); + Vector smaller(8, 0x3C); + store.Adopt(key, kTex1DTarget, 0, IntVec3{2, 1, 1}, smaller.data(), smaller.size()); + EXPECT_TRUE(store.IsCovered(key, kTex1DTarget, 0)); + EXPECT_EQ(store.LevelDeclaredByteBound(key, kTex1DTarget, 0), 8u); + + // The 2D and 3D spellings of the same reset. + const Uint64 key2D = Server::StagedTextureStore::KeyForHandle(TestHandle(43, 1)); + store.NoteLevelDefined(key2D, kTex2DTarget, 0, IntVec3{0, 0, 1}, + static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D), rgba8); + EXPECT_EQ(store.LevelExtentOrUndefined(key2D, kTex2DTarget, 0), IntVec3(0, 0, 1)); + const Uint64 key3D = Server::StagedTextureStore::KeyForHandle(TestHandle(44, 1)); + constexpr Uint16 kTex3DTarget = static_cast(TextureUploadTarget::Texture3D); + store.NoteLevelDefined(key3D, kTex3DTarget, 0, IntVec3{0, 0, 0}, + static_cast(MG_Pipe::MGPipeResourceTarget::Tex3D), rgba8); + EXPECT_TRUE(store.IsLevelDefined(key3D, kTex3DTarget, 0)); + EXPECT_EQ(store.LevelDeclaredByteBound(key3D, kTex3DTarget, 0), 0u); +} + +// The wire half of the same rule: the carrier's only noncanonical shape is a depth word past +// 32 bits. A zero component is an empty level, which the store above takes. +TEST(StagedTextureStoreTest, AZeroExtentCarrierIsCanonicalAndAHighDepthWordIsNot) { + MG_Pipe::MGPResourceDesc desc{}; + desc.Target = static_cast(MG_Pipe::MGPipeResourceTarget::Tex1D); + MG_Pipe::MGPipeSetRespecifiedLevel(desc, kTex2DTarget, 0, 0, 1, 1); + EXPECT_TRUE(MG_Pipe::MGPipeRespecifiedExtentCarrierIsCanonical(desc)) + << "glTexImage1D(width 0) is legal GL; its carrier must not be refused"; + MG_Pipe::MGPipeSetRespecifiedLevel(desc, kTex2DTarget, 0, 0, 0, 0); + EXPECT_TRUE(MG_Pipe::MGPipeRespecifiedExtentCarrierIsCanonical(desc)); + desc.BufSize |= (Uint64{1} << 32); + EXPECT_FALSE(MG_Pipe::MGPipeRespecifiedExtentCarrierIsCanonical(desc)); +} + TEST(StagedTextureStoreTest, MultisampleAndTextureBufferTargetsCannotAcceptStagedTexelBytes) { EXPECT_FALSE(Server::StagedTextureTargetSupportsSubData( static_cast(MG_Pipe::MGPipeResourceTarget::Tex2DMS)));