diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 93585cc9..9d62a6c5 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -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(target) >= 0 && + static_cast(target) < static_cast(TextureTarget::TextureTargetCount)) + ? (1u << static_cast(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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 5b203f5b..b077fb5f 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -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. diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index b5e55321..590c325c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -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); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index 3a2bcea0..dee01658 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -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( diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index fcd1b809..684f13ba 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -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(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(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(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 " diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 50ec65fb..8a9b5116 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -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 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 m_2dArrayCompatibleUnsupported; std::unordered_map, TextureIdentityHash> m_aliveObjects; std::unordered_map m_textureResources; // Textures that have been bound to a GL image unit (see MarkStorageImageTexture). diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index c93353ea..7837d147 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -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 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. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index e6aba2c3..b7fcbb72 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -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( diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index 7f8312b9..cdf82fd6 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -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("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; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index db6342bf..f5579433 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -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; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h index 37ba8ce5..171a8100 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h @@ -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. diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 2716534d..1b373bb6 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -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");