diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index c144b4f6..857a7753 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -112,6 +112,8 @@ add_executable(MobileGLIntegrationTest Scenarios/RelinkStageSetScenario.cpp Scenarios/GuiBatchScenario.cpp Scenarios/UnboundImageDescriptorScenario.cpp + Scenarios/IntegerBorderColorScenario.cpp + Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp new file mode 100644 index 00000000..d42560ed --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.cpp @@ -0,0 +1,204 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearTexImageUndefinedLevelZeroScenario.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 - glClearTexImage ON A TEXTURE WHOSE GL LEVEL 0 WAS NEVER DEFINED. +// +// KHR-GL4[456].clear_tex_image.* builds exactly one shape: fillTexture() issues ONE +// glTexImage2D(GL_TEXTURE_2D, m_texLevel, ...) - the only texImage2D in the whole format/level +// family - sets GL_TEXTURE_MAX_LEVEL to that level, clears it and reads it back with +// glGetTexImage(..., m_texLevel, ...). For m_texLevel > 0 the levels BELOW the defined one have no +// storage at all, and the split in the conformance results was on that alone: every texLevel_0 body +// passed on DirectVulkan and every texLevel != 0 body failed, across all four internal formats and +// all three entry points. +// +// The frontend understands this shape - the clear is a pure CPU-shadow write, and +// ValidateTextureImageQuery deliberately does not demand mip completeness for a readback. The +// Vulkan backend did not: VkTextureManager takes storage mip 0 as the physical image extent, so a +// texture with no level 0 got no VkImage, SyncTextureAndGetDescriptor answered nullptr, and +// VulkanRenderer::GetTextureImage took a silent early return - leaving the caller's buffer exactly +// as it found it. The conformance failures carried no at all, because nothing raised a GL +// error: the destination was simply never written, so the test compared its own zero-initialized +// buffer against the clear value. +// +// The fix this pins is the readback fallback: with NO VkImage, nothing GPU-side can ever have +// written the texture, so the CPU shadow IS its content and is the correct answer. It is gated on +// "no image exists at all" and not on "syncing was inconvenient - a blanket shadow answer would +// return stale bytes for every render-to-texture result instead. +// +// NOT covered here, and deliberately: such a texture still has no VkImage, so it remains invisible +// to SAMPLING and rendering on DirectVulkan. Backing the image from the lowest defined level is a +// separate change (it moves every GL-level-to-subresource translation in the backend); this +// scenario asserts the readback contract only, and the DirectGLES leg - which has always been able +// to define a lone level N - is the built-in control for what the answer should be. + +#include +#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 { + + // The conformance family's own shape: a mid-chain level of a texture that has nothing else. + constexpr GLint kDefinedLevel = 3; + constexpr GLsizei kLevelExtent = 8; + + struct Texel8 { + GLubyte r = 0, g = 0, b = 0, a = 0; + bool operator==(const Texel8& other) const { + return r == other.r && g == other.g && b == other.b && a == other.a; + } + }; + + std::ostream& operator<<(std::ostream& os, const Texel8& c) { + return os << "rgba(" << int(c.r) << "," << int(c.g) << "," << int(c.b) << "," << int(c.a) << ")"; + } + + // The conformance test's clear value is a single repeated component; 5 is what it uses, and + // it is deliberately neither 0 (an unwritten destination) nor 255 (a saturated one). + constexpr Texel8 kClearValue{5, 5, 5, 5}; + constexpr Texel8 kInitialValue{200, 100, 50, 255}; + + class ClearTexImageUndefinedLevelZeroScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + DrainErrors(); + } + + void TearDown() override { + if (!Ready()) return; + if (m_texture != 0) { + glBindTexture(GL_TEXTURE_2D, 0); + glDeleteTextures(1, &m_texture); + m_texture = 0; + } + DrainErrors(); + } + + static void DrainErrors() { + for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) { + } + } + + // One level and nothing else, through glTexImage2D - deliberately NOT glTexStorage2D, + // which would define the whole chain and could not express "level 0 does not exist". + void MakeTextureWithOnlyLevel(GLint level) { + if (m_texture != 0) glDeleteTextures(1, &m_texture); + glGenTextures(1, &m_texture); + glBindTexture(GL_TEXTURE_2D, m_texture); + const std::vector initial(static_cast(kLevelExtent) * kLevelExtent, kInitialValue); + glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA8, kLevelExtent, kLevelExtent, 0, GL_RGBA, GL_UNSIGNED_BYTE, + initial.data()); + // What the conformance case does: MAX_LEVEL names the one level that exists, and + // BASE_LEVEL is left at its default 0 - which is what makes level 0 undefined AND + // nominally the base level, the shape the backend could not express. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + ASSERT_EQ(FirstGLError(), 0u) << "texture setup with only level " << level; + } + + std::vector ReadLevel(GLint level) { + std::vector pixels(static_cast(kLevelExtent) * kLevelExtent, Texel8{0, 0, 0, 0}); + glBindTexture(GL_TEXTURE_2D, m_texture); + glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage(level " << level << ") left a GL error behind"; + return pixels; + } + + void ExpectAllTexels(const char* what, const std::vector& pixels, Texel8 expected) { + std::size_t offenders = 0; + Texel8 firstBad{}; + for (const Texel8& pixel : pixels) { + if (pixel == expected) continue; + if (offenders == 0) firstBad = pixel; + ++offenders; + } + EXPECT_EQ(offenders, 0u) << what << ": got " << firstBad << " instead of " << expected << " (" + << offenders << " of " << pixels.size() << " texels wrong)"; + } + + GLuint m_texture = 0; + }; + + } // namespace + + // The regression. Before the fix glGetTexImage wrote nothing at all on DirectVulkan, so the + // caller's buffer kept whatever it already held - which is why the conformance failures showed + // the test's own zero-initialized memory and carried no GL error. + TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearAndReadBackALevelWhoseLowerLevelsDoNotExist) { + if (!Ready()) GTEST_SKIP(); + MakeTextureWithOnlyLevel(kDefinedLevel); + + // Pre-flight: the level reads back as what was uploaded. This is what makes the assertion + // after the clear falsifiable - without it, a readback that silently wrote nothing could not + // be told from one that wrote the right answer. + ExpectAllTexels("before the clear", ReadLevel(kDefinedLevel), kInitialValue); + + glClearTexImage(m_texture, kDefinedLevel, GL_RGBA, GL_UNSIGNED_BYTE, &kClearValue); + EXPECT_EQ(FirstGLError(), 0u) << "glClearTexImage was rejected"; + + ExpectAllTexels("after the clear", ReadLevel(kDefinedLevel), kClearValue); + Gl().EndFrame(); + } + + // The same shape through glClearTexSubImage, which is a separate entry point in the conformance + // family and failed on exactly the same bodies. + TEST_F(ClearTexImageUndefinedLevelZeroScenario, ClearSubImageOfALevelWhoseLowerLevelsDoNotExist) { + if (!Ready()) GTEST_SKIP(); + MakeTextureWithOnlyLevel(kDefinedLevel); + + glClearTexSubImage(m_texture, kDefinedLevel, 0, 0, 0, kLevelExtent, kLevelExtent, 1, GL_RGBA, + GL_UNSIGNED_BYTE, &kClearValue); + EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected"; + + ExpectAllTexels("after the sub-image clear", ReadLevel(kDefinedLevel), kClearValue); + Gl().EndFrame(); + } + + // The negative control: an ORDINARY texture, whose level 0 does exist, must keep answering from + // the GPU image rather than being diverted onto the shadow. A fallback that fired unconditionally + // would pass the two tests above and this one too - but it would also hand back stale bytes for + // anything the GPU had written, which is why the partial-clear check below matters: the readback + // has to see a region the backend cleared and a region it did not, in one image. + TEST_F(ClearTexImageUndefinedLevelZeroScenario, AnOrdinaryLevelZeroTextureStillReadsBackCorrectly) { + if (!Ready()) GTEST_SKIP(); + MakeTextureWithOnlyLevel(0); + + ExpectAllTexels("before the clear", ReadLevel(0), kInitialValue); + + // Clear only the left half, so the answer is neither "all initial" nor "all cleared". + glClearTexSubImage(m_texture, 0, 0, 0, 0, kLevelExtent / 2, kLevelExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE, + &kClearValue); + EXPECT_EQ(FirstGLError(), 0u) << "glClearTexSubImage was rejected"; + + const std::vector pixels = ReadLevel(0); + ASSERT_EQ(pixels.size(), static_cast(kLevelExtent) * kLevelExtent); + for (int y = 0; y < kLevelExtent; ++y) { + for (int x = 0; x < kLevelExtent; ++x) { + const Texel8 expected = x < kLevelExtent / 2 ? kClearValue : kInitialValue; + const Texel8 actual = pixels[static_cast(y) * kLevelExtent + x]; + ASSERT_EQ(actual, expected) << "at (" << x << "," << y << ")"; + } + } + Gl().EndFrame(); + } + +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.cpp new file mode 100644 index 00000000..6ae8c739 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.cpp @@ -0,0 +1,268 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/IntegerBorderColorScenario.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 - AN INTEGER GL_TEXTURE_BORDER_COLOR REACHES AN isampler2D AS AN INTEGER. +// +// KHR-GL46.texture_border_clamp.Texture2D{R32I,R32UI} (and the 2DArray/3D siblings) set the border +// colour with glSamplerParameterIiv/Iuiv, sample outside the texture through an integer sampler and +// expect the value back. MobileGL returned 1132396544 on Espryt - which is 0x437F0000, the IEEE-754 +// bits of 255.0f, i.e. the float border-colour register read through an integer sampler - and 0 on +// Magma, where the border fell through to VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK. +// +// Two independent halves, and this scenario covers both because it goes through the frontend: +// +// * the STATE had no record of which entry point wrote the border colour. All three +// representations are kept numerically in step, so the value alone cannot say whether the +// application called glTexParameterfv or glTexParameterIiv. +// * each backend then had exactly one border-colour call site: glTexParameterfv / +// glSamplerParameterfv on DirectGLES, and a snap-to-one-of-four-predefined-values on +// DirectVulkan that never emitted the VK_BORDER_COLOR_INT_* family at all. +// +// The border value is deliberately outside every predefined VkBorderColor and outside anything a +// float register could round-trip: (255, -1, 7, 3) is neither transparent black, nor opaque black, +// nor opaque white, so on DirectVulkan it can only be delivered through VK_EXT_custom_border_color. +// That makes the scenario a real test of the extension path on lavapipe rather than a palette hit. +// +// Both an integer image view and an integer border colour are involved, which is the other half of +// the Vulkan rule: VK_BORDER_COLOR_FLOAT_* on an integer image view is undefined behaviour +// regardless of the value, so even a border of (0,0,0,1) has to resolve to INT_OPAQUE_BLACK. +// InsideTexelsAreUnaffected is what keeps that from being asserted vacuously. + +#include +#include +#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 { + + constexpr int kOutputWidth = 8; + constexpr int kOutputHeight = 8; + + // The texture's own texel, and the border. Neither is a Vulkan palette entry, and the border + // is deliberately not derivable from the texel. + constexpr std::int32_t kInsideTexel[4] = {11, 22, 33, 44}; + constexpr std::int32_t kBorderColor[4] = {255, -1, 7, 3}; + + constexpr const char* kVertexSource = R"(#version 330 core +void main() +{ + switch (gl_VertexID) + { + case 0: gl_Position = vec4(-1.0, 1.0, 0.0, 1.0); break; + case 1: gl_Position = vec4( 1.0, 1.0, 0.0, 1.0); break; + case 2: gl_Position = vec4(-1.0,-1.0, 0.0, 1.0); break; + case 3: gl_Position = vec4( 1.0,-1.0, 0.0, 1.0); break; + } +} +)"; + + // One channel per draw, so a failure names the component that is wrong. The coordinate is a + // uniform rather than a literal so the same program serves the border sample and the inside + // sample and nothing can be constant-folded differently between them. + std::string FragmentSource(int channel) { + static const char* kChannels[4] = {"x", "y", "z", "w"}; + return std::string("#version 330 core\n\nuniform isampler2D smp;\nuniform vec2 uCoord;\n\n" + "out int out_color;\n\nvoid main()\n{\n out_color = texture(smp, uCoord).") + + kChannels[channel] + ";\n}\n"; + } + + class IntegerBorderColorScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + + // 2x2 RGBA32I. Integer textures are not filterable, so NEAREST is mandatory. + const std::int32_t texels[4][4] = {{kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}, + {kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}, + {kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}, + {kInsideTexel[0], kInsideTexel[1], kInsideTexel[2], kInsideTexel[3]}}; + glGenTextures(1, &m_sourceTexture); + glBindTexture(GL_TEXTURE_2D, m_sourceTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32I, 2, 2); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA_INTEGER, GL_INT, texels); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + ASSERT_EQ(FirstGLError(), 0u) << "source texture setup left a GL error behind"; + + // 8x8 R32I render target: an integer readback, so nothing is normalized on the way + // out and a wrong value is reported as the number it actually was. + glGenTextures(1, &m_outputTexture); + glBindTexture(GL_TEXTURE_2D, m_outputTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32I, kOutputWidth, kOutputHeight); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_outputTexture, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + glGenVertexArrays(1, &m_vao); + ASSERT_EQ(FirstGLError(), 0u) << "output framebuffer setup left a GL error behind"; + } + + void TearDown() override { + if (!Ready()) return; + if (m_sampler != 0) { + glBindSampler(0, 0); + glDeleteSamplers(1, &m_sampler); + m_sampler = 0; + } + if (m_vao != 0) glDeleteVertexArrays(1, &m_vao); + if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo); + if (m_outputTexture != 0) glDeleteTextures(1, &m_outputTexture); + if (m_sourceTexture != 0) glDeleteTextures(1, &m_sourceTexture); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + // Samples `coord` through the integer sampler and returns every texel the draw wrote. + std::vector RenderChannel(int channel, float coordX, float coordY) { + const std::string fragment = FragmentSource(channel); + std::string error; + const unsigned int program = CompileProgram(kVertexSource, fragment.c_str(), &error); + if (program == 0) { + ADD_FAILURE() << "channel " << channel << ": program did not build: " << error; + return {}; + } + + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glViewport(0, 0, kOutputWidth, kOutputHeight); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + // A clear value nothing under test can produce, so an undrawn target is not mistaken + // for a correct one. + const GLint clearValue[4] = {-559038737, 0, 0, 0}; + glClearBufferiv(GL_COLOR, 0, clearValue); + + glUseProgram(program); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_sourceTexture); + glUniform1i(glGetUniformLocation(program, "smp"), 0); + glUniform2f(glGetUniformLocation(program, "uCoord"), coordX, coordY); + glBindVertexArray(m_vao); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + glBindVertexArray(0); + + std::vector texels(static_cast(kOutputWidth) * kOutputHeight, 0); + glReadPixels(0, 0, kOutputWidth, kOutputHeight, GL_RED_INTEGER, GL_INT, texels.data()); + glUseProgram(0); + glDeleteProgram(program); + return texels; + } + + void ExpectAllTexels(const char* what, int channel, std::int32_t expected, + const std::vector& texels) { + if (texels.empty()) return; + std::size_t offenders = 0; + std::int32_t firstBad = 0; + for (const std::int32_t texel : texels) { + if (texel == expected) continue; + if (offenders == 0) firstBad = texel; + ++offenders; + } + EXPECT_EQ(offenders, 0u) << what << " component " << channel << " returned " << firstBad + << " instead of " << expected << " (" << offenders << " of " << texels.size() + << " texels wrong)"; + } + + // Every component of the border, in one place, so both the texture-object and the + // sampler-object case assert exactly the same thing. + void ExpectBorderIsDelivered(const char* what) { + for (int channel = 0; channel < 4; ++channel) { + // (-0.5, -0.5) is a full texture width outside the image on both axes, so + // CLAMP_TO_BORDER can only answer with the border colour. + const std::vector texels = RenderChannel(channel, -0.5f, -0.5f); + EXPECT_EQ(FirstGLError(), 0u) << what << ": the border draw left a GL error behind"; + ExpectAllTexels(what, channel, kBorderColor[channel], texels); + } + } + + GLuint m_sourceTexture = 0; + GLuint m_outputTexture = 0; + GLuint m_fbo = 0; + GLuint m_vao = 0; + GLuint m_sampler = 0; + }; + + } // namespace + + // The floor, and the control that keeps the two tests below from passing vacuously: an INSIDE + // sample has to fetch the texture's own texel. If this fails the sampler, the shader or the + // integer readback is broken and nothing about the border colour has been measured. + TEST_F(IntegerBorderColorScenario, InsideTexelsAreUnaffectedByTheBorderColour) { + if (!Ready()) GTEST_SKIP(); + + glBindTexture(GL_TEXTURE_2D, m_sourceTexture); + glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor); + ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected"; + + for (int channel = 0; channel < 4; ++channel) { + const std::vector texels = RenderChannel(channel, 0.5f, 0.5f); + EXPECT_EQ(FirstGLError(), 0u) << "the inside draw left a GL error behind"; + ExpectAllTexels("inside sample", channel, kInsideTexel[channel], texels); + } + Gl().EndFrame(); + } + + // The regression, texture-object spelling. glTexParameterIiv is the entry point the frontend + // already accepted and then flattened into the same FloatVec4 every other spelling wrote. + TEST_F(IntegerBorderColorScenario, TexParameterIivBorderColourSurvivesToAnIntegerSampler) { + if (!Ready()) GTEST_SKIP(); + + glBindTexture(GL_TEXTURE_2D, m_sourceTexture); + glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, kBorderColor); + ASSERT_EQ(FirstGLError(), 0u) << "glTexParameterIiv(GL_TEXTURE_BORDER_COLOR) was rejected"; + + ExpectBorderIsDelivered("glTexParameterIiv"); + Gl().EndFrame(); + } + + // The regression, sampler-object spelling - which is the one the conformance cases actually use, + // and a separate code path in both backends (BackendSamplerObject::Sync on DirectGLES, and the + // sampler cache key on DirectVulkan, where a border colour that is not part of the key would + // alias two samplers that differ only in it). + TEST_F(IntegerBorderColorScenario, SamplerParameterIivBorderColourSurvivesToAnIntegerSampler) { + if (!Ready()) GTEST_SKIP(); + + glGenSamplers(1, &m_sampler); + ASSERT_NE(m_sampler, 0u); + glSamplerParameteri(m_sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(m_sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glSamplerParameteri(m_sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + glSamplerParameterIiv(m_sampler, GL_TEXTURE_BORDER_COLOR, kBorderColor); + ASSERT_EQ(FirstGLError(), 0u) << "sampler-object setup was rejected"; + + // The texture object carries a DIFFERENT border colour, so a pass here cannot come from the + // texture's own state leaking through: GL 4.6 core 8.10 says a bound sampler object's state + // wins over the texture's for every sampling parameter. + const std::int32_t decoyBorder[4] = {0, 0, 0, 0}; + glBindTexture(GL_TEXTURE_2D, m_sourceTexture); + glTexParameterIiv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, decoyBorder); + glBindSampler(0, m_sampler); + ASSERT_EQ(FirstGLError(), 0u) << "binding the sampler object was rejected"; + + ExpectBorderIsDelivered("glSamplerParameterIiv"); + glBindSampler(0, 0); + Gl().EndFrame(); + } + +} // namespace MGITest