[Feat] (MG_Backend, MG_Impl, MG_Util): attach one layer of any layered texture on DirectVulkan

Whether a backend can attach a single layer of a texture to a framebuffer was one
Bool, so it could only give the most conservative answer any target needed.
DirectVulkan therefore declined every layer of every target and
direct_state_access.framebuffers_texture_layer_attachment failed with 542
messages across four targets.

The three ways a GL layer maps onto Vulkan are independent capabilities, so the
flag becomes a per-TextureTarget mask. A 2D or 2D multisample array layer IS a
VkImage array layer and needed nothing but the gate opened. A cube map array is
one 2D image with arrayLayers = 6 * cubeCount and CUBE_COMPATIBLE, which is a
shape VkTextureManager simply did not have - it is declined softly when the depth
is not a whole number of cubes or the level is not square, because that function's
Bool return exists for unrepresentable shapes and asserting there would abort on
ordinary input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all. A 3D texture's layer is
a z slice, which needs a 2D-array-compatible image and a per-slice clear, because
vkCmdClearColorImage cannot address a subset of a 3D image's slices - a render
pass whose only content is its LOAD_OP_CLEAR can, since its attachment is a 2D
view over that one slice.

VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is asked for per format and withdrawn per
format, mirroring the MUTABLE_FORMAT pattern already in this file: the capability
is per format+usage, so a single global probe answers a different question than
the one the frontend goes on to ask. Losing it costs per-slice attachment for that
format; failing creation would lose the texture.

Three things found on the way that are not the headline:

glFramebufferTextureLayer, the non-DSA twin, had no gate at all and additionally
refused cube map arrays that GL 4.5 requires it to accept. GL 4.6 core 9.2.8 makes
the two entry points equivalent, so they now decline in the same places - leaving
one ungated is what let an unrepresentable attachment reach the renderer.

ComputeFullMipLevelCount takes max(x, y, z), and for every array shape z is the
layer count rather than a mip-able axis, so a 4x4 array with 192 layers asked for
six mip levels on an image whose legal maximum is three
(VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own extent can bound
it. lavapipe had been letting that through.

A layered GL clear queues layerCount = depth, which is illegal for a
VK_IMAGE_TYPE_3D image (VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins it to
0/1, read as the whole mip level) and the old code passed it straight through.

Takes framebuffers_texture_layer_attachment green on DirectVulkan, so the whole
direct_state_access suite is 371/371 there; Espryt stays 370/371, the remaining
case being the fp64 one it declines by design.

Known and deliberately not fixed here, with a FIXME at the site:
KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support now fails on
DirectVulkan - a layered clear of a 3D texture reads back zeros. Those cases exist
only in the GL44+ lists, above the 4.0 this backend reports. An A/B of a 6935-case
subset (cube map array, texture storage, framebuffer, 3D, the full DSA suite and
the GL33 texture group) is otherwise clean on both backends: 16 cases fixed and
none broken on Espryt, 15 fixed and those 2 broken on Magma, and zero difference
anywhere at GL 4.0 or below. The FIXME records which causes were already ruled out
by bisection so the next reader does not repeat them.
This commit is contained in:
BZLZHH
2026-08-05 12:07:04 -04:00
parent c8c7b19579
commit ba81ee114e
12 changed files with 416 additions and 24 deletions
+22 -1
View File
@@ -358,7 +358,28 @@ namespace MobileGL {
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
// so a backend that never sets it gets the conservative answer.
Bool SupportsPerLayerFramebufferAttachment = false;
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
// set for each supported target. Deliberately per target rather than one flag: the three
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
// a backend that never sets it gets the conservative answer.
Uint32 PerLayerFramebufferAttachmentTargets = 0;
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
return (static_cast<Int>(target) >= 0 &&
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
? (1u << static_cast<Uint32>(target))
: 0u;
}
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
}
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
@@ -1112,8 +1112,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// SyncAttachmentObject routes a layered upload target to glFramebufferTextureLayer with the
// attachment's layer passed through, so this backend really does render to the layer it was
// given - provided the driver resolved the entry point at all.
m_dynamicParameters.SupportsPerLayerFramebufferAttachment =
DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr;
// SyncAttachmentObject (Managers.cpp, the glFramebufferTextureLayer branch) routes exactly
// five upload targets to glFramebufferTextureLayer with the attachment's layer passed
// through, so this backend really does render to the layer it was given - provided the driver
// resolved the entry point at all. The cube map array is the one target that also needs
// ES-level support before it has any storage to attach.
m_dynamicParameters.PerLayerFramebufferAttachmentTargets = 0;
if (DirectGLES::g_GLESFuncs.glFramebufferTextureLayer != nullptr) {
using DynParams = MG_Backend::DynamicBackendParameters;
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture1DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
if (m_GLESCapabilities.SupportsTextureCubeMapArray) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
// land on this backend regardless of what the driver underneath happens to support.
@@ -810,6 +810,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
m_dynamicParameters.SupportsWideLines = m_vulkanCaps.SupportsWideLines;
// A 2D or 2D multisample array texture is a VK_IMAGE_TYPE_2D image whose GL depth IS its
// arrayLayers, so a GL layer is a Vulkan array layer with nothing to translate.
// ResolveAttachmentBaseArrayLayer already passes the attachment's layer through. The other
// layered targets are declared separately as their own machinery lands.
{
using DynParams = MG_Backend::DynamicBackendParameters;
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DArray) |
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture2DMultisampleArray);
// A cube map array is one 2D image with arrayLayers = 6 * cubeCount, so a GL layer is a
// Vulkan array layer here too - but the image cannot be created without imageCubeArray.
// A 3D texture's GL layer is a z slice, which only a 2D view over a 2D-array-compatible
// image can name. Optimistic: a format that refuses the flag is caught at image creation
// and declines the slice view there, which the clear path handles as a soft miss.
if (m_vulkanCaps.Supports2DArrayCompatible3DImages) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::Texture3D);
}
if (m_vulkanCaps.SupportsImageCubeArray) {
m_dynamicParameters.PerLayerFramebufferAttachmentTargets |=
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
@@ -87,9 +87,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static VkImageViewType ResolveAttachmentViewType(
const MG_State::GLState::FramebufferAttachmentObject& attachment,
const VkTextureManager::TextureResource& resource) {
return !attachment.IsLayered() && IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ?
VK_IMAGE_VIEW_TYPE_2D :
resource.viewType;
if (attachment.IsLayered()) {
return resource.viewType;
}
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
// the image's own view type is. The cube-face upload targets always meant this; a cube map
// array attached through glFramebufferTextureLayer means it too, and a CUBE_ARRAY view over
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
// produces a non-layered cube attachment without a face upload target - and is kept for
// symmetry with CUBE_ARRAY.
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
return VK_IMAGE_VIEW_TYPE_2D;
}
return resource.viewType;
}
static MG_State::GLState::ITextureObject* ResolveCompleteColorAttachmentTexture(
@@ -564,6 +564,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outShape.depth = 1;
outShape.arrayLayers = 6;
return true;
case TextureUploadTarget::CubeMapArray:
case TextureUploadTarget::ProxyCubeMapArray:
// GL_TEXTURE_CUBE_MAP_ARRAY is an array texture whose layers happen to be cube faces:
// one 2D image with arrayLayers = 6 * cubeCount, CUBE_COMPATIBLE so the whole thing can
// be sampled as a samplerCubeArray. glTexStorage3D hands the 6*n through as the GL depth
// and the upload path's depthSelectsArrayLayer already lists VK_IMAGE_VIEW_TYPE_CUBE_ARRAY,
// so the copies address layers correctly.
//
// A depth that is not a whole number of cubes, or a non-square level, has no Vulkan shape
// - declined the way every other unrepresentable target is. This function's Bool return
// exists for exactly that; asserting here would abort the process on ordinary application
// input, GL_PROXY_TEXTURE_CUBE_MAP_ARRAY above all.
if (texelSize.z() <= 0 || (texelSize.z() % 6) != 0 || texelSize.x() != texelSize.y()) {
return false;
}
outShape.imageType = VK_IMAGE_TYPE_2D;
outShape.viewType = VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
outShape.imageFlags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
outShape.depth = 1;
outShape.arrayLayers = static_cast<Uint32>(texelSize.z());
return true;
default:
return false;
}
@@ -822,8 +843,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resource == nullptr || resource->image == VK_NULL_HANDLE || mipLevel >= resource->mipLevels) {
return VK_NULL_HANDLE;
}
if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
baseArrayLayer + layerCount > resource->arrayLayers) {
// A 3D image has arrayLayers == 1 and keeps its GL layers on the z axis, so a per-slice
// attachment view is a 2D view whose "array layer" is the slice - legal only on a
// 2D-array-compatible image (VUID-VkImageViewCreateInfo-image-04970), which
// SyncTextureResource asks for and may have had refused per format.
if (resource->viewType == VK_IMAGE_VIEW_TYPE_3D && viewType == VK_IMAGE_VIEW_TYPE_2D) {
const Uint32 sliceCount = std::max(resource->depth >> mipLevel, 1u);
if ((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) == 0 ||
layerCount == 0 || baseArrayLayer >= sliceCount || baseArrayLayer + layerCount > sliceCount) {
MGLOG_D("%s: cannot name slice span [%u, %u) of 3D textureId=%d (mip %u has %u slices, "
"2D-array-compatible=%d)",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
mipLevel, sliceCount,
(int)((resource->imageCreateFlags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0));
return VK_NULL_HANDLE;
}
} else if (layerCount == 0 || baseArrayLayer >= resource->arrayLayers ||
baseArrayLayer + layerCount > resource->arrayLayers) {
MGLOG_D("%s: invalid layer span [%u, %u) for textureId=%d arrayLayers=%u",
__func__, baseArrayLayer, baseArrayLayer + layerCount, texture.GetExternalIndex(),
resource->arrayLayers);
@@ -1492,11 +1528,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// preserve-copy path below carries the pixels over), so sequentially-
// defined atlas mips do not recreate per level, and glGenerateMipmap -
// which defines every level before syncing - works unchanged.
const Uint32 backingMipLevels =
isMultisampleTexture ? 1u
: (mipLevels > 1 ? std::max(mipLevels, ComputeFullMipLevelCount(texelSize)) : 1u);
TextureShapeInfo shapeInfo{};
const Bool supportedShape = TryResolveTextureShapeInfo(texture, uploadTarget, texelSize, shapeInfo);
// ComputeFullMipLevelCount takes max(x, y, z), and for every ARRAY shape z is the layer
// count, not a mip-able axis: a 4x4 array with 192 layers asked for 6 levels on an image
// whose legal maximum is 3 (VUID-VkImageCreateInfo-mipLevels-00958). Only the image's own
// extent - width, height and shapeInfo.depth, which is 1 for every array - can bound it.
// lavapipe has been letting this through unvalidated; a strict driver would not.
const IntVec3 mipExtent{texelSize.x(), texelSize.y(), static_cast<Int>(shapeInfo.depth)};
const Uint32 fullMipLevels = ComputeFullMipLevelCount(mipExtent);
const Uint32 backingMipLevels =
isMultisampleTexture ? 1u : (mipLevels > 1 ? std::min(std::max(mipLevels, fullMipLevels), fullMipLevels) : 1u);
if (!supportedShape) {
// A gap in this backend's coverage, not a broken invariant: the GL front end accepts
// targets this manager has no Vulkan image shape for yet (cube map arrays above all).
@@ -1554,6 +1596,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0;
const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage;
VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags;
// One z slice of a 3D texture can only be attached to a framebuffer through a 2D view over
// it, which needs the image to be 2D-array-compatible (Vulkan 1.1 core, promoted from
// VK_KHR_maintenance1). Asked for optimistically and withdrawn per format below if the
// driver refuses - losing it only costs per-slice attachment, while failing creation would
// lose the texture entirely.
if (shapeInfo.imageType == VK_IMAGE_TYPE_3D && !isMultisampleTexture &&
m_2dArrayCompatibleUnsupported.find(format) == m_2dArrayCompatibleUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
}
if (storageImageCapable && IsMutableStorageImageFormat(format) &&
m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) {
imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
@@ -1711,7 +1762,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
imageInfo.pNext = &formatListInfo;
}
if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) {
if (isMultisampleTexture || (imageInfo.flags & (VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT |
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT)) != 0) {
VkImageFormatProperties imageFormatProperties{};
VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
@@ -1734,6 +1786,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties);
}
if (imageFormatResult != VK_SUCCESS && !isMultisampleTexture &&
(imageInfo.flags & VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT) != 0) {
// Losing 2D-array compatibility only costs per-slice framebuffer attachment for this
// format; failing creation would lose the texture entirely. Remembered so later syncs
// neither reprobe nor flag-mismatch against this image and recreate it.
MGLOG_W("%s: VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT is unsupported for format=%d "
"textureId=%d; creating without it (per-slice framebuffer attachment will be "
"unavailable for it)",
__func__, static_cast<Int>(format), texture.GetExternalIndex());
m_2dArrayCompatibleUnsupported.insert(format);
imageInfo.flags &= ~VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT;
imageCreateFlags = imageInfo.flags;
imageFormatResult = vkGetPhysicalDeviceImageFormatProperties(
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage,
imageInfo.flags, &imageFormatProperties);
}
if (imageFormatResult != VK_SUCCESS ||
(isMultisampleTexture && (imageFormatProperties.sampleCounts & resolvedSampleCount) == 0)) {
MGLOG_D("%s: image flags=0x%x sampleCount=%d are unsupported for textureId=%d target=%s "
@@ -487,6 +487,10 @@ private:
// Formats whose mutable-image probe failed on this device; their images are created
// without MUTABLE_FORMAT_BIT so repeat syncs neither re-probe nor flag-mismatch.
std::unordered_set<VkFormat> m_mutableFormatUnsupported;
// Formats whose 3D images refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT. Per format+usage,
// exactly like the mutable-format verdict above, so it is answered at image creation and
// remembered rather than probed once globally.
std::unordered_set<VkFormat> m_2dArrayCompatibleUnsupported;
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
// Textures that have been bound to a GL image unit (see MarkStorageImageTexture).
@@ -5921,6 +5921,83 @@ void main() {
QueueClearBufferPayload(buffer, drawbuffer, payload);
}
Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue) {
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) return false;
if (m_frameContext.GetCurrentFrameIndex() >= m_deferredDepthMipmapCleanup.size()) return false;
// A 2D view over one z slice. Returns VK_NULL_HANDLE when the image is not
// 2D-array-compatible, which is the whole reason this can fail.
const VkImageView sliceView = m_textureManager->GetOrCreateAttachmentViewAtMipLevel(
texture, mipLevel, depthSlice, 1, VK_IMAGE_VIEW_TYPE_2D);
if (sliceView == VK_NULL_HANDLE) return false;
VkAttachmentDescription colorAttachment{};
colorAttachment.format = resource->format;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
// Hand the slice back in the layout the caller already tracks for the whole image, so its
// closing barrier stays truthful and resource->layout is never touched from in here.
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkAttachmentReference colorRef{};
colorRef.attachment = 0;
colorRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorRef;
VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
VkRenderPass renderPass = VK_NULL_HANDLE;
if (vkCreateRenderPass(m_device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) return false;
const Uint32 levelWidth = std::max(resource->extent.width >> mipLevel, 1u);
const Uint32 levelHeight = std::max(resource->extent.height >> mipLevel, 1u);
VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = 1;
framebufferInfo.pAttachments = &sliceView;
framebufferInfo.width = levelWidth;
framebufferInfo.height = levelHeight;
framebufferInfo.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
if (vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer) != VK_SUCCESS) {
vkDestroyRenderPass(m_device, renderPass, nullptr);
return false;
}
VkRenderPassBeginInfo beginInfo{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
beginInfo.renderPass = renderPass;
beginInfo.framebuffer = framebuffer;
beginInfo.renderArea.extent = {levelWidth, levelHeight};
beginInfo.clearValueCount = 1;
beginInfo.pClearValues = &clearValue;
// The load op is the whole operation: begin and end with nothing in between.
vkCmdBeginRenderPass(commandBuffer, &beginInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdEndRenderPass(commandBuffer);
// The image view is owned and memoised by the texture resource; only these two are throwaway.
auto& deferredCleanup = m_deferredDepthMipmapCleanup[m_frameContext.GetCurrentFrameIndex()];
deferredCleanup.renderPasses.push_back(renderPass);
deferredCleanup.framebuffers.push_back(framebuffer);
return true;
}
Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture) {
Vector<PendingClearEntry> pendingClears;
@@ -5957,16 +6034,79 @@ void main() {
MOBILEGL_ASSERT(pendingClear.key.mipLevel < resource->mipLevels,
"MaterializePendingClearForTexture: textureId=%d pending clear mip=%u out of range %u",
texture.GetExternalIndex(), pendingClear.key.mipLevel, resource->mipLevels);
MOBILEGL_ASSERT(pendingClear.key.baseArrayLayer + pendingClear.key.layerCount <= resource->arrayLayers,
"MaterializePendingClearForTexture: textureId=%d pending clear layer span [%u, %u) exceeds arrayLayers=%u",
// FIXME: a layered clear of a GL_TEXTURE_3D texture still reads back wrong.
// KHR-GL44/45/46.geometry_shader.layered_framebuffer.clear_call_support fails on
// DirectVulkan: it attaches a 4-deep 3D texture with glFramebufferTexture (layered),
// clears with glClearBufferiv, then reads each slice back through
// glFramebufferTextureLayer and gets zeros. Those cases exist only in the GL44+ lists,
// above the 4.0 this backend reports, so they are outside the current conformance
// claim - but the feature (layered attachment, GL 3.2) is not, so an application can
// reach this.
//
// Already ruled out by bisecting with temporary bypasses, so do not re-test these:
// - the per-slice render-pass clear below (disabling it changes nothing)
// - the per-target gate in FramebufferTextureLayer_State (it already permits
// Texture3D here; bypassing it changes nothing)
// - VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT on the 3D image (not requesting it
// changes nothing)
// What IS fixed here is the subresource range below: a layered GL clear queues
// layerCount = depth, which is illegal for a VK_IMAGE_TYPE_3D image, and the old code
// passed it straight through - running the case standalone against the previous build
// trips MOBILEGL_ASSERT(baseArrayLayer + layerCount <= arrayLayers) as 0 + 4 <= 1.
//
// Note when picking this up: the case does not reproduce standalone the way it behaves
// in a batch run (batch passed before this change, standalone asserted), so it depends
// on state left by earlier cases. Reproduce it inside a chunk, not on its own.
//
// A 3D image keeps its GL layers on the z axis (arrayLayers == 1), so the pending
// clear's "layer" is a slice index bounded by the mip level's depth.
const Bool clearAddressesDepthSlices = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
const Uint32 clearableLayers = clearAddressesDepthSlices
? std::max(resource->depth >> pendingClear.key.mipLevel, 1u)
: resource->arrayLayers;
MOBILEGL_ASSERT(pendingClear.key.baseArrayLayer + pendingClear.key.layerCount <= clearableLayers,
"MaterializePendingClearForTexture: textureId=%d pending clear layer span [%u, %u) exceeds %u",
texture.GetExternalIndex(), pendingClear.key.baseArrayLayer,
pendingClear.key.baseArrayLayer + pendingClear.key.layerCount, resource->arrayLayers);
pendingClear.key.baseArrayLayer + pendingClear.key.layerCount, clearableLayers);
// Whether this clear names a strict SUBSET of the level. A layered attachment
// (glFramebufferTexture) queues layerCount = the whole depth, a single-slice one
// (glFramebufferTextureLayer) queues 1 - so the key already distinguishes them, and it is
// the clear's span that decides, not the image's slice count. Reading the latter sent a
// layered clear of a 3D texture down the per-slice path, where it cleared slice zero and
// left the rest stale (geometry_shader.layered_framebuffer.clear_call_support).
const Bool clearsWholeLevel =
pendingClear.key.baseArrayLayer == 0 && pendingClear.key.layerCount >= clearableLayers;
if (clearAddressesDepthSlices && clearableLayers > 1 && !clearsWholeLevel) {
// vkCmdClearColorImage cannot clear a subset of a 3D image's slices:
// VUID-vkCmdClearColorImage-baseArrayLayer-01472 pins baseArrayLayer to 0 and
// layerCount to 1 for VK_IMAGE_TYPE_3D, i.e. the whole mip level. A render pass whose
// only content is its LOAD_OP_CLEAR does address exactly one slice, because its
// attachment is a 2D view over that slice.
auto clearPayload3D = pendingClear.payload;
PreCompensateSrgbClearColor(clearPayload3D, resource->format);
VkClearValue sliceClearValue{};
sliceClearValue.color = MakeVkClearColorValue(clearPayload3D, ColorFormatLacksAlpha(&texture));
if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel,
pendingClear.key.baseArrayLayer, sliceClearValue)) {
// The device or the format refused VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, so
// there is no way to name this slice. Leaving it uncleared is wrong pixels;
// asserting would abort a process that glFramebufferTextureLayer can reach at will.
MGLOG_W("MaterializePendingClearForTexture: textureId=%d slice %u could not be cleared "
"(no 2D-array-compatible view)",
texture.GetExternalIndex(), pendingClear.key.baseArrayLayer);
}
continue;
}
VkImageSubresourceRange subresourceRange{};
subresourceRange.baseMipLevel = pendingClear.key.mipLevel;
subresourceRange.levelCount = 1;
subresourceRange.baseArrayLayer = pendingClear.key.baseArrayLayer;
subresourceRange.layerCount = pendingClear.key.layerCount;
// VUID-vkCmdClearColorImage-baseArrayLayer-01472: for a VK_IMAGE_TYPE_3D image the range
// must name baseArrayLayer 0 and layerCount 1, which Vulkan reads as "the whole mip
// level" - the z extent is not an array dimension. A layered GL clear queues
// layerCount = depth, which is the right GL answer and an illegal Vulkan one.
subresourceRange.baseArrayLayer = clearAddressesDepthSlices ? 0u : pendingClear.key.baseArrayLayer;
subresourceRange.layerCount = clearAddressesDepthSlices ? 1u : pendingClear.key.layerCount;
auto clearPayload = pendingClear.payload;
if ((resource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
@@ -9993,6 +10133,10 @@ void main() {
// it vkCreateShaderModule is invalid usage (VUID-VkShaderModuleCreateInfo-pCode-08740),
// which is why SupportsFloat64VertexAttributes gates the entry point on the same feature.
deviceFeatures.shaderFloat64 = supportedDeviceFeatures.shaderFloat64;
// Required before a VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created
// (VUID-VkImageViewCreateInfo-viewType-01004). Without it a cube map array texture cannot
// get its sampled or full view, so SyncTextureResource fails and the texture stays unbacked.
deviceFeatures.imageCubeArray = supportedDeviceFeatures.imageCubeArray;
// Required for desktop GL image load/store semantics. iterationRP writes storage
// images from vertex and fragment stages and uses formats outside Vulkan's small
// mandatory storage-image set.
@@ -815,6 +815,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLenum filter);
// Clears one z slice of a VK_IMAGE_TYPE_3D colour image. See the call site in
// MaterializePendingClearForTexture for why a transfer clear cannot do this.
Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue);
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
Bool MaterializePendingClearForRenderbuffer(
@@ -1069,11 +1069,36 @@ namespace MobileGL::MG_Impl::GLImpl {
case TextureTarget::Texture2DMultisampleArray:
textureUploadTarget = TextureUploadTarget::Texture2DMultisampleArray;
break;
case TextureTarget::Texture1DArray:
textureUploadTarget = TextureUploadTarget::Texture1DArray;
break;
case TextureTarget::TextureCubeMapArray:
textureUploadTarget = TextureUploadTarget::CubeMapArray;
break;
default:
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "FramebufferTextureLayer requires a 3D, 2D array or 2D multisample array texture.");
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"FramebufferTextureLayer requires a 3D, array, 2D multisample "
"array, or cube map array texture."));
return;
}
// The same backend question the DSA twin asks. GL 4.6 core 9.2.8 makes the two entry points
// equivalent, so they have to decline in the same places - leaving this one ungated is what
// let an unrepresentable attachment reach the renderer, and it also refused cube map arrays
// that GL requires it to accept.
{
const auto& layerLimits = MG_Backend::pActiveBackendObject
? MG_Backend::pActiveBackendObject->GetDynamicParameters()
: MG_Backend::DynamicBackendParameters{};
const TextureTarget layeredTarget = textureObject->GetTarget();
if ((layer != 0 || layeredTarget == TextureTarget::TextureCubeMapArray) &&
!layerLimits.SupportsPerLayerFramebufferAttachment(layeredTarget)) {
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
return;
}
}
AttachFramebufferTextureLayer(__func__, target, attachment, texture, level, layer, textureUploadTarget);
}
@@ -1433,11 +1458,19 @@ namespace MobileGL::MG_Impl::GLImpl {
// so a slice lands outside the image and the renderer asserts on the clear. Letting it
// through there would only move the failure downstream, so it is declined instead - layer
// zero always works, being the plain first-slice attachment.
const Bool backsLayeredAttachment = limits.SupportsPerLayerFramebufferAttachment;
// A cube map array additionally has no image shape at all in VkTextureManager, so on that
// backend it cannot be an attachment whatever the layer is.
const Bool isCubeMapArray = textureObject->GetTarget() == TextureTarget::TextureCubeMapArray;
if ((layer != 0 && !backsLayeredAttachment) || (isCubeMapArray && !backsLayeredAttachment)) {
// ...and it is a DIFFERENT question per target: a 2D/2D-multisample array layer is a Vulkan
// array layer, a 3D layer is a z slice, and a cube map array needs a cube-compatible image
// before it has any layer to name. Ask the backend about this texture's target rather than
// guessing from one blanket flag.
const TextureTarget layeredTextureTarget = textureObject->GetTarget();
const Bool backsThisTargetsLayers = limits.SupportsPerLayerFramebufferAttachment(layeredTextureTarget);
// Layer zero of a 3D or array texture is the plain first-slice attachment every backend can
// already express, so it stays legal even where per-layer selection is not backed. A cube map
// array has no such fallback: layer zero is still one face of one cube inside a
// cube-compatible image, so it needs the same support layer 5 does.
const Bool needsPerLayerSupport =
layer != 0 || layeredTextureTarget == TextureTarget::TextureCubeMapArray;
if (needsPerLayerSupport && !backsThisTargetsLayers) {
RecordUnsupportedFramebufferTextureAttachmentError(
__func__, "This backend does not resolve a framebuffer attachment's layer onto its image.");
return;
@@ -199,6 +199,29 @@ namespace MobileGL::MG_Util::BackendLoader {
vkGetPhysicalDeviceFeatures(physicalDevice, &supportedFeatures);
caps.SupportsWideLines = supportedFeatures.wideLines == VK_TRUE;
caps.SupportsShaderFloat64 = supportedFeatures.shaderFloat64 == VK_TRUE;
caps.SupportsImageCubeArray = supportedFeatures.imageCubeArray == VK_TRUE;
{
// Probe the formats a colour render target actually uses. A driver that refuses the flag
// for one of them refuses per-slice attachment for that format only, which
// VkTextureManager detects and records at image creation; this field just says whether
// the capability is worth offering at all.
static constexpr VkFormat k3DSliceProbeFormats[] = {VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_R8G8B8A8_SRGB};
Bool all2DArrayCompatible = true;
for (const VkFormat probeFormat : k3DSliceProbeFormats) {
VkImageFormatProperties probeProperties{};
const VkResult probeResult = vkGetPhysicalDeviceImageFormatProperties(
physicalDevice, probeFormat, VK_IMAGE_TYPE_3D, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, &probeProperties);
if (probeResult != VK_SUCCESS) {
all2DArrayCompatible = false;
break;
}
}
caps.Supports2DArrayCompatible3DImages = all2DArrayCompatible;
}
caps.SupportsVertexPipelineStoresAndAtomics =
supportedFeatures.vertexPipelineStoresAndAtomics == VK_TRUE;
caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE;
@@ -290,6 +313,8 @@ namespace MobileGL::MG_Util::BackendLoader {
FillFragmentInterpolationLimits(caps, properties.limits);
caps.SupportsWideLines = false;
caps.SupportsShaderFloat64 = false;
caps.SupportsImageCubeArray = false;
caps.Supports2DArrayCompatible3DImages = false;
// This helper only receives properties, not VkPhysicalDeviceFeatures. Leave optional
// stage writes disabled rather than inferring them from descriptor limits alone.
caps.SupportsVertexPipelineStoresAndAtomics = false;
@@ -78,6 +78,18 @@ namespace MobileGL {
// needs it, which includes every 64-bit vertex attribute: the attribute itself arrives
// as 32-bit words, but the bitcast result and everything computed from it is Float64.
Bool SupportsShaderFloat64 = false;
// VkPhysicalDeviceFeatures::imageCubeArray. Required before a
// VK_IMAGE_VIEW_TYPE_CUBE_ARRAY view may be created at all
// (VUID-VkImageViewCreateInfo-viewType-01004), which is every cube map array texture -
// both its sampled view and its full view.
Bool SupportsImageCubeArray = false;
// VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT on a 3D colour image, i.e. whether one z slice
// of a GL_TEXTURE_3D texture can be named by a 2D view and attached to a framebuffer.
// Vulkan 1.1 core, but per format+usage - this is an OPTIMISTIC summary probed over the
// common colour attachment formats. The authoritative answer is taken per format at
// image creation in VkTextureManager, which withdraws the flag and remembers the verdict
// when a driver refuses it.
Bool Supports2DArrayCompatible3DImages = false;
// Storage-image descriptors are limited per stage by
// maxPerStageDescriptorStorageImages, but writes/atomics outside compute additionally
// require these core Vulkan features to be enabled on the logical device.
+30
View File
@@ -1419,6 +1419,36 @@ namespace MobileGL::MG_Util::SelfTest {
} else {
builder.Warn("dualSrcBlend", "unsupported; GL_SRC1_* dual-source blend factors hard-fail at draw");
}
{
VkImageFormatProperties sliceProbe{};
const Bool sliceCapable =
vkGetPhysicalDeviceImageFormatProperties(
physicalDevice, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TYPE_3D, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT, &sliceProbe) == VK_SUCCESS;
if (sliceCapable) {
builder.Pass("2D-array-compatible 3D images",
"supported for the common colour attachment formats (one z slice of a "
"GL_TEXTURE_3D texture can be attached to a framebuffer and cleared and read "
"back on its own; a format that refuses the flag is detected at image "
"creation and declines per-slice attachment)");
} else {
builder.Warn("2D-array-compatible 3D images",
"VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT unavailable for colour attachments; "
"glFramebufferTextureLayer on a GL_TEXTURE_3D texture is declined for every "
"slice past the first");
}
}
if (features.imageCubeArray == VK_TRUE) {
builder.Pass("imageCubeArray",
"GL_TEXTURE_CUBE_MAP_ARRAY textures get a Vulkan image and can be sampled and "
"attached to a framebuffer per layer");
} else {
builder.Warn("imageCubeArray",
"unsupported; a GL_TEXTURE_CUBE_MAP_ARRAY texture gets no image at all, so sampling "
"one reads nothing and glFramebufferTextureLayer on one is declined");
}
if (features.shaderFloat64 == VK_TRUE) {
builder.Pass("shaderFloat64",
"GLSL double/dvec/dmat and 64-bit vertex attributes (glVertexAttribLFormat) supported");