mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
1134 lines
63 KiB
C++
1134 lines
63 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/LayeredAttachmentShapeScenario.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 - THE ATTACHMENT SHAPES A LAYERED FRAMEBUFFER CAN TAKE, AND THE ONE VIEW TYPE
|
|
// VULKAN ACCEPTS FOR ALL OF THEM.
|
|
//
|
|
// glFramebufferTexture on a GL_TEXTURE_3D or a GL_TEXTURE_CUBE_MAP_ARRAY makes a LAYERED
|
|
// framebuffer: one attachment that covers every slice / layer-face, addressed by a geometry
|
|
// shader writing gl_Layer. Vulkan has exactly one legal spelling for that
|
|
// (VUID-VkFramebufferCreateInfo-flags-04113: an attachment view must be VK_IMAGE_VIEW_TYPE_2D
|
|
// or _2D_ARRAY), and DirectVulkan used to hand vkCreateFramebuffer the IMAGE's own view type
|
|
// instead:
|
|
//
|
|
// * GL_TEXTURE_3D -> VK_IMAGE_VIEW_TYPE_3D. A 3D image has arrayLayers == 1 and keeps its
|
|
// layers on z, so the layer-span guard measured [0, depth) against 1, refused, and returned
|
|
// VK_NULL_HANDLE - which then went into pAttachments as a null handle.
|
|
// * GL_TEXTURE_CUBE_MAP_ARRAY -> VK_IMAGE_VIEW_TYPE_CUBE_ARRAY. A perfectly valid view, of a
|
|
// type no framebuffer may take. The driver dereferenced or rejected it inside
|
|
// vkCreateFramebuffer.
|
|
//
|
|
// Both exits were guarded only by MOBILEGL_ASSERT, which an INFO build (the production and CTS
|
|
// default) compiles to nothing - so both were process kills, not wrong pixels: 51 lost QPA
|
|
// records over 7 conformance bodies, one runner restart each.
|
|
//
|
|
// The same function is what routes a NON-layered slice of a 3D texture
|
|
// (glFramebufferTextureLayer), and it had the mirror-image hole: it asked for a 3D view there
|
|
// too, so the per-slice branch that exists for exactly this case was unreachable and every
|
|
// slice above z = 0 came back VK_NULL_HANDLE.
|
|
//
|
|
// The first seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array
|
|
// with its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and
|
|
// 1D ARRAY whose queued glClear is consumed outside a render pass. Each one asserts LAYER ROUTING,
|
|
// not merely survival: what a layer receives is a function of its own index, so an attachment that
|
|
// collapsed onto layer 0, or attached one face of a cube, fails on the layers it did not reach
|
|
// rather than passing quietly. Every texture is seeded with a poison value first, so "the draw
|
|
// never landed here" reads differently from "the wrong layer landed here".
|
|
//
|
|
// Case (8) is the same collapse one step downstream, and case (6) is what found it: the READBACK
|
|
// of a cube map ignored the face it was asked for and answered +X for all six. Every case here
|
|
// that reads a layered target back depends on the readback addressing the layer it names, so it
|
|
// belongs beside them - and case (6) had to be written around it, which is the strongest argument
|
|
// there is that it was never pinned.
|
|
//
|
|
// One of them turned out not to be a DirectVulkan bug at all. glFramebufferTexture on
|
|
// GL_DEPTH_STENCIL_ATTACHMENT is a shorthand the front end splits into a depth and a stencil
|
|
// attachment, and the split dropped the call's `layered` flag - so a layered colour attachment
|
|
// sat beside a non-layered depth/stencil one and BOTH backends silently lost the draw. That is
|
|
// the shape texture_cube_map_array.stencil_attachments_*_layered and
|
|
// geometry_shader.layered_framebuffer.stencil_support are built on, and it is why they fail on
|
|
// Espryt as well as crashing on Magma. Case (5) is what found it.
|
|
//
|
|
// DirectGLES is the control: it hands the same GL calls to the driver, so a red on both backends
|
|
// means the scenario is wrong - or the defect is in the shared front end, as it was above - and a
|
|
// red on DirectVulkan alone means Magma is.
|
|
|
|
#include <cstddef>
|
|
#include <cstdlib>
|
|
#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 kExtent = 4;
|
|
// Four z slices: enough that "only slice 0 was written" and "the whole thing was written"
|
|
// are different answers, and small enough that the geometry shader stays well inside
|
|
// GL_MAX_GEOMETRY_OUTPUT_VERTICES.
|
|
constexpr int k3DSlices = 4;
|
|
// Two cubes. One cube would let "attached a single cube" pass; twelve layer-faces would
|
|
// not.
|
|
constexpr int kCubeLayerFaces = 12;
|
|
// The slice a non-layered 3D attachment names. Not 0: slice 0 is the one address that is
|
|
// right whether or not the slice is resolved at all.
|
|
constexpr int kSubjectSlice = 2;
|
|
|
|
// Layers of the 1D array whose clear the last case checks. Its layer count lives in the
|
|
// state-side HEIGHT, not in z, which is the whole reason it is here.
|
|
constexpr int kOneDArrayLayers = 4;
|
|
|
|
// A colour no pass paints, uploaded before every draw. A layer that reads it back was
|
|
// never rendered to.
|
|
constexpr GLubyte kPoison = 0xAB;
|
|
|
|
// The glClear colour the two materialise cases use. Chosen as exact 8-bit values and fed
|
|
// to glClearColor as n/255, so the round trip through a UNORM8 target is lossless and a
|
|
// mismatch means a real miss rather than rounding.
|
|
constexpr Rgba8 kClearColor{17, 68, 187, 255};
|
|
|
|
// The six cube faces in the order GL numbers them, which is also the order Vulkan keeps
|
|
// them in as array layers (GL 4.6 core 8.5.3 / VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT).
|
|
const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
|
|
|
|
// What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write
|
|
// says which layer it came from) and b names the PASS (so "the second draw was not
|
|
// rejected" is distinguishable from "the first draw never happened").
|
|
Rgba8 ExpectedColor(int layer, int pass) {
|
|
return {static_cast<GLubyte>(10 + layer * 20), static_cast<GLubyte>(200 - layer * 10),
|
|
static_cast<GLubyte>(3 + pass * 60), 255};
|
|
}
|
|
|
|
std::string Describe(const Rgba8& color) {
|
|
return "(" + std::to_string(color.r) + ", " + std::to_string(color.g) + ", " +
|
|
std::to_string(color.b) + ", " + std::to_string(color.a) + ")";
|
|
}
|
|
|
|
// A full-viewport triangle built from gl_VertexID, so nothing here needs a vertex buffer
|
|
// and the draw cannot fail for a reason that has nothing to do with the attachment.
|
|
// u_depth is the NDC z the whole primitive sits at - the depth/stencil case needs two
|
|
// different ones.
|
|
const char* const kVertexSource = R"(#version 420 core
|
|
uniform float u_depth;
|
|
void main()
|
|
{
|
|
vec2 corner = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);
|
|
gl_Position = vec4(corner, u_depth, 1.0);
|
|
}
|
|
)";
|
|
|
|
// The layer count is baked in as a literal rather than passed as a uniform: a
|
|
// non-constant loop bound in a geometry shader is legal but is one more thing the
|
|
// ESSL transpile could get wrong, and this scenario is not about that.
|
|
std::string MakeGeometrySource(int layerCount) {
|
|
return "#version 420 core\n"
|
|
"layout(triangles) in;\n"
|
|
"layout(triangle_strip, max_vertices = " +
|
|
std::to_string(layerCount * 3) +
|
|
") out;\n"
|
|
"flat out int v_layer;\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" for (int layer = 0; layer < " +
|
|
std::to_string(layerCount) +
|
|
"; ++layer) {\n"
|
|
" for (int i = 0; i < 3; ++i) {\n"
|
|
" gl_Layer = layer;\n"
|
|
" v_layer = layer;\n"
|
|
" gl_Position = gl_in[i].gl_Position;\n"
|
|
" EmitVertex();\n"
|
|
" }\n"
|
|
" EndPrimitive();\n"
|
|
" }\n"
|
|
"}\n";
|
|
}
|
|
|
|
const char* const kLayeredFragmentSource = R"(#version 420 core
|
|
flat in int v_layer;
|
|
uniform int u_pass;
|
|
out vec4 o_color;
|
|
void main()
|
|
{
|
|
o_color = vec4(float(10 + v_layer * 20) / 255.0,
|
|
float(200 - v_layer * 10) / 255.0,
|
|
float(3 + u_pass * 60) / 255.0,
|
|
1.0);
|
|
}
|
|
)";
|
|
|
|
// The two clear cases do not draw into the layered attachment at all - they SAMPLE it, so
|
|
// the queued clear is consumed by MaterializePendingClearForTexture rather than by a render
|
|
// pass's LOAD_OP_CLEAR. What the sample returns is irrelevant; being sampled is the point.
|
|
const char* const kCubeSampleFragmentSource = R"(#version 420 core
|
|
uniform samplerCube u_source;
|
|
out vec4 o_color;
|
|
void main() { o_color = texture(u_source, vec3(1.0, 0.0, 0.0)); }
|
|
)";
|
|
|
|
const char* const kOneDArraySampleFragmentSource = R"(#version 420 core
|
|
uniform sampler1DArray u_source;
|
|
out vec4 o_color;
|
|
void main() { o_color = texture(u_source, vec2(0.5, 0.0)); }
|
|
)";
|
|
|
|
// The non-layered case has no geometry stage at all - the slice comes from the
|
|
// attachment, not from gl_Layer - so it names its layer through a uniform.
|
|
const char* const kFlatFragmentSource = R"(#version 420 core
|
|
uniform int u_layer;
|
|
uniform int u_pass;
|
|
out vec4 o_color;
|
|
void main()
|
|
{
|
|
o_color = vec4(float(10 + u_layer * 20) / 255.0,
|
|
float(200 - u_layer * 10) / 255.0,
|
|
float(3 + u_pass * 60) / 255.0,
|
|
1.0);
|
|
}
|
|
)";
|
|
|
|
class LayeredAttachmentShapeScenario : public ScenarioTest {
|
|
protected:
|
|
void SetUp() override {
|
|
ScenarioTest::SetUp();
|
|
if (!Ready()) return;
|
|
glGenVertexArrays(1, &m_vao);
|
|
glBindVertexArray(m_vao);
|
|
DrainErrors();
|
|
}
|
|
|
|
void TearDown() override {
|
|
if (!Ready()) return;
|
|
glUseProgram(0);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
for (const GLuint fbo : m_fbos) glDeleteFramebuffers(1, &fbo);
|
|
m_fbos.clear();
|
|
for (const GLuint texture : m_textures) glDeleteTextures(1, &texture);
|
|
m_textures.clear();
|
|
for (const GLuint program : m_programs) glDeleteProgram(program);
|
|
m_programs.clear();
|
|
glBindVertexArray(0);
|
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
|
m_vao = 0;
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
DrainErrors();
|
|
}
|
|
|
|
static void DrainErrors() {
|
|
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
|
|
}
|
|
}
|
|
|
|
// 0 on a DirectGLES driver without GL_EXT_geometry_shader and on a DirectVulkan
|
|
// device without the geometryShader feature. The same probe GeometryDrawModeScenario
|
|
// and IoBlockNameCollisionScenario use.
|
|
static bool BackendHostsGeometry() {
|
|
GLint maxGeometryOutputVertices = 0;
|
|
glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeometryOutputVertices);
|
|
DrainErrors();
|
|
return maxGeometryOutputVertices >= kCubeLayerFaces * 3;
|
|
}
|
|
|
|
static std::string InfoLog(GLuint object, bool isShader) {
|
|
GLint length = 0;
|
|
if (isShader) {
|
|
glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
} else {
|
|
glGetProgramiv(object, GL_INFO_LOG_LENGTH, &length);
|
|
}
|
|
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
|
if (isShader) {
|
|
glGetShaderInfoLog(object, length + 1, nullptr, buffer.data());
|
|
} else {
|
|
glGetProgramInfoLog(object, length + 1, nullptr, buffer.data());
|
|
}
|
|
return buffer.data();
|
|
}
|
|
|
|
// geometrySource may be null, which builds the no-geometry-stage program the
|
|
// non-layered case uses.
|
|
GLuint BuildProgram(const char* geometrySource, const char* fragmentSource) {
|
|
std::vector<GLuint> shaders;
|
|
const auto compile = [&](GLenum stage, const char* source) {
|
|
const GLuint shader = glCreateShader(stage);
|
|
glShaderSource(shader, 1, &source, nullptr);
|
|
glCompileShader(shader);
|
|
GLint compiled = 0;
|
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
|
|
shaders.push_back(shader);
|
|
if (compiled == GL_FALSE) {
|
|
ADD_FAILURE() << "stage 0x" << std::hex << stage << std::dec
|
|
<< " did not compile: " << InfoLog(shader, true);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
bool ok = compile(GL_VERTEX_SHADER, kVertexSource);
|
|
if (ok && geometrySource != nullptr) ok = compile(GL_GEOMETRY_SHADER, geometrySource);
|
|
if (ok) ok = compile(GL_FRAGMENT_SHADER, fragmentSource);
|
|
if (!ok) {
|
|
for (const GLuint shader : shaders) glDeleteShader(shader);
|
|
return 0;
|
|
}
|
|
|
|
const GLuint program = glCreateProgram();
|
|
for (const GLuint shader : shaders) glAttachShader(program, shader);
|
|
glLinkProgram(program);
|
|
for (const GLuint shader : shaders) glDeleteShader(shader);
|
|
GLint linked = 0;
|
|
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
|
if (linked == GL_FALSE) {
|
|
ADD_FAILURE() << "the program did not link: " << InfoLog(program, false);
|
|
glDeleteProgram(program);
|
|
return 0;
|
|
}
|
|
m_programs.push_back(program);
|
|
return program;
|
|
}
|
|
|
|
GLuint TrackTexture() {
|
|
GLuint texture = 0;
|
|
glGenTextures(1, &texture);
|
|
m_textures.push_back(texture);
|
|
return texture;
|
|
}
|
|
|
|
GLuint TrackFramebuffer() {
|
|
GLuint fbo = 0;
|
|
glGenFramebuffers(1, &fbo);
|
|
m_fbos.push_back(fbo);
|
|
return fbo;
|
|
}
|
|
|
|
// An RGBA8 3D texture, every texel poisoned.
|
|
GLuint MakePoisoned3DColor() {
|
|
const GLuint texture = TrackTexture();
|
|
glBindTexture(GL_TEXTURE_3D, texture);
|
|
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexStorage3D(GL_TEXTURE_3D, 1, GL_RGBA8, kExtent, kExtent, k3DSlices);
|
|
const std::vector<GLubyte> seed(
|
|
static_cast<std::size_t>(kExtent) * kExtent * k3DSlices * 4, kPoison);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, kExtent, kExtent, k3DSlices, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, seed.data());
|
|
glBindTexture(GL_TEXTURE_3D, 0);
|
|
return texture;
|
|
}
|
|
|
|
// An RGBA8 cube-map array of kCubeLayerFaces layer-faces, every texel poisoned.
|
|
GLuint MakePoisonedCubeArrayColor() {
|
|
const GLuint texture = TrackTexture();
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, GL_RGBA8, kExtent, kExtent, kCubeLayerFaces);
|
|
const std::vector<GLubyte> seed(
|
|
static_cast<std::size_t>(kExtent) * kExtent * kCubeLayerFaces * 4, kPoison);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexSubImage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 0, 0, 0, 0, kExtent, kExtent, kCubeLayerFaces,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0);
|
|
return texture;
|
|
}
|
|
|
|
// A plain RGBA8 CUBE MAP (not an array), every face poisoned. This is the shape whose
|
|
// layered attachment records the +X face as its representative upload target, so its
|
|
// level size reads z = 1 - the reason a shared layer-count helper is needed at all.
|
|
GLuint MakePoisonedCubeMap() {
|
|
const GLuint texture = TrackTexture();
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent);
|
|
const std::vector<GLubyte> seed(static_cast<std::size_t>(kExtent) * kExtent * 4, kPoison);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
for (int face = 0; face < 6; ++face) {
|
|
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
|
kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
|
|
}
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
|
return texture;
|
|
}
|
|
|
|
// A cube map whose six faces are UPLOADED with their own colours - the same
|
|
// ExpectedColor(face, 0) the painted cube of case (8) ends up holding, so both can be
|
|
// checked with one expectation. Uploaded rather than rendered means the CPU shadow and
|
|
// the image agree, which is the premise the BY-NAME readback needs; see case (8).
|
|
GLuint MakeFaceColoredCubeMap() {
|
|
const GLuint texture = TrackTexture();
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RGBA8, kExtent, kExtent);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
for (int face = 0; face < 6; ++face) {
|
|
const std::vector<Rgba8> seed(static_cast<std::size_t>(kExtent) * kExtent,
|
|
ExpectedColor(face, 0));
|
|
glTexSubImage2D(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, 0, 0, kExtent,
|
|
kExtent, GL_RGBA, GL_UNSIGNED_BYTE, seed.data());
|
|
}
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
|
return texture;
|
|
}
|
|
|
|
// An RGBA8 1D array, every layer poisoned. glTexImage2D's HEIGHT is the layer count -
|
|
// that is what GL_TEXTURE_1D_ARRAY means, and it is why reading the level size's z
|
|
// gives 1 however many layers there are.
|
|
GLuint MakePoisoned1DArray() {
|
|
const GLuint texture = TrackTexture();
|
|
glBindTexture(GL_TEXTURE_1D_ARRAY, texture);
|
|
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
const std::vector<GLubyte> seed(static_cast<std::size_t>(kExtent) * kOneDArrayLayers * 4, kPoison);
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
|
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kExtent, kOneDArrayLayers, 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, seed.data());
|
|
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
|
return texture;
|
|
}
|
|
|
|
// A scratch 2D colour target for the sampling draw. It exists only so the draw has
|
|
// somewhere to go that is NOT the layered attachment under test - a draw into that
|
|
// would open a render pass and consume the pending clear through LOAD_OP_CLEAR, which
|
|
// is the other consumer and the one that was already right.
|
|
GLuint MakeScratchColorFbo() {
|
|
const GLuint scratch = TrackTexture();
|
|
glBindTexture(GL_TEXTURE_2D, scratch);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kExtent, kExtent);
|
|
glBindTexture(GL_TEXTURE_2D, 0);
|
|
const GLuint fbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, scratch, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
return fbo;
|
|
}
|
|
|
|
// One draw that SAMPLES `texture`, into `intoFbo`. This is what drags the queued clear
|
|
// through MaterializePendingClearForTexture (VulkanRenderer's sampled-texture
|
|
// pre-pass), which is the consumer that used to write the clear key's layerCount
|
|
// straight into a VkImageSubresourceRange.
|
|
void DrawSampling(GLuint program, GLuint intoFbo, GLenum textureTarget, GLuint texture) {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, intoFbo);
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
glActiveTexture(GL_TEXTURE0);
|
|
glBindTexture(textureTarget, texture);
|
|
glUseProgram(program);
|
|
const GLint sourceLocation = glGetUniformLocation(program, "u_source");
|
|
ASSERT_GE(sourceLocation, 0) << "u_source was not reflected";
|
|
glUniform1i(sourceLocation, 0);
|
|
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
|
|
ASSERT_GE(depthLocation, 0) << "u_depth was not reflected";
|
|
glUniform1f(depthLocation, 0.0f);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
glBindTexture(textureTarget, 0);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
// Every texel of `texels` is the clear colour. +/-1 per channel, which no rounding can
|
|
// exceed and which cannot be confused with the poison (0xAB) it replaced.
|
|
void ExpectAllCleared(const std::vector<Rgba8>& texels, int perTexelStride, const char* what) {
|
|
for (std::size_t i = 0; i < texels.size(); ++i) {
|
|
const Rgba8& actual = texels[i];
|
|
const bool ok = std::abs(static_cast<int>(actual.r) - kClearColor.r) <= 1 &&
|
|
std::abs(static_cast<int>(actual.g) - kClearColor.g) <= 1 &&
|
|
std::abs(static_cast<int>(actual.b) - kClearColor.b) <= 1;
|
|
if (ok) continue;
|
|
ADD_FAILURE() << what << ": unit " << (static_cast<int>(i) / perTexelStride) << " texel "
|
|
<< (static_cast<int>(i) % perTexelStride) << " is " << Describe(actual)
|
|
<< ", expected " << Describe(kClearColor)
|
|
<< (actual.r == kPoison && actual.g == kPoison
|
|
? " - the poison, so the clear never reached this one"
|
|
: "");
|
|
// One message per unit is enough to say what happened.
|
|
i = (static_cast<std::size_t>(i) / perTexelStride + 1) * perTexelStride - 1;
|
|
}
|
|
}
|
|
|
|
// A depth (or packed depth-stencil) cube-map array of the same shape. No upload: a
|
|
// depth array is filled by clearing through an attachment, which is the state the
|
|
// gating cases start from anyway.
|
|
GLuint MakeCubeArrayDepth(GLenum internalFormat) {
|
|
const GLuint texture = TrackTexture();
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, texture);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
glTexStorage3D(GL_TEXTURE_CUBE_MAP_ARRAY, 1, internalFormat, kExtent, kExtent, kCubeLayerFaces);
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY, 0);
|
|
return texture;
|
|
}
|
|
|
|
// glGetTexImage rather than a per-layer glReadPixels: a cube-map array has no
|
|
// per-layer attachment on every backend, and glGetTexImage is the readback both of
|
|
// them answer for whole-level layered targets (LayeredTextureReadbackScenario pins
|
|
// that contract). It is a real GPU readback on DirectVulkan - the texture manager
|
|
// copies the image into a staging buffer - so a stale CPU shadow cannot pass it.
|
|
std::vector<Rgba8> ReadLevel(GLenum target, GLuint texture, int layers) {
|
|
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent * layers, Rgba8{});
|
|
glBindTexture(target, texture);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glGetTexImage(target, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
|
glBindTexture(target, 0);
|
|
return texels;
|
|
}
|
|
|
|
// Every texel of every layer must be that layer's expected colour. Reported per layer
|
|
// so a failure names which one, and the poison is called out by name.
|
|
void ExpectEveryLayer(const std::vector<Rgba8>& texels, int layers, int pass, const char* what) {
|
|
for (int layer = 0; layer < layers; ++layer) {
|
|
const Rgba8 expected = ExpectedColor(layer, pass);
|
|
for (int y = 0; y < kExtent; ++y) {
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
const std::size_t index =
|
|
(static_cast<std::size_t>(layer) * kExtent + y) * kExtent + x;
|
|
const Rgba8 actual = texels[index];
|
|
if (actual == expected) continue;
|
|
ADD_FAILURE()
|
|
<< what << ": layer " << layer << " texel (" << x << ", " << y << ") is "
|
|
<< Describe(actual) << ", expected " << Describe(expected)
|
|
<< (actual.r == kPoison && actual.g == kPoison
|
|
? " - the poison, so nothing was ever rendered into this layer"
|
|
: "");
|
|
// One message per layer is enough to say what happened.
|
|
y = kExtent;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every texel of one cube FACE is that face's own colour. When it is not, the message
|
|
// says whose colour answered instead - which is the whole point here: a readback that
|
|
// ignores the face token does not return garbage, it returns another face's perfectly
|
|
// plausible texels, and "+X's colour came back for -Y" is the sentence that names the
|
|
// defect. `what` is the spelling under test, since three of them read the same faces.
|
|
void ExpectFaceColor(const std::vector<Rgba8>& texels, int face, const char* what) {
|
|
const Rgba8 expected = ExpectedColor(face, 0);
|
|
for (std::size_t i = 0; i < texels.size(); ++i) {
|
|
const Rgba8 actual = texels[i];
|
|
if (actual == expected) continue;
|
|
std::string blame;
|
|
if (actual.r == kPoison && actual.g == kPoison) {
|
|
blame = " - the poison, so nothing was ever written to this face";
|
|
} else {
|
|
for (int other = 0; other < 6; ++other) {
|
|
if (other != face && actual == ExpectedColor(other, 0)) {
|
|
blame = std::string(" - which is face ") + kFaceNames[other] + "'s colour";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
ADD_FAILURE() << what << ": face " << kFaceNames[face] << " texel " << i << " is "
|
|
<< Describe(actual) << ", expected " << Describe(expected) << blame;
|
|
// One message per face is enough to say what happened.
|
|
break;
|
|
}
|
|
}
|
|
|
|
::testing::AssertionResult FramebufferIsComplete() {
|
|
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
|
if (status == GL_FRAMEBUFFER_COMPLETE) return ::testing::AssertionSuccess();
|
|
return ::testing::AssertionFailure() << "framebuffer status 0x" << std::hex << status;
|
|
}
|
|
|
|
// One layered pass over the whole attachment.
|
|
void DrawLayered(GLuint program, int pass, float depth) {
|
|
glUseProgram(program);
|
|
const GLint passLocation = glGetUniformLocation(program, "u_pass");
|
|
ASSERT_GE(passLocation, 0) << "u_pass was not reflected";
|
|
glUniform1i(passLocation, pass);
|
|
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
|
|
ASSERT_GE(depthLocation, 0) << "u_depth was not reflected";
|
|
glUniform1f(depthLocation, depth);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
}
|
|
|
|
GLuint m_vao = 0;
|
|
std::vector<GLuint> m_textures;
|
|
std::vector<GLuint> m_fbos;
|
|
std::vector<GLuint> m_programs;
|
|
};
|
|
|
|
// (1) A LAYERED GL_TEXTURE_3D colour attachment. Pre-fix this is the null VkImageView:
|
|
// the attachment asked for a 3D view, whose [0, 4) layer span was measured against the
|
|
// image's arrayLayers == 1 and refused, and VK_NULL_HANDLE went to vkCreateFramebuffer.
|
|
TEST_F(LayeredAttachmentShapeScenario, LayeredThreeDColorAttachmentReachesEverySlice) {
|
|
if (!Ready()) return;
|
|
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
|
|
|
const std::string geometrySource = MakeGeometrySource(k3DSlices);
|
|
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
|
if (program == 0) return;
|
|
|
|
const GLuint color = MakePoisoned3DColor();
|
|
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed";
|
|
|
|
const GLuint fbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching the 3D texture layered failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored";
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored";
|
|
ExpectEveryLayer(texels, k3DSlices, /*pass=*/0, "layered GL_TEXTURE_3D colour attachment");
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// (2) The same texture attached ONE SLICE at a time, which is the other half of the same
|
|
// view-type decision. Pre-fix a non-layered 3D attachment also asked for a 3D view, so
|
|
// the per-slice branch never ran and slice 2 resolved to VK_NULL_HANDLE. Needs no
|
|
// geometry stage - the slice comes from the attachment.
|
|
TEST_F(LayeredAttachmentShapeScenario, NonLayeredThreeDSliceAttachmentWritesOnlyThatSlice) {
|
|
if (!Ready()) return;
|
|
|
|
const GLuint program = BuildProgram(nullptr, kFlatFragmentSource);
|
|
if (program == 0) return;
|
|
|
|
const GLuint color = MakePoisoned3DColor();
|
|
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 3D texture failed";
|
|
|
|
const GLuint fbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0, kSubjectSlice);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching slice " << kSubjectSlice << " failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
glUseProgram(program);
|
|
const GLint layerLocation = glGetUniformLocation(program, "u_layer");
|
|
const GLint passLocation = glGetUniformLocation(program, "u_pass");
|
|
const GLint depthLocation = glGetUniformLocation(program, "u_depth");
|
|
ASSERT_GE(layerLocation, 0);
|
|
ASSERT_GE(passLocation, 0);
|
|
ASSERT_GE(depthLocation, 0);
|
|
glUniform1i(layerLocation, kSubjectSlice);
|
|
glUniform1i(passLocation, 0);
|
|
glUniform1f(depthLocation, 0.0f);
|
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the per-slice draw errored";
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_3D, color, k3DSlices);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the 3D level back errored";
|
|
|
|
const Rgba8 expected = ExpectedColor(kSubjectSlice, 0);
|
|
const Rgba8 poison{kPoison, kPoison, kPoison, kPoison};
|
|
for (int slice = 0; slice < k3DSlices; ++slice) {
|
|
const Rgba8& target = (slice == kSubjectSlice) ? expected : poison;
|
|
for (int y = 0; y < kExtent; ++y) {
|
|
for (int x = 0; x < kExtent; ++x) {
|
|
const std::size_t index =
|
|
(static_cast<std::size_t>(slice) * kExtent + y) * kExtent + x;
|
|
const Rgba8 actual = texels[index];
|
|
if (actual == target) continue;
|
|
ADD_FAILURE() << "slice " << slice << " texel (" << x << ", " << y << ") is "
|
|
<< Describe(actual) << ", expected " << Describe(target)
|
|
<< (slice == kSubjectSlice
|
|
? " - the attached slice was not the one written"
|
|
: " - a slice the attachment did not name was written");
|
|
y = kExtent;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// (3) A LAYERED GL_TEXTURE_CUBE_MAP_ARRAY colour attachment. Pre-fix this is the other
|
|
// exit: a valid CUBE_ARRAY view of a type no framebuffer may take, handed straight to
|
|
// vkCreateFramebuffer.
|
|
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayColorAttachmentReachesEveryLayerFace) {
|
|
if (!Ready()) return;
|
|
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
|
|
|
const GLuint color = MakePoisonedCubeArrayColor();
|
|
if (const GLenum error = FirstGLError()) {
|
|
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
|
}
|
|
|
|
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
|
|
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
|
if (program == 0) return;
|
|
|
|
const GLuint fbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube-map array layered failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the layered draw errored";
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
|
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
|
|
"layered GL_TEXTURE_CUBE_MAP_ARRAY colour attachment");
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// (4) A layered cube-map-array DEPTH attachment, proved to have covered every layer-face:
|
|
//
|
|
// pass 0 paints at z = 0 against a depth buffer cleared to 1;
|
|
// pass 1 paints at z = +0.5, which GL_LESS must reject.
|
|
//
|
|
// A layer that reads back pass 1's colour is a layer the depth attachment never covered -
|
|
// which is exactly what attaching one layer-face of it, or none, looks like. Runs on both
|
|
// backends: this is the cross-backend control for the packed case below.
|
|
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthAttachmentGatesEveryLayerFace) {
|
|
if (!Ready()) return;
|
|
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
|
|
|
const GLuint color = MakePoisonedCubeArrayColor();
|
|
if (const GLenum error = FirstGLError()) {
|
|
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
|
}
|
|
|
|
const GLuint depth = MakeCubeArrayDepth(GL_DEPTH_COMPONENT24);
|
|
if (const GLenum error = FirstGLError()) {
|
|
GTEST_SKIP() << "no depth GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
|
}
|
|
|
|
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
|
|
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
|
if (program == 0) return;
|
|
|
|
const GLuint fbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depth, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth pair failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
glDepthMask(GL_TRUE);
|
|
glClearDepth(1.0);
|
|
glClear(GL_DEPTH_BUFFER_BIT);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth attachment errored";
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
glDepthFunc(GL_LESS);
|
|
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
|
DrawLayered(program, /*pass=*/1, /*depth=*/0.5f); // farther: GL_LESS must reject it
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the two layered draws errored";
|
|
|
|
glDisable(GL_DEPTH_TEST);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
|
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
|
|
"layered cube-map-array depth attachment (pass 1's colour on a layer means the "
|
|
"depth test did not cover it)");
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// (5) The PACKED depth-stencil shape the conformance suite crashes on:
|
|
// texture_cube_map_array.stencil_attachments_*_layered attaches a cube-map array as COLOR0
|
|
// AND the same-shaped GL_DEPTH24_STENCIL8 array as GL_DEPTH_STENCIL_ATTACHMENT, both
|
|
// layered. Both aspects are proved to have covered every layer-face:
|
|
//
|
|
// pass 0 paints at z = 0 with the stencil op writing 1;
|
|
// pass 1 paints at z = +0.5, which the depth test must reject;
|
|
// pass 2 paints with the depth test off but a stencil func of EQUAL 0, which the
|
|
// stencil written by pass 0 must reject.
|
|
//
|
|
// The probe in front of the gating is where this scenario earned its keep. The attachment
|
|
// point ITSELF was broken: glFramebufferTexture(GL_DEPTH_STENCIL_ATTACHMENT) is a
|
|
// shorthand that the front end splits into a depth and a stencil attachment, and the split
|
|
// dropped the call's `layered` flag (GL_Framebuffer.cpp,
|
|
// AttachFramebufferTextureWithUploadTarget). A layered colour attachment therefore sat
|
|
// beside a NON-layered depth/stencil one, and both backends lost the draw entirely - with
|
|
// no GL error and glCheckFramebufferStatus answering COMPLETE. DirectVulkan built the
|
|
// depth/stencil view with layerCount 1 under a framebuffer declaring 12 layers
|
|
// (VUID-VkFramebufferCreateInfo-flags-04535, which the validation layers report on this
|
|
// exact case); DirectGLES attached one layer of it beside a layered colour target, which
|
|
// the driver answers with GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS. Case (4) above is what
|
|
// isolates it to the attachment point: the same cube-map array on GL_DEPTH_ATTACHMENT
|
|
// rendered and gated correctly throughout.
|
|
//
|
|
// So the probe stays, as an assertion rather than as scaffolding: it turns that regression
|
|
// back into ONE message about the shape instead of twelve about individual layers.
|
|
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapArrayDepthStencilAttachmentGatesEveryLayerFace) {
|
|
if (!Ready()) return;
|
|
if (!BackendHostsGeometry()) GTEST_SKIP() << "no geometry stage: nothing can write gl_Layer";
|
|
|
|
const GLuint color = MakePoisonedCubeArrayColor();
|
|
if (const GLenum error = FirstGLError()) {
|
|
GTEST_SKIP() << "no usable GL_TEXTURE_CUBE_MAP_ARRAY on this backend: " << GLErrorName(error);
|
|
}
|
|
|
|
const GLuint depthStencil = MakeCubeArrayDepth(GL_DEPTH24_STENCIL8);
|
|
if (const GLenum error = FirstGLError()) {
|
|
GTEST_SKIP() << "no depth-stencil GL_TEXTURE_CUBE_MAP_ARRAY on this backend: "
|
|
<< GLErrorName(error);
|
|
}
|
|
|
|
const std::string geometrySource = MakeGeometrySource(kCubeLayerFaces);
|
|
const GLuint program = BuildProgram(geometrySource.c_str(), kLayeredFragmentSource);
|
|
if (program == 0) return;
|
|
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
|
|
// The probe: its own colour attachment (so the subject texture keeps its poison), the
|
|
// same depth-stencil attachment, and both tests off - so every layer-face must come
|
|
// back painted, whatever the gating below then decides.
|
|
{
|
|
const GLuint probeColor = MakePoisonedCubeArrayColor();
|
|
const GLuint probeFbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, probeFbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, probeColor, 0);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
DrawLayered(program, /*pass=*/3, /*depth=*/0.0f);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
const std::vector<Rgba8> probeTexels =
|
|
ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, probeColor, kCubeLayerFaces);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the probe draw or readback errored";
|
|
ExpectEveryLayer(probeTexels, kCubeLayerFaces, /*pass=*/3,
|
|
"a layered draw with the depth and stencil tests DISABLED, into a colour + "
|
|
"GL_DEPTH_STENCIL_ATTACHMENT cube-map-array pair (all poison means the "
|
|
"attachment pair lost the draw outright, which is what a non-layered "
|
|
"depth/stencil attachment beside a layered colour one looks like)");
|
|
// The gating assertions below can only add noise once the shape itself is broken.
|
|
if (::testing::Test::HasNonfatalFailure()) return;
|
|
}
|
|
|
|
const GLuint fbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, color, 0);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, depthStencil, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching the layered colour + depth-stencil pair failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
|
|
glDepthMask(GL_TRUE);
|
|
glStencilMask(0xFFu);
|
|
glClearDepth(1.0);
|
|
glClearStencil(0);
|
|
glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered depth-stencil attachment errored";
|
|
|
|
glEnable(GL_DEPTH_TEST);
|
|
glDepthFunc(GL_LESS);
|
|
glEnable(GL_STENCIL_TEST);
|
|
glStencilFunc(GL_ALWAYS, 1, 0xFFu);
|
|
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
|
|
DrawLayered(program, /*pass=*/0, /*depth=*/0.0f);
|
|
|
|
// Farther than pass 0, so GL_LESS must reject it on every layer.
|
|
glStencilFunc(GL_ALWAYS, 1, 0xFFu);
|
|
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
|
|
DrawLayered(program, /*pass=*/1, /*depth=*/0.5f);
|
|
|
|
// Depth out of the way; only the stencil pass 0 wrote can reject this one.
|
|
glDepthFunc(GL_ALWAYS);
|
|
glStencilFunc(GL_EQUAL, 0, 0xFFu);
|
|
DrawLayered(program, /*pass=*/2, /*depth=*/-0.5f);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the three layered draws errored";
|
|
|
|
glDisable(GL_DEPTH_TEST);
|
|
glDisable(GL_STENCIL_TEST);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
const std::vector<Rgba8> texels = ReadLevel(GL_TEXTURE_CUBE_MAP_ARRAY, color, kCubeLayerFaces);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the cube-map-array level back errored";
|
|
ExpectEveryLayer(texels, kCubeLayerFaces, /*pass=*/0,
|
|
"layered cube-map-array depth-stencil attachment (a later pass's colour means "
|
|
"the depth or stencil test did not cover that layer)");
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// (6) and (7) leave the render pass alone entirely and pin the OTHER consumer of a layered
|
|
// attachment's layer count.
|
|
//
|
|
// A glClear on a texture-backed FBO with the scissor test off is not executed on the spot:
|
|
// it is queued (VkClearManager), and then exactly one of two things consumes it - the next
|
|
// render pass's LOAD_OP_CLEAR over the attachment view, or MaterializePendingClearForTexture
|
|
// if the texture is used outside a pass first (sampled, blitted, copied, read back). The
|
|
// second path writes the queued key's layerCount straight into a VkImageSubresourceRange
|
|
// and then POPS the entry, so whatever it misses is lost for good - the render pass never
|
|
// gets a second chance at it.
|
|
//
|
|
// Both consumers must therefore agree about how many layers a layered attachment spans, and
|
|
// they are now literally the same function (ResolveAttachmentLayerCount, VkTextureManager.h).
|
|
// These two cases are the shapes where a raw `size.z()` and the real answer differ, and
|
|
// neither is reachable through the cases above: a cube MAP records the +X face as its
|
|
// representative upload target (z = 1, six real faces) and a 1D ARRAY keeps its layer count
|
|
// in the state-side height (z = 1, N real layers). The cube-map-ARRAY and 3D shapes the
|
|
// earlier cases use both carry their count in z, so they agree either way and cannot see it.
|
|
//
|
|
// The draw goes into a scratch 2D target, never into the layered attachment, so the
|
|
// materialise path is the only consumer that can fire.
|
|
TEST_F(LayeredAttachmentShapeScenario, LayeredCubeMapClearMaterialisedBySamplingReachesEveryFace) {
|
|
if (!Ready()) return;
|
|
|
|
const GLuint program = BuildProgram(nullptr, kCubeSampleFragmentSource);
|
|
if (program == 0) return;
|
|
|
|
const GLuint cube = MakePoisonedCubeMap();
|
|
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed";
|
|
|
|
const GLuint layeredFbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, cube, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching the cube map layered failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered cube-map attachment errored";
|
|
|
|
// Consume the queued clear through the sampled-texture path, with no draw into the
|
|
// layered FBO in between.
|
|
const GLuint scratchFbo = MakeScratchColorFbo();
|
|
ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete";
|
|
DrawSampling(program, scratchFbo, GL_TEXTURE_CUBE_MAP, cube);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored";
|
|
|
|
// Every face, read back through an FBO that names THAT face.
|
|
//
|
|
// Not glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face): when this case was written
|
|
// that spelling could not see per-face state on DirectVulkan at all - measured against
|
|
// a tree where only +X had been cleared it returned the cleared colour for all six
|
|
// faces - so a case built on it would have been unfalsifiable. That is a readback
|
|
// defect rather than an attachment one, and case (8) below is where it is pinned and
|
|
// fixed; this case keeps the independent spelling deliberately, because it must go on
|
|
// measuring the CLEAR whatever the readback does. glFramebufferTexture2D +
|
|
// glReadPixels names one face and nothing else, and the pending clear is long gone by
|
|
// now (materialised and popped above), so this readback cannot alter what it is
|
|
// measuring.
|
|
for (int face = 0; face < 6; ++face) {
|
|
const GLuint faceFbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, faceFbo);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), cube, 0);
|
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_TRUE(FramebufferIsComplete()) << "cube face " << kFaceNames[face] << " is not attachable";
|
|
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glReadPixels(0, 0, kExtent, kExtent, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "reading cube face " << kFaceNames[face] << " back errored";
|
|
ExpectAllCleared(texels, kExtent * kExtent,
|
|
(std::string("layered GL_TEXTURE_CUBE_MAP glClear materialised by sampling, "
|
|
"face ") +
|
|
kFaceNames[face])
|
|
.c_str());
|
|
}
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// The 1D-array half of the same divergence. Pre-existing rather than introduced by this
|
|
// branch (the clear copy never had ToVulkanLevelExtent), and fixed by the same hoist.
|
|
TEST_F(LayeredAttachmentShapeScenario, LayeredOneDArrayClearMaterialisedBySamplingReachesEveryLayer) {
|
|
if (!Ready()) return;
|
|
|
|
const GLuint program = BuildProgram(nullptr, kOneDArraySampleFragmentSource);
|
|
if (program == 0) return;
|
|
|
|
const GLuint array = MakePoisoned1DArray();
|
|
if (const GLenum error = FirstGLError()) {
|
|
GTEST_SKIP() << "no usable GL_TEXTURE_1D_ARRAY on this backend: " << GLErrorName(error);
|
|
}
|
|
|
|
const GLuint layeredFbo = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, layeredFbo);
|
|
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, array, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "attaching the 1D array layered failed";
|
|
ASSERT_TRUE(FramebufferIsComplete());
|
|
|
|
// The viewport is the LEVEL's shape: a 1D array level is `kExtent` wide and one row
|
|
// tall, whatever its layer count.
|
|
glViewport(0, 0, kExtent, 1);
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glClearColor(kClearColor.r / 255.0f, kClearColor.g / 255.0f, kClearColor.b / 255.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "clearing the layered 1D-array attachment errored";
|
|
|
|
const GLuint scratchFbo = MakeScratchColorFbo();
|
|
ASSERT_TRUE(FramebufferIsComplete()) << "the scratch 2D target is not complete";
|
|
DrawSampling(program, scratchFbo, GL_TEXTURE_1D_ARRAY, array);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the sampling draw errored";
|
|
|
|
// GL hands a 1D array back as a two-dimensional image whose ROWS are the layers.
|
|
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kOneDArrayLayers, Rgba8{});
|
|
glBindTexture(GL_TEXTURE_1D_ARRAY, array);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glGetTexImage(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
|
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "reading the 1D-array level back errored";
|
|
ExpectAllCleared(texels, kExtent,
|
|
"layered GL_TEXTURE_1D_ARRAY glClear materialised by sampling (unit = layer)");
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
// (8) THE CUBE FACE TOKEN A READBACK IS GIVEN, AND WHETHER IT HONOURS IT.
|
|
//
|
|
// Case (6) above had to route around glGetTexImage(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face)
|
|
// entirely: measured against a tree where only the +X face had been cleared, that spelling
|
|
// returned +X's colour for all six face tokens. This case is that observation turned into
|
|
// an assertion, and it is about the READBACK, not the attachment.
|
|
//
|
|
// THE DEFECT. DirectVulkan's GetTextureImage derived its copy geometry from the IMAGE's
|
|
// target alone. A plain GL_TEXTURE_CUBE_MAP is not one of the array targets, so the layer
|
|
// count collapsed to one - correct, one face IS one layer - but nothing ever turned the
|
|
// face the TARGET TOKEN named into the copy's baseArrayLayer, which stayed 0. All six face
|
|
// tokens therefore read array layer 0 and answered +X: five of a cube map's six faces were
|
|
// unreadable through the entry point GL provides for reading them. Nothing announces it -
|
|
// the call succeeds, raises no error, and hands back entirely plausible texels from the
|
|
// wrong face. The conversion it was missing already existed twice over, as the clear and
|
|
// render-pass managers' ResolveAttachmentBaseArrayLayer.
|
|
//
|
|
// glGetTextureSubImage is the same question asked by name: GL 4.6 core 8.11.4 addresses a
|
|
// cube map's faces through zoffset. That spelling was not merely reading the wrong face,
|
|
// it could not read ANY face - measured pre-fix, all six returned INVALID_OPERATION on
|
|
// both backends. Two independent reasons, and it took both to make even zoffset 0 fail:
|
|
// the z range was measured against the level's z, which is one face's 1, so five of the
|
|
// six looked like a partial read; and the destination-size check summed all six faces, so
|
|
// the one face's worth of buffer a single-face read has any reason to pass was rejected as
|
|
// too small.
|
|
//
|
|
// Each face is painted its OWN colour, so a collapse onto layer 0 does not merely read
|
|
// "wrong": the failure names the face that answered. The cube is poisoned first and then
|
|
// painted through the GPU, so an answer served from the stale CPU shadow is also called out
|
|
// by name rather than passing. And the per-face FBO + glReadPixels read is the control: it
|
|
// names one face and nothing else, so if IT disagrees the defect is in how the faces were
|
|
// written and this case is measuring the wrong thing.
|
|
//
|
|
// DirectGLES attaches the named face to a scratch FBO and reads that, so it answers the
|
|
// face token correctly throughout - a red there means this case is wrong. Its by-name
|
|
// readback is a different matter and gets a texture of its own; see the third block.
|
|
TEST_F(LayeredAttachmentShapeScenario, CubeMapFaceReadbackAnswersTheFaceItWasAskedFor) {
|
|
if (!Ready()) return;
|
|
|
|
const GLuint cube = MakePoisonedCubeMap();
|
|
ASSERT_EQ(FirstGLError(), 0u) << "creating the RGBA8 cube map failed";
|
|
|
|
// Paint every face its own colour through an FBO that names that one face. A clear
|
|
// rather than a draw, so nothing here depends on a shader stage being present.
|
|
glDisable(GL_SCISSOR_TEST);
|
|
glDisable(GL_DEPTH_TEST);
|
|
glViewport(0, 0, kExtent, kExtent);
|
|
GLuint faceFbos[6] = {};
|
|
for (int face = 0; face < 6; ++face) {
|
|
faceFbos[face] = TrackFramebuffer();
|
|
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), cube, 0);
|
|
glDrawBuffer(GL_COLOR_ATTACHMENT0);
|
|
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
|
ASSERT_TRUE(FramebufferIsComplete()) << "cube face " << kFaceNames[face] << " is not attachable";
|
|
const Rgba8 want = ExpectedColor(face, 0);
|
|
glClearColor(want.r / 255.0f, want.g / 255.0f, want.b / 255.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
}
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
ASSERT_EQ(FirstGLError(), 0u) << "painting the six faces errored";
|
|
|
|
// The control. If this is red, the faces do not hold six different values and the two
|
|
// readbacks below are being measured against a premise that is not true.
|
|
for (int face = 0; face < 6; ++face) {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, faceFbos[face]);
|
|
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
glReadPixels(0, 0, kExtent, kExtent, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
EXPECT_EQ(FirstGLError(), 0u) << "the control read of face " << kFaceNames[face] << " errored";
|
|
ExpectFaceColor(texels, face, "control: per-face FBO + glReadPixels");
|
|
}
|
|
|
|
// The subject: the face TOKEN.
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP, cube);
|
|
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
|
for (int face = 0; face < 6; ++face) {
|
|
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
|
glGetTexImage(static_cast<GLenum>(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face), 0, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, texels.data());
|
|
EXPECT_EQ(FirstGLError(), 0u) << "glGetTexImage of face " << kFaceNames[face] << " errored";
|
|
ExpectFaceColor(texels, face, "glGetTexImage(GL_TEXTURE_CUBE_MAP_<face>)");
|
|
}
|
|
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
|
|
|
// The same question by name, where zoffset is the face.
|
|
//
|
|
// On a cube map UPLOADED face by face rather than the painted one above, because the
|
|
// by-name readback has no backend entry outside DirectVulkan and answers from the CPU
|
|
// shadow there - a separate, pre-existing gap that has nothing to do with which face
|
|
// gets read. Asking it about GPU-painted content would make this red on DirectGLES for
|
|
// a reason the case is not about; asking it about uploaded content leaves exactly one
|
|
// thing either backend can get wrong, which is the face. DirectVulkan still answers
|
|
// this one out of the image, so the layer collapse is just as visible here.
|
|
const GLuint uploaded = MakeFaceColoredCubeMap();
|
|
ASSERT_EQ(FirstGLError(), 0u) << "uploading the six faces failed";
|
|
for (int face = 0; face < 6; ++face) {
|
|
std::vector<Rgba8> texels(static_cast<std::size_t>(kExtent) * kExtent, Rgba8{});
|
|
glGetTextureSubImage(uploaded, 0, 0, 0, face, kExtent, kExtent, 1, GL_RGBA, GL_UNSIGNED_BYTE,
|
|
static_cast<GLsizei>(texels.size() * sizeof(Rgba8)), texels.data());
|
|
EXPECT_EQ(FirstGLError(), 0u) << "glGetTextureSubImage of face " << kFaceNames[face] << " errored";
|
|
ExpectFaceColor(texels, face, "glGetTextureSubImage(zoffset = face)");
|
|
}
|
|
|
|
Gl().EndFrame();
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace MGITest
|