Files
MobileGL/MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.cpp
T

909 lines
50 KiB
C++

// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureViewScenario.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
//
// glTextureView (ARB_texture_view / GL 4.6 core 8.18) end to end on both backends.
//
// THE DEFECT. glTextureView was a stub that logged once and returned. That is worse than not
// having the function: MobileGL advertises GL 4.6, so LWJGL resolves a non-null pointer, an
// application's capability check passes, it takes the texture-view path, and the view texture it
// then samples has no storage at all. Nothing errors; the picture is simply wrong. The Better
// Clouds Minecraft mod is exactly this shape - its GLCompat gates `supportsTextureView` on
// `caps.glTextureView != NULL`, which was already true, so it ran its FULL path against a view
// that aliased nothing.
//
// WHAT A VIEW IS, and why a copy cannot stand in for one. A view is a second texture NAME over
// the SAME storage. Two consequences the tests below pin, both of which a copy fails:
// * writes through either name are visible through the other (CoherencyIsBidirectional), and
// * the two names carry INDEPENDENT per-texture parameters at the same time - which is the
// entire point for Better Clouds: one D24S8 image, sampled in ONE shading pass through the
// parent with DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX and through the view with
// GL_DEPTH_COMPONENT (BetterCloudsCoveragePipeline below).
//
// MECHANISM PER BACKEND. DirectVulkan: the view resolves to the storage texture's ONE
// TextureResource - one VkImage, one tracked layout, one upload path - and its own VkImageViews
// (sub-range, reinterpreted VkFormat, its own aspect) are cached in alternateSampledViews /
// attachmentViews keyed by the whole window. DirectGLES: the view gets its own ES name minted by
// EXT/OES_texture_view over the storage texture's name, so the driver supplies the aliasing and
// per-name parameters come for free. Without that extension the frontend refuses glTextureView
// with GL_INVALID_OPERATION and withholds GL_ARB_texture_view rather than emulate by copying -
// see NoExtensionSupportIsRefusedRatherThanFaked.
//
// CONTROLS. Every case here would pass on a stub for at least one of its assertions, so each one
// also asserts something the stub cannot produce: a non-zero sampled value, a DIFFERENT value
// through the two names, or a value that changed after a write through the other name.
#include <array>
#include <cstdint>
#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 kSize = 64;
// The lower strip no cloud quad covers, so coverage 0 / depth 0 is asserted too - a
// uniform image would otherwise pass a test that only ever looked at covered texels.
constexpr int kUncoveredTop = 16;
constexpr const char* kQuadVertexSource = R"(#version 330 core
in vec2 aPos;
uniform vec4 uRect; // x0, y0, x1, y1 in NDC
uniform float uDepth; // NDC z
void main() {
vec2 p = mix(uRect.xy, uRect.zw, aPos);
gl_Position = vec4(p, uDepth, 1.0);
}
)";
// Mirrors betterclouds_coverage.fsh's shape: a second fragment output at location 1 whose
// draw buffer is GL_NONE. The mod declares and writes it while glDrawBuffers names only
// COLOR_ATTACHMENT0, so a layer that mishandles a write to a NONE draw buffer would either
// error or clobber attachment 0.
constexpr const char* kCoverageFragmentSource = R"(#version 330 core
layout (location = 0) out vec4 outColor;
layout (location = 1) out float outUnused;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
outUnused = 1.0 / 255.0;
}
)";
// The Better Clouds shading pass, reduced to its sampling. Both fetches name the SAME
// D24S8 image through two GL texture names bound to two units in this one invocation.
// `ivec2(gl_FragCoord)` (a truncating vec4 -> ivec2 constructor) is the mod's own spelling
// at betterclouds_shading.fsh:56, kept verbatim because a strict GLSL front end can reject
// it; the depth fetch uses the conventional `.xy` form the mod uses at line 117.
constexpr const char* kShadingFragmentSource = R"(#version 330 core
uniform usampler2D uCoverage; // the PARENT, DEPTH_STENCIL_TEXTURE_MODE = GL_STENCIL_INDEX
uniform sampler2D uDepthView; // the VIEW, DEPTH_STENCIL_TEXTURE_MODE = GL_DEPTH_COMPONENT
out vec4 outColor;
void main() {
uint coverage = texelFetch(uCoverage, ivec2(gl_FragCoord), 0).r;
float depth = texelFetch(uDepthView, ivec2(gl_FragCoord.xy), 0).r;
outColor = vec4(float(coverage) * 0.25, depth, 0.0, 1.0);
gl_FragDepth = depth;
}
)";
// Reads a reinterpreting view (GL_R32UI over GL_RGBA8 storage - both VIEW_CLASS_32_BITS)
// and unpacks the word back into the four bytes it was written as.
constexpr const char* kDecodeWordFragmentSource = R"(#version 330 core
uniform usampler2D uWords;
out vec4 outColor;
void main() {
uint word = texelFetch(uWords, ivec2(gl_FragCoord.xy), 0).r;
outColor = vec4(float((word ) & 0xFFu) / 255.0,
float((word >> 8) & 0xFFu) / 255.0,
float((word >> 16) & 0xFFu) / 255.0,
float((word >> 24) & 0xFFu) / 255.0);
}
)";
constexpr const char* kSampleFragmentSource = R"(#version 330 core
uniform sampler2D uTexture;
uniform float uLod;
out vec4 outColor;
void main() {
outColor = textureLod(uTexture, gl_FragCoord.xy / 64.0, uLod);
}
)";
std::string Describe(const Rgba8& c) {
return "rgba(" + std::to_string(c.r) + "," + std::to_string(c.g) + "," + std::to_string(c.b) + "," +
std::to_string(c.a) + ")";
}
class TextureViewScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
if (!TextureViewUsable()) {
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName()
<< " (GL_ARB_texture_view not advertised)";
}
}
void TearDown() override {
if (!Ready()) return;
for (const GLuint texture : m_textures) {
glDeleteTextures(1, &texture);
}
m_textures.clear();
for (const GLuint fbo : m_fbos) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glDeleteFramebuffers(1, &fbo);
}
m_fbos.clear();
for (const GLuint rbo : m_rbos) {
glDeleteRenderbuffers(1, &rbo);
}
m_rbos.clear();
for (const GLuint program : m_programs) {
glDeleteProgram(program);
}
m_programs.clear();
if (m_vao != 0) {
glBindVertexArray(0);
glDeleteVertexArrays(1, &m_vao);
m_vao = 0;
}
if (m_vbo != 0) {
glDeleteBuffers(1, &m_vbo);
m_vbo = 0;
}
}
// A trivial same-format full-range view. It exercises nothing the cases below test,
// so a backend that simply does not have the feature skips instead of failing every
// one of them - the same shape CopyImageLayeredScenario uses for glCopyImageSubData.
bool TextureViewUsable() {
GLuint storage = 0;
glGenTextures(1, &storage);
glBindTexture(GL_TEXTURE_2D, storage);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
glBindTexture(GL_TEXTURE_2D, 0);
GLuint view = 0;
glGenTextures(1, &view);
while (glGetError() != GL_NO_ERROR) {
}
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
const bool usable = glGetError() == GL_NO_ERROR;
glDeleteTextures(1, &view);
glDeleteTextures(1, &storage);
return usable;
}
GLuint MakeVao() {
if (m_vao != 0) return m_vao;
// A unit quad; the vertex shader maps it onto whatever NDC rect uRect names, so
// one buffer serves every draw here.
static constexpr float kQuad[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
return m_vao;
}
GLuint MakeProgram(const char* vertexSource, const char* fragmentSource) {
std::string error;
const GLuint program = CompileProgram(vertexSource, fragmentSource, &error);
EXPECT_NE(program, 0u) << "program failed to build: " << error;
if (program != 0) m_programs.push_back(program);
return program;
}
GLuint MakeTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
GLuint MakeFbo() {
GLuint fbo = 0;
glGenFramebuffers(1, &fbo);
m_fbos.push_back(fbo);
return fbo;
}
// A 2D texture with immutable storage and NEAREST filtering, i.e. what every case
// here views. Levels beyond 1 stay undefined until a caller fills them.
GLuint MakeImmutable2D(GLenum internalFormat, int levels, int width, int height) {
const GLuint texture = MakeTexture();
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, levels, internalFormat, width, height);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
return texture;
}
void DrawQuad(GLuint program, float x0, float y0, float x1, float y1, float depth) {
glUseProgram(program);
glUniform4f(glGetUniformLocation(program, "uRect"), x0, y0, x1, y1);
const GLint depthLocation = glGetUniformLocation(program, "uDepth");
if (depthLocation >= 0) glUniform1f(depthLocation, depth);
glBindVertexArray(MakeVao());
glDrawArrays(GL_TRIANGLES, 0, 6);
}
// Reads the colour texture currently attached to `fbo` as COLOR_ATTACHMENT0.
Image ReadFbo(GLuint fbo, int width, int height) {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glReadBuffer(GL_COLOR_ATTACHMENT0);
return ReadPixels(width, height);
}
// Every pixel of the inclusive region must match `expected` within `tolerance` per
// channel. Whole-region rather than a spot check, for the reason HeadlessGL.h gives:
// three of four vertices carrying stale data still paints a correct centre pixel.
void ExpectRegion(const Image& image, int x0, int x1, int y0, int y1, Rgba8 expected, int tolerance,
const char* what) {
int offenders = 0;
Rgba8 firstOffender{};
int firstX = -1;
int firstY = -1;
for (int y = y0; y <= y1; ++y) {
for (int x = x0; x <= x1; ++x) {
const Rgba8 actual = image.At(x, y);
const bool ok = std::abs(int(actual.r) - int(expected.r)) <= tolerance &&
std::abs(int(actual.g) - int(expected.g)) <= tolerance &&
std::abs(int(actual.b) - int(expected.b)) <= tolerance &&
std::abs(int(actual.a) - int(expected.a)) <= tolerance;
if (!ok) {
if (offenders == 0) {
firstOffender = actual;
firstX = x;
firstY = y;
}
++offenders;
}
}
}
EXPECT_EQ(offenders, 0) << what << ": " << offenders << " of "
<< (x1 - x0 + 1) * (y1 - y0 + 1) << " pixels disagree; first at (" << firstX
<< ", " << firstY << ") is " << Describe(firstOffender) << ", expected "
<< Describe(expected) << " +/- " << tolerance;
}
std::vector<GLuint> m_textures;
std::vector<GLuint> m_fbos;
std::vector<GLuint> m_rbos;
std::vector<GLuint> m_programs;
GLuint m_vao = 0;
GLuint m_vbo = 0;
};
// ------------------------------------------------------------------------------------
// The driving case: the Better Clouds full-mode pipeline, in its real order.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, BetterCloudsCoveragePipeline) {
if (!Ready() || IsSkipped()) return;
// --- Resources.java:230-251, in order ---------------------------------------------
const GLuint coverageColor = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
const GLuint coverage = MakeTexture();
glBindTexture(GL_TEXTURE_2D, coverage);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH24_STENCIL8, kSize, kSize);
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_DEPTH_STENCIL_TEXTURE_MODE, GL_STENCIL_INDEX);
const GLuint coverageFbo = MakeFbo();
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, coverageColor, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, coverage, 0);
const GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0};
glDrawBuffers(1, drawBuffers);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "the coverage framebuffer is incomplete; the mod would silently demote to its "
"fallback configuration here (Resources.java:187-209)";
// The view is made from a name glGenTextures has only RESERVED - it has never been
// bound, so glTextureView has to instantiate the texture object itself.
const GLuint coverageDepthView = MakeTexture();
glTextureView(coverageDepthView, GL_TEXTURE_2D, coverage, GL_DEPTH24_STENCIL8, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "glTextureView raised an error";
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
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_DEPTH_STENCIL_TEXTURE_MODE, GL_DEPTH_COMPONENT);
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "setting up the view raised an error";
// The two names must be distinguishable through the queries, or nothing below proves
// which one produced a sample.
GLint parentMode = 0;
GLint viewMode = 0;
glBindTexture(GL_TEXTURE_2D, coverage);
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &parentMode);
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
glGetTexParameteriv(GL_TEXTURE_2D, GL_DEPTH_STENCIL_TEXTURE_MODE, &viewMode);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(parentMode, GL_STENCIL_INDEX) << "the parent must keep the stencil aspect";
EXPECT_EQ(viewMode, GL_DEPTH_COMPONENT)
<< "the view must carry its OWN depth-stencil mode; sharing one parameter set with "
"the parent is precisely what a texture view exists to avoid";
// --- OpenGLRenderer.java:244-344, the coverage pass -------------------------------
const GLuint coverageProgram = MakeProgram(kQuadVertexSource, kCoverageFragmentSource);
ASSERT_NE(coverageProgram, 0u);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, coverageFbo);
glViewport(0, 0, kSize, kSize);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// Reverse-Z, as the mod runs it (OpenGLRenderer.java:236/241).
glClearDepth(0.0);
glDepthFunc(GL_GEQUAL);
glDisable(GL_BLEND);
glEnable(GL_STENCIL_TEST);
glStencilMask(0xff);
glClearStencil(0);
// The coverage COUNT: one increment per depth-passing cloud fragment.
glStencilOp(GL_KEEP, GL_INCR, GL_INCR);
glStencilFunc(GL_ALWAYS, 0xff, 0xff);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Quad 1 covers everything above the uncovered strip, at window depth 0.25.
const float stripTop = 2.0f * (float(kUncoveredTop) / float(kSize)) - 1.0f;
DrawQuad(coverageProgram, -1.0f, stripTop, 1.0f, 1.0f, -0.5f);
// Quad 2 covers the right half of that, at window depth 0.75 - nearer under GEQUAL,
// so it both passes the depth test and increments the stencil a second time.
DrawQuad(coverageProgram, 0.0f, stripTop, 1.0f, 1.0f, 0.5f);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the coverage pass raised an error";
// --- OpenGLRenderer.java:393-464, the shading pass --------------------------------
// A different draw framebuffer, exactly as the mod does (it hands the frame back to
// Blaze3D before shading). The coverage texture stays ATTACHED to coverageFbo while
// being sampled here, which is the shape a lazy/deferred FBO binding gets wrong.
ColorFbo destination = MakeColorFbo(kSize, kSize);
ASSERT_NE(destination.fbo, 0u);
GLuint destinationDepth = 0;
glGenRenderbuffers(1, &destinationDepth);
m_rbos.push_back(destinationDepth);
glBindRenderbuffer(GL_RENDERBUFFER, destinationDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSize, kSize);
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, destinationDepth);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClearDepth(0.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDepthFunc(GL_GEQUAL);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
// The mod's own indexed/non-indexed colour-mask pair (OpenGLRenderer.java:411-412).
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glColorMaski(0, GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
const GLuint shadingProgram = MakeProgram(kQuadVertexSource, kShadingFragmentSource);
ASSERT_NE(shadingProgram, 0u);
glUseProgram(shadingProgram);
// Unit 1 = the view (depth aspect), unit 3 = the parent (stencil aspect), the mod's
// own unit assignment (Resources.java:309/311).
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, coverageDepthView);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, coverage);
glUniform1i(glGetUniformLocation(shadingProgram, "uDepthView"), 1);
glUniform1i(glGetUniformLocation(shadingProgram, "uCoverage"), 3);
glActiveTexture(GL_TEXTURE0);
DrawQuad(shadingProgram, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the shading pass raised an error";
const Image shaded = ReadFbo(destination.fbo, kSize, kSize);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
// R = coverage * 0.25 (so 1 -> 64, 2 -> 128), G = the depth read THROUGH THE VIEW.
// A stub view samples (0,0,0,1), which fails the green channel of both covered
// regions; a view that inherited the parent's stencil aspect fails them too.
constexpr int kTolerance = 3;
ExpectRegion(shaded, 1, kSize - 2, 1, kUncoveredTop - 2, Rgba8{0, 0, 0, 255}, kTolerance,
"the uncovered strip must read coverage 0 and cleared depth 0");
ExpectRegion(shaded, 1, kSize / 2 - 2, kUncoveredTop + 1, kSize - 2, Rgba8{64, 64, 0, 255}, kTolerance,
"one cloud quad: stencil 1 through the parent, window depth 0.25 through the view");
ExpectRegion(shaded, kSize / 2 + 1, kSize - 2, kUncoveredTop + 1, kSize - 2, Rgba8{128, 191, 0, 255},
kTolerance,
"two overlapping cloud quads: stencil 2 through the parent, window depth 0.75 "
"through the view");
DestroyColorFbo(destination);
}
// ------------------------------------------------------------------------------------
// Storage sharing, in both directions. This is the assertion a copy-based emulation
// fails, and the reason the no-EXT path refuses rather than emulates.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, CoherencyIsBidirectional) {
if (!Ready() || IsSkipped()) return;
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// Render red through the PARENT's name...
const GLuint fbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// ...and read it back through the VIEW's.
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "a texture view must be attachable like any other texture";
Image throughView = ReadFbo(viewFbo, kSize, kSize);
ExpectRegion(throughView, 0, kSize - 1, 0, kSize - 1, Rgba8{255, 0, 0, 255}, 1,
"a write through the parent must be visible through the view");
// Now the other direction: write green through the VIEW, read through the PARENT.
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const Image throughParent = ReadFbo(fbo, kSize, kSize);
ExpectRegion(throughParent, 0, kSize - 1, 0, kSize - 1, Rgba8{0, 255, 0, 255}, 1,
"a write through the view must be visible through the parent - they are one "
"storage, not two");
}
// ------------------------------------------------------------------------------------
// Format reinterpretation within a view class (GL 4.6 core table 8.21).
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, ReinterpretingViewReadsTheSameBitsThroughAnotherFormat) {
if (!Ready() || IsSkipped()) return;
// GL_RGBA8 and GL_R32UI are both VIEW_CLASS_32_BITS, so one may be viewed as the
// other. Filling the RGBA8 storage with a known byte pattern makes the R32UI view's
// answer a fact about the BITS rather than about the colour.
const GLuint storage = MakeImmutable2D(GL_RGBA8, 1, kSize, kSize);
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i + 0] = 0x40;
texels[i + 1] = 0x80;
texels[i + 2] = 0xC0;
texels[i + 3] = 0xFF;
}
glBindTexture(GL_TEXTURE_2D, storage);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the storage raised an error";
// NEGATIVE CONTROL. Everything below reads the storage through a REINTERPRETING view,
// so a test that only asserted the view's answer could not tell "the reinterpret is
// wrong" from "the seed never reached the GPU at all". Read the same texels through
// the parent's own format first.
const GLuint parentFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, parentFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image seeded = ReadFbo(parentFbo, kSize, kSize);
ExpectRegion(seeded, 0, kSize - 1, 0, kSize - 1, Rgba8{0x40, 0x80, 0xC0, 0xFF}, 1,
"control: the storage must hold the seeded byte pattern before any view reads it");
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_R32UI, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "an in-class reinterpret must be accepted";
glBindTexture(GL_TEXTURE_2D, view);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
GLint viewFormat = 0;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &viewFormat);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(viewFormat, GL_R32UI) << "the view must report its OWN internal format";
// A REAL GL_R32UI texture holding the very word the storage's bytes spell. The
// assertion below is that the view and this texture sample IDENTICALLY.
//
// Comparing against a reference texture rather than against a hard-coded colour is
// deliberate. Sampling a 32-bit integer texture is not itself what this scenario is
// about, and llvmpipe's ES driver does it inconsistently (verified outside MobileGL,
// with a raw-EGL program that reproduces the same wrong decode with NO view in play).
// Holding both sides to the same driver factors that out completely: whatever the
// driver makes of a usampler2D fetch, the view has to make the same thing of it, or
// it is not delivering the storage's bits. A view that samples zero, that lands on
// the wrong texels, or that lost its format still fails.
constexpr std::uint32_t kExpectedWord = 0xFFC08040u; // little-endian A,B,G,R
const GLuint reference = MakeImmutable2D(GL_R32UI, 1, kSize, kSize);
std::vector<std::uint32_t> words(static_cast<std::size_t>(kSize) * kSize, kExpectedWord);
glBindTexture(GL_TEXTURE_2D, reference);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kSize, kSize, GL_RED_INTEGER, GL_UNSIGNED_INT, words.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the reference texture failed";
const GLuint program = MakeProgram(kQuadVertexSource, kDecodeWordFragmentSource);
ASSERT_NE(program, 0u);
ColorFbo destination = MakeColorFbo(kSize, kSize);
ASSERT_NE(destination.fbo, 0u);
const auto decodeThrough = [&](GLuint texture) {
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(program, "uWords"), 0);
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
EXPECT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "sampling raised an error";
return ReadFbo(destination.fbo, kSize, kSize);
};
const Image throughReference = decodeThrough(reference);
const Image throughView = decodeThrough(view);
// Guard against the degenerate agreement of two black images: the reference must
// itself carry something, or "identical" would prove nothing.
const Rgba8 referenceTexel = throughReference.At(kSize / 2, kSize / 2);
ASSERT_FALSE(referenceTexel == (Rgba8{0, 0, 0, 0}))
<< "the reference GL_R32UI texture sampled as nothing, so the comparison below is vacuous";
std::size_t mismatches = 0;
for (int y = 0; y < kSize; ++y) {
for (int x = 0; x < kSize; ++x) {
if (!(throughView.At(x, y) == throughReference.At(x, y))) ++mismatches;
}
}
EXPECT_EQ(mismatches, 0u)
<< "the GL_R32UI view of GL_RGBA8 storage must sample exactly what a real GL_R32UI texture "
"holding the same word does; view centre is " << Describe(throughView.At(kSize / 2, kSize / 2))
<< ", reference centre is " << Describe(referenceTexel);
DestroyColorFbo(destination);
}
// ------------------------------------------------------------------------------------
// Sub-ranges: one mip level of two, and one layer of an array.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, ViewOfOneMipLevelAddressesThatLevelAsItsOwnLevelZero) {
if (!Ready() || IsSkipped()) return;
const GLuint storage = MakeImmutable2D(GL_RGBA8, 2, kSize, kSize);
// Level 0 red, level 1 blue, so the view's answer names the level it opened onto.
const GLuint seedFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
glViewport(0, 0, kSize, kSize);
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 1);
glViewport(0, 0, kSize / 2, kSize / 2);
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the mip chain raised an error";
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
GLint minLevel = -1;
GLint numLevels = -1;
GLint immutableLevels = -1;
glBindTexture(GL_TEXTURE_2D, view);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_IMMUTABLE_LEVELS, &immutableLevels);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(minLevel, 1);
EXPECT_EQ(numLevels, 1);
// GL 4.6 core 8.18: inherited from the ORIGINAL, not set to <numlevels>.
EXPECT_EQ(immutableLevels, 2) << "TEXTURE_IMMUTABLE_LEVELS is the original texture's value";
// The view's level 0 IS the parent's level 1: attaching level 0 of the view must find
// the blue half-size image.
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image levelOne = ReadFbo(viewFbo, kSize / 2, kSize / 2);
ExpectRegion(levelOne, 0, kSize / 2 - 1, 0, kSize / 2 - 1, Rgba8{0, 0, 255, 255}, 1,
"the view's level 0 must be the parent's level 1 (blue), not its level 0 (red)");
}
TEST_F(TextureViewScenario, ViewOfOneArrayLayerAddressesThatLayer) {
if (!Ready() || IsSkipped()) return;
constexpr int kLayers = 4;
constexpr int kChosenLayer = 2;
const GLuint storage = MakeTexture();
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// A different colour per layer, so a view that lost its layer offset reads the wrong
// one rather than merely reading nothing.
const GLuint seedFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glViewport(0, 0, kSize, kSize);
for (int layer = 0; layer < kLayers; ++layer) {
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
glClearColor(float(layer) / 8.0f, 1.0f - float(layer) / 8.0f, 0.5f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the array layers raised an error";
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, kChosenLayer, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "2D_ARRAY -> 2D is a legal view pair";
GLint minLayer = -1;
GLint numLayers = -1;
glBindTexture(GL_TEXTURE_2D, view);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LAYER, &minLayer);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LAYERS, &numLayers);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(minLayer, kChosenLayer);
EXPECT_EQ(numLayers, 1);
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, view, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image sliced = ReadFbo(viewFbo, kSize, kSize);
const Rgba8 expected{static_cast<std::uint8_t>(kChosenLayer * 255 / 8),
static_cast<std::uint8_t>(255 - kChosenLayer * 255 / 8), 128, 255};
ExpectRegion(sliced, 0, kSize - 1, 0, kSize - 1, expected, 2,
"a single-layer 2D view of an array must address the layer it named");
}
// ------------------------------------------------------------------------------------
// Writing THROUGH a layer-sliced view. The read direction is covered above; this is the
// write direction, and it is the one that can corrupt the parent rather than merely
// return the wrong pixels - a view whose texel path forgot its layer origin writes over
// the parent's layer 0 while the application believes it addressed layer minLayer.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, WritingThroughALayerSlicedViewLandsOnItsOwnLayers) {
if (!Ready() || IsSkipped()) return;
constexpr int kLayers = 4;
constexpr int kViewMinLayer = 2;
const GLuint storage = MakeTexture();
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kSize, kSize, kLayers);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const auto layerFill = [](int layer) {
return Rgba8{static_cast<std::uint8_t>(10 + layer * 20),
static_cast<std::uint8_t>(200 - layer * 20), 30, 255};
};
// Seeded by CPU sub-image rather than by rendering, deliberately: this scenario is
// about the view's LAYER ORIGIN, and seeding through the GPU would additionally
// depend on a CPU sub-image reaching a layer whose content the GPU wrote - which
// DirectVulkan does not currently do even for a plain array texture (no view
// involved), and which would make a failure here unattributable.
const auto uploadLayer = [&](GLuint texture, int layer, Rgba8 colour) {
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i + 0] = colour.r;
texels[i + 1] = colour.g;
texels[i + 2] = colour.b;
texels[i + 3] = colour.a;
}
glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, layer, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
texels.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
};
for (int layer = 0; layer < kLayers; ++layer) {
uploadLayer(storage, layer, layerFill(layer));
}
const GLuint fbo = MakeFbo();
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "seeding the layers raised an error";
// A two-layer window starting at layer 2, so a lost offset lands on layer 0 - which
// the assertions below would see as an untouched layer that moved.
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D_ARRAY, storage, GL_RGBA8, 0, 1, kViewMinLayer, 2);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// Write the view's OWN layer 0, i.e. the storage's layer 2.
constexpr Rgba8 kPainted{255, 0, 255, 255};
std::vector<std::uint8_t> texels(static_cast<std::size_t>(kSize) * kSize * 4);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i + 0] = kPainted.r;
texels[i + 1] = kPainted.g;
texels[i + 2] = kPainted.b;
texels[i + 3] = kPainted.a;
}
glBindTexture(GL_TEXTURE_2D_ARRAY, view);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
texels.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "writing through the view raised an error";
// POSITIVE CONTROL, through the parent's own name and into a layer outside the view's
// window. It makes the assertions below able to tell "the view lost its layer origin"
// from "a CPU sub-image into this array does not reach the GPU at all", which is a
// different question and not one a texture view can answer.
constexpr Rgba8 kControl{0, 0, 255, 255};
std::vector<std::uint8_t> controlTexels(texels.size());
for (std::size_t i = 0; i < controlTexels.size(); i += 4) {
controlTexels[i + 0] = kControl.r;
controlTexels[i + 1] = kControl.g;
controlTexels[i + 2] = kControl.b;
controlTexels[i + 3] = kControl.a;
}
glBindTexture(GL_TEXTURE_2D_ARRAY, storage);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 1, kSize, kSize, 1, GL_RGBA, GL_UNSIGNED_BYTE,
controlTexels.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "the control write raised an error";
// Read every layer of the PARENT back: only the one the view's layer 0 maps to may
// have changed.
for (int layer = 0; layer < kLayers; ++layer) {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, storage, 0, layer);
glReadBuffer(GL_COLOR_ATTACHMENT0);
const Image image = ReadPixels(kSize, kSize);
Rgba8 expected = layerFill(layer);
const char* what = "a layer outside the view's window must not have been written";
if (layer == kViewMinLayer) {
expected = kPainted;
what = "the view's layer 0 must be the storage layer it named";
} else if (layer == 1) {
expected = kControl;
what = "control: a sub-image written through the PARENT must reach its layer";
}
ExpectRegion(image, 0, kSize - 1, 0, kSize - 1, expected, 2, what);
}
}
// ------------------------------------------------------------------------------------
// Views of views compose; the composed view still reaches the ROOT storage.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, ViewOfAViewComposesTheLevelRanges) {
if (!Ready() || IsSkipped()) return;
constexpr int kLevels = 3;
const GLuint storage = MakeImmutable2D(GL_RGBA8, kLevels, kSize, kSize);
const GLuint seedFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, seedFbo);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
for (int level = 0; level < kLevels; ++level) {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, level);
glViewport(0, 0, kSize >> level, kSize >> level);
glClearColor(0.0f, 0.0f, float(level + 1) / 4.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// First view opens onto levels [1, 3); the second takes level 1 OF THAT, which is the
// root's level 2. GL 4.6 core 8.18 makes the offsets add.
const GLuint firstView = MakeTexture();
glTextureView(firstView, GL_TEXTURE_2D, storage, GL_RGBA8, 1, 2, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const GLuint secondView = MakeTexture();
glTextureView(secondView, GL_TEXTURE_2D, firstView, GL_RGBA8, 1, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR)) << "origtexture may itself be a view";
GLint minLevel = -1;
GLint numLevels = -1;
glBindTexture(GL_TEXTURE_2D, secondView);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_MIN_LEVEL, &minLevel);
glGetTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_VIEW_NUM_LEVELS, &numLevels);
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(minLevel, 2) << "TEXTURE_VIEW_MIN_LEVEL adds the original's";
EXPECT_EQ(numLevels, 1);
const GLuint viewFbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, viewFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, secondView, 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const Image composed = ReadFbo(viewFbo, kSize >> 2, kSize >> 2);
ExpectRegion(composed, 0, (kSize >> 2) - 1, 0, (kSize >> 2) - 1, Rgba8{0, 0, 191, 255}, 2,
"the composed view must land on the root's level 2");
}
// ------------------------------------------------------------------------------------
// GL name-deletion semantics: the storage outlives the original's NAME.
// ------------------------------------------------------------------------------------
TEST_F(TextureViewScenario, DeletingTheOriginalKeepsTheViewUsable) {
if (!Ready() || IsSkipped()) return;
GLuint storage = 0;
glGenTextures(1, &storage);
glBindTexture(GL_TEXTURE_2D, storage);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kSize, kSize);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
const GLuint view = MakeTexture();
glTextureView(view, GL_TEXTURE_2D, storage, GL_RGBA8, 0, 1, 0, 1);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const GLuint fbo = MakeFbo();
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, storage, 0);
glViewport(0, 0, kSize, kSize);
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glClearColor(0.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// The NAME goes; the storage may not, because a view still references it
// (GL 4.6 core 5.1.2 - an object is not deleted while anything still refers to it).
glDeleteTextures(1, &storage);
EXPECT_EQ(glIsTexture(storage), static_cast<GLboolean>(GL_FALSE));
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
// Sample the view through a shader, so the answer comes from a live descriptor rather
// than from an attachment the frontend might have kept alive by other means.
ColorFbo destination = MakeColorFbo(kSize, kSize);
ASSERT_NE(destination.fbo, 0u);
glBindFramebuffer(GL_FRAMEBUFFER, destination.fbo);
glViewport(0, 0, kSize, kSize);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
const GLuint program = MakeProgram(kQuadVertexSource, kSampleFragmentSource);
ASSERT_NE(program, 0u);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, view);
glUniform1i(glGetUniformLocation(program, "uTexture"), 0);
glUniform1f(glGetUniformLocation(program, "uLod"), 0.0f);
DrawQuad(program, -1.0f, -1.0f, 1.0f, 1.0f, 0.0f);
ASSERT_EQ(glGetError(), static_cast<GLenum>(GL_NO_ERROR));
const Image sampled = ReadFbo(destination.fbo, kSize, kSize);
ExpectRegion(sampled, 1, kSize - 2, 1, kSize - 2, Rgba8{0, 255, 255, 255}, 2,
"the view must still reach its storage after the original's name was deleted");
DestroyColorFbo(destination);
}
} // namespace
} // namespace MGITest