mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user