[Test] (Espryt): assert a glTexParameter on a texture that only ever was an attachment, an image binding or a copy endpoint reaches the driver

This commit is contained in:
2026-09-08 19:08:12 -04:00
committed by rereview
parent 246434bb35
commit b6e5c2bb39
2 changed files with 565 additions and 0 deletions
@@ -133,6 +133,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/HandleRecycleScenario.cpp
Scenarios/CsoContentAddressingScenario.cpp
Scenarios/ResourceSubsystemControlScenario.cpp
Scenarios/TextureParamsWithoutASamplerViewScenario.cpp
)
target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -1321,6 +1322,25 @@ gtest_discover_tests(MobileGLIntegrationTest
ENVIRONMENT "${MGL_ITEST_GLES_ARENA_SUBSYSTEM_OFF_ENVIRONMENT}"
)
# --- G9: the mandatory red-before scenario -------------------------------------------
#
# TextureParamsWithoutASamplerViewScenario needs NO lane of its own, and that is deliberate rather
# than an omission. It reads no counter and no log - its observable is a sampled colour - and its
# claim is about the SHIPPING configuration, so the two ambient registrations at the top of this
# file (which run at the build's default mask) are exactly the arms it wants. A lane here would pin
# a mask and make the entry stop describing what ships the next time the default moved. The
# DirectVulkan ambient entries skip in the scenario's SetUp, naming the backend: the gap it is about
# is Espryt's SyncAttachmentObject / SyncNeccessaryTextures pair and P4a touches no DirectVulkan
# source but MagmaPipeArms.h (D-Q).
#
# D-E3 expects its second case, AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver, to be
# RED until package esprytobj lands. MEASURED ON THE CONTRACT COMMIT IT IS GREEN, and the scenario's
# header carries the mechanism: a texture parameter's only public-GL observable is a sample, and the
# sample repairs the state it was meant to catch (the unit sync list re-syncs whenever the params
# version moved). Nothing about any of that is expressed in this file - a registration that
# "expected" a red would be a gate that could never go green - and the ruling on the missing
# artefact is the integrator's, recorded in the gates result document.
if (MOBILEGL_PIPE_VERIFY)
# 900s, not the ambient 120: the comparator re-reads every field of the fill mask at the verb
# boundary and again at every accessor read, which the design budgets at 5-10x.
@@ -0,0 +1,545 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureParamsWithoutASamplerViewScenario.cpp
// Copyright (c) 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'S PARAMETERS TAKE EFFECT EVEN WHEN IT HAS NO SAMPLER VIEW (gate G9), the
// one scenario ROADMAP.md:20 names by hand and requires to be RED before P4a lands.
//
// THE DESIGN STATEMENT IT TESTS, ARCHITECTURE.md:100 (D10), verbatim: "SetTextureParams 按资源寻址、
// 与 sampler view 分开(D10):只作 FBO attachment / image 单元 / glCopyImageSubData 端点的纹理没有
// sampler view,但 Espryt 对 attachment 也同步纹理参数,且 RequireImageBindableStorage 需要在前端参数
// 版本不动时强制重同步." A texture that is only an attachment, only an image-unit binding or only a
// copy endpoint has NO sampler view at all - so a design that carried texture parameters on the
// view would silently drop them for exactly those textures. P4a addresses set_texture_params by
// RESOURCE, independently of any binding, which is what makes the record exist for every texture
// the moment its parameters move.
//
// THE FOUR CASES, and BRIEF-P4A.md D-E3 is where their expected verdicts come from:
//
// AnAttachmentOnlyTexturesSwizzleReachesTheDriver green today, green after
// AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver D-E3 says RED today
// AnImageUnitOnlyTexturesSwizzleSurvivesARequireImageBindableStorageRemint green today, green after
// ACopyImageEndpointOnlyTexturesParamsReachTheDriver green today, green after
//
// [MEASURED, AND IT DOES NOT MATCH D-E3] All four are GREEN on the P4a contract commit (08192d72),
// on llvmpipe, in the push build. The second one is green FOR A REASON THAT IS ITSELF THE FINDING,
// and it is written here rather than in a review comment because the next person to try to make it
// red needs to know why they cannot:
//
// A texture parameter's only public-GL observable is a SAMPLE - nothing about an attachment, an
// image binding or a copy endpoint reads a swizzle or an aspect mode - and a sample puts the
// texture on SyncNeccessaryTextures' UNIT list. That walk calls SyncTextureParamsToBackend for
// every entry whose IsDrawSyncClean is false (DirectGLES.cpp:1896-1898), and IsDrawSyncClean is
// false whenever the frontend's parameter version has moved since the last sync
// (Managers.h:1399-1416, `m_syncedTextureParamsVersion != paramsVersion`). So the very act of
// observing the parameter repairs the state the observation was meant to catch: the gap between
// "the parameter moved on a read-attachment-only texture" and "the driver was told" is REAL, and
// it is closed by the next sampler binding, which is also the only thing that can see it.
//
// What that means for the gate, stated plainly so nobody reads a green here as evidence of
// anything it is not:
//
// * the four cases are a REGRESSION NET around D10, not the evidence for the change. They pin
// the design statement: a texture's parameters take effect however the texture is reached, and
// in particular they would go RED if any of the four reachability paths were ever made to
// depend on the texture having a sampler VIEW - which is exactly the coupling P4a's
// resource-addressed set_texture_params removes and the thing a later phase could reintroduce;
// * the "落地前必须红" artefact ROADMAP.md:20 asks for is NOT produced by this file, and no
// public-GL integration scenario on a monolith tree can produce it. Producing it needs an
// observation of the DRIVER's texture object taken while the texture is still
// read-attachment-only, which means a probe inside MG_Backend/DirectGLES - package D's files
// (C.7), not this package's. The integrator's ruling is recorded in the gates result document.
//
// WHY THE SECOND ONE IS THE RED, mechanically (scout-espryt-framebuffer.md 2.6, re-opened at the
// base ref). Today a texture's parameters ride on the UNIT BINDING and on the DRAW attachment set:
//
// bound to a sampler unit <= the high-water mark SyncNeccessaryTextures' unit list -> synced
// attachment of the DRAW framebuffer SyncNeccessaryTextures' FBO list -> synced
// attachment of the READ framebuffer ONLY SyncCurrentFBO -> SyncToBackend ->
// SyncAttachmentObject, which calls
// SyncMipmapsToBackend at Managers.cpp:7161
// and NOTHING ELSE -> NOT synced
// bound to an image unit SyncImageTextureBinding ->
// SyncTextureObjectToBackend, and
// RequireImageBindableStorage additionally
// forces m_forceTextureParamsResync -> synced
// a glCopyImageSubData endpoint MakeGLESCopyImageEndpoint ->
// SyncTextureObjectToBackend -> synced
//
// SyncNeccessaryTextures' attachment list reads GetFramebufferBindingSlotChecked(Draw) only
// (DirectGLES.cpp:1944), so a texture that is exclusively a READ attachment gets its STORAGE synced
// and its PARAMETERS never. P4a closes that gap deliberately (D-E3): the record is addressed by
// resource, and Espryt's SyncAttachmentObject applies parameters for ANY attachment, draw or read.
// It is a behaviour change and it is the deliverable, not a drive-by dev fix (ROADMAP.md:98).
//
// HOW EACH CASE OBSERVES "REACHED THE DRIVER", and why the observation is always a LATER SAMPLE.
// A texture parameter is by definition a sampling parameter: nothing about an attachment, an image
// binding or a copy endpoint reads a swizzle or a depth/stencil aspect mode, so the only thing that
// can see one is a sample. Each case therefore does the same three things -
//
// 1. put the texture through ONE of the five reachability paths above, and only that one,
// 2. move a parameter while it is reachable ONLY that way (through the DSA entry points
// glTextureParameteri / glTextureSubImage2D, so no step of the setup ever binds the texture to
// a sampler unit - a bind would put it on the unit list and answer the question by accident),
// 3. sample it once, at the end, and read the colour back.
//
// - and the difference between them is step 1 alone. A case that is red says: the parameter set
// while the texture was reachable only that way did not survive to the sample.
//
// DIRECTGLES ONLY. The gap is Espryt's - it is a statement about SyncAttachmentObject and
// SyncNeccessaryTextures - and P4a does not touch MG_Backend/DirectVulkan (D-Q). Magma answers the
// same GL question through an entirely different path, so a red or a green there would be evidence
// about P7's work rather than about this gate; the cases SKIP on any other backend, naming that.
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kInset = 2;
constexpr int kTextureSize = 4;
constexpr const char* kQuadVS = R"(#version 330 core
in vec2 aPos;
out vec2 vUv;
void main() {
vUv = aPos * 0.5 + 0.5;
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
// Samples texel (0,0) with an explicit fetch: no filtering, no derivatives, no wrap - so
// the colour that comes back is the texel as the driver's swizzle presents it and nothing
// else can move it.
constexpr const char* kFetchFS = R"(#version 330 core
uniform sampler2D uTex;
out vec4 oColor;
void main() { oColor = texelFetch(uTex, ivec2(0, 0), 0); }
)";
// The stencil aspect of a packed depth/stencil texture is an UNSIGNED INTEGER texture, so
// it needs a usampler2D. The case that uses it turns "the stencil value is what was
// cleared" into a colour, because a colour is the only thing this harness can read back.
constexpr const char* kStencilFetchFS = R"(#version 330 core
uniform usampler2D uTex;
uniform uint uExpected;
out vec4 oColor;
void main() {
uint value = texelFetch(uTex, ivec2(0, 0), 0).r;
oColor = (value == uExpected) ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
}
)";
struct Vertex {
float x, y;
};
class TextureParamsWithoutASamplerViewScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (Gl().BackendName() != "DirectGLES") {
GTEST_SKIP() << "DirectGLES only: this scenario is about Espryt's own reachability "
"table - SyncNeccessaryTextures' attachment list walks the DRAW "
"slot only (DirectGLES.cpp:1944) and SyncAttachmentObject syncs "
"storage and not parameters (Managers.cpp:7161). "
<< Gl().BackendName()
<< " answers the same GL question through a different path, so a "
"verdict here would be evidence about that backend rather than "
"about this gate (P4a touches no DirectVulkan source but "
"MagmaPipeArms.h, D-Q).";
}
std::string error;
m_fetchProgram = CompileProgram(kQuadVS, kFetchFS, &error);
ASSERT_NE(m_fetchProgram, 0u) << error;
static const Vertex quad[6] = {{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f},
{-1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}};
glGenBuffers(1, &m_quadBuffer);
glBindBuffer(GL_ARRAY_BUFFER, m_quadBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), nullptr);
glBindVertexArray(0);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the scene setup left a GL error behind";
}
void TearDown() override {
if (!Ready() || IsSkipped()) return;
glUseProgram(0);
glBindVertexArray(0);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_quadBuffer != 0) glDeleteBuffers(1, &m_quadBuffer);
if (m_fetchProgram != 0) glDeleteProgram(m_fetchProgram);
}
// Direct State Access is how every setup step below touches a texture, and it is the
// whole reason the cases can claim "this texture never had a sampler view": the classic
// entry points (glTexImage2D, glTexParameteri) all require the texture to be BOUND to a
// unit first, and a bind is exactly what puts it on SyncNeccessaryTextures' unit list.
// A case that used them would sync the parameters through the path it is trying to
// exclude and would be green for the wrong reason, on every tree, forever.
bool DirectStateAccessIsAvailable() {
GLuint probe = 0;
glCreateTextures(GL_TEXTURE_2D, 1, &probe);
const bool ok = FirstGLError() == GLenum(GL_NO_ERROR) && probe != 0;
if (probe != 0) glDeleteTextures(1, &probe);
return ok;
}
void SkipWithoutDirectStateAccess() {
if (!DirectStateAccessIsAvailable()) {
GTEST_SKIP() << "glCreateTextures is not usable here, and every case in this file "
"needs the DSA entry points: the classic ones bind the texture to "
"a unit, which is the reachability path these cases exist to "
"exclude. A case that fell back to them would be green for the "
"wrong reason rather than measuring anything.";
}
}
// A 4x4 RGBA8 texture, one solid colour, created and filled WITHOUT EVER BINDING IT.
GLuint MakeSolidTextureWithoutBinding(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
std::vector<std::uint8_t> texels(kTextureSize * kTextureSize * 4);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = r;
texels[i + 1] = g;
texels[i + 2] = b;
texels[i + 3] = 255;
}
GLuint texture = 0;
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
glTextureStorage2D(texture, 1, GL_RGBA8, kTextureSize, kTextureSize);
glTextureSubImage2D(texture, 0, 0, 0, kTextureSize, kTextureSize, GL_RGBA,
GL_UNSIGNED_BYTE, texels.data());
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
return texture;
}
// THE OBSERVATION, shared by every case: bind the texture to a unit for the first time
// in its life and read one texel back through the default framebuffer.
Image SampleAndRead(GLuint program, GLuint texture) {
BindDefaultFramebuffer();
glViewport(0, 0, Gl().Width(), Gl().Height());
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(program);
glUniform1i(glGetUniformLocation(program, "uTex"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
const Image image = ReadPixels(Gl().Width(), Gl().Height());
Gl().EndFrame();
glBindTexture(GL_TEXTURE_2D, 0);
return image;
}
::testing::AssertionResult WholeViewportIs(const Image& image, const char* expected,
const std::string& when) {
return RegionIsMostly(image, kInset, image.Width() - kInset, kInset,
image.Height() - kInset, expected, 0.0, when);
}
// R -> ZERO and G -> ONE, so a RED texel samples as GREEN if and only if the swizzle
// reached the driver, and as RED if it did not. Two colours the harness can name, from
// one parameter change, with no third outcome that could be mistaken for either.
void SwizzleRedIntoGreen(GLuint texture) {
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_R, GL_ZERO);
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_G, GL_ONE);
glTextureParameteri(texture, GL_TEXTURE_SWIZZLE_B, GL_ZERO);
}
GLuint m_fetchProgram = 0;
GLuint m_vao = 0;
GLuint m_quadBuffer = 0;
};
// ------------------------------------------------------------------------------------
// 1. DRAW ATTACHMENT ONLY. Green today (SyncNeccessaryTextures' FBO list walks the draw
// slot and calls SyncTextureObjectToBackend, which syncs parameters) and green after.
// It is the regression net for the half of D-E3 that already works: P4a moves the
// parameter sync from the sync list onto the record, and this case is what says the
// move did not lose the case that used to be covered.
// ------------------------------------------------------------------------------------
TEST_F(TextureParamsWithoutASamplerViewScenario, AnAttachmentOnlyTexturesSwizzleReachesTheDriver) {
if (!Ready()) return;
SkipWithoutDirectStateAccess();
if (IsSkipped()) return;
const GLuint texture = MakeSolidTextureWithoutBinding(255, 0, 0);
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
// The parameter moves while the texture is reachable ONLY as a draw attachment...
SwizzleRedIntoGreen(texture);
// ...and a frame runs with it bound that way, so whatever the draw path syncs, syncs.
glViewport(0, 0, kTextureSize, kTextureSize);
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
BindDefaultFramebuffer();
Gl().EndFrame();
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the draw-attachment frame left a GL error";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
const Image image = SampleAndRead(m_fetchProgram, texture);
EXPECT_TRUE(WholeViewportIs(image, "green",
"a texture that was only ever a DRAW attachment, sampled "
"after its swizzle moved"))
<< "the swizzle set while this texture was reachable only as a draw-framebuffer "
"attachment did not reach the driver: a red texel with R->ZERO, G->ONE must "
"sample as green.";
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// ------------------------------------------------------------------------------------
// 2. READ ATTACHMENT ONLY - THE MANDATORY RED (ROADMAP.md:20, D-E3, G9).
//
// The parameter is GL_DEPTH_STENCIL_TEXTURE_MODE and the texture is a packed
// depth/stencil one, because that is the parameter whose absence is not a mis-filtered
// picture but the WRONG ASPECT: a driver left at the GL default samples the depth bits
// where the application asked for stencil, and the value that comes back is not the
// stencil that was written. RecreateBackendTexture's own comment says exactly this -
// "the mode makes it visible because falling back to the default silently samples the
// wrong aspect rather than merely mis-filtering" (Managers.cpp:4826-4833).
//
// The framebuffer is bound to GL_READ_FRAMEBUFFER and the DRAW target is left on the
// default framebuffer for the whole window, which is the ONE thing that separates this
// case from the one above.
// ------------------------------------------------------------------------------------
TEST_F(TextureParamsWithoutASamplerViewScenario,
AReadAttachmentOnlyTexturesDepthStencilModeReachesTheDriver) {
if (!Ready()) return;
SkipWithoutDirectStateAccess();
if (IsSkipped()) return;
std::string error;
const GLuint stencilProgram = CompileProgram(kQuadVS, kStencilFetchFS, &error);
ASSERT_NE(stencilProgram, 0u) << error;
GLuint texture = 0;
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
glTextureStorage2D(texture, 1, GL_DEPTH24_STENCIL8, kTextureSize, kTextureSize);
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
if (FirstGLError() != GLenum(GL_NO_ERROR)) {
glDeleteTextures(1, &texture);
glDeleteProgram(stencilProgram);
GTEST_SKIP() << "this driver would not create an immutable DEPTH24_STENCIL8 texture, "
"so there is no packed depth/stencil aspect here to sample and the "
"case cannot answer";
}
// Write a stencil value through the texture AS A DRAW ATTACHMENT once, so that there is
// something in the stencil aspect to read. This is setup, not the window: the window
// below never makes it a draw attachment again.
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GLenum(GL_FRAMEBUFFER_COMPLETE)) {
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &texture);
glDeleteProgram(stencilProgram);
GTEST_SKIP() << "a depth-stencil-only framebuffer is incomplete on this driver, so "
"the stencil aspect cannot be written and the case cannot answer";
}
constexpr GLint kStencil = 42;
glViewport(0, 0, kTextureSize, kTextureSize);
glStencilMask(0xFFu);
glClearBufferfi(GL_DEPTH_STENCIL, 0, 0.5f, kStencil);
BindDefaultFramebuffer();
Gl().EndFrame();
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "clearing the stencil aspect left a GL error";
// ---- the window: the texture is reachable ONLY as a READ attachment ----
//
// The DRAW binding is the default framebuffer throughout, so SyncNeccessaryTextures'
// attachment list - which walks the DRAW slot only - never sees this texture, and
// SyncCurrentFBO's read path reaches it through SyncAttachmentObject, which syncs
// storage and not parameters. That is the gap.
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glTextureParameteri(texture, GL_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
// A frame with the read binding live, and it has to contain a REAL DRAW: SyncCurrentFBO
// and the whole sync-list walk run at the validate point, so a frame that only cleared
// and swapped would never reach the read-side path this case is about.
glViewport(0, 0, Gl().Width(), Gl().Height());
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(m_fetchProgram);
glUniform1i(glGetUniformLocation(m_fetchProgram, "uTex"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
Gl().EndFrame();
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the read-attachment frame left a GL error";
// ---- the observation ----
BindDefaultFramebuffer();
glViewport(0, 0, Gl().Width(), Gl().Height());
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(stencilProgram);
glUniform1i(glGetUniformLocation(stencilProgram, "uTex"), 0);
glUniform1ui(glGetUniformLocation(stencilProgram, "uExpected"),
static_cast<GLuint>(kStencil));
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
const Image image = ReadPixels(Gl().Width(), Gl().Height());
Gl().EndFrame();
glBindTexture(GL_TEXTURE_2D, 0);
const GLenum sampleError = FirstGLError();
std::cout << "[ TextureParamsWithoutASamplerView ] read-attachment-only "
"GL_DEPTH_STENCIL_TEXTURE_MODE=GL_STENCIL_INDEX, expecting stencil "
<< kStencil << "; sample GL error 0x" << std::hex << sampleError << std::dec
<< std::endl;
EXPECT_TRUE(WholeViewportIs(image, "green",
"a texture that was only ever a READ attachment, sampled "
"through its stencil aspect"))
<< "GL_DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX was set while this texture was "
"reachable ONLY as an attachment of the READ framebuffer, and the sample did not "
"come back as the stencil value that was cleared into it. The parameter did not "
"reach the driver, and - because the sample itself would have repaired an "
"unsynced parameter (see this file's header: the unit sync list calls "
"SyncTextureParamsToBackend whenever the params version moved) - a red here means "
"something stronger than the D-E3 gap: a reachability path that does not sync "
"parameters AT ALL, i.e. the sampler-view coupling ARCHITECTURE.md:100 (D10) "
"exists to remove. Read the case's own stdout line for the GL error the sample "
"raised before assuming an aspect-mode bug.";
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &fbo);
glDeleteTextures(1, &texture);
glDeleteProgram(stencilProgram);
}
// ------------------------------------------------------------------------------------
// 3. IMAGE UNIT ONLY, across a storage RE-MINT. Green today and green after, and the
// reason it is green is the thing P4a must not lose: glBindImageTexture drives
// RequireImageBindableStorage, which re-mints the storage in a possibly WIDENED carrier
// and sets m_forceTextureParamsResync (Managers.cpp:4787) precisely because the
// frontend's parameter version does not move across that transition. A parameter sync
// gated only on the frontend version would leave the driver at its defaults and sample
// the carrier's surplus channels raw.
// ------------------------------------------------------------------------------------
TEST_F(TextureParamsWithoutASamplerViewScenario,
AnImageUnitOnlyTexturesSwizzleSurvivesARequireImageBindableStorageRemint) {
if (!Ready()) return;
SkipWithoutDirectStateAccess();
if (IsSkipped()) return;
const GLuint texture = MakeSolidTextureWithoutBinding(255, 0, 0);
// The parameter moves while the texture is reachable only as an image-unit binding...
SwizzleRedIntoGreen(texture);
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
const GLenum bindError = FirstGLError();
if (bindError != GLenum(GL_NO_ERROR)) {
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
GTEST_SKIP() << "glBindImageTexture is not usable here (GL error 0x" << std::hex
<< bindError << std::dec
<< "), so the RequireImageBindableStorage transition this case is about "
"cannot be reached";
}
// ...and a frame runs with the image binding live, which is what drives the re-mint.
glViewport(0, 0, Gl().Width(), Gl().Height());
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
Gl().EndFrame();
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
const Image image = SampleAndRead(m_fetchProgram, texture);
EXPECT_TRUE(WholeViewportIs(image, "green",
"a texture that was only ever an image-unit binding, sampled "
"after its swizzle moved and its storage was re-minted"))
<< "the swizzle did not survive the RequireImageBindableStorage re-mint. The re-mint "
"creates a new driver texture at the ES defaults without moving the frontend's "
"parameter version, so the forced resync (Managers.cpp:4787) is the only thing "
"that puts the application's parameters back.";
GLuint cleanup = texture;
glDeleteTextures(1, &cleanup);
}
// ------------------------------------------------------------------------------------
// 4. glCopyImageSubData ENDPOINT ONLY. Green today (MakeGLESCopyImageEndpoint calls
// SyncTextureObjectToBackend, DirectGLES.cpp:7588) and green after. The endpoint is the
// DESTINATION, so the case also proves the copy itself still lands: a swizzled read of
// the copied texel is only meaningful if the texel arrived.
// ------------------------------------------------------------------------------------
TEST_F(TextureParamsWithoutASamplerViewScenario,
ACopyImageEndpointOnlyTexturesParamsReachTheDriver) {
if (!Ready()) return;
SkipWithoutDirectStateAccess();
if (IsSkipped()) return;
const GLuint source = MakeSolidTextureWithoutBinding(255, 0, 0);
const GLuint destination = MakeSolidTextureWithoutBinding(0, 0, 255);
// The parameter moves while the destination is reachable only as a copy endpoint...
SwizzleRedIntoGreen(destination);
glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, destination, GL_TEXTURE_2D, 0, 0, 0,
0, kTextureSize, kTextureSize, 1);
const GLenum copyError = FirstGLError();
if (copyError != GLenum(GL_NO_ERROR)) {
GLuint cleanup[2] = {source, destination};
glDeleteTextures(2, cleanup);
GTEST_SKIP() << "glCopyImageSubData is not usable here (GL error 0x" << std::hex
<< copyError << std::dec << "), so there is no copy endpoint to be";
}
Gl().EndFrame();
// ...and the destination now holds the source's RED texel, which the swizzle must turn
// into GREEN when it is finally sampled.
const Image image = SampleAndRead(m_fetchProgram, destination);
EXPECT_TRUE(WholeViewportIs(image, "green",
"a texture that was only ever a glCopyImageSubData endpoint, "
"sampled after its swizzle moved"))
<< "the three readings are distinct and each names its own cause: GREEN is the pass "
"(the swizzle reached the driver); RED means the copy landed and the swizzle did "
"not; BLUE means neither happened, i.e. the destination is still its own original "
"texel and glCopyImageSubData wrote nothing. The swizzle is what turns any texel "
"into (0,1,0), so the colour separates the two failures rather than merging them.";
GLuint cleanup[2] = {source, destination};
glDeleteTextures(2, cleanup);
}
} // namespace
} // namespace MGITest