diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index de34cfba..52699ba4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -203,6 +203,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); XXHASH_VERIFY( @@ -443,6 +444,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the // struct's bytes match the hash the payload was keyed by. ms.minSampleShading = payload.minSampleShading; + // GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until + // now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it. + // The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does. + ms.pSampleMask = &payload.sampleMask; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index f808395a..6cb4a0eb 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -44,6 +44,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { // (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784). Bool sampleShadingEnable = false; Float minSampleShading = 0.0f; + // glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask. One + // word is the whole mask: GL_MAX_SAMPLE_MASK_WORDS is 1 on both backends, and Vulkan + // reads ceil(rasterizationSamples / 32) words. Pipeline state like the two above - + // Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 - so it is + // hashed with them, and 0xffffffff (the GL default, and what a null pSampleMask + // means) has to keep producing the pipeline it always did. + Uint32 sampleMask = 0xffffffffu; Uint32 subpass = 0; VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; Bool primitiveRestartEnable = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index 17cd48df..336a1813 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -37,6 +37,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // glGenTextures ever hands this out, and nothing looks a placeholder up by name - so the // id only has to stay clear of the application's, exactly like the sampled fallback's. constexpr Uint kUnboundStorageImageExternalIndex = 0xFFFFFF01u; + // The multisample sampled fallbacks. Separate ids for the same reason as the two above: + // they must not collide with anything glGenTextures can hand out. + constexpr Uint kFallbackTexture2DMultisampleExternalIndex = 0xFFFFFF02u; + constexpr Uint kFallbackTexture2DMultisampleArrayExternalIndex = 0xFFFFFF03u; // MobileGL's own stand-in textures, by the reserved ids above. Nothing an application can // do reaches one, so anything keyed on the GL object an application bound - image-unit @@ -44,7 +48,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool IsPlaceholderTexture(const MG_State::GLState::ITextureObject* texture) { if (texture == nullptr) return false; const Uint index = static_cast(texture->GetExternalIndex()); - return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex; + return index == kFallbackTexture2DExternalIndex || index == kUnboundStorageImageExternalIndex || + index == kFallbackTexture2DMultisampleExternalIndex || + index == kFallbackTexture2DMultisampleArrayExternalIndex; } // The R32 member of each numeric class. Every one of the three is a MANDATORY-support @@ -369,6 +375,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_textureManager = nullptr; m_samplerManager = nullptr; m_fallbackTexture2D.reset(); + m_fallbackTexture2DMultisample.reset(); + m_fallbackTexture2DMultisampleArray.reset(); } void UniformManager::BeginFrame(Uint32 frameIndex) { @@ -1377,11 +1385,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { } SharedPtr UniformManager::GetFallbackTexture(TextureTarget target) const { - // The fallback is a single-sampled 2D image, so it can only stand in for a sampler that - // would accept one. A multisample sampler in particular cannot: its descriptor demands a - // multisample view, and handing it this one is invalid Vulkan, not a degraded picture. - // Report that there is no fallback and let the caller decline the draw - aborting the - // process over an unbound sampler is never the right answer. + // A multisample sampler cannot be served by the single-sampled 2D image below - its + // descriptor demands a multisample view - so it gets its own placeholder rather than no + // placeholder at all. Without one, ResolveSamplerDescriptor declined and + // BindProgramUniformBuffers dropped the WHOLE draw, which is how every + // sample_variables.*.samples_0 body failed: the CTS's resolve program declares both a + // sampler2D and a sampler2DMS and deliberately points the unused one at an empty texture + // unit, and at samples_0 the unused one is the sampler2DMS. GL says sampling an + // incomplete texture is undefined, not fatal, so the draw has to happen. + if (target == TextureTarget::Texture2DMultisample || + target == TextureTarget::Texture2DMultisampleArray) { + return GetFallbackMultisampleTexture(target); + } if (target != TextureTarget::Texture2D && target != TextureTarget::TextureRectangle) { MGLOG_E_ONCE("UniformManager::GetFallbackTexture: no fallback exists for target=%d", static_cast(target)); @@ -1405,6 +1420,45 @@ namespace MobileGL::MG_Backend::DirectVulkan { return m_fallbackTexture2D; } + SharedPtr UniformManager::GetFallbackMultisampleTexture( + TextureTarget target) const { + const Bool arrayed = target == TextureTarget::Texture2DMultisampleArray; + auto& slot = arrayed ? m_fallbackTexture2DMultisampleArray : m_fallbackTexture2DMultisample; + if (slot != nullptr) { + return slot; + } + + const TextureUploadTarget uploadTarget = arrayed ? TextureUploadTarget::Texture2DMultisampleArray + : TextureUploadTarget::Texture2DMultisample; + SharedPtr texture; + if (arrayed) { + texture = MakeShared( + kFallbackTexture2DMultisampleArrayExternalIndex); + } else { + texture = MakeShared( + kFallbackTexture2DMultisampleExternalIndex); + } + texture->SetInternalFormat(TextureInternalFormat::RGBA8); + // TWO samples, never one. VUID-RuntimeSpirv-samples-08726 forbids an OpTypeImage with + // MS = 1 from reading a VK_SAMPLE_COUNT_1_BIT image, which is exactly the hazard + // VkTextureManager::SyncTextureResource's one-sample floor exists to avoid; a placeholder + // that re-created it would be worse than none. + texture->SetSamples(2); + texture->SetFixedSampleLocations(true); + // No upload, and MarkStorageDirty(dirty = false) to say so: a multisample image cannot be + // written by a transfer at all - it deliberately carries no TRANSFER_DST usage - so unlike + // the 2D fallback this one cannot be given (0, 0, 0, 1) content. Its texels are undefined, + // which is precisely what GL 4.6 core 8.17 promises for a texelFetch on a multisample + // texture that is not complete. The point of the placeholder is that the DRAW happens. + texture->AllocateStorage(uploadTarget, 0, {.texelSize = {1, 1, 1}, .byteSize = 0}); + texture->TruncateMipmapLevels(uploadTarget, 1); + texture->MarkStorageDirty(uploadTarget, 0, false); + slot = texture; + MGLOG_D("UniformManager::GetFallbackMultisampleTexture: created placeholder target=%d", + static_cast(target)); + return slot; + } + VkBufferView UniformManager::AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain, Bool storage) { MOBILEGL_ASSERT(m_bufferManager != nullptr, "AcquireUnboundTexelBufferView: buffer manager is null"); @@ -1589,11 +1643,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // ResolveSamplerDescriptor will substitute the fallback texture for this binding; // include it in the sampled set so the pre-render-pass sync/transition pass covers // its first use instead of leaving that work to happen inside an active pass. - if (preferredTarget != TextureTarget::Texture2D && - preferredTarget != TextureTarget::TextureRectangle) { + // Ask GetFallbackTexture rather than re-listing the targets it serves: that list grew + // a multisample arm and the two must not drift apart. + texture = GetFallbackTexture(preferredTarget).get(); + if (texture == nullptr) { return false; } - texture = GetFallbackTexture(preferredTarget).get(); // The substitution changed the texture, so the "no override" arm of the effective // sampler has to follow it to the fallback's own. if (!samplerOverride) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 55c494fa..b5d4dd1a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -180,6 +180,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element); SharedPtr GetFallbackTexture(TextureTarget target) const; + // The multisample arm of GetFallbackTexture. Separate object per target and no upload + // path: a multisample image cannot be written by a transfer, so its texels stay undefined + // - which is what GL promises for a texelFetch on an incomplete multisample texture. + SharedPtr GetFallbackMultisampleTexture(TextureTarget target) const; // ---- placeholders for UNBOUND image-backed descriptors ------------------------- // GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing // to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete @@ -293,6 +297,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkTextureManager* m_textureManager = nullptr; VkSamplerManager* m_samplerManager = nullptr; mutable SharedPtr m_fallbackTexture2D; + mutable SharedPtr m_fallbackTexture2DMultisample; + mutable SharedPtr m_fallbackTexture2DMultisampleArray; // See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily // populated, never evicted (a program's declared formats are a fixed, tiny set) and torn // down with the manager. The texel views are keyed by format AND by storage-vs-sampled diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 606d2b60..89f3abed 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4767,9 +4767,9 @@ void main() { // build in GetOrCreatePipeline - any new GL-state read there must be added here: // - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides // the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest, - // PrimitiveRestart(+FixedIndex), SampleShading, plus the depth write mask + // PrimitiveRestart(+FixedIndex), SampleShading, SampleMask, plus the depth write mask // - patch vertices, polygon mode, cull face mode, depth func, logic op, - // min sample shading + // min sample shading, the glSampleMaski word // - front/back stencil ops + compare funcs (ref/mask are dynamic state) // - per draw buffer up to the render pass's colour span: indexed blend enable, // blend factors/equations, indexed colour write mask (broadcast from index 0 @@ -4795,6 +4795,7 @@ void main() { capabilityBits |= p.PrimitiveRestartFixedIndexEnabled ? 1ull << 7 : 0; capabilityBits |= p.DepthMask ? 1ull << 8 : 0; capabilityBits |= p.SampleShadingEnabled ? 1ull << 9 : 0; + capabilityBits |= p.SampleMaskEnabled ? 1ull << 10 : 0; Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits); // glMinSampleShading. Hashed by BITS, not by value: this memo compares hashes rather than // versions, so an unhashed float would let a pipeline built at one rate be handed back @@ -4804,6 +4805,13 @@ void main() { std::memcpy(&minSampleShadingBits, &p.MinSampleShadingValue, sizeof(minSampleShadingBits)); hash = CombinePipelineStateWord(hash, static_cast(minSampleShadingBits)); } + // glSampleMaski's word, for the same reason glMinSampleShading's bits are hashed above: + // this memo compares hashes, not versions, so a mask that moved between two otherwise + // identical draws has to key a different pipeline. Hashed unconditionally rather than only + // while GL_SAMPLE_MASK is enabled - the enable bit is already in capabilityBits, and + // folding one more word costs nothing on a path that only recomputes when the + // pipeline-state version moved. + hash = CombinePipelineStateWord(hash, static_cast(p.SampleMaskValue)); hash = CombinePipelineStateWord(hash, static_cast(p.PatchVertices)); // The default tessellation levels belong here for the same reason PatchVertices does: // when a program has an evaluation stage and no control stage, both are compiled into the @@ -5199,6 +5207,12 @@ void main() { .sampleShadingEnable = m_sampleRateShadingFeatureEnabled && MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleShading), .minSampleShading = MG_State::pGLContext->GetMinSampleShadingValue(), + // GL_SAMPLE_MASK off means full coverage, which is what an all-ones mask says and what + // a null pSampleMask used to say by omission. One word: MaxSampleMaskWords is clamped + // to 1 on both backends, so glSampleMaski only ever writes index 0. + .sampleMask = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::SampleMask) + ? MG_State::pGLContext->GetRenderStateParameters().SampleMaskValue + : 0xffffffffu, .subpass = 0, .topology = vkTopology, .primitiveRestartEnable = primitiveRestartEnabled, @@ -7610,7 +7624,8 @@ void main() { Bool VulkanRenderer::ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, - Uint32 depthSlice, const VkClearValue& clearValue) { + Uint32 depthSlice, const VkClearValue& clearValue, + VkImageLayout finalLayout) { 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; @@ -7623,7 +7638,11 @@ void main() { VkAttachmentDescription colorAttachment{}; colorAttachment.format = resource->format; - colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; + // The image's own count, not a hardcoded one: a render-pass attachment must match the + // image it is given (VUID-VkFramebufferCreateInfo-pAttachments-00880), and this helper is + // now also the multisample path - a multisample image carries no TRANSFER_DST usage, so a + // load-op clear is the only legal way to clear it at all. + colorAttachment.samples = resource->sampleCount; colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -7631,7 +7650,7 @@ void main() { 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; + colorAttachment.finalLayout = finalLayout; VkAttachmentReference colorRef{}; colorRef.attachment = 0; @@ -7699,9 +7718,26 @@ void main() { "MaterializePendingClearForTexture requires no active render pass on the target buffer"); auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture); - MOBILEGL_ASSERT(resource != nullptr, - "MaterializePendingClearForTexture: SyncTextureAndGetDescriptor failed for textureId=%d", - texture.GetExternalIndex()); + if (resource == nullptr) { + // Declined sync (an incomplete texture, say). Nothing to clear into, and every line + // below dereferences this - the assert that used to stand here is compiled out of + // every build past DEBUG. + MGLOG_E_ONCE("MaterializePendingClearForTexture: no texture resource for textureId=%d; the queued clears " + "stay queued", + texture.GetExternalIndex()); + return false; + } + + // A multisample image is not a transfer target: SyncTextureResource deliberately withholds + // TRANSFER_DST/TRANSFER_SRC from every one of them, so the vkCmdClearColorImage below - + // and the TRANSFER_DST transition ahead of it - are invalid usage + // (VUID-vkCmdClearColorImage-image-00002) on exactly the shape a + // glClearBufferfv-then-sample sequence produces. Clear it the one way that is legal at + // any sample count instead: a throwaway render pass whose whole content is its load-op + // clear, which is also what the 3D-slice case below already does. + if (resource->sampleCount != VK_SAMPLE_COUNT_1_BIT) { + return MaterializeMultisamplePendingClear(commandBuffer, texture, *resource, pendingClears); + } VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkAccessFlags srcAccessMask = 0; @@ -7841,6 +7877,77 @@ void main() { return true; } + Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer, + MG_State::GLState::ITextureObject& texture, + VkTextureManager::TextureResource& resource, + const Vector& pendingClears) { + // Colour only. GL can queue a depth/stencil clear on a multisample texture too, and the + // load-op idiom would serve it just as well, but this helper attaches its view as a + // COLOUR attachment; declining is honest and leaves the queue intact for a later path. + if ((resource.aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) { + MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d is a multisample depth/stencil texture; " + "its queued clear cannot be materialised out of a render pass yet", + texture.GetExternalIndex()); + return false; + } + + Bool allCleared = true; + for (const auto& pendingClear : pendingClears) { + if (pendingClear.key.mipLevel >= resource.mipLevels) { + MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d pending clear mip=%u out of range %u", + texture.GetExternalIndex(), pendingClear.key.mipLevel, resource.mipLevels); + allCleared = false; + continue; + } + auto clearPayload = pendingClear.payload; + PreCompensateSrgbClearColor(clearPayload, resource.format); + VkClearValue clearValue{}; + clearValue.color = MakeVkClearColorValue(clearPayload, ColorFormatLacksAlpha(&texture)); + + // A multisample texture has exactly one level and, for the 2D target, one layer; the + // array target's layers are cleared one at a time, which is what this helper's + // per-layer view gives us. + const Uint32 firstLayer = pendingClear.key.baseArrayLayer; + const Uint32 layerCount = std::max(pendingClear.key.layerCount, 1u); + for (Uint32 layer = firstLayer; layer < firstLayer + layerCount; ++layer) { + if (layer >= resource.arrayLayers) break; + // COLOR_ATTACHMENT_OPTIMAL, not TRANSFER_DST: the image never has transfer usage, + // and a render target is where it came from and where it is going. + if (!ClearDepthSliceWithRenderPass(commandBuffer, texture, pendingClear.key.mipLevel, layer, + clearValue, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)) { + MGLOG_E_ONCE("MaterializeMultisamplePendingClear: textureId=%d layer %u could not be cleared", + texture.GetExternalIndex(), layer); + allCleared = false; + } + } + } + if (!allCleared) { + return false; + } + + // The load-op clear left every touched layer in COLOR_ATTACHMENT_OPTIMAL (each pass's + // finalLayout), so that - not the tracked layout on entry - is what the closing barrier + // has to start from. + // TransitionImageLayout takes the tracked layout by reference and updates it, so seeding + // it is both how the barrier learns its source and how resource->layout ends up right. + resource.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + const Bool ok = VkTextureManager::TransitionImageLayout( + commandBuffer, resource.image, resource.layout, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, + VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + resource.aspect, 0, resource.mipLevels); + if (!ok) { + MGLOG_E_ONCE("MaterializeMultisamplePendingClear: failed to transition textureId=%d to the sampled layout", + texture.GetExternalIndex()); + return false; + } + + m_clearManager->PopPendingClear(&texture); + MGLOG_D("MaterializeMultisamplePendingClear: textureId=%d pending clear materialised through a load-op pass", + texture.GetExternalIndex()); + return true; + } + Bool VulkanRenderer::MaterializePendingClearForRenderbuffer( VkCommandBuffer commandBuffer, const SharedPtr& renderbuffer) { if (renderbuffer == nullptr) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 1b75e615..9f3648ed 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -1279,13 +1279,25 @@ 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. + // Clears one layer of a colour image through a throwaway render pass whose entire content + // is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z + // slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a + // MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the + // layout the caller already tracks for the whole image, so this never has to touch + // resource->layout. Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture, Uint32 mipLevel, - Uint32 depthSlice, const VkClearValue& clearValue); + Uint32 depthSlice, const VkClearValue& clearValue, + VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); + // The multisample arm of the above. Split out rather than branched inline because it + // shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so + // neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one. + Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer, + MG_State::GLState::ITextureObject& texture, + VkTextureManager::TextureResource& resource, + const Vector& pendingClears); Bool MaterializePendingClearForRenderbuffer( VkCommandBuffer commandBuffer, const SharedPtr& renderbuffer);