From 533a2564821335ee38999543b039e807fa4119a9 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 9 Oct 2026 04:50:17 -0400 Subject: [PATCH] Magma: wire clears defer into the next pass's load op and pass-begin barriers coalesce (passes 11.5 -> 4.3, barriers 15.5 -> 9.0 per frame; inproc apply 1.41 -> 1.24 ms; red-once: no materialize fails 4 ClearOrder cases, a dropped fold fails 2). --- .../DirectVulkan/Renderer/UniformManager.h | 8 +- .../Renderer/VkTextureManager.cpp | 9 + .../DirectVulkan/Renderer/VkTextureManager.h | 25 ++ .../DirectVulkan/Renderer/VulkanRenderer.h | 29 +- .../DirectVulkan/Renderer/WireDraw.inc | 177 +++++++-- .../DirectVulkan/Renderer/WireFramebuffer.inc | 354 +++++++++++++++++- MobileGL/MG_IntegrationTest/CMakeLists.txt | 2 + MobileGL/MG_IntegrationTest/P8Coverage.cmake | 2 + .../Scenarios/ClearOrderScenario.cpp | 353 +++++++++++++++++ .../Scenarios/PassOrderScenario.cpp | 251 +++++++++++++ 10 files changed, 1172 insertions(+), 38 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/ClearOrderScenario.cpp create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/PassOrderScenario.cpp diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 08060b82..8c01cbdf 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -9,6 +9,7 @@ #pragma once #include "ProgramFactory.h" +#include "RenderPassGuard.h" #include "MagmaProgramSource.h" #include "VkBufferManager.h" #include "VkSamplerManager.h" @@ -66,6 +67,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // A command buffer (re)began recording: descriptor bindings recorded into // the previous buffer do not carry over, so drop the bind-dedup shadow. void OnCommandBufferBoundary() { m_lastBindValid = false; } + // The image-write epoch the last full memory barrier made visible (see m_wireImageBarrierEpoch), + // and the note a caller that just recorded that same barrier makes. + Uint64 WireImageBarrierEpoch() const { return m_wireImageBarrierEpoch; } + void NoteWireImageBarrier() const { m_wireImageBarrierEpoch = WireImageWriteEpoch(); } // A ProgramFactory eviction just destroyed this layout: purge every frame // slot's cached descriptor sets for it, so a recycled handle value can never // stale-hit sets written for the dead layout's bindings. The sets are @@ -532,7 +537,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_samplerResolveMemoHighWater = binding + 1; } } - // WireImageWriteEpoch() when ResolveWireImageDescriptor last made image writes visible. + // WireImageWriteEpoch() when ResolveWireImageDescriptor, or a new draw pass's attachment + // barrier (VulkanRenderer::TransitionWireImage), last made image writes visible. mutable Uint64 m_wireImageBarrierEpoch = 0; // P14: the wire arm's sampler memo, one entry per texture unit. The sampler a unit resolves // to is a function of the sampler CSO it names (immutable for a {slot, gen, Serial} within diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index b17d23e7..ffdac4b0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1881,6 +1881,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { lookup = {key, m_resourceEraseEpoch, &it->second}; } TextureResource& resource = *lookup.resource; + // P15: an image with deferred clears runs them before any use that is not an attachment + // resolve (PendingClearHold), and before a shape or texel change of its own even then: + // a mip-chain growth copies the content, and an upload lands after the clear it followed. + if (!resource.pendingClears.empty() && pVulkanRenderer != nullptr) { + const Bool current = resource.image != VK_NULL_HANDLE && resource.syncedWireSerial == record.Serial && + record.PendingUploads.empty() && + (!requireStorage || (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) != 0); + if (m_pendingClearHold == 0 || !current) pVulkanRenderer->MaterializeWirePendingClears(resource); + } if (resource.image != VK_NULL_HANDLE && resource.syncedWireSerial == record.Serial && record.PendingUploads.empty() && (!requireStorage || (resource.usageFlags & VK_IMAGE_USAGE_STORAGE_BIT) != 0)) { return &resource; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 8b42136e..5aff014d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -114,6 +114,26 @@ public: // manager keys its per-draw fast path on this so an attachment's image recreation // invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1). Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; } + // P15 (wire deferred clears): while one of these lives, SyncTextureResourceByHandle leaves an + // image's pending clears pending - its caller is resolving the image as an ATTACHMENT, and + // either folds the clears into the pass it begins (LOAD_OP_CLEAR) or replaces them with a + // newer clear. Every other caller (sampling, blits, copies, readbacks, uploads, mip + // generation) materializes them first, so a use that forgets the hold is merely slower. + class PendingClearHold { + public: + explicit PendingClearHold(VkTextureManager& manager) : m_manager(manager) { ++m_manager.m_pendingClearHold; } + ~PendingClearHold() { --m_manager.m_pendingClearHold; } + PendingClearHold(const PendingClearHold&) = delete; + PendingClearHold& operator=(const PendingClearHold&) = delete; + private: + VkTextureManager& m_manager; + }; + // A whole-subresource clear that was recorded but has not run yet (VulkanRenderer::DeferWireClear). + struct WirePendingClear { + Uint32 level = 0, layer = 0, layers = 1; + VkImageAspectFlags aspects = 0; + VkClearValue value{}; + }; // Bumped whenever any tracked texture resource is erased; cached // TextureResource pointers are valid only while this is unchanged. Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; } @@ -338,6 +358,9 @@ public: SharedPtr yuvOwner; SharedPtr yuvSource; Uint64 yuvConvertedFrame = 0; + // P15: clears of this image still to run (WirePendingClear). Part of the image, so a + // respecify (which moves a new image in, below) drops them with the old one. + Vector pendingClears; TextureResource() = default; TextureResource(const TextureResource&) = delete; @@ -383,6 +406,7 @@ public: std::swap(this->yuvOwner, that.yuvOwner); std::swap(this->yuvSource, that.yuvSource); std::swap(this->yuvConvertedFrame, that.yuvConvertedFrame); + std::swap(this->pendingClears, that.pendingClears); } void Reset() { @@ -638,6 +662,7 @@ public: private: // Bumped in SyncTextureResource right after vmaCreateImage(texture). See GetTextureImageEpoch(). Uint64 m_textureImageEpoch = 1; + Uint32 m_pendingClearHold = 0; // PendingClearHold's depth // See AdvanceRecordingGeneration. Starts above every resource's default // stamp of 0 so a fresh resource counts as untouched. Uint64 m_recordingGeneration = 1; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 9b56e9e8..50777acd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -162,6 +162,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // queue submission order already puts every earlier draw's access to the old texels // ahead of the copy. The preserve arm keeps the wait: it retires the old image. // (rd12 Magma: the wait was ~1.8 ms/frame of fence stall, one per lightmap update.) + // P15 (WireFramebuffer.inc): runs an image's deferred clears now, outside any pass. + void MaterializeWirePendingClears(VkTextureManager::TextureResource& resource); Bool FlushWirePendingCommandsForTextureUpload() { return !HasPendingRecordedWork() || FlushPendingCommands(); } @@ -587,7 +589,30 @@ namespace MobileGL::MG_Backend::DirectVulkan { WireImage ResolveWireImage(const MG_Pipe::MGPFramebufferState& fbo, const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect, Bool isWriteTarget = false); - void TransitionWireImage(WireImage& image, VkImageLayout layout); + void TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier = false); + // P15: WIRE DEFERRED CLEARS (WireFramebuffer.inc). A clear of images the open pass + // holds is recorded inside it; a whole-image clear of others waits on the image + // (WirePendingClear) for the next pass that draws into it (LOAD_OP_CLEAR) or the first + // other use (MaterializeWirePendingClears). + GLbitfield ClearWireAttachmentsOfOpenPass(const MG_Pipe::MGPFramebufferState& fbo, + const ClearAttachmentPayload& payload, GLint drawbuffer); + GLbitfield DeferWireClear(const MG_Pipe::MGPFramebufferState& fbo, const ClearAttachmentPayload& payload, + GLint drawbuffer); + // An attachment of the pass being prepared whose image carries pending clears; taken + // when the pass begins (BeginWireDrawPassFolding) or applied inside it if it is open. + struct WireFoldClear { + Uint32 attachment = 0; + VkImageAspectFlags aspects = 0; + VkTextureManager::TextureResource* resource = nullptr; + Uint32 level = 0, layer = 0, layers = 1; + }; + Vector m_wireFoldClears; + static Bool WireImageCanDeferClear(const WireImage& image); + static void NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects, const VkClearValue& value); + VkRenderPass WireClearVariant(RenderPassEntry& begun, Uint64 variantKey); + Bool BeginWireDrawPassFolding(FrameContext::FrameData& frame, RenderPassEntry& begun, Uint64 variantKey, + const VkClearValue* values); + void ApplyWireFoldClearsInPass(FrameContext::FrameData& frame, RenderPassEntry& pass); void ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, const ClearAttachmentPayload& payload, GLint drawbuffer = -1); void BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1, @@ -829,6 +854,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { void DestroyWireDrawPassCacheEntry(WireDrawPassCacheEntry& entry); Vector m_wireDrawPassCache; WireDrawPassKey m_wireDrawPassKey; + // P15: the open pass's attachment that is `image` (WireFramebuffer.inc's deferred clears). + static Uint32 OpenPassAttachmentOf(const WireImage& image, const Vector& attachments); WireRenderPassCompatibilityTable m_wireRenderPassCompatibility; // The interned compatibility id of m_wireDrawPass's attachments, or 0 when not known yet. // A draw that continues the pass (its WireDrawPassKey compares equal, which covers every diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index b877a484..0270e49e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -275,6 +275,7 @@ const MG_Pipe::MGPFramebufferState& fbo, WireDrawScratch& scratch, Bool passContinues, Bool& continuing) { continuing = true; + m_wireFoldClears.clear(); if (passContinues) { auto& store = MG_Record::ServerStagedTexture(); if (!m_wirePassMarksValid || m_wirePassMarksClearGeneration != store.GpuDirtyClearGeneration()) { @@ -312,7 +313,13 @@ 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); + // An attachment resolve: its deferred clears stay pending, to become this pass's load + // op (BeginWireDrawPassFolding) - WireFramebuffer.inc's deferred-clear section. + WireImage image; + { + VkTextureManager::PendingClearHold hold(*m_textureManager); + image = ResolveWireImage(fbo, surface, aspect, /*isWriteTarget=*/true); + } if (!image.image) return VK_ATTACHMENT_UNUSED; m_textureManager->FlushPendingUploads(); m_wirePassMemo.attachments.push_back( @@ -345,6 +352,9 @@ description.storeOp = description.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; description.initialLayout = description.finalLayout = layout; attachments.push_back(description); + if (image.resource != nullptr && !image.resource->pendingClears.empty()) + m_wireFoldClears.push_back({static_cast(attachments.size() - 1), info.subresourceRange.aspectMask, + image.resource, image.level, image.layer, image.layers}); extent.width = std::min(extent.width, image.extent.width); extent.height = std::min(extent.height, image.extent.height); layers = std::min(layers, image.layers); @@ -399,7 +409,8 @@ // 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); + for (auto& image : sameLayoutAttachments) + TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL, /*coalesceWithLastFullBarrier=*/true); } if (!continuing) { RetireWireDrawPass(); @@ -541,6 +552,8 @@ // pass's attachments are provably the ones this draw would resolve (WirePassMemo). Bool passContinues = WirePassMemoHolds(*fbo, frame); if (!passContinues) { + // Attachment resolves: deferred clears stay pending for the pass (PendingClearHold). + VkTextureManager::PendingClearHold hold(*m_textureManager); 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); @@ -794,6 +807,11 @@ if (passContinues) passContinues = WirePassMemoHolds(*fbo, frame); // A memo that holds is the open pass itself (WirePassMemoHolds compared the active pass). Bool continuing = true; + // FROM HERE TO THE PASS BEGIN, EVERY IMAGE RESOLVE IS AN ATTACHMENT'S (the pass key walk, + // the pipeline's attachment formats): their deferred clears must reach the pass as its + // load op. Run anywhere in between, a clear would land after the walk's barriers and right + // before the pass that loads the image (a read-after-write hazard). + VkTextureManager::PendingClearHold passHold(*m_textureManager); 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. @@ -873,7 +891,8 @@ 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)) { + if (open && !m_wireFoldClears.empty()) ApplyWireFoldClearsInPass(frame, pass); + if (!open && !BeginWireDrawPassFolding(frame, pass, 0, nullptr)) { MGL_WIRE_DECLINE_AT(BeginRenderPass, "the render pass for this draw could not be begun"); return false; } @@ -930,6 +949,8 @@ // recording) before the pass is chosen, unless the open pass provably holds them. Bool passContinues = WirePassMemoHolds(fbo, frame); if (!passContinues) { + // Attachment resolves: deferred clears stay pending for the pass (PendingClearHold). + VkTextureManager::PendingClearHold hold(*m_textureManager); 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); @@ -943,6 +964,8 @@ if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); if (passContinues) passContinues = WirePassMemoHolds(fbo, frame); Bool continuing = true; + // As in SetupWireDraw: attachment resolves only, from the walk to the pass begin. + VkTextureManager::PendingClearHold passHold(*m_textureManager); if (!PrepareWireDrawPass(frame, fbo, m_wireDrawScratch, passContinues, continuing)) return remaining; const auto& key = m_wireDrawPassKey; auto& pass = *m_wireDrawPass; @@ -1061,40 +1084,136 @@ values[a].depthStencil = clear.clearValue.depthStencil; } } - VkRenderPass variant = VK_NULL_HANDLE; - for (const auto& entry : begun.clearVariants) - if (entry.first == variantKey) variant = entry.second; - if (variant == VK_NULL_HANDLE) { - auto descriptions = begun.wireAttachments; - for (Uint32 a = 0; a < attachmentCount; ++a) { - if (variantKey >> (2 * a) & 1) descriptions[a].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - if (variantKey >> (2 * a + 1) & 1) descriptions[a].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - } - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = static_cast(begun.wireColors.size()); - subpass.pColorAttachments = begun.wireColors.data(); - subpass.pDepthStencilAttachment = - begun.wireDepth.attachment != VK_ATTACHMENT_UNUSED ? &begun.wireDepth : nullptr; - VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; - info.attachmentCount = attachmentCount; - info.pAttachments = descriptions.data(); - info.subpassCount = 1; - info.pSubpasses = &subpass; - VK_VERIFY(vkCreateRenderPass(m_device, &info, nullptr, &variant)); - begun.clearVariants.push_back({variantKey, variant}); - } - if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, begun, variant, values, attachmentCount)) - return remaining; + if (!BeginWireDrawPassFolding(frame, begun, variantKey, values)) return remaining; return remaining & ~handled; } if (!open && active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer); - if (!open && !VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) return remaining; + if (open && !m_wireFoldClears.empty()) ApplyWireFoldClearsInPass(frame, pass); + if (!open && !BeginWireDrawPassFolding(frame, pass, 0, nullptr)) return remaining; const VkClearRect rect{area, 0, key.layers}; vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect); return remaining & ~handled; } + // The draw pass `begun` with LOAD_OP_CLEAR for the attachments `variantKey` names (bit 2a: + // attachment a's colour or depth, bit 2a+1: its stencil). Built once per key and kept on the + // pass, like the pass itself. + VkRenderPass VulkanRenderer::WireClearVariant(RenderPassEntry& begun, Uint64 variantKey) { + for (const auto& entry : begun.clearVariants) + if (entry.first == variantKey) return entry.second; + const Uint32 attachmentCount = static_cast(begun.wireAttachments.size()); + auto descriptions = begun.wireAttachments; + for (Uint32 a = 0; a < attachmentCount; ++a) { + if (variantKey >> (2 * a) & 1) descriptions[a].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + if (variantKey >> (2 * a + 1) & 1) descriptions[a].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + } + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = static_cast(begun.wireColors.size()); + subpass.pColorAttachments = begun.wireColors.data(); + subpass.pDepthStencilAttachment = + begun.wireDepth.attachment != VK_ATTACHMENT_UNUSED ? &begun.wireDepth : nullptr; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = attachmentCount; + info.pAttachments = descriptions.data(); + info.subpassCount = 1; + info.pSubpasses = &subpass; + VkRenderPass variant = VK_NULL_HANDLE; + VK_VERIFY(vkCreateRenderPass(m_device, &info, nullptr, &variant)); + begun.clearVariants.push_back({variantKey, variant}); + return variant; + } + + // P15: EVERY DRAW PASS BEGINS HERE. The pending clears of its attachments + // (m_wireFoldClears, gathered by PrepareWireDrawPass) become their load op and leave the + // images; a clear the caller already put in `variantKey` (its own glClear, newer than any + // pending one) wins over a pending clear of the same attachment, which is then simply gone. + Bool VulkanRenderer::BeginWireDrawPassFolding(FrameContext::FrameData& frame, RenderPassEntry& begun, + Uint64 variantKey, const VkClearValue* values) { + const Uint32 attachmentCount = static_cast(begun.wireAttachments.size()); + constexpr Uint32 kMaxAttachments = MG_Pipe::kMGPipeMaxColorAttachments + 1; + VkClearValue merged[kMaxAttachments]{}; + if (values != nullptr) + for (Uint32 a = 0; a < attachmentCount && a < kMaxAttachments; ++a) merged[a] = values[a]; + for (const auto& fold : m_wireFoldClears) { + if (fold.attachment >= attachmentCount || fold.attachment >= kMaxAttachments || fold.resource == nullptr) + continue; + const Uint32 a = fold.attachment; + auto& pending = fold.resource->pendingClears; + for (SizeT i = 0; i < pending.size();) { + auto& entry = pending[i]; + if (entry.level != fold.level || entry.layer != fold.layer || entry.layers != fold.layers) { + ++i; + continue; + } + const VkImageAspectFlags taken = entry.aspects & fold.aspects; + if ((taken & VK_IMAGE_ASPECT_COLOR_BIT) && !(variantKey >> (2 * a) & 1)) { + variantKey |= 1ull << (2 * a); + merged[a].color = entry.value.color; + } + if ((taken & VK_IMAGE_ASPECT_DEPTH_BIT) && !(variantKey >> (2 * a) & 1)) { + variantKey |= 1ull << (2 * a); + merged[a].depthStencil.depth = entry.value.depthStencil.depth; + } + if ((taken & VK_IMAGE_ASPECT_STENCIL_BIT) && !(variantKey >> (2 * a + 1) & 1)) { + variantKey |= 1ull << (2 * a + 1); + merged[a].depthStencil.stencil = entry.value.depthStencil.stencil; + } + entry.aspects &= ~taken; + if (entry.aspects == 0) pending.erase(pending.begin() + static_cast(i)); + else ++i; + } + } + m_wireFoldClears.clear(); + if (variantKey == 0) return VkRenderPassManager::BeginRenderPass(frame.commandBuffer, begun); + return VkRenderPassManager::BeginRenderPass(frame.commandBuffer, begun, WireClearVariant(begun, variantKey), + merged, attachmentCount); + } + + // The same pending clears when the pass is already open (a continued pass whose attachments + // gained a pending clear since it began - the pass-key walk found them): recorded inside it. + void VulkanRenderer::ApplyWireFoldClearsInPass(FrameContext::FrameData& frame, RenderPassEntry& pass) { + VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{}; + Uint32 count = 0; + for (const auto& fold : m_wireFoldClears) { + if (fold.resource == nullptr) continue; + auto& pending = fold.resource->pendingClears; + for (SizeT i = 0; i < pending.size();) { + auto& entry = pending[i]; + if (entry.level != fold.level || entry.layer != fold.layer || entry.layers != fold.layers) { + ++i; + continue; + } + const VkImageAspectFlags taken = entry.aspects & fold.aspects; + if ((taken & VK_IMAGE_ASPECT_COLOR_BIT) && count < MG_Pipe::kMGPipeMaxColorAttachments + 1) { + for (Uint32 c = 0; c < pass.wireColors.size(); ++c) { + if (pass.wireColors[c].attachment != fold.attachment) continue; + auto& clear = clears[count++]; + clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clear.colorAttachment = c; + clear.clearValue.color = entry.value.color; + break; + } + } + const VkImageAspectFlags depthStencil = taken & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT); + if (depthStencil && pass.wireDepth.attachment == fold.attachment && + count < MG_Pipe::kMGPipeMaxColorAttachments + 1) { + auto& clear = clears[count++]; + clear.aspectMask = depthStencil; + clear.clearValue.depthStencil = entry.value.depthStencil; + } + entry.aspects &= ~taken; + if (entry.aspects == 0) pending.erase(pending.begin() + static_cast(i)); + else ++i; + } + } + m_wireFoldClears.clear(); + if (count == 0) return; + const VkClearRect rect{{{0, 0}, {static_cast(pass.extent.x()), static_cast(pass.extent.y())}}, + 0, pass.layers}; + vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect); + } + // ---- 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 525f701b..3bc16b0d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -194,7 +194,7 @@ return out; } - void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout) { + void VulkanRenderer::TransitionWireImage(WireImage& image, VkImageLayout layout, Bool coalesceWithLastFullBarrier) { auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); if (VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer); @@ -208,15 +208,31 @@ // tracked layout - and CopyWireDepthStencilBlit moves the two aspects in two legs. if (WireFormatIsPackedDepthStencil(image.format)) barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + // P15: A DRAW PASS'S ATTACHMENTS SHARE ONE FULL BARRIER, and the sampled-image + // barrier shares it too. The memory barrier below is global (ALL_COMMANDS, every write + // made visible), the same one ResolveWireImageDescriptor records, so a second one with no + // image written since the last (WireImageWriteEpoch unchanged) orders nothing. Only the + // new-pass caller (PrepareWireDrawPass) opts in: its attachments are written inside the + // pass it is about to begin, and that pass's end moves the epoch. A transfer caller + // writes right after this call with the epoch already moved, so it must neither skip nor + // note - noting there would let a later barrier be skipped past its write. + // FCL Magma: 27.5 barriers and 12.6 render passes per frame against dev's 3.4 and 4.1, + // most of them a second barrier per attachment and a pass closed and reopened at the + // first sampled texture after every pass begin. if (image.layout == layout) { // GENERAL->GENERAL is not a layout transition, but the next load, // sample or blit still depends on earlier attachment/shader writes. // The old per-verb queue idle must not be its only ordering edge. - VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; - memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; - memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; - vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr); + const Bool covered = coalesceWithLastFullBarrier && m_uniformManager && + WireImageWriteEpoch() == m_uniformManager->WireImageBarrierEpoch(); + if (!covered) { + VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr); + if (coalesceWithLastFullBarrier && m_uniformManager) m_uniformManager->NoteWireImageBarrier(); + } } if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout, stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access, @@ -230,13 +246,337 @@ } } + // ---- P15: WIRE DEFERRED CLEARS ------------------------------------------------------------- + // + // Minecraft (1.21.5) clears its targets through framebuffers of their own - a depth-only one + // for the shared depth texture, a colour+depth one per target - and then draws through the + // main framebuffer. A clear that begins a LOAD_OP_CLEAR pass of its own made every such clear a + // pass that did nothing but clear, closed by the very next draw: 11.5 passes a frame against + // dev's 4.1 (whose clear manager folded the same clears into the next pass's load op), each + // one an extra full load/store of the attachments on a tiler. + // + // So a clear of an image the open pass holds is recorded inside that pass, and a whole-image + // clear of any other image WAITS ON THE IMAGE (WirePendingClear, kept on the image's own + // resource with its level and layers - never keyed by a GL name). The next draw pass that has + // it as an attachment begins with LOAD_OP_CLEAR for it (PrepareWireDrawPass / + // BeginWireDrawPassFolding); any other use reaches the image through + // SyncTextureResourceByHandle, which runs it first (MaterializeWirePendingClears). The default + // framebuffer never defers: its in-pass clear is a known Adreno problem (see + // ClearWireFramebufferInPass), and it is presented, not sampled. + + // Whether `image` can carry a deferred clear: a plain single-sample 2D(-array) image of this + // session with a float colour or a depth/stencil format, clearable by a transfer command. + Bool VulkanRenderer::WireImageCanDeferClear(const WireImage& image) { + const auto* resource = image.resource; + if (resource == nullptr || image.isDefault || image.is3D || image.samples != VK_SAMPLE_COUNT_1_BIT) return false; + if (resource->sharedImageId != 0 || resource->yuvImageId != 0) return false; + if ((resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) == 0) return false; + if (image.aspect == VK_IMAGE_ASPECT_COLOR_BIT && + (vkuFormatIsSRGB(image.format) || vkuFormatIsSINT(image.format) || vkuFormatIsUINT(image.format) || + vkuFormatIsCompressed(image.format))) + return false; + return true; + } + + void VulkanRenderer::MaterializeWirePendingClears(VkTextureManager::TextureResource& resource) { + if (resource.pendingClears.empty()) return; + if (resource.image == VK_NULL_HANDLE) { + resource.pendingClears.clear(); + return; + } + auto& frame = m_frameContext.GetCurrent(); + if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); + EndActiveRenderPassOn(frame.commandBuffer); + // After every earlier access to the image (the clears were issued after them). + if (resource.layout != VK_IMAGE_LAYOUT_GENERAL) { + if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, resource.image, resource.layout, + VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + resource.layout == VK_IMAGE_LAYOUT_UNDEFINED ? 0 : VK_ACCESS_MEMORY_WRITE_BIT, + VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, resource.aspect, 0, resource.mipLevels)) + MagmaWireFatal("pending-clear-transition"); + } else { + VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr); + } + for (const auto& pending : resource.pendingClears) { + const VkImageSubresourceRange range{pending.aspects, pending.level, 1, pending.layer, pending.layers}; + if (pending.aspects & VK_IMAGE_ASPECT_COLOR_BIT) + vkCmdClearColorImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL, + &pending.value.color, 1, &range); + else + vkCmdClearDepthStencilImage(frame.commandBuffer, resource.image, VK_IMAGE_LAYOUT_GENERAL, + &pending.value.depthStencil, 1, &range); + } + resource.pendingClears.clear(); + // Recorded writes: the next sampled-image or attachment barrier must cover them. + BumpWireImageWriteEpoch(); + } + + // Records `aspects` of `image` cleared to `value` as pending, replacing whatever an earlier + // pending clear of the same subresource said for those aspects. + void VulkanRenderer::NoteWirePendingClear(const WireImage& image, VkImageAspectFlags aspects, + const VkClearValue& value) { + auto& pending = image.resource->pendingClears; + for (SizeT i = 0; i < pending.size();) { + auto& entry = pending[i]; + if (entry.level == image.level && entry.layer == image.layer && entry.layers == image.layers) { + entry.aspects &= ~aspects; + if (entry.aspects == 0) { + pending.erase(pending.begin() + static_cast(i)); + continue; + } + } + ++i; + } + VkTextureManager::WirePendingClear entry{}; + entry.level = image.level; + entry.layer = image.layer; + entry.layers = image.layers; + entry.aspects = aspects; + entry.value = value; + pending.push_back(entry); + } + + // The open wire draw pass's attachment that IS `image` (same image, level and layers), or + // VK_ATTACHMENT_UNUSED. + Uint32 VulkanRenderer::OpenPassAttachmentOf(const WireImage& image, + const Vector& attachments) { + for (Uint32 a = 0; a < attachments.size(); ++a) { + const auto& key = attachments[a]; + if (key.image == image.image && key.level == image.level && key.layer == image.layer && + key.layers == image.layers) + return a; + } + return VK_ATTACHMENT_UNUSED; + } + + // A clear whose images the OPEN draw pass holds - through another framebuffer than the + // pass's own (the depth-only clear framebuffer) - is recorded inside that pass. Returns the + // buffers it did not take. + GLbitfield VulkanRenderer::ClearWireAttachmentsOfOpenPass(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 || fbo.IsDefault || m_wirePreparationDepth != 0 || !m_wireDrawPass) return remaining; + auto& frame = m_frameContext.GetCurrent(); + const auto* active = VkRenderPassManager::GetActiveRenderPass(); + if (active == nullptr || active->commandBuffer != frame.commandBuffer || active->hash != m_wireDrawPass->hash) + return remaining; + const auto& key = m_wireDrawPassKey; + const RenderPassEntry* pass = m_wireDrawPass.get(); + const VkCommandBuffer passBuffer = active->commandBuffer; + const Uint64 passHash = active->hash; + 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; // the standalone path answers an empty box + area = {{x0, y0}, {static_cast(x1 - x0), static_cast(y1 - y0)}}; + } + // The image must be exactly the pass's extent, or the pass's render area is not its area. + const auto fits = [&](const WireImage& image) { + return image.extent.width == key.width && image.extent.height == key.height; + }; + VkClearAttachment clears[MG_Pipe::kMGPipeMaxColorAttachments + 1]{}; + Uint32 count = 0; + GLbitfield handled = 0; + VkTextureManager::PendingClearHold hold(*m_textureManager); + 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 slot = fbo.DrawBuffers[i]; + if (slot < 0 || slot >= static_cast(MG_Pipe::kMGPipeMaxColorAttachments)) continue; + if (fbo.Color[slot].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() || payload.colorEncoding != ClearColorEncoding::Float) { + all = false; + break; + } + const WireImage image = ResolveWireImage(fbo, fbo.Color[slot], VK_IMAGE_ASPECT_COLOR_BIT, true); + const Uint32 a = image.image ? OpenPassAttachmentOf(image, key.attachments) : VK_ATTACHMENT_UNUSED; + Uint32 reference = VK_ATTACHMENT_UNUSED; + for (Uint32 c = 0; a != VK_ATTACHMENT_UNUSED && c < key.colorReferences.size(); ++c) + if (key.colorReferences[c] == a) reference = c; + if (reference == VK_ATTACHMENT_UNUSED || !fits(image) || vkuFormatIsSRGB(image.format) || + vkuFormatIsSRGB(key.attachments[a].format) || vkuFormatIsSINT(image.format) || + vkuFormatIsUINT(image.format)) { + all = false; + break; + } + auto& clear = colorClears[colorCount++]; + clear.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clear.colorAttachment = reference; + clear.clearValue.color = MakeVkClearColorValue(payload, false); + } + if (all) { + for (Uint32 c = 0; c < colorCount; ++c) clears[count++] = colorClears[c]; + handled |= GL_COLOR_BUFFER_BIT; + } + } + VkImageAspectFlags depthStencilAspects = 0; + const auto depthStencilHeld = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) { + const WireImage image = ResolveWireImage(fbo, surface, aspect, true); + return image.image && fits(image) && key.depthReference != VK_ATTACHMENT_UNUSED && + OpenPassAttachmentOf(image, key.attachments) == key.depthReference && + (key.attachments[key.depthReference].aspect & aspect) != 0; + }; + if (remaining & GL_DEPTH_BUFFER_BIT) { + if (!MG_Pipe::gPipeInputs.GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) { + handled |= GL_DEPTH_BUFFER_BIT; + } else if (depthStencilHeld(fbo.Depth, 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 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) { + handled |= GL_STENCIL_BUFFER_BIT; + } else if (mask == 255u && depthStencilHeld(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT)) { + 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}; + } + // A resolve above may have submitted (a lazy upload) or run another image's deferred clear, + // closing the pass: then nothing is recorded and nothing is taken. + active = VkRenderPassManager::GetActiveRenderPass(); + if (active == nullptr || active->commandBuffer != passBuffer || active->hash != passHash || + m_wireDrawPass.get() != pass) + return remaining; + if (count > 0) { + const VkClearRect rect{area, 0, key.layers}; + vkCmdClearAttachments(frame.commandBuffer, count, clears, 1, &rect); + } + return remaining & ~handled; + } + + // A whole-image clear of images the open pass does not hold waits on the images (see the + // section comment). Returns the buffers it did not take. + GLbitfield VulkanRenderer::DeferWireClear(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 || fbo.IsDefault || drawbuffer >= 0 || m_wirePreparationDepth != 0) return remaining; + const Bool scissor = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest); + const auto wholeImage = [&](const WireImage& image) { + if (!scissor) return true; + const auto& box = MG_Pipe::gPipeInputs.GetScissorBox(); + return box.x() <= 0 && box.y() <= 0 && box.x() + box.z() >= static_cast(image.extent.width) && + box.y() + box.w() >= static_cast(image.extent.height); + }; + const Bool passOpen = VkRenderPassManager::GetActiveRenderPass() != nullptr && m_wireDrawPass; + const auto heldByOpenPass = [&](const WireImage& image) { + return passOpen && OpenPassAttachmentOf(image, m_wireDrawPassKey.attachments) != VK_ATTACHMENT_UNUSED; + }; + struct Target { + WireImage image; + VkImageAspectFlags aspects; + VkClearValue value; + Bool texture; + }; + Target targets[MG_Pipe::kMGPipeMaxColorAttachments + 2]{}; + Uint32 targetCount = 0; + GLbitfield handled = 0; + VkTextureManager::PendingClearHold hold(*m_textureManager); + if ((remaining & GL_COLOR_BUFFER_BIT) && payload.colorEncoding == ClearColorEncoding::Float) { + Bool all = true; + const Uint32 first = targetCount; + for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments && all; ++i) { + const Int32 slot = fbo.DrawBuffers[i]; + if (slot < 0 || slot >= static_cast(MG_Pipe::kMGPipeMaxColorAttachments)) continue; + const auto& surface = fbo.Color[slot]; + if (surface.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; + } + const WireImage image = ResolveWireImage(fbo, surface, VK_IMAGE_ASPECT_COLOR_BIT, true); + if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) { + all = false; + break; + } + auto& target = targets[targetCount++]; + target.image = image; + target.aspects = VK_IMAGE_ASPECT_COLOR_BIT; + target.value.color = MakeVkClearColorValue(payload, false); + target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture; + } + if (all) handled |= GL_COLOR_BUFFER_BIT; + else targetCount = first; + } + const auto depthStencil = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Bool { + const WireImage image = ResolveWireImage(fbo, surface, aspect, true); + if (!image.image || !WireImageCanDeferClear(image) || !wholeImage(image) || heldByOpenPass(image)) return false; + for (Uint32 t = 0; t < targetCount; ++t) { + auto& target = targets[t]; + if (target.image.image == image.image && target.image.level == image.level && + target.image.layer == image.layer && target.image.layers == image.layers && + !(target.aspects & VK_IMAGE_ASPECT_COLOR_BIT)) { + target.aspects |= aspect; // a combined depth/stencil image: one entry, both aspects + return true; + } + } + auto& target = targets[targetCount++]; + target.image = image; + target.aspects = aspect; + target.value.depthStencil = {payload.depth, payload.stencil}; + target.texture = surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture; + return true; + }; + if (remaining & GL_DEPTH_BUFFER_BIT) { + if (!MG_Pipe::gPipeInputs.GetDepthMask() || fbo.Depth.Kind == MG_Pipe::kMGPipeSurfaceKindNone) + handled |= GL_DEPTH_BUFFER_BIT; + else if (depthStencil(fbo.Depth, 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 || fbo.Stencil.Kind == MG_Pipe::kMGPipeSurfaceKindNone) + handled |= GL_STENCIL_BUFFER_BIT; + else if (mask == 255u && depthStencil(fbo.Stencil, VK_IMAGE_ASPECT_STENCIL_BIT)) + handled |= GL_STENCIL_BUFFER_BIT; + } + m_textureManager->FlushPendingUploads(); + for (Uint32 t = 0; t < targetCount; ++t) { + const auto& target = targets[t]; + NoteWirePendingClear(target.image, target.aspects, target.value); + // A GPU write as far as the CPU shadow is concerned, as a recorded clear is. + if (target.texture) + m_textureManager->MarkWireTextureGpuWritten(target.image.storage, target.image.level, + target.image.layer, target.image.layers); + } + return remaining & ~handled; + } + void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, 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. + // P15: in the open pass when it is this framebuffer's (the in-pass clear) or merely holds + // its images (another framebuffer's), deferred on the images when it covers them whole, + // and only then a pass of its own (the in-pass clear again, which begins one) or the + // standalone path. ClearAttachmentPayload payload = clearRequest; - payload.mask = ClearWireFramebufferInPass(fbo, clearRequest, drawbuffer); + if (m_wirePreparationDepth == 0 && WirePassMemoHolds(fbo, m_frameContext.GetCurrent())) + payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer); + if (payload.mask) payload.mask = ClearWireAttachmentsOfOpenPass(fbo, payload, drawbuffer); + if (payload.mask) payload.mask = DeferWireClear(fbo, payload, drawbuffer); + if (payload.mask) payload.mask = ClearWireFramebufferInPass(fbo, payload, drawbuffer); if (!payload.mask) return; RetireWireDrawPass(); // A stream that neither presents nor finishes (an off-screen loop) never submits, so its diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index c0440692..08334833 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -195,7 +195,9 @@ add_executable(MobileGLIntegrationTest Scenarios/DefaultFramebufferAcrossSwapScenario.cpp Scenarios/ScenarioIsolationScenario.cpp Scenarios/UploadOrderScenario.cpp + Scenarios/PassOrderScenario.cpp Scenarios/DefaultScissorFollowsSurfaceScenario.cpp + Scenarios/ClearOrderScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/P8Coverage.cmake b/MobileGL/MG_IntegrationTest/P8Coverage.cmake index e6ff372d..b636d2e9 100644 --- a/MobileGL/MG_IntegrationTest/P8Coverage.cmake +++ b/MobileGL/MG_IntegrationTest/P8Coverage.cmake @@ -31,6 +31,7 @@ set(MGL_P8A_AMBIENT_CASES AdvertisedLimitsScenario.IndexedBufferBindingsAreReportedVerbatimOnBothWidths BufferTextureScenario.* ClearTexImageUndefinedLevelZeroScenario.* + ClearOrderScenario.* ClipDistanceScenario.* CopyImageLevelRangeScenario.* CopyImagePacked16Scenario.* @@ -84,6 +85,7 @@ set(MGL_P8A_AMBIENT_CASES OrientationScenario.* P4aFinalFixScenario.* P4aSeamAuditScenario.* + PassOrderScenario.* PipelineFailureScenario.* PointSizeDemotionScenario.* ProgramPipelineScenario.ASamplerUnitRewrittenBetweenDrawsKeepsPaintingTheRightTexture diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ClearOrderScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ClearOrderScenario.cpp new file mode 100644 index 00000000..438bdd5e --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/ClearOrderScenario.cpp @@ -0,0 +1,353 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ClearOrderScenario.cpp +// Copyright (c) 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 TAKES EFFECT WHERE THE APPLICATION ISSUED IT, WHICHEVER FRAMEBUFFER IT WENT THROUGH. +// +// Minecraft clears its targets through framebuffers of their own (a depth-only one for the shared +// depth texture, one per colour target) and draws through another framebuffer holding the same +// textures. Magma keeps such a clear on the image and folds it into the load op of the next pass +// that draws into the image, records it inside the open pass when that pass holds the image, and +// runs it before any other use (sampling, blit, readback, upload, respecify). Each case below is +// one of those paths, with no readback between the clear and the work that depends on it: +// - a depth clear through a depth-only framebuffer, then a depth-tested draw through the main one +// (folded into the next pass), and the same while the main pass is open (inside it); +// - a colour clear through a framebuffer of its own, then a draw covering half the texture +// through another framebuffer holding it (the other half keeps the clear colour); +// - a cleared texture sampled, blitted from, uploaded into, read back and re-specified before +// any pass draws into it; +// - a scissored clear, which clears only its box. + +#include +#include + +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { +namespace { +using Pixel = std::array; +constexpr Pixel red{255, 0, 0, 255}, green{0, 255, 0, 255}, blue{0, 0, 255, 255}, black{0, 0, 0, 255}; +constexpr int kSize = 8, kTargetWidth = 16; +constexpr const char* kVertex = R"(#version 330 core +uniform float z; +void main() { + vec2 p[3] = vec2[3](vec2(-1,-1), vec2(3,-1), vec2(-1,3)); + gl_Position = vec4(p[gl_VertexID], z, 1); +})"; +constexpr const char* kSample = R"(#version 330 core +uniform sampler2D src; +layout(location=0) out vec4 color; +void main() { color = texelFetch(src, ivec2(gl_FragCoord.xy) % 8, 0); })"; +constexpr const char* kTint = R"(#version 330 core +uniform vec4 tint; +layout(location=0) out vec4 color; +void main() { color = tint; })"; + +class ClearOrderScenario : public ScenarioTest { +protected: + GLuint target = 0, sample = 0, tint = 0, vao = 0; + std::vector textures, fbos; + + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glDisable(GL_BLEND); + glDisable(GL_CULL_FACE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glActiveTexture(GL_TEXTURE0); + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + target = NewFramebuffer(NewColor(kTargetWidth, kSize, black), 0); + sample = Link(kSample); + tint = Link(kTint); + } + + void TearDown() override { + if (Ready()) { + glUseProgram(0); + glBindVertexArray(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDisable(GL_DEPTH_TEST); + glDisable(GL_SCISSOR_TEST); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClearDepth(1.0); + if (vao) glDeleteVertexArrays(1, &vao); + if (!fbos.empty()) glDeleteFramebuffers(GLsizei(fbos.size()), fbos.data()); + if (!textures.empty()) glDeleteTextures(GLsizei(textures.size()), textures.data()); + if (sample) glDeleteProgram(sample); + if (tint) glDeleteProgram(tint); + } + ScenarioTest::TearDown(); + } + + GLuint NewColor(int width, int height, Pixel color) { + GLuint texture = 0; + glGenTextures(1, &texture); + textures.push_back(texture); + const std::vector pixels(size_t(width) * size_t(height), color); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + return texture; + } + + GLuint NewDepth(int width, int height) { + GLuint texture = 0; + glGenTextures(1, &texture); + textures.push_back(texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + return texture; + } + + // `color` and/or `depth` may be 0. + GLuint NewFramebuffer(GLuint color, GLuint depth) { + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + fbos.push_back(fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + if (color) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, color, 0); + if (depth) glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth, 0); + if (!color) { + glDrawBuffer(GL_NONE); + glReadBuffer(GL_NONE); + } + EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + return fbo; + } + + static GLuint Link(const char* fragment) { + const GLuint linked = glCreateProgram(); + const std::array kinds{GL_VERTEX_SHADER, GL_FRAGMENT_SHADER}; + const std::array sources{kVertex, fragment}; + for (size_t i = 0; i < kinds.size(); ++i) { + const GLuint shader = glCreateShader(kinds[i]); + glShaderSource(shader, 1, &sources[i], nullptr); + glCompileShader(shader); + glAttachShader(linked, shader); + glDeleteShader(shader); + } + glLinkProgram(linked); + GLint ok = GL_FALSE; + glGetProgramiv(linked, GL_LINK_STATUS, &ok); + EXPECT_EQ(ok, GL_TRUE); + return linked; + } + + static void ClearColor(GLuint fbo, Pixel color) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glClearColor(color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, color[3] / 255.0f); + glClear(GL_COLOR_BUFFER_BIT); + } + + // Draws `color` over (x, y, w, h) of `fbo` at depth `z` (NDC). + void Fill(GLuint fbo, Pixel color, int x, int y, int w, int h, float z = 0.0f) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glUseProgram(tint); + glUniform1f(glGetUniformLocation(tint, "z"), z); + glUniform4f(glGetUniformLocation(tint, "tint"), color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, + color[3] / 255.0f); + glViewport(x, y, w, h); + glDrawArrays(GL_TRIANGLES, 0, 3); + } + + // Samples `texture` into the 8x8 tile `tile` of the target. + void SampleInto(GLuint texture, int tile) { + glBindFramebuffer(GL_FRAMEBUFFER, target); + glUseProgram(sample); + glUniform1f(glGetUniformLocation(sample, "z"), 0.0f); + glBindTexture(GL_TEXTURE_2D, texture); + glViewport(tile * kSize, 0, kSize, kSize); + glDrawArrays(GL_TRIANGLES, 0, 3); + } + + void ExpectPixel(GLuint fbo, int x, int y, Pixel expected, const char* why) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + Pixel actual{}; + glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, actual.data()); + for (size_t channel = 0; channel < actual.size(); ++channel) + EXPECT_EQ(int(actual[channel]), int(expected[channel])) << why << " channel=" << channel; + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << why; + } + + // Main framebuffer (colour + depth) and a depth-only clear framebuffer sharing its depth. + struct Pair { + GLuint color, depth, main, depthOnly; + }; + Pair NewPair() { + Pair pair{}; + pair.color = NewColor(kSize, kSize, black); + pair.depth = NewDepth(kSize, kSize); + pair.main = NewFramebuffer(pair.color, pair.depth); + pair.depthOnly = NewFramebuffer(0, pair.depth); + Prime(pair.main); + Prime(pair.depthOnly); + return pair; + } + + // Uses `fbo` once - a clear and a draw - so the cases below run on images whose records have + // settled, the steady state a game frame sees (a first use republishes the attachments' records, + // and Magma then runs a pending clear at once rather than folding it into the next pass). + void Prime(GLuint fbo) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_ALWAYS); + Fill(fbo, black, 0, 0, kSize, kSize); + glDepthFunc(GL_LESS); + glDisable(GL_DEPTH_TEST); + glFinish(); + } + + // Draws one pixel of the target nothing checks: whatever pass held the case's images is then + // closed, so a clear of them is not taken inside it. + void SwitchPassAway() { Fill(target, black, kTargetWidth - 1, kSize - 1, 1, 1); } +}; + +TEST_F(ClearOrderScenario, ADepthClearThroughADepthOnlyFramebufferReachesTheNextPass) { + if (!Ready() || IsSkipped()) return; + const Pair pair = NewPair(); + glBindFramebuffer(GL_FRAMEBUFFER, pair.main); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClearDepth(0.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // depth 0: nothing passes LESS + glFinish(); + SwitchPassAway(); + glBindFramebuffer(GL_FRAMEBUFFER, pair.depthOnly); + glClearDepth(1.0); + glClear(GL_DEPTH_BUFFER_BIT); // through the other framebuffer + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + Fill(pair.main, green, 0, 0, kSize, kSize, 0.0f); + glDisable(GL_DEPTH_TEST); + ExpectPixel(pair.main, 4, 4, green, "the draw after the depth-only clear sees the cleared depth"); +} + +TEST_F(ClearOrderScenario, ADepthClearThroughADepthOnlyFramebufferLandsInsideTheOpenPass) { + if (!Ready() || IsSkipped()) return; + const Pair pair = NewPair(); + glBindFramebuffer(GL_FRAMEBUFFER, pair.main); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClearDepth(1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glFinish(); + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + Fill(pair.main, red, 0, 0, kSize, kSize, -0.5f); // the main pass is open, depth now -0.5 -> 0.25 + glBindFramebuffer(GL_FRAMEBUFFER, pair.depthOnly); + glClearDepth(1.0); + glClear(GL_DEPTH_BUFFER_BIT); // the open pass holds this depth texture + Fill(pair.main, green, 0, 0, kSize, kSize, 0.0f); // behind the red one unless the clear landed + glDisable(GL_DEPTH_TEST); + ExpectPixel(pair.main, 4, 4, green, "the draw after the clear passes the cleared depth"); +} + +TEST_F(ClearOrderScenario, AColourClearThroughItsOwnFramebufferReachesAnotherFramebufferHoldingTheTexture) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewColor(kSize, kSize, black); + const GLuint own = NewFramebuffer(texture, 0); + const GLuint depth = NewDepth(kSize, kSize); + const GLuint other = NewFramebuffer(texture, depth); + Prime(own); + Prime(other); + SwitchPassAway(); + ClearColor(own, red); + Fill(other, blue, 0, 0, kSize / 2, kSize); // the left half, through the other framebuffer + SampleInto(texture, 0); + ExpectPixel(target, 1, 4, blue, "the draw through the other framebuffer lands over the clear"); + ExpectPixel(target, 6, 4, red, "the clear holds where the draw did not reach"); +} + +TEST_F(ClearOrderScenario, AClearedTextureIsSampledBeforeAnyPassDrawsIntoIt) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewColor(kSize, kSize, green); + const GLuint fbo = NewFramebuffer(texture, 0); + glFinish(); + ClearColor(fbo, red); + SampleInto(texture, 0); + ExpectPixel(target, 4, 4, red, "the sample sees the clear"); +} + +TEST_F(ClearOrderScenario, AClearedTextureIsBlittedFrom) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewColor(kSize, kSize, green); + const GLuint fbo = NewFramebuffer(texture, 0); + glFinish(); + ClearColor(fbo, red); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, target); + glBlitFramebuffer(0, 0, kSize, kSize, 0, 0, kSize, kSize, GL_COLOR_BUFFER_BIT, GL_NEAREST); + ExpectPixel(target, 4, 4, red, "the blit reads the cleared texels"); +} + +TEST_F(ClearOrderScenario, AnUploadAfterAClearLandsOverIt) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewColor(kSize, kSize, black); + const GLuint fbo = NewFramebuffer(texture, 0); + glFinish(); + ClearColor(fbo, red); + const std::vector patch(4, green); + glBindTexture(GL_TEXTURE_2D, texture); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, patch.data()); + SampleInto(texture, 0); + ExpectPixel(target, 1, 1, green, "the upload after the clear is not overwritten by it"); + ExpectPixel(target, 6, 6, red, "the clear holds outside the upload"); +} + +TEST_F(ClearOrderScenario, AClearedTextureIsReadBack) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewColor(kSize, kSize, green); + const GLuint fbo = NewFramebuffer(texture, 0); + glFinish(); + ClearColor(fbo, red); + ExpectPixel(fbo, 4, 4, red, "the readback sees the clear"); +} + +TEST_F(ClearOrderScenario, ARespecifiedTextureKeepsItsNewTexelsAndNotAnEarlierClear) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewColor(kSize, kSize, green); + const GLuint fbo = NewFramebuffer(texture, 0); + glFinish(); + ClearColor(fbo, red); + const std::vector fresh(size_t(kSize) * kSize, blue); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kSize, kSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, fresh.data()); + SampleInto(texture, 0); + ExpectPixel(target, 4, 4, blue, "the re-specification replaces the cleared texels"); +} + +TEST_F(ClearOrderScenario, AScissoredClearClearsOnlyItsBox) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewColor(kSize, kSize, green); + const GLuint fbo = NewFramebuffer(texture, 0); + glFinish(); + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, kSize / 2, kSize); + ClearColor(fbo, red); + glDisable(GL_SCISSOR_TEST); + SampleInto(texture, 0); + ExpectPixel(target, 1, 4, red, "inside the scissor box"); + ExpectPixel(target, 6, 4, green, "outside the scissor box"); +} + +} // namespace +} // namespace MGITest diff --git a/MobileGL/MG_IntegrationTest/Scenarios/PassOrderScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/PassOrderScenario.cpp new file mode 100644 index 00000000..c03514f5 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/PassOrderScenario.cpp @@ -0,0 +1,251 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PassOrderScenario.cpp +// Copyright (c) 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 - WORK ON AN IMAGE IN ONE PASS IS ORDERED AGAINST THE NEXT PASS THAT TOUCHES IT. +// +// Magma records a draw pass per framebuffer and orders a new pass after earlier image writes with +// a memory barrier, shared between the pass's attachments and the sampled images of its draws. +// Each case below hands an image from one piece of work to the next with no readback in between, +// so only that barrier orders them: +// - render into a texture, then sample it in the next pass (read after write); +// - render into a texture, render elsewhere, render into the texture again (write after write); +// - blit into a texture, then sample it (a render-pass blit, then read); +// - copy into a texture with glCopyImageSubData, then sample it (an out-of-pass transfer write, +// then read - the shape whose barrier must not be skipped on the strength of a barrier +// recorded between the transfer's pass close and its write); +// - blit one half of a texture onto its other half, then sample it (the one transfer whose +// image stays in GENERAL, so a memory barrier and no layout transition orders it); +// - sample a texture, then render into it (write after read). +// A software rasterizer executes in order and renders these right without the barrier, so the +// gate that goes red when the barrier is skipped is synchronization validation (submit-time), +// run over these cases; the pixel checks hold the semantics. + +#include +#include + +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { +namespace { +using Pixel = std::array; +constexpr Pixel red{255, 0, 0, 255}, green{0, 255, 0, 255}, blue{0, 0, 255, 255}, black{0, 0, 0, 255}; +constexpr int kWidth = 16, kHeight = 8, kSize = 8; +constexpr const char* kVertex = R"(#version 330 core +void main() { + vec2 p[3] = vec2[3](vec2(-1,-1), vec2(3,-1), vec2(-1,3)); + gl_Position = vec4(p[gl_VertexID], 0, 1); +})"; +constexpr const char* kSample = R"(#version 330 core +uniform sampler2D src; +layout(location=0) out vec4 color; +void main() { color = texelFetch(src, ivec2(gl_FragCoord.xy) % 8, 0); })"; +constexpr const char* kTint = R"(#version 330 core +uniform vec4 tint; +layout(location=0) out vec4 color; +void main() { color = tint; })"; + +class PassOrderScenario : public ScenarioTest { +protected: + GLuint target = 0, sample = 0, tint = 0, vao = 0; + GLint tintLocation = -1; + std::vector textures, fbos; + + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glDisable(GL_BLEND); + glDisable(GL_CULL_FACE); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glActiveTexture(GL_TEXTURE0); + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + target = NewFramebuffer(NewTexture(kWidth, kHeight, black)); + sample = Link(kSample); + tint = Link(kTint); + tintLocation = glGetUniformLocation(tint, "tint"); + } + + void TearDown() override { + if (Ready()) { + glUseProgram(0); + glBindVertexArray(0); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + if (vao) glDeleteVertexArrays(1, &vao); + if (!fbos.empty()) glDeleteFramebuffers(GLsizei(fbos.size()), fbos.data()); + if (!textures.empty()) glDeleteTextures(GLsizei(textures.size()), textures.data()); + if (sample) glDeleteProgram(sample); + if (tint) glDeleteProgram(tint); + } + ScenarioTest::TearDown(); + } + + GLuint NewTexture(int width, int height, Pixel color) { + GLuint texture = 0; + glGenTextures(1, &texture); + textures.push_back(texture); + const std::vector pixels(size_t(width) * size_t(height), color); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + return texture; + } + + GLuint NewFramebuffer(GLuint texture) { + GLuint fbo = 0; + glGenFramebuffers(1, &fbo); + fbos.push_back(fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + return fbo; + } + + static GLuint Link(const char* fragment) { + const GLuint linked = glCreateProgram(); + const std::array kinds{GL_VERTEX_SHADER, GL_FRAGMENT_SHADER}; + const std::array sources{kVertex, fragment}; + for (size_t i = 0; i < kinds.size(); ++i) { + const GLuint shader = glCreateShader(kinds[i]); + glShaderSource(shader, 1, &sources[i], nullptr); + glCompileShader(shader); + glAttachShader(linked, shader); + glDeleteShader(shader); + } + glLinkProgram(linked); + GLint ok = GL_FALSE; + glGetProgramiv(linked, GL_LINK_STATUS, &ok); + EXPECT_EQ(ok, GL_TRUE); + return linked; + } + + // Fills the viewport (x, y, w, h) of `fbo` with `color` by drawing. + void Fill(GLuint fbo, Pixel color, int x, int y, int w, int h) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glUseProgram(tint); + glUniform4f(tintLocation, color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, color[3] / 255.0f); + glViewport(x, y, w, h); + glDrawArrays(GL_TRIANGLES, 0, 3); + } + + // Samples `texture` into the 8x8 tile `tile` of the target. + void SampleInto(GLuint texture, int tile) { + glBindFramebuffer(GL_FRAMEBUFFER, target); + glUseProgram(sample); + glBindTexture(GL_TEXTURE_2D, texture); + glViewport(tile * kSize, 0, kSize, kHeight); + glDrawArrays(GL_TRIANGLES, 0, 3); + } + + void ExpectPixel(int x, int y, Pixel expected, const char* why) { + glBindFramebuffer(GL_FRAMEBUFFER, target); + Pixel actual{}; + glReadPixels(x, y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, actual.data()); + for (size_t channel = 0; channel < actual.size(); ++channel) + EXPECT_EQ(int(actual[channel]), int(expected[channel])) << why << " channel=" << channel; + EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << why; + } +}; + +TEST_F(PassOrderScenario, ATextureRenderedInOnePassIsSampledByTheNext) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewTexture(kSize, kSize, black); + const GLuint fbo = NewFramebuffer(texture); + glFinish(); + Fill(fbo, green, 0, 0, kSize, kSize); + SampleInto(texture, 0); + SampleInto(texture, 1); + ExpectPixel(4, 4, green, "the next pass samples what the previous pass rendered"); + ExpectPixel(12, 4, green, "a second draw in the same pass samples it too"); +} + +TEST_F(PassOrderScenario, ATextureRenderedAgainAfterAnotherPassKeepsBothWrites) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewTexture(kSize, kSize, black); + const GLuint fbo = NewFramebuffer(texture); + glFinish(); + Fill(fbo, red, 0, 0, kSize, kSize); + Fill(target, black, 0, 0, kWidth, kHeight); // another pass in between + Fill(fbo, blue, 0, 0, kSize / 2, kSize); // the left half again + SampleInto(texture, 0); + ExpectPixel(1, 4, blue, "the second write to the texture lands over the first"); + ExpectPixel(6, 4, red, "the first write survives where the second did not reach"); +} + +TEST_F(PassOrderScenario, ATextureBlittedIntoIsSampledByTheNextDraw) { + if (!Ready() || IsSkipped()) return; + const GLuint source = NewTexture(kSize, kSize, green); + const GLuint sourceFbo = NewFramebuffer(source); + const GLuint texture = NewTexture(kSize, kSize, black); + const GLuint fbo = NewFramebuffer(texture); + glFinish(); + glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glBlitFramebuffer(0, 0, kSize, kSize, 0, 0, kSize, kSize, GL_COLOR_BUFFER_BIT, GL_NEAREST); + SampleInto(texture, 0); + ExpectPixel(4, 4, green, "the draw after the blit samples the blitted texels"); +} + +TEST_F(PassOrderScenario, ATextureCopiedIntoIsSampledByTheNextDraw) { + if (!Ready() || IsSkipped()) return; + const GLuint source = NewTexture(kSize, kSize, green); + const GLuint texture = NewTexture(kSize, kSize, black); + glFinish(); + SampleInto(texture, 1); // a pass is open on the target when the copy arrives + glCopyImageSubData(source, GL_TEXTURE_2D, 0, 0, 0, 0, texture, GL_TEXTURE_2D, 0, 0, 0, 0, kSize, kSize, 1); + SampleInto(texture, 0); + ExpectPixel(4, 4, green, "the draw after the copy samples the copied texels"); + ExpectPixel(12, 4, black, "the draw before the copy samples the old texels"); +} + +TEST_F(PassOrderScenario, ATextureBlittedWithinItselfIsSampledByTheNextDraw) { + if (!Ready() || IsSkipped()) return; + // GL allows a blit between non-overlapping regions of one image; ES 3.x makes identical read + // and draw buffers GL_INVALID_OPERATION, and DirectGLES forwards the blit to the driver as is, + // so the blit is dropped there (a DirectGLES conformance gap of its own, not this case's subject). + if (Gl().BackendName() == "DirectGLES") GTEST_SKIP() << "DirectGLES does not emulate a same-image blit"; + const GLuint texture = NewTexture(kSize, kSize, red); + const GLuint fbo = NewFramebuffer(texture); + glFinish(); + Fill(fbo, green, 0, 0, kSize / 2, kSize); + SampleInto(texture, 1); // a pass is open on the target when the blit arrives + // The left half onto the right half of the same image: one image is both ends of the + // transfer, so it stays in GENERAL and its only ordering edge is the memory barrier. + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glBlitFramebuffer(0, 0, kSize / 2, kSize, kSize / 2, 0, kSize, kSize, GL_COLOR_BUFFER_BIT, GL_NEAREST); + SampleInto(texture, 0); + ExpectPixel(6, 4, green, "the draw after the blit samples the blitted half"); + ExpectPixel(14, 4, red, "the draw before the blit samples the right half's old texels"); +} + +TEST_F(PassOrderScenario, ATextureSampledBeforeItIsRenderedIntoKeepsTheOldTexelsForThatDraw) { + if (!Ready() || IsSkipped()) return; + const GLuint texture = NewTexture(kSize, kSize, red); + const GLuint fbo = NewFramebuffer(texture); + glFinish(); + SampleInto(texture, 0); + Fill(fbo, blue, 0, 0, kSize, kSize); + SampleInto(texture, 1); + ExpectPixel(4, 4, red, "the draw before the render samples the old texels"); + ExpectPixel(12, 4, blue, "the draw after the render samples the new texels"); +} + +} // namespace +} // namespace MGITest