diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 66e41f18..a4956432 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -120,6 +120,11 @@ namespace MobileGL::MG_Config { // does whenever local_size_x is a multiple of the native width). ForceOff returns // to the raw driver builtin. QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto; + // MOBILEGL_MAGMA_INPASS_CLEAR: a wire-arm glClear / glClearBuffer* whose targets are the + // attachments of the draw pass for the bound framebuffer is recorded inside that pass + // (vkCmdClearAttachments) rather than in a render pass of its own built for the clear. + // Auto is ON; ForceOff restores the standalone clear for every surface. + QuirkOverride MagmaInPassClear = QuirkOverride::Auto; // MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension // string. `double` in a shader always WORKS - it is narrowed to 32 bits before any // module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 1018c448..f3a688d5 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -345,6 +345,7 @@ namespace MobileGL::MG_ConfigLoader { QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH"); features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER"); features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS"); + features.MagmaInPassClear = QueryEnvQuirkOverride("MOBILEGL_MAGMA_INPASS_CLEAR"); features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64"); features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK"); features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index b6386994..f29f2a0d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -757,6 +757,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { struct WireColorBlitResources; WireColorBlitResources* m_wireColorBlitResources = nullptr; void DestroyWireColorBlitResources(); + void ResetWireColorBlitFramePool(Uint32 frameIndex); // P7 wave 2-B, CONTRACT-P7 ยง5.1 (A): the baked depth-mip program (WireDepthMipmap.inc). // One level of a depth chain, source and destination being two depth subresources of // the same image; the caller loops. It owns no frontend object, which is what the @@ -812,13 +813,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { WireDrawPassKey key; UniquePtr pass; Vector views; + // The last submission that recorded this pass (P15 S0): it may be destroyed only once + // that submission completed. + Uint64 lastUseSubmit = 0; }; // Objects only: every draw still ends/begins its pass and records the - // existing memory dependencies. Each slot is cleared after its fence. + // existing memory dependencies. P15 S0: ONE cache across frame slots, so a framebuffer + // drawn every frame keeps its render pass, framebuffer and views instead of rebuilding them + // each frame. An entry whose key's image epochs moved can never hit again (the key holds + // them) and is destroyed once its last use completed (ClearWireDrawPassCache). Bool AcquireCachedWireDrawPass(const WireDrawPassKey& key); void ClearWireDrawPassCache(Uint32 frameIndex); void ClearAllWireDrawPassCaches(); - Vector> m_wireDrawPassCaches; + void DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry); + Vector m_wireDrawPassCache; WireDrawPassKey m_wireDrawPassKey; WireRenderPassCompatibilityTable m_wireRenderPassCompatibility; // The interned compatibility id of m_wireDrawPass's attachments, or 0 when not known yet. @@ -872,6 +880,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool WirePassMemoHolds(const MG_Pipe::MGPFramebufferState& fbo, const FrameContext::FrameData& frame) const; Uint64 m_wirePassMarksClearGeneration = 0; WireDrawScratch m_wireDrawScratch; + Bool PrepareWireDrawPass(FrameContext::FrameData& frame, const MG_Pipe::MGPFramebufferState& fbo, + WireDrawScratch& scratch, Bool passContinues, Bool& continuing); + // P15: glClear / glClearBuffer* into the attachments of the wire draw pass, recorded inside it + // (vkCmdClearAttachments). Returns the GL clear-mask bits it could not take; those go through + // the standalone path. + GLbitfield ClearWireFramebufferInPass(const MG_Pipe::MGPFramebufferState& fbo, + const ClearAttachmentPayload& payload, GLint drawbuffer); struct WireRetiredObjects { Uint64 submitIndex = 0; UniquePtr drawPass; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc index 95b03ffb..b3d08771 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc @@ -14,8 +14,19 @@ VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE; Vector targets; + // P15 S0: one descriptor pool per frame slot for the blit's set, reset when the slot's + // previous recording is known complete (ClearWireDrawPassCache). A pool created and + // destroyed per blit was ~0.16 ms a blit on Adreno 750. A full pool falls back to that. + static constexpr Uint32 kFramePoolSets = 64; + Vector framePools; }; + void VulkanRenderer::ResetWireColorBlitFramePool(Uint32 frameIndex) { + auto& pools = m_wireColorBlitResources->framePools; + if (frameIndex < pools.size() && pools[frameIndex] != VK_NULL_HANDLE) + vkResetDescriptorPool(m_device, pools[frameIndex], 0); + } + void VulkanRenderer::DestroyWireColorBlitResources() { if (!m_wireColorBlitResources) return; // Shutdown calls this after vkDeviceWaitIdle and after all per-submit @@ -25,6 +36,8 @@ vkDestroyPipeline(m_device, target.pipeline, nullptr); vkDestroyRenderPass(m_device, target.renderPass, nullptr); } + for (const auto pool : cache.framePools) + if (pool != VK_NULL_HANDLE) vkDestroyDescriptorPool(m_device, pool, nullptr); vkDestroySampler(m_device, cache.nearest, nullptr); vkDestroySampler(m_device, cache.linear, nullptr); vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr); @@ -328,17 +341,36 @@ // would rewrite the source view of an older queued blit. VkDescriptorPool descriptorPool = VK_NULL_HANDLE; VkDescriptorSet descriptorSet = VK_NULL_HANDLE; - const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; - VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - pool.maxSets = 1; - pool.poolSizeCount = 1; - pool.pPoolSizes = &poolSize; - VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool)); VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - alloc.descriptorPool = descriptorPool; alloc.descriptorSetCount = 1; alloc.pSetLayouts = &cache.descriptorLayout; - VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet)); + { + // The frame slot's pool (P15 S0): its sets live until the slot's next frame begins, + // which is after this recording completed. + const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex(); + if (cache.framePools.size() <= frameIndex) cache.framePools.resize(frameIndex + 1, VK_NULL_HANDLE); + if (cache.framePools[frameIndex] == VK_NULL_HANDLE) { + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + WireColorBlitResources::kFramePoolSets}; + VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + pool.maxSets = WireColorBlitResources::kFramePoolSets; + pool.poolSizeCount = 1; + pool.pPoolSizes = &poolSize; + VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &cache.framePools[frameIndex])); + } + alloc.descriptorPool = cache.framePools[frameIndex]; + if (vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet) != VK_SUCCESS) descriptorSet = VK_NULL_HANDLE; + } + if (descriptorSet == VK_NULL_HANDLE) { + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; + VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + pool.maxSets = 1; + pool.poolSizeCount = 1; + pool.pPoolSizes = &poolSize; + VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool)); + alloc.descriptorPool = descriptorPool; + VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet)); + } // GL_LINEAR filters only a STRETCHED blit; an unscaled one (mirrored or not, on any of the // four transforms) is a copy and samples NEAREST, which is exact where linear filtering // at a quantised coordinate is not (WireColorBlitFilter.h). The format check above still @@ -399,7 +431,7 @@ WireRetiredObjects retired{}; retired.framebuffers.push_back(framebuffer); retired.imageViews = {sourceView, destinationView}; - retired.descriptorPools.push_back(descriptorPool); + if (descriptorPool != VK_NULL_HANDLE) retired.descriptorPools.push_back(descriptorPool); RetireWireObjects(Move(retired)); if (destination.isDefault) m_swapchainObject.SetImageContentDefined(destination.swapchainImageIndex, true); return true; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index d31ff4f6..855d838d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -127,28 +127,25 @@ auto& frame = m_frameContext.GetCurrent(); if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer); - const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex(); - if (m_wireDrawPassCaches.size() < m_frameContext.GetFrameCount()) - m_wireDrawPassCaches.resize(m_frameContext.GetFrameCount()); - auto& cache = m_wireDrawPassCaches[frameIndex]; - // Bound a present-less stream of distinct framebuffer configurations. - // Overflow retains the original submit-index retirement path. - constexpr SizeT kMaxCachedWirePassesPerFrame = 32; - if (cache.size() < kMaxCachedWirePassesPerFrame) { - cache.push_back({Move(m_wireDrawPassKey), Move(m_wireDrawPass), Move(m_wireDrawViews)}); - return; - } - WireRetiredObjects retired{}; - retired.drawPass = Move(m_wireDrawPass); - retired.imageViews = Move(m_wireDrawViews); - RetireWireObjects(Move(retired)); + // The pass was recorded into the current recording: its last use is the next submission. + m_wireDrawPassCache.push_back( + {Move(m_wireDrawPassKey), Move(m_wireDrawPass), Move(m_wireDrawViews), GetWireNextSubmitIndex()}); m_wireDrawPassKey = {}; + // Bound a stream of distinct framebuffer configurations: the oldest entry goes through the + // submit-indexed retirement, which waits for its last use like every retired object. + constexpr SizeT kMaxCachedWirePasses = 64; + if (m_wireDrawPassCache.size() > kMaxCachedWirePasses) { + auto& oldest = m_wireDrawPassCache.front(); + WireRetiredObjects retired{}; + retired.drawPass = Move(oldest.pass); + retired.imageViews = Move(oldest.views); + RetireWireObjects(Move(retired)); + m_wireDrawPassCache.erase(m_wireDrawPassCache.begin()); + } } Bool VulkanRenderer::AcquireCachedWireDrawPass(const WireDrawPassKey& key) { - const Uint32 frameIndex = m_frameContext.GetCurrentFrameIndex(); - if (frameIndex >= m_wireDrawPassCaches.size()) return false; - auto& cache = m_wireDrawPassCaches[frameIndex]; + auto& cache = m_wireDrawPassCache; for (auto it = cache.begin(); it != cache.end(); ++it) { // Exact values, not a compatibility hash: two FBOs with the same // formats can name different images, windows or draw-buffer slots. @@ -162,19 +159,47 @@ return false; } - void VulkanRenderer::ClearWireDrawPassCache(Uint32 frameIndex) { - if (frameIndex >= m_wireDrawPassCaches.size()) return; - auto& cache = m_wireDrawPassCaches[frameIndex]; - for (auto& entry : cache) { - entry.pass.reset(); - for (const auto view : entry.views) vkDestroyImageView(m_device, view, nullptr); - } - cache.clear(); + void VulkanRenderer::DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry) { + entry.pass.reset(); + for (const auto view : entry.views) vkDestroyImageView(m_device, view, nullptr); + entry.views.clear(); } + // At a frame slot's begin, after its fence (Present): the slot's blit sets are free, and every + // cached pass whose image epochs moved - it can never hit again - is dropped before the texture + // manager may release retired images: destroyed now when its last use completed, else retired + // behind that use (which also keeps any image it names alive until then). + void VulkanRenderer::ClearWireDrawPassCache(Uint32 frameIndex) { + if (m_wireColorBlitResources) ResetWireColorBlitFramePool(frameIndex); + const Uint64 imageEpoch = m_textureManager->GetTextureImageEpoch(); + const Uint64 eraseEpoch = m_textureManager->GetResourceEraseEpoch(); + SizeT kept = 0; + for (SizeT i = 0; i < m_wireDrawPassCache.size(); ++i) { + auto& entry = m_wireDrawPassCache[i]; + if (entry.key.textureImageEpoch == imageEpoch && entry.key.resourceEraseEpoch == eraseEpoch) { + if (kept != i) m_wireDrawPassCache[kept] = Move(entry); + ++kept; + continue; + } + if (entry.lastUseSubmit <= m_completedSubmitCounter) { + DestroyWireDrawPassCacheEntry(entry); + } else { + WireRetiredObjects retired{}; + retired.drawPass = Move(entry.pass); + retired.imageViews = Move(entry.views); + RetireWireObjects(Move(retired)); + } + } + m_wireDrawPassCache.resize(kept); + } + + // Idle points only (shutdown, swapchain replacement, the device-idle drain): nothing in flight. void VulkanRenderer::ClearAllWireDrawPassCaches() { - for (Uint32 frameIndex = 0; frameIndex < m_wireDrawPassCaches.size(); ++frameIndex) - ClearWireDrawPassCache(frameIndex); + for (auto& entry : m_wireDrawPassCache) DestroyWireDrawPassCacheEntry(entry); + m_wireDrawPassCache.clear(); + if (m_wireColorBlitResources) + for (Uint32 frameIndex = 0; frameIndex < m_frameContext.GetFrameCount(); ++frameIndex) + ResetWireColorBlitFramePool(frameIndex); } void VulkanRenderer::CollectWireObjects(Uint64 completedSubmit, Bool all) { @@ -235,6 +260,217 @@ return true; } + // P15: the draw pass for `fbo`, continued (passContinues: the WirePassMemo holds) or + // looked up / built from a full attachment walk. Shared by SetupWireDraw and the in-pass + // clear (WireFramebuffer.inc): a clear into the attachments of the open pass is recorded + // inside it instead of closing it and building a pass of its own. Leaves m_wireDrawPass and + // m_wireDrawPassKey describing the pass, `continuing` true when it is the open one. + Bool VulkanRenderer::PrepareWireDrawPass(FrameContext::FrameData& frame, + const MG_Pipe::MGPFramebufferState& fbo, WireDrawScratch& scratch, Bool passContinues, + Bool& continuing) { + continuing = true; + if (passContinues) { + auto& store = MG_Record::ServerStagedTexture(); + if (!m_wirePassMarksValid || m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) { + for (const auto& mark : m_wirePassMemo.marks) + m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers); + m_wirePassMarksValid = true; + m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration(); + } + } else { + m_wirePassMemo.valid = false; + auto& attachments = scratch.attachments; + auto& viewInfos = scratch.viewInfos; + auto& colors = scratch.colors; + attachments.clear(); + viewInfos.clear(); + colors.clear(); + VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL}; + VkExtent2D extent{~0u, ~0u}; + Uint32 layers = ~0u; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; + Uint64 passHash = 0; + auto& passKey = scratch.passKey; + passKey.ResetKeepingCapacity(); + passKey.isDefault = fbo.IsDefault; + passKey.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb); + if (fbo.IsDefault) { + passKey.swapchain = m_swapchainObject.GetHandle(); + // A drawing pass on the default framebuffer is what makes the freshly acquired + // swapchain image the one GL addresses (see m_defaultFramebufferImageIndex); the + // attachments below resolve the same index, so this key has to be taken after it. + passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true); + } + auto& sameLayoutAttachments = scratch.sameLayoutAttachments; + sameLayoutAttachments.clear(); + scratch.gpuWriteMarks.clear(); + m_wirePassMemo.attachments.clear(); + const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 { + WireImage image = ResolveWireImage(fbo, surface, aspect, /*isWriteTarget=*/true); + if (!image.image) return VK_ATTACHMENT_UNUSED; + m_textureManager->FlushPendingUploads(); + m_wirePassMemo.attachments.push_back( + {image.resource, image.storage, surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer}); + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) + image.format = ResolveSrgbAttachmentWriteFormat(image.format, + MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb)); + const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ? + VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL; + // Already in its attachment layout: its barrier (which closes any open pass) waits until + // it is known whether the open pass continues with this very attachment. + if (image.layout == layout) sameLayoutAttachments.push_back(image); + else TransitionWireImage(image, layout); + VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + info.image = image.image; + info.viewType = image.viewType; + info.format = image.format; + info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers}; + if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT) + info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + viewInfos.push_back(info); + passKey.attachments.push_back({image.storage, static_cast(surface.Kind), image.image, + image.viewType, image.format, info.subresourceRange.aspectMask, aspect, + image.level, image.layer, image.layers, image.levels, image.extent.width, image.extent.height, + image.samples}); + VkAttachmentDescription description{}; + description.format = image.format; + description.samples = image.samples; + description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + description.initialLayout = description.finalLayout = layout; + attachments.push_back(description); + extent.width = std::min(extent.width, image.extent.width); + extent.height = std::min(extent.height, image.extent.height); + layers = std::min(layers, image.layers); + samples = image.samples; + passHash = XXH64(&description, sizeof(description), passHash); + // Marked below, once the pass is known: a continued pass repeats the same marks. + if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) + scratch.gpuWriteMarks.push_back({image.storage, image.level, image.layer, image.layers}); + if (image.isDefault) { + if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true); + else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true); + } + return static_cast(attachments.size() - 1); + }; + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + const Int32 slot = fbo.DrawBuffers[i]; + const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT); + colors.push_back({index, VK_IMAGE_LAYOUT_GENERAL}); + } + while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back(); + if (fbo.IsDefault || fbo.Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + depth.attachment = add(fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT); + else if (fbo.Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + depth.attachment = add(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); + if (attachments.empty()) { + MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments"); + return false; + } + passKey.width = extent.width; + passKey.height = extent.height; + passKey.layers = layers; + passKey.depthReference = depth.attachment; + for (const auto& color : colors) passKey.colorReferences.push_back(color.attachment); + // A native handle can be recycled after image replacement or death. + // Sample epochs after every lazy resolve, then compare them with the key. + passKey.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); + passKey.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch(); + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = static_cast(colors.size()); + subpass.pColorAttachments = colors.data(); + subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = static_cast(attachments.size()); + passInfo.pAttachments = attachments.data(); + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + continuing = m_wireDrawPass && m_wireDrawPassKey == passKey; + { + const auto* active = VkRenderPassManager::GetActiveRenderPass(); + const Bool stillOpen = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer; + // A new pass orders itself after earlier writes to its attachments; a continued one + // is the same pass, whose attachment writes are ordered by rasterization. + if (!stillOpen) + for (auto& image : sameLayoutAttachments) TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL); + } + if (!continuing) { + RetireWireDrawPass(); + m_wireDrawPassCompatibilityId = 0; + } + if (!continuing && !AcquireCachedWireDrawPass(passKey)) { + // A copy, not a move: passKey is the scratch's, and a new pass is the rare case. + m_wireDrawPassKey = passKey; + Vector views; + views.reserve(viewInfos.size()); + for (const auto& info : viewInfos) { + VkImageView view = VK_NULL_HANDLE; + VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view)); + m_wireDrawViews.push_back(view); + views.push_back(view); + } + m_wireDrawPass = MakeUnique(); + auto& created = *m_wireDrawPass; + VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &created.renderPass)); + VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + fbInfo.renderPass = created.renderPass; + fbInfo.attachmentCount = static_cast(views.size()); + fbInfo.pAttachments = views.data(); + fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers; + VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &created.framebuffer)); + created.attachmentCount = static_cast(attachments.size()); + created.colorAttachmentCount = static_cast(colors.size()); + created.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED; + created.sampleCount = samples; + created.extent = {static_cast(extent.width), static_cast(extent.height)}; + created.layers = layers; + created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash); + } + // The attachments' GPU-write marks (MarkWireTextureGpuWritten): idempotent while nothing + // clears a GPU-dirty mark, so a draw that continues a pass whose draws already marked the + // same attachments - the pass key compared equal - repeats them only after a clear. + { + auto& store = MG_Record::ServerStagedTexture(); + if (!continuing || !m_wirePassMarksValid || + m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) { + for (const auto& mark : scratch.gpuWriteMarks) + m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers); + m_wirePassMarksValid = true; + m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration(); + } + } + { + // What this walk proved, for WirePassMemoHolds on the next draw. + const auto& st = MG_Pipe::MGPipeApplier(); + auto& memo = m_wirePassMemo; + memo.valid = !fbo.IsDefault; + memo.framebufferSerial = st.FramebufferSerial; + memo.contextSerial = st.ContextSerial; + memo.drawFramebuffer = st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)]; + memo.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); + memo.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch(); + memo.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb); + memo.passHash = m_wireDrawPass->hash; + memo.marks.assign(scratch.gpuWriteMarks.begin(), scratch.gpuWriteMarks.end()); + } + // The compatibility key is built from this walk's attachments only when the pass is new. + if (m_wireDrawPassCompatibilityId == 0) { + auto& pipelinePassKey = scratch.compatibilityKey; + pipelinePassKey.attachmentFormatsAndSamples.clear(); + pipelinePassKey.colorReferences.clear(); + for (const auto& attachment : attachments) + pipelinePassKey.attachmentFormatsAndSamples.push_back( + (static_cast(attachment.format) << 32) | static_cast(attachment.samples)); + for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment); + pipelinePassKey.depthReference = depth.attachment; + m_wireDrawPassCompatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey); + } + } // !passContinues + return true; + } + + Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) { auto& state = MG_Pipe::MGPipeApplier(); @@ -533,204 +769,7 @@ if (passContinues) passContinues = WirePassMemoHolds(*fbo, frame); // A memo that holds is the open pass itself (WirePassMemoHolds compared the active pass). Bool continuing = true; - if (passContinues) { - auto& store = MG_Record::ServerStagedTexture(); - if (!m_wirePassMarksValid || m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) { - for (const auto& mark : m_wirePassMemo.marks) - m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers); - m_wirePassMarksValid = true; - m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration(); - } - } else { - m_wirePassMemo.valid = false; - auto& attachments = scratch.attachments; - auto& viewInfos = scratch.viewInfos; - auto& colors = scratch.colors; - attachments.clear(); - viewInfos.clear(); - colors.clear(); - VkAttachmentReference depth{VK_ATTACHMENT_UNUSED, VK_IMAGE_LAYOUT_GENERAL}; - VkExtent2D extent{~0u, ~0u}; - Uint32 layers = ~0u; - VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; - Uint64 passHash = 0; - auto& passKey = scratch.passKey; - passKey.ResetKeepingCapacity(); - passKey.isDefault = fbo->IsDefault; - passKey.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb); - if (fbo->IsDefault) { - passKey.swapchain = m_swapchainObject.GetHandle(); - // A drawing pass on the default framebuffer is what makes the freshly acquired - // swapchain image the one GL addresses (see m_defaultFramebufferImageIndex); the - // attachments below resolve the same index, so this key has to be taken after it. - passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true); - } - auto& sameLayoutAttachments = scratch.sameLayoutAttachments; - sameLayoutAttachments.clear(); - scratch.gpuWriteMarks.clear(); - m_wirePassMemo.attachments.clear(); - const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 { - WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true); - if (!image.image) return VK_ATTACHMENT_UNUSED; - m_textureManager->FlushPendingUploads(); - m_wirePassMemo.attachments.push_back( - {image.resource, image.storage, surface.Kind == MG_Pipe::kMGPipeSurfaceKindRenderbuffer}); - if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) - image.format = ResolveSrgbAttachmentWriteFormat(image.format, - MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb)); - const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ? - VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL; - // Already in its attachment layout: its barrier (which closes any open pass) waits until - // it is known whether the open pass continues with this very attachment. - if (image.layout == layout) sameLayoutAttachments.push_back(image); - else TransitionWireImage(image, layout); - VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; - info.image = image.image; - info.viewType = image.viewType; - info.format = image.format; - info.subresourceRange = {aspect, image.level, 1, image.layer, image.layers}; - if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT) - info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; - viewInfos.push_back(info); - passKey.attachments.push_back({image.storage, static_cast(surface.Kind), image.image, - image.viewType, image.format, info.subresourceRange.aspectMask, aspect, - image.level, image.layer, image.layers, image.levels, image.extent.width, image.extent.height, - image.samples}); - VkAttachmentDescription description{}; - description.format = image.format; - description.samples = image.samples; - description.loadOp = description.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; - description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; - description.initialLayout = description.finalLayout = layout; - attachments.push_back(description); - extent.width = std::min(extent.width, image.extent.width); - extent.height = std::min(extent.height, image.extent.height); - layers = std::min(layers, image.layers); - samples = image.samples; - passHash = XXH64(&description, sizeof(description), passHash); - // Marked below, once the pass is known: a continued pass repeats the same marks. - if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) - scratch.gpuWriteMarks.push_back({image.storage, image.level, image.layer, image.layers}); - if (image.isDefault) { - if (aspect == VK_IMAGE_ASPECT_COLOR_BIT) m_swapchainObject.SetImageContentDefined(image.swapchainImageIndex, true); - else m_swapchainObject.SetDepthStencilContentDefined(image.swapchainImageIndex, true); - } - return static_cast(attachments.size() - 1); - }; - for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { - const Int32 slot = fbo->DrawBuffers[i]; - const Uint32 index = slot < 0 ? VK_ATTACHMENT_UNUSED : add(fbo->Color[slot], VK_IMAGE_ASPECT_COLOR_BIT); - colors.push_back({index, VK_IMAGE_LAYOUT_GENERAL}); - } - while (!colors.empty() && colors.back().attachment == VK_ATTACHMENT_UNUSED) colors.pop_back(); - if (fbo->IsDefault || fbo->Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone) - depth.attachment = add(fbo->Depth, VK_IMAGE_ASPECT_DEPTH_BIT); - else if (fbo->Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) - depth.attachment = add(fbo->Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); - if (attachments.empty()) { - MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments"); - return false; - } - passKey.width = extent.width; - passKey.height = extent.height; - passKey.layers = layers; - passKey.depthReference = depth.attachment; - for (const auto& color : colors) passKey.colorReferences.push_back(color.attachment); - // A native handle can be recycled after image replacement or death. - // Sample epochs after every lazy resolve, then compare them with the key. - passKey.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); - passKey.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch(); - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = static_cast(colors.size()); - subpass.pColorAttachments = colors.data(); - subpass.pDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED ? &depth : nullptr; - VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; - passInfo.attachmentCount = static_cast(attachments.size()); - passInfo.pAttachments = attachments.data(); - passInfo.subpassCount = 1; - passInfo.pSubpasses = &subpass; - continuing = m_wireDrawPass && m_wireDrawPassKey == passKey; - { - const auto* active = VkRenderPassManager::GetActiveRenderPass(); - const Bool stillOpen = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer; - // A new pass orders itself after earlier writes to its attachments; a continued one - // is the same pass, whose attachment writes are ordered by rasterization. - if (!stillOpen) - for (auto& image : sameLayoutAttachments) TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL); - } - if (!continuing) { - RetireWireDrawPass(); - m_wireDrawPassCompatibilityId = 0; - } - if (!continuing && !AcquireCachedWireDrawPass(passKey)) { - // A copy, not a move: passKey is the scratch's, and a new pass is the rare case. - m_wireDrawPassKey = passKey; - Vector views; - views.reserve(viewInfos.size()); - for (const auto& info : viewInfos) { - VkImageView view = VK_NULL_HANDLE; - VK_VERIFY(vkCreateImageView(m_device, &info, nullptr, &view)); - m_wireDrawViews.push_back(view); - views.push_back(view); - } - m_wireDrawPass = MakeUnique(); - auto& created = *m_wireDrawPass; - VK_VERIFY(vkCreateRenderPass(m_device, &passInfo, nullptr, &created.renderPass)); - VkFramebufferCreateInfo fbInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; - fbInfo.renderPass = created.renderPass; - fbInfo.attachmentCount = static_cast(views.size()); - fbInfo.pAttachments = views.data(); - fbInfo.width = extent.width; fbInfo.height = extent.height; fbInfo.layers = layers; - VK_VERIFY(vkCreateFramebuffer(m_device, &fbInfo, nullptr, &created.framebuffer)); - created.attachmentCount = static_cast(attachments.size()); - created.colorAttachmentCount = static_cast(colors.size()); - created.hasDepthStencilAttachment = depth.attachment != VK_ATTACHMENT_UNUSED; - created.sampleCount = samples; - created.extent = {static_cast(extent.width), static_cast(extent.height)}; - created.layers = layers; - created.hash = created.compatibilityHash = XXH64(colors.data(), colors.size() * sizeof(colors[0]), passHash); - } - // The attachments' GPU-write marks (MarkWireTextureGpuWritten): idempotent while nothing - // clears a GPU-dirty mark, so a draw that continues a pass whose draws already marked the - // same attachments - the pass key compared equal - repeats them only after a clear. - { - auto& store = MG_Record::ServerStagedTexture(); - if (!continuing || !m_wirePassMarksValid || - m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) { - for (const auto& mark : scratch.gpuWriteMarks) - m_textureManager->MarkWireTextureGpuWritten(mark.storage, mark.level, mark.layer, mark.layers); - m_wirePassMarksValid = true; - m_wirePassMarksClearGeneration = store.GpuDirtyClearGeneration(); - } - } - { - // What this walk proved, for WirePassMemoHolds on the next draw. - const auto& st = MG_Pipe::MGPipeApplier(); - auto& memo = m_wirePassMemo; - memo.valid = !fbo->IsDefault; - memo.framebufferSerial = st.FramebufferSerial; - memo.contextSerial = st.ContextSerial; - memo.drawFramebuffer = st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)]; - memo.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); - memo.resourceEraseEpoch = m_textureManager->GetResourceEraseEpoch(); - memo.framebufferSrgb = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::FramebufferSrgb); - memo.passHash = m_wireDrawPass->hash; - memo.marks.assign(scratch.gpuWriteMarks.begin(), scratch.gpuWriteMarks.end()); - } - // The compatibility key is built from this walk's attachments only when the pass is new. - if (m_wireDrawPassCompatibilityId == 0) { - auto& pipelinePassKey = scratch.compatibilityKey; - pipelinePassKey.attachmentFormatsAndSamples.clear(); - pipelinePassKey.colorReferences.clear(); - for (const auto& attachment : attachments) - pipelinePassKey.attachmentFormatsAndSamples.push_back( - (static_cast(attachment.format) << 32) | static_cast(attachment.samples)); - for (const auto& color : colors) pipelinePassKey.colorReferences.push_back(color.attachment); - pipelinePassKey.depthReference = depth.attachment; - m_wireDrawPassCompatibilityId = m_wireRenderPassCompatibility.Intern(pipelinePassKey); - } - } // !passContinues + if (!PrepareWireDrawPass(frame, *fbo, scratch, passContinues, continuing)) return false; auto& pass = *m_wireDrawPass; // Interned once per pass (above): a continued pass has the same compatibility. const Uint64 compatibilityId = m_wireDrawPassCompatibilityId; @@ -826,6 +865,150 @@ return true; } + // P15 S0: A CLEAR INTO THE OPEN PASS'S ATTACHMENTS IS RECORDED INSIDE THE PASS. + // + // The standalone path (ClearWireFramebuffer's per-surface lambda) closes the draw pass, then + // builds an image view, a render pass and a framebuffer for every cleared surface, runs a pass + // of its own and retires all three: about 70 us a clear on Adreno 750, and every following draw + // to the same framebuffer opens a new pass (FCL MC 1.21.5: 6 clears a frame, 0.42 ms). Here the + // clear takes the pass a draw to this framebuffer would use (PrepareWireDrawPass: the open one, + // a cached one, or a new one through the same walk) and becomes a vkCmdClearAttachments in it, + // so the draws after it continue the same pass. + // + // vkCmdClearAttachments inside the subpass is ordered with the pass's draws as a draw is, writes + // through the same attachment views (the UNORM twin of an sRGB image while GL_FRAMEBUFFER_SRGB + // is off, which is exactly GL's raw clear - no pre-compensation), and needs no layout change: + // wire attachments live in GENERAL. What it cannot express goes back to the standalone path, + // per GL buffer bit: a partial colour or stencil write mask (that path warns and skips, as + // before), an attachment whose extent or layer count differs from the pass's (the pass covers the + // common area only), and a stencil clear the pass's depth attachment does not carry. + GLbitfield VulkanRenderer::ClearWireFramebufferInPass(const MG_Pipe::MGPFramebufferState& fbo, + const ClearAttachmentPayload& payload, GLint drawbuffer) { + GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + if (!remaining || MG_Config::Features.MagmaInPassClear == MG_Config::QuirkOverride::ForceOff) return remaining; + // Nested inside a draw's preparation the scratch and the pass belong to that draw. + if (m_wirePreparationDepth != 0) return remaining; + auto& frame = m_frameContext.GetCurrent(); + WirePreparationScope preparing(m_wirePreparationDepth); + // As SetupWireDraw: resolve every attachment (a lazy upload or promotion may submit the + // recording) before the pass is chosen, unless the open pass provably holds them. + Bool passContinues = WirePassMemoHolds(fbo, frame); + if (!passContinues) { + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) { + const Int32 slot = fbo.DrawBuffers[i]; + if (slot >= 0) (void)ResolveWireImage(fbo, fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT); + } + if (fbo.IsDefault || fbo.Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + (void)ResolveWireImage(fbo, fbo.Depth, VK_IMAGE_ASPECT_DEPTH_BIT); + else if (fbo.Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone) + (void)ResolveWireImage(fbo, fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT); + } + m_textureManager->FlushPendingUploads(); + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + if (passContinues) passContinues = WirePassMemoHolds(fbo, frame); + Bool continuing = true; + if (!PrepareWireDrawPass(frame, fbo, m_wireDrawScratch, passContinues, continuing)) return remaining; + const auto& key = m_wireDrawPassKey; + auto& pass = *m_wireDrawPass; + const auto fits = [&](Uint32 attachment) { + if (attachment >= key.attachments.size()) return false; + const auto& a = key.attachments[attachment]; + return a.width == key.width && a.height == key.height && a.layers == key.layers; + }; + + VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{}; + Uint32 count = 0; + GLbitfield handled = 0; + if (remaining & GL_COLOR_BUFFER_BIT) { + Bool all = true; + Uint32 colorCount = 0; + VkClearAttachment colorClears[MG_Pipe::kMGPipeMaxColorAttachments]{}; + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) { + if (drawbuffer >= 0 && static_cast(drawbuffer) != i) continue; + const Int32 attachment = fbo.DrawBuffers[i]; + if (attachment < 0 || attachment >= static_cast(MG_Pipe::kMGPipeMaxColorAttachments)) continue; + if (!fbo.IsDefault && fbo.Color[attachment].Kind == MG_Pipe::kMGPipeSurfaceKindNone) continue; + const auto mask = MG_Pipe::gPipeInputs.GetColorMaskIndexed(i); + if (!mask.r() && !mask.g() && !mask.b() && !mask.a()) continue; + if (!mask.r() || !mask.g() || !mask.b() || !mask.a()) { all = false; break; } + if (i >= key.colorReferences.size() || key.colorReferences[i] == VK_ATTACHMENT_UNUSED || + !fits(key.colorReferences[i])) { all = false; break; } + auto& clear = colorClears[colorCount++]; + clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clear.colorAttachment = i; + clear.clearValue.color = MakeVkClearColorValue(payload, false); + } + if (all) { + for (Uint32 c = 0; c < colorCount; ++c) clears[count++] = colorClears[c]; + handled |= GL_COLOR_BUFFER_BIT; + } + } + const WireDrawAttachmentKey* depthKey = + key.depthReference != VK_ATTACHMENT_UNUSED && fits(key.depthReference) ? &key.attachments[key.depthReference] + : nullptr; + VkImageAspectFlags depthStencilAspects = 0; + if (remaining & GL_DEPTH_BUFFER_BIT) { + if (!MG_Pipe::gPipeInputs.GetDepthMask()) { + handled |= GL_DEPTH_BUFFER_BIT; // nothing to write, as on the standalone path + } else if (!fbo.IsDefault && fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) { + handled |= GL_DEPTH_BUFFER_BIT; // no depth surface: nothing to clear + } else if (depthKey && depthKey->requestedAspect == VK_IMAGE_ASPECT_DEPTH_BIT && + (depthKey->aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) { + depthStencilAspects |= VK_IMAGE_ASPECT_DEPTH_BIT; + handled |= GL_DEPTH_BUFFER_BIT; + } + } + if (remaining & GL_STENCIL_BUFFER_BIT) { + const Uint32 mask = MG_Pipe::gPipeInputs.GetStencilState(StencilFace::Front).WriteMask & 255u; + if (mask == 0) { + handled |= GL_STENCIL_BUFFER_BIT; + } else if (!fbo.IsDefault && fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) { + handled |= GL_STENCIL_BUFFER_BIT; + } else if (mask == 255u && depthKey && (depthKey->aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) { + // The pass's depth/stencil attachment is the stencil surface itself: a stencil-only + // framebuffer, the default framebuffer, or a depth surface that is the same image + // subresource (a DEPTH_STENCIL attachment point). + const auto& depth = fbo.Depth; + const auto& stencil = fbo.Stencil; + const Bool same = fbo.IsDefault || depthKey->requestedAspect == VK_IMAGE_ASPECT_STENCIL_BIT || + (depth.Kind == stencil.Kind && depth.Res == stencil.Res && + depth.Level == stencil.Level && depth.Layer == stencil.Layer && depth.Layered == stencil.Layered); + if (same) { + depthStencilAspects |= VK_IMAGE_ASPECT_STENCIL_BIT; + handled |= GL_STENCIL_BUFFER_BIT; + } + } + } + if (depthStencilAspects) { + auto& clear = clears[count++]; + clear.aspectMask = depthStencilAspects; + clear.clearValue.depthStencil = {payload.depth, payload.stencil}; + } + if (count == 0) return remaining & ~handled; + + VkRect2D area{{0, 0}, {key.width, key.height}}; + if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) { + const auto& box = MG_Pipe::gPipeInputs.GetScissorBox(); + const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0); + const Int32 x1 = std::min(box.x() + box.z(), static_cast(key.width)); + const Int32 y1 = std::min(box.y() + box.w(), static_cast(key.height)); + if (x0 >= x1 || y0 >= y1) return remaining & ~(handled | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | + GL_STENCIL_BUFFER_BIT); // an empty box clears nothing + area = {{x0, y0}, {static_cast(x1 - x0), static_cast(y1 - y0)}}; + if (fbo.IsDefault && !MapDefaultFramebufferReadbackRect(x0, y0, x1 - x0, y1 - y0, VkExtent2D{key.width, key.height}, + m_swapchainObject.GetPreTransform(), &area.offset, &area.extent)) + MagmaWireFatal("clear-surface-transform"); + } + const auto* active = VkRenderPassManager::GetActiveRenderPass(); + const Bool open = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer && + active->hash == pass.hash; + if (!open && active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer); + if (!open && !VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return remaining; + const VkClearRect rect{area, 0, key.layers}; + vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect); + return remaining & ~handled; + } + // ---- P8-D: THE WIRE ARM'S INDIRECT DRAWS READ THEIR WORDS ON THE GPU ---------------------- // // The shape the monolith arm records (VulkanRenderer.cpp's MultiDraw{Elements,Arrays}Indirect diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 898a65e7..525f701b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -231,7 +231,13 @@ } void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, - const ClearAttachmentPayload& payload, GLint drawbuffer) { + const ClearAttachmentPayload& clearRequest, GLint drawbuffer) { + if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return; + // P15 S0: what the draw pass can take is cleared inside it (ClearWireFramebufferInPass, + // WireDraw.inc); only the buffers it hands back take the standalone pass below. + ClearAttachmentPayload payload = clearRequest; + payload.mask = ClearWireFramebufferInPass(fbo, clearRequest, drawbuffer); + if (!payload.mask) return; RetireWireDrawPass(); // A stream that neither presents nor finishes (an off-screen loop) never submits, so its // recording and the per-clear objects retired below would grow until host memory runs diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 4691cf08..9be0e16d 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -96,6 +96,8 @@ add_executable(MobileGLIntegrationTest Scenarios/FragCoordOriginScenario.cpp Scenarios/ClientVertexArrayScenario.cpp Scenarios/ClearThenReadPixelsScenario.cpp + # P15 S0: a clear between draws into the same framebuffer (DirectVulkan records it in the pass). + Scenarios/InPassClearScenario.cpp Scenarios/SampleVariablesScenario.cpp Scenarios/DepthStencilReadbackScenario.cpp Scenarios/DepthStencilReadbackMatrixScenario.cpp @@ -2452,6 +2454,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endmacro() mgl_itest_register_split_arms("ClearThenReadPixelsScenario.*" "") + mgl_itest_register_split_arms("InPassClearScenario.*" "") # GL_ARB_shader_objects (P13): ARB-created shader / program objects travel the record arm like # core-created ones, so the scenario runs on every split arm as well as both monoliths. mgl_itest_register_split_arms("ArbShaderObjectsScenario.*" "") diff --git a/MobileGL/MG_IntegrationTest/Scenarios/InPassClearScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/InPassClearScenario.cpp new file mode 100644 index 00000000..38b7d786 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/InPassClearScenario.cpp @@ -0,0 +1,295 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/InPassClearScenario.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - A CLEAR BETWEEN DRAWS INTO THE SAME FRAMEBUFFER. +// +// P15 S0: DirectVulkan's wire arm records a glClear / glClearBuffer* whose targets are the +// attachments of the draw pass for the bound framebuffer INSIDE that pass +// (VulkanRenderer::ClearWireFramebufferInPass, vkCmdClearAttachments) instead of closing it and +// running a pass of its own. What that path has to get right, and what each case pins: +// +// * ORDER and SCISSOR: a draw, a scissored clear, a second draw - all in one pass. The clear +// must land after the first draw and before the second, inside the scissor box only. +// * THE DRAW-BUFFER INDEX: VkClearAttachment::colorAttachment is the SUBPASS colour slot, which +// the wire pass lays out by draw-buffer slot, not by framebuffer attachment point. With +// glDrawBuffers({NONE, ATTACHMENT0}) a clear writes attachment 0 and leaves attachment 1 alone; +// glClearBufferfv(GL_COLOR, 1, ...) is the same slot by number. +// * DEPTH and STENCIL: a depth / stencil clear between two depth- / stencil-tested draws decides +// what the second draw passes. +// * sRGB: a clear of an sRGB attachment with GL_FRAMEBUFFER_SRGB off stores the raw value, with it +// on the encoded one - through the pass's attachment view, which is the UNORM twin when off. +// +// DirectGLES is the control (a host GL driver); MOBILEGL_MAGMA_INPASS_CLEAR=0 is the standalone +// path, which the same assertions hold for. + +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + constexpr const char* kVS = R"(#version 330 core +in vec2 aPos; +uniform float uDepth; +void main() { gl_Position = vec4(aPos, uDepth, 1.0); } +)"; + + constexpr const char* kFS = R"(#version 330 core +uniform vec4 uColor; +layout(location = 0) out vec4 o0; +layout(location = 1) out vec4 o1; +void main() { o0 = uColor; o1 = uColor; } +)"; + + constexpr int kSize = 64; + + class InPassClearScenario : public ScenarioTest { + protected: + unsigned int program = 0; + GLuint vao = 0, vbo = 0; + + void Build() { + std::string error; + program = CompileProgram(kVS, kFS, &error); + ASSERT_NE(program, 0u) << error; + static const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr); + } + + void Quad(float r, float g, float b, float a, float depth = 0.0f) { + glUseProgram(program); + glUniform4f(glGetUniformLocation(program, "uColor"), r, g, b, a); + glUniform1f(glGetUniformLocation(program, "uDepth"), depth); + glBindVertexArray(vao); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + } + + void TearDown() override { + if (vbo) glDeleteBuffers(1, &vbo); + if (vao) glDeleteVertexArrays(1, &vao); + if (program) glDeleteProgram(program); + ScenarioTest::TearDown(); + } + + static GLuint Texture(GLenum internalFormat) { + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, kSize, kSize); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + return texture; + } + + static Rgba8 ReadAt(GLenum readBuffer, int x, int y) { + glReadBuffer(readBuffer); + const Image image = ReadPixelsRect(x, y, 1, 1); + return image.At(0, 0); + } + }; + + ::testing::AssertionResult Near(const Rgba8& got, int r, int g, int b, int a, int tolerance = 2) { + const auto close = [&](int v, int want) { return v >= want - tolerance && v <= want + tolerance; }; + if (close(got.r, r) && close(got.g, g) && close(got.b, b) && close(got.a, a)) + return ::testing::AssertionSuccess(); + return ::testing::AssertionFailure() + << "got (" << int(got.r) << ", " << int(got.g) << ", " << int(got.b) << ", " << int(got.a) + << "), want (" << r << ", " << g << ", " << b << ", " << a << ")"; + } + + } // namespace + + TEST_F(InPassClearScenario, AScissoredClearBetweenTwoDrawsLandsBetweenThem) { + if (!Ready()) return; + Build(); + const GLuint color = Texture(GL_RGBA8); + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + glViewport(0, 0, kSize, kSize); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); + + Quad(1.0f, 0.0f, 0.0f, 1.0f); // red everywhere + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, kSize / 2, kSize); // left half + glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); // green, left half + glScissor(0, 0, kSize, kSize / 4); // bottom quarter + Quad(0.0f, 0.0f, 1.0f, 1.0f); // blue, bottom quarter (scissor clips the draw) + glDisable(GL_SCISSOR_TEST); + EXPECT_EQ(FirstGLError(), 0u); + + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 4, kSize * 3 / 4), 0, 255, 0, 255)) << "left top: the clear"; + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize * 3 / 4, kSize * 3 / 4), 255, 0, 0, 255)) + << "right top: the first draw, outside the clear's scissor box"; + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 4, kSize / 8), 0, 0, 255, 255)) + << "left bottom: the second draw, after the clear"; + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize * 3 / 4, kSize / 8), 0, 0, 255, 255)); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &color); + } + + TEST_F(InPassClearScenario, AClearWritesTheDrawBufferSlotsAttachmentOnly) { + if (!Ready()) return; + Build(); + const GLuint color0 = Texture(GL_RGBA8), color1 = Texture(GL_RGBA8); + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color0, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, color1, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + glViewport(0, 0, kSize, kSize); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); + + // Both attachments red through a draw to both. + const GLenum both[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1}; + glDrawBuffers(2, both); + Quad(1.0f, 0.0f, 0.0f, 1.0f); + // Draw buffer 1 is attachment 0 and draw buffer 0 is nothing: a draw + clear in that + // layout touches attachment 0 only. + const GLenum swapped[] = {GL_NONE, GL_COLOR_ATTACHMENT0}; + glDrawBuffers(2, swapped); + Quad(1.0f, 1.0f, 0.0f, 1.0f); // yellow into attachment 0 (opens the pass in this layout) + glClearColor(0.0f, 0.0f, 1.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 0, 0, 255, 255)) + << "attachment 0 is draw buffer 1: cleared to blue"; + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT1, kSize / 2, kSize / 2), 255, 0, 0, 255)) + << "attachment 1 is in no draw buffer: still the first draw's red"; + + // glClearBufferfv names the draw-buffer slot by number. + glDrawBuffers(2, both); + Quad(1.0f, 0.0f, 0.0f, 1.0f); + const float green[] = {0.0f, 1.0f, 0.0f, 1.0f}; + glClearBufferfv(GL_COLOR, 1, green); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 255, 0, 0, 255)) << "slot 0 untouched"; + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT1, kSize / 2, kSize / 2), 0, 255, 0, 255)) << "slot 1 cleared"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &color0); + glDeleteTextures(1, &color1); + } + + TEST_F(InPassClearScenario, DepthAndStencilClearsBetweenTestedDrawsDecideTheSecondDraw) { + if (!Ready()) return; + Build(); + const GLuint color = Texture(GL_RGBA8), depthStencil = Texture(GL_DEPTH24_STENCIL8); + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthStencil, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + glViewport(0, 0, kSize, kSize); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_TRUE); + glStencilMask(0xff); + glClearDepth(1.0); + glClearStencil(0); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + // Depth: a near draw (z = -0.5 -> 0.25 in the window) writes depth; a clear to 1.0 + // between it and a farther draw (z = 0.0 -> 0.5) is the only way that draw passes LESS. + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + Quad(1.0f, 0.0f, 0.0f, 1.0f, -0.5f); + glClear(GL_DEPTH_BUFFER_BIT); + Quad(0.0f, 1.0f, 0.0f, 1.0f, 0.0f); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 0, 255, 0, 255)) + << "the depth clear between the draws let the farther draw pass"; + + // Stencil: a draw that writes 1 everywhere, a stencil clear to 2, then a draw that only + // passes where stencil == 2. + glDisable(GL_DEPTH_TEST); + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 1, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + Quad(1.0f, 0.0f, 0.0f, 1.0f); + glClearStencil(2); + glClear(GL_STENCIL_BUFFER_BIT); + glStencilFunc(GL_EQUAL, 2, 0xff); + glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); + Quad(0.0f, 0.0f, 1.0f, 1.0f); + glDisable(GL_STENCIL_TEST); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 0, 0, 255, 255)) + << "the stencil clear between the draws let the EQUAL 2 draw pass"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &color); + glDeleteTextures(1, &depthStencil); + } + + TEST_F(InPassClearScenario, AnSrgbClearStoresRawWithEncodingOffAndEncodedWithItOn) { + if (!Ready()) return; + Build(); + const GLuint color = Texture(GL_SRGB8_ALPHA8); + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + glViewport(0, 0, kSize, kSize); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glDisable(GL_SCISSOR_TEST); + + glDisable(GL_FRAMEBUFFER_SRGB); + Quad(1.0f, 0.0f, 0.0f, 1.0f); // opens the pass + glClearColor(0.5f, 0.5f, 0.5f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 128, 128, 128, 255)) + << "GL_FRAMEBUFFER_SRGB off: the raw value"; + + glEnable(GL_FRAMEBUFFER_SRGB); + Quad(1.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glDisable(GL_FRAMEBUFFER_SRGB); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_TRUE(Near(ReadAt(GL_COLOR_ATTACHMENT0, kSize / 2, kSize / 2), 188, 188, 188, 255)) + << "GL_FRAMEBUFFER_SRGB on: linear 0.5 encoded"; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteTextures(1, &color); + } + +} // namespace MGITest