[Fix] (DirectVulkan): one shared attachment layer count for the render pass and the clear key, and direct VkResult checks in the render-pass builder

This commit is contained in:
2026-08-27 05:04:44 -04:00
parent 747864777e
commit de09646d5e
4 changed files with 312 additions and 43 deletions
@@ -8,6 +8,10 @@
#include "VkClearManager.h" #include "VkClearManager.h"
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
// clear key's layer span has to be the same one the render pass builds its attachment view from.
#include "VkTextureManager.h"
#include "MG_State/GLState/Core.h" #include "MG_State/GLState/Core.h"
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h" #include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h" #include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
@@ -100,13 +104,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ResolveAttachmentBaseArrayLayer(uploadTarget); return ResolveAttachmentBaseArrayLayer(uploadTarget);
} }
static Uint32 ResolveAttachmentLayerCount( // ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
const MG_State::GLState::FramebufferAttachmentObject& attachment) { // raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
if (attachment.IsLayered()) { // not a cosmetic difference: the count below is not key-only, it is written straight into
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1)); // VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
} // the entry - so a layered cube map's glClear reached one face and the other five were lost
return 1u; // for good, while the very same queued clear cleared all six through the render pass's
} // LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment( static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) { const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
@@ -86,29 +86,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return ToStorageArrayLayer(texture, face); return ToStorageArrayLayer(texture, face);
} }
// The attachment's size is GL geometry, and GL_TEXTURE_1D_ARRAY keeps its layer count in the // ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
// state-side HEIGHT rather than in z (see ToVulkanLevelExtent, which exists for exactly this // VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
// remap). Reading z directly gave every layered 1D-array attachment layerCount = 1, so a // VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
// geometry shader writing gl_Layer = 1..n had its output silently dropped and the parent's
// upper layers were never written at all.
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (attachment.IsLayered()) {
const auto& texture = attachment.GetTexture();
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
// A layered CUBE MAP names all six faces (GL 4.6 core 9.2.8), but the attachment model
// records only the REPRESENTATIVE upload target for it - the +X face
// (ResolveRepresentableFramebufferTextureUploadTarget) - and that face's level size has
// z = 1. Reading z here therefore attached one face to a layered framebuffer, so a
// geometry shader writing gl_Layer = 1..5 lost five sixths of its output. The image's
// six layers are the cube's faces, exactly as for a cube ARRAY (whose representative
// target does carry 6n in z and needs no special case).
if (target == TextureTarget::TextureCubeMap) {
return 6u;
}
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
}
return 1u;
}
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have // VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view // been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
@@ -1503,13 +1483,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
renderPassCreateInfo.dependencyCount = 2; renderPassCreateInfo.dependencyCount = 2;
renderPassCreateInfo.pDependencies = subpassDependencies; renderPassCreateInfo.pDependencies = subpassDependencies;
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
// line on every draw for the life of the process - a decline caches nothing, so every
// later draw to the same framebuffer re-enters this path and fails again.
VkRenderPass renderPass = VK_NULL_HANDLE; VkRenderPass renderPass = VK_NULL_HANDLE;
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass)); const VkResult renderPassResult =
// VK_VERIFY only logs (and only in an INFO build); the handle is the truth. Caching an vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
// entry whose VkRenderPass is null would hand VK_NULL_HANDLE to vkCmdBeginRenderPass and if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
// to every pipeline built against it. MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
if (renderPass == VK_NULL_HANDLE) { "render pass",
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed for FBO %u; declining the render pass", VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
fbo.GetExternalIndex()); fbo.GetExternalIndex());
return nullptr; return nullptr;
} }
@@ -1525,13 +1514,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
framebufferCreateInfo.width = width; framebufferCreateInfo.width = width;
framebufferCreateInfo.height = height; framebufferCreateInfo.height = height;
framebufferCreateInfo.layers = framebufferLayers; framebufferCreateInfo.layers = framebufferLayers;
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
VkFramebuffer framebuffer = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer)); const VkResult framebufferResult =
if (framebuffer == VK_NULL_HANDLE) { vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
// The render pass has no entry to own it yet, so it is destroyed here rather than // The render pass has no entry to own it yet, so it is destroyed here rather than
// leaked - RenderPassEntry's destructor is the only other thing that would. // leaked - RenderPassEntry's destructor is the only other thing that would.
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed for FBO %u (%dx%d, %u attachments, " MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
"%u layers); declining the render pass", "%u attachments, %u layers); declining the render pass",
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
fbo.GetExternalIndex(), width, height, fbo.GetExternalIndex(), width, height,
static_cast<Uint32>(attachmentViews.size()), framebufferLayers); static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
vkDestroyRenderPass(m_device, renderPass, nullptr); vkDestroyRenderPass(m_device, renderPass, nullptr);
@@ -10,8 +10,10 @@
#include "../VkIncludes.h" #include "../VkIncludes.h"
#include <Includes.h> #include <Includes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureObject.h> #include <MG_State/GLState/TextureState/TextureObject.h>
#include <vk_mem_alloc.h> #include <vk_mem_alloc.h>
#include <algorithm>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -41,6 +43,37 @@ inline IntVec3 ToVulkanLevelExtent(TextureTarget stateTarget, const IntVec3& glT
return glTexelSize; return glTexelSize;
} }
// How many Vulkan array layers (or, for a 3D image, z slices) a GL framebuffer attachment spans.
//
// THE ONE COPY, deliberately. This used to exist twice - privately in VkRenderPassManager.cpp and
// again in VkClearManager.cpp - and the two are not independent: the render pass builds the
// attachment view and VkFramebufferCreateInfo::layers from one, while the CLEAR key built from the
// other is written verbatim into VkImageSubresourceRange::layerCount when a queued glClear is
// materialised outside a render pass (MaterializePendingClearForTexture). They are two consumers
// of the same GL clear, so any disagreement means the same glClear produces two different pictures
// depending only on which path happens to consume it first - and the materialise path then POPS
// the entry, so the other one never runs. Fixing one copy and leaving the other is exactly how
// that split gets introduced; keep them the same function.
//
// Two shapes make this more than `size.z()`:
// * GL_TEXTURE_1D_ARRAY keeps its layer count in the state-side HEIGHT (see ToVulkanLevelExtent
// just above), so z reads 1 and every layer above the first was silently dropped.
// * GL_TEXTURE_CUBE_MAP is attached layered as its REPRESENTATIVE upload target, the +X face
// (ResolveRepresentableFramebufferTextureUploadTarget), and one face's level size has z = 1 -
// but a layered cube attachment names all six faces (GL 4.6 core 9.2.8), which are the image's
// six array layers. A cube ARRAY needs no such arm: its representative target carries 6n in z.
inline Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
if (!attachment.IsLayered()) {
return 1u;
}
const auto& texture = attachment.GetTexture();
const TextureTarget target = texture != nullptr ? texture->GetTarget() : TextureTarget::Unknown;
if (target == TextureTarget::TextureCubeMap) {
return 6u;
}
return static_cast<Uint32>(std::max(ToVulkanLevelExtent(target, attachment.GetSize()).z(), 1));
}
// A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture // A GL framebuffer attachment's level/layer, and a GL image unit's, are relative to the texture
// the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they // the application NAMED. When that texture was created by glTextureView (ARB_texture_view) they
// are relative to the VIEW, and have to be shifted into the storage image's numbering before they // are relative to the VIEW, and have to be shifted into the storage image's numbering before they
@@ -32,9 +32,10 @@
// too, so the per-slice branch that exists for exactly this case was unreachable and every // 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. // slice above z = 0 came back VK_NULL_HANDLE.
// //
// The five cases below are those shapes - layered 3D, one 3D slice, layered cube-map array, and // The seven cases below are those shapes - layered 3D, one 3D slice, layered cube-map array with
// its depth and packed depth-stencil attachments - and each one asserts LAYER ROUTING, not merely // its depth and packed depth-stencil attachments, and (cases 6 and 7) a layered cube MAP and 1D
// survival: the colour a layer receives is a function of its own index, so an attachment that // 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 // 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 // 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". // never landed here" reads differently from "the wrong layer landed here".
@@ -52,6 +53,7 @@
// red on DirectVulkan alone means Magma is. // red on DirectVulkan alone means Magma is.
#include <cstddef> #include <cstddef>
#include <cstdlib>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -81,10 +83,19 @@ namespace MGITest {
// right whether or not the slice is resolved at all. // right whether or not the slice is resolved at all.
constexpr int kSubjectSlice = 2; 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 // A colour no pass paints, uploaded before every draw. A layer that reads it back was
// never rendered to. // never rendered to.
constexpr GLubyte kPoison = 0xAB; 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};
// What pass `pass` paints on layer `layer`. r and g name the LAYER (so a mis-routed write // 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 // 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"). // rejected" is distinguishable from "the first draw never happened").
@@ -148,6 +159,21 @@ void main()
float(3 + u_pass * 60) / 255.0, float(3 + u_pass * 60) / 255.0,
1.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 // The non-layered case has no geometry stage at all - the slice comes from the
@@ -312,6 +338,103 @@ void main()
return texture; 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;
}
// 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 // 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 // depth array is filled by clearing through an attachment, which is the state the
// gating cases start from anyway. // gating cases start from anyway.
@@ -708,5 +831,122 @@ void main()
Gl().EndFrame(); 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 one at a time - the per-face spelling is what names the
// offender when only +X was cleared.
static const char* const kFaceNames[6] = {"+X", "-X", "+Y", "-Y", "+Z", "-Z"};
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) << "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());
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
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();
}
} // namespace } // namespace
} // namespace MGITest } // namespace MGITest