// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.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 #include "VulkanRendererInternal.h" namespace MobileGL::MG_Backend::DirectVulkan { namespace { // A wire pass variant key holds two clear bits per attachment from bit 0 and, from this // bit on, one bit per attachment whose content is undefined (WireClearVariant). constexpr Uint32 kWireDiscardBit = 32; static_assert(2 * (MG_Pipe::kMGPipeMaxColorAttachments + 1) <= kWireDiscardBit, "the clear bits of a wire pass variant key overlap its discard bits"); } // namespace // GL_EXT_fragment_shading_rate's pipeline rate and combiner ops for the draw about to be // recorded. The packed rate is log2(width) << 2 | log2(height), which is exactly the extent // VK_KHR_fragment_shading_rate takes, and the combiner op indices follow // VkFragmentShadingRateCombinerOpKHR's order. Combiner op 1 combines with the attachment rate, // which this backend does not enable, so it stays KEEP whatever the GL state holds. static void ShadowedSetFragmentShadingRate(VkCommandBuffer commandBuffer, PFN_vkCmdSetFragmentShadingRateKHR setFragmentShadingRate) { const RenderStateParameters& p = MG_Pipe::gPipeInputs->GetRenderStateParameters(); auto& shadow = *g_dynamicStateShadow; if (shadow.fragmentShadingRateValid && shadow.fragmentShadingRate == p.ShadingRate && shadow.fragmentShadingRateCombinerOp0 == p.ShadingRateCombinerOp0 && shadow.fragmentShadingRateCombinerOp1 == p.ShadingRateCombinerOp1) { return; } shadow.fragmentShadingRateValid = true; shadow.fragmentShadingRate = p.ShadingRate; shadow.fragmentShadingRateCombinerOp0 = p.ShadingRateCombinerOp0; shadow.fragmentShadingRateCombinerOp1 = p.ShadingRateCombinerOp1; const Uint8 rate = p.ShadingRate == kMGInvalidShadingRateEncoding ? 0 : p.ShadingRate; const VkExtent2D fragmentSize{1u << ((rate >> 2) & 3u), 1u << (rate & 3u)}; const VkFragmentShadingRateCombinerOpKHR combinerOps[2] = { p.ShadingRateCombinerOp0 <= VK_FRAGMENT_SHADING_RATE_COMBINER_OP_MUL_KHR ? static_cast(p.ShadingRateCombinerOp0) : VK_FRAGMENT_SHADING_RATE_COMBINER_OP_KEEP_KHR, VK_FRAGMENT_SHADING_RATE_COMBINER_OP_KEEP_KHR}; setFragmentShadingRate(commandBuffer, &fragmentSize, combinerOps); } static Bool TryGetCurrentVertexAttributeUploadPayload( const MG_State::GLState::CurrentVertexAttributeValue& currentValue, GLenum glType, VkFormat& outFormat, const void*& outData, VkDeviceSize& outSize) { switch (glType) { case GL_FLOAT: outFormat = VK_FORMAT_R32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float); return true; case GL_FLOAT_VEC2: outFormat = VK_FORMAT_R32G32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float) * 2; return true; case GL_FLOAT_VEC3: outFormat = VK_FORMAT_R32G32B32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float) * 3; return true; case GL_FLOAT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SFLOAT; outData = currentValue.floatValue.data(); outSize = sizeof(Float) * 4; return true; case GL_INT: outFormat = VK_FORMAT_R32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32); return true; case GL_INT_VEC2: outFormat = VK_FORMAT_R32G32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32) * 2; return true; case GL_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32) * 3; return true; case GL_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_SINT; outData = currentValue.intValue.data(); outSize = sizeof(Int32) * 4; return true; case GL_UNSIGNED_INT: outFormat = VK_FORMAT_R32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32); return true; case GL_UNSIGNED_INT_VEC2: outFormat = VK_FORMAT_R32G32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32) * 2; return true; case GL_UNSIGNED_INT_VEC3: outFormat = VK_FORMAT_R32G32B32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32) * 3; return true; case GL_UNSIGNED_INT_VEC4: outFormat = VK_FORMAT_R32G32B32A32_UINT; outData = currentValue.uintValue.data(); outSize = sizeof(Uint32) * 4; return true; default: return false; } } // Server-owned draw/dispatch setup. The legacy setup remains the monolith implementation. static MagmaProgramSource WireProgramSource(MG_Pipe::MGPipeHandle handle) { auto& state = MG_Pipe::MGPipeApplier(); const Bool composite = handle.Slot >= MG_Pipe::kMGPipeShaderCsoCompositeSlotBase; const auto& records = composite ? state.CompositeShaderCsos : state.ShaderCsos; const SizeT index = composite ? handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase : handle.Slot; if (MG_Pipe::MGPipeHandleIsNull(handle) || index >= records.size() || !records[index].Live || records[index].Gen != handle.Gen || !records[index].Archive) MagmaWireFatal("program-record"); return MagmaProgramSource(handle, records[index]); } // Through the same seam as MagmaWireFatal, and for the same reason (P7 wave 0). The family // stays RoleViolation: reaching the legacy buffer arm inside a Magma server is a role // invariant the two halves must hold, not a verb the wire failed to migrate. // GL 4.6 core 9.8: a framebuffer is layered when its attachments are (completeness makes // them agree), and only then does gl_Layer select a layer. static Bool WireFramebufferIsLayered(const MG_Pipe::MGPFramebufferState& fbo) { if (fbo.IsDefault) return false; const auto layered = [](const MG_Pipe::MGPSurface& surface) { return surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture && surface.Layered != 0; }; for (const auto& color : fbo.Color) { if (layered(color)) return true; } return layered(fbo.Depth) || layered(fbo.Stencil); } [[noreturn]] static void WireBufferLegacyFatal() { MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::RoleViolation, "MGPipe: Fatal{RoleViolation, \"buffer-legacy-arm\"} (Magma P7 buffer consumer)"); } static const MG_Pipe::MGPStreamOutputBegin& WireXfbSpan() { const auto& state = MG_Pipe::MGPipeApplier(); const auto found = state.StreamOutputSpans.find(state.BoundStreamOutputLifetimeId); if (found == state.StreamOutputSpans.end()) MagmaWireFatal("xfb-span-record"); return found->second; } Bool VulkanRenderer::BeginWireXfbCaptureForDraw(FrameContext::FrameData& frame) { const auto& span = WireXfbSpan(); const auto program = WireProgramSource(span.CaptureProgram); const Uint32 count = static_cast(std::min(program.GetTransformFeedbackBufferCount(), 4)); if (!count || !program.GetTransformFeedbackVaryingCount()) return false; VkBuffer buffers[4]{}; VkDeviceSize offsets[4]{}; VkDeviceSize sizes[4]{}; Bool hasTarget = false; for (Uint32 i = 0; i < count; ++i) { // A leading gl_NextBuffer leaves a hole in the binding indices. No // output names that zero-stride target, and GL need not bind a buffer. if (!program.GetTransformFeedbackStride(i)) continue; const auto& target = span.Targets[i]; BufferSlice slice{}; if (MG_Pipe::MGPipeHandleIsNull(target.Res) || !m_bufferManager.AcquireWireSlice(BufferKind::Vertex, target.Res, slice)) MagmaWireFatal("xfb-buffer-resource"); if (target.Offset >= slice.size) return false; const Uint64 size = target.Size == 0 || target.Size == MG_Pipe::kMGPipeWholeBuffer ? slice.size - target.Offset : target.Size; if (!size || size > slice.size - target.Offset) return false; buffers[i] = slice.buffer; offsets[i] = slice.offset + target.Offset; sizes[i] = size; hasTarget = true; // The server owns the storage. Publish dirtiness through the existing // reverse channel so CPU readback does not reuse the client's old shadow. m_bufferManager.MarkWireBufferGpuWritten(target.Res, target.Offset, size); } if (!hasTarget) return false; return BeginXfbCaptureWithBuffers(frame, count, buffers, offsets, sizes); } static Bool ConvertWireVertexStream(const MGPVertexAttribWire& attr, VertexInputStateFactory::VertexStreamConversion conversion, const Uint8* source, SizeT stride, SizeT count, Vector& output) { using Conversion = VertexInputStateFactory::VertexStreamConversion; const auto type = static_cast(attr.Type); const SizeT elementBytes = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra); if (conversion == Conversion::Repack) { output.resize(count * elementBytes); for (SizeT i = 0; i < count; ++i) Memcpy(output.data() + i * elementBytes, source + i * stride, elementBytes); return true; } output.resize(count * attr.Size * sizeof(Float)); // GL 4.6 core 10.2.1 (equations 2.1 / 2.2) for a normalized fixed-point component; a // double is not fixed-point, so its normalized flag changes nothing. const Bool normalize = attr.Normalized && type != DataType::Float64; const auto convert = [&]() { for (SizeT i = 0; i < count; ++i) { for (SizeT component = 0; component < attr.Size; ++component) { T value{}; Memcpy(&value, source + i * stride + component * sizeof(T), sizeof(T)); Float converted = static_cast(value); if constexpr (std::is_integral_v) { if (normalize) { constexpr double kMax = static_cast(std::numeric_limits::max()); converted = static_cast(std::is_signed_v ? std::max(static_cast(value) / kMax, -1.0) : static_cast(value) / kMax); } } Memcpy(output.data() + (i * attr.Size + component) * sizeof(Float), &converted, sizeof(Float)); } } return true; }; switch (type) { case DataType::Uint2101010Rev: case DataType::Int2101010Rev: { // x, y, z in 10-bit fields from bit 0, w in the top 2 bits; GL 4.6 core 10.2.1. if (attr.Size != 4 || attr.IsBgra) return false; const Bool isSigned = type == DataType::Int2101010Rev; for (SizeT i = 0; i < count; ++i) { Uint32 word = 0; Memcpy(&word, source + i * stride, sizeof(word)); for (Uint32 component = 0; component < 4; ++component) { const Uint32 bits = component < 3 ? 10u : 2u; const Uint32 field = (word >> (component * 10u)) & ((1u << bits) - 1u); const Int32 value = isSigned && (field >> (bits - 1u)) ? static_cast(field) - static_cast(1u << bits) : static_cast(field); Float converted = static_cast(value); if (normalize) converted = isSigned ? std::max(converted / static_cast((1u << (bits - 1u)) - 1u), -1.0f) : converted / static_cast((1u << bits) - 1u); Memcpy(output.data() + (i * 4 + component) * sizeof(Float), &converted, sizeof(Float)); } } return true; } case DataType::Float64: return convert.template operator()(); case DataType::Int8: return convert.template operator()(); case DataType::Uint8: return convert.template operator()(); case DataType::Int16: return convert.template operator()(); case DataType::Uint16: return convert.template operator()(); case DataType::Int32: return convert.template operator()(); case DataType::Uint32: return convert.template operator()(); default: return false; } } void VulkanRenderer::DestroyWireDrawPass() { // Only shutdown/swapchain replacement calls this, after device idle. if (m_wireDrawPass && m_frameContext.GetFrameCount() != 0) { auto& frame = m_frameContext.GetCurrent(); if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer); } m_wireDrawPass.reset(); m_wireDrawPassCompatibilityId = 0; m_wirePassMarksValid = false; m_wirePassMemo.valid = false; for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr); m_wireDrawViews.clear(); m_wireDrawPassKey = {}; } void VulkanRenderer::RetireWireObjects(WireRetiredObjects objects) { // The current recording is the next graphics submission. Do not use a // frame number: mid-frame flushes and readbacks can split one frame. objects.submitIndex = m_submitCounter + 1; m_wireRetiredObjects.push_back(Move(objects)); } void VulkanRenderer::RetireWireDrawPass() { m_wireDrawPassCompatibilityId = 0; m_wirePassMarksValid = false; m_wirePassMemo.valid = false; if (!m_wireDrawPass) return; auto& frame = m_frameContext.GetCurrent(); if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass()) VkRenderPassManager::EndRenderPass(frame.commandBuffer); // 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) { 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. if (!(it->key == key)) continue; m_wireDrawPassKey = Move(it->key); m_wireDrawPass = Move(it->pass); m_wireDrawViews = Move(it->views); cache.erase(it); return true; } return false; } 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(); const Bool keepAcrossFrames = MG_Config::Features.MagmaPassCache != MG_Config::QuirkOverride::ForceOff; SizeT kept = 0; for (SizeT i = 0; i < m_wireDrawPassCache.size(); ++i) { auto& entry = m_wireDrawPassCache[i]; const Bool live = keepAcrossFrames ? entry.key.textureImageEpoch == imageEpoch && entry.key.resourceEraseEpoch == eraseEpoch : entry.lastUseSubmit > m_completedSubmitCounter; if (live) { 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 (auto& entry : m_wireDrawPassCache) DestroyWireDrawPassCacheEntry(entry); m_wireDrawPassCache.clear(); if (m_wireColorBlitResources) { for (Uint32 frameIndex = 0; frameIndex < m_frameContext.GetFrameCount(); ++frameIndex) ResetWireColorBlitFramePool(frameIndex); RetireWireColorBlitTargets(/*all=*/true); } } void VulkanRenderer::CollectWireObjects(Uint64 completedSubmit, Bool all) { SizeT retained = 0; for (SizeT index = 0; index < m_wireRetiredObjects.size(); ++index) { auto& entry = m_wireRetiredObjects[index]; if (!all && entry.submitIndex > completedSubmit) { if (retained != index) m_wireRetiredObjects[retained] = Move(entry); ++retained; continue; } // Framebuffers/descriptor sets stop naming their views first. entry.drawPass.reset(); for (const auto pool : entry.descriptorPools) vkDestroyDescriptorPool(m_device, pool, nullptr); for (const auto framebuffer : entry.framebuffers) vkDestroyFramebuffer(m_device, framebuffer, nullptr); for (const auto pass : entry.renderPasses) vkDestroyRenderPass(m_device, pass, nullptr); for (const auto view : entry.imageViews) vkDestroyImageView(m_device, view, nullptr); } // Erase once: repeated front erases move the whole remaining cohort for // every draw retired by a fence, turning a batch retirement quadratic. m_wireRetiredObjects.resize(retained); } struct WirePreparationScope { Uint32& depth; explicit WirePreparationScope(Uint32& value) : depth(value) { ++depth; } ~WirePreparationScope() { --depth; } }; Bool VulkanRenderer::WirePassMemoHolds(const MG_Pipe::MGPFramebufferState& fbo, const FrameContext::FrameData& frame) const { const auto& memo = m_wirePassMemo; if (!memo.valid || fbo.IsDefault || !m_wireDrawPass || !frame.isCommandRecording) return false; const auto* active = VkRenderPassManager::GetActiveRenderPass(); if (active == nullptr || active->commandBuffer != frame.commandBuffer || active->hash != m_wireDrawPass->hash || active->hash != memo.passHash) return false; const auto& st = MG_Pipe::MGPipeApplier(); if (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)) return false; // The resources are good while the erase epoch holds. Each must still be what the walk // left: in GENERAL and synced to its record's serial with nothing pending. for (const auto& attachment : memo.attachments) { const auto* resource = attachment.resource; if (resource == nullptr || resource->layout != VK_IMAGE_LAYOUT_GENERAL) return false; const auto& records = attachment.renderbuffer ? st.RenderbufferResources : st.TextureResources; const auto handle = attachment.storage; if (handle.Slot >= records.size()) return false; const auto& record = records[handle.Slot]; if (!record.Live || record.Gen != handle.Gen || record.Serial != resource->syncedWireSerial || !record.PendingUploads.empty()) return false; } 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.cpp): 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; m_wireFoldClears.clear(); m_wireDiscardLoads.clear(); 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 { // An attachment resolve: its deferred clears stay pending, to become this pass's load // op (BeginWireDrawPassFolding) - WireFramebuffer.cpp'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( {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); 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); 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) { // A present leaves the default framebuffer undefined (VulkanRenderer::Present): // the first pass after it has nothing to load. const Bool defined = aspect == VK_IMAGE_ASPECT_COLOR_BIT ? m_swapchainObject.IsImageContentDefined(image.swapchainImageIndex) : m_swapchainObject.IsDepthStencilContentDefined(image.swapchainImageIndex); if (!defined) m_wireDiscardLoads.push_back(static_cast(attachments.size() - 1)); 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()) { // ARB_framebuffer_no_attachments (GL 4.6 core 9.2): a framebuffer with no image // attached rasterizes at its default width/height/layers/samples, which the record // carries in that case (FillGeometry). Vulkan expresses it directly as a render pass // with no attachments over a framebuffer of that extent. const Bool hasAnyAttachment = fbo.Depth.Kind != MG_Pipe::kMGPipeSurfaceKindNone || fbo.Stencil.Kind != MG_Pipe::kMGPipeSurfaceKindNone || std::any_of(std::begin(fbo.Color), std::end(fbo.Color), [](const MG_Pipe::MGPSurface& surface) { return surface.Kind != MG_Pipe::kMGPipeSurfaceKindNone; }); if (fbo.IsDefault || hasAnyAttachment || fbo.Width == 0 || fbo.Height == 0) { MGL_WIRE_DECLINE_AT(NoAttachments, "the draw framebuffer resolved to no attachments"); return false; } const auto& limits = m_physicalDevice.properties.limits; extent.width = std::min(fbo.Width, limits.maxFramebufferWidth); extent.height = std::min(fbo.Height, limits.maxFramebufferHeight); layers = std::clamp(fbo.Layers, 1u, std::max(limits.maxFramebufferLayers, 1u)); // The smallest count the device rasterizes attachment-less passes at that covers the // request (GL lets the effective count exceed the requested one), else its largest. samples = VK_SAMPLE_COUNT_1_BIT; const VkSampleCountFlags supported = limits.framebufferNoAttachmentsSampleCounts; if (fbo.Samples > 1) { for (Uint32 bit = VK_SAMPLE_COUNT_2_BIT; bit <= VK_SAMPLE_COUNT_64_BIT; bit <<= 1) { if ((supported & bit) == 0) continue; samples = static_cast(bit); if (bit >= fbo.Samples) break; } } passKey.noAttachmentSamples = static_cast(samples); passHash = XXH64(&samples, sizeof(samples), passHash); } 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, /*coalesceWithLastFullBarrier=*/true); } 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); created.wireAttachments.assign(attachments.begin(), attachments.end()); created.wireColors.assign(colors.begin(), colors.end()); created.wireDepth = depth; } // 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(); // The per-draw containers (VulkanRenderer.h's WireDrawScratch). Nothing re-enters this // function from inside it today; if something ever does, it gets its own set rather // than clearing the outer draw's. WireDrawScratch nestedScratch; WireDrawScratch& scratch = m_wirePreparationDepth == 0 ? m_wireDrawScratch : nestedScratch; const auto program = WireProgramSource(state.BoundShaderCso); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V"); return false; } // P8-D: the wire arm's own indirect draw (DrawWireIndirectNative) is the one caller that // may pass this aspect; a monolith indirect entry point reached here is still the role // violation. if ((aspects & DrawSetupAspect::IndirectDrawBuffer) && !m_wireNativeIndirectDraw) WireBufferLegacyFatal(); MakeXfbWritesVisible(); const auto* fbo = state.DrawFramebuffer(); if (!fbo) MagmaWireFatal("draw-framebuffer-record"); // THE PREVIOUS DRAW'S RENDER PASS STAYS OPEN until this draw's attachments are known: a // draw to the same attachments continues it (below), and on a tiler a pass per draw also // loaded and stored every attachment once per draw. Resource work in between that may not // sit inside a pass (a barrier, a copy, a clear) closes it itself (RenderPassGuard.h). // CPU preparation may synchronize GPU-produced vertex/index bytes. Do // not rewind an arena while this draw owns as-yet-unrecorded slices. WirePreparationScope preparing(m_wirePreparationDepth); const RenderStateParameters& clipControl = MG_Pipe::gPipeInputs->GetRenderStateParameters(); ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionFlags(ProgramFactory::ApplyClipControlToTransformFlags( GetBaseTransformFlagsRaw(fbo->IsDefault), clipControl.ClipOrigin, clipControl.ClipDepthMode)); if (m_transformFeedbackFeatureEnabled && MG_Pipe::gPipeInputs->IsTransformFeedbackActive() && program.GetTransformFeedbackVaryingCount() > 0) flags |= ProgramFactory::CompileOptionBit::XfbCapture; // The variant this program's last draw with these flags settled on: what the draw reads // from it before the narrow-integer pick below is the same in every variant. const auto* programObject = &m_programFactory->GetOrCreateDrawProgram(program, flags); if (!(aspects & DrawSetupAspect::IndexBuffer) && programObject->readsBaseVertexBuiltin) { flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex; programObject = &m_programFactory->GetOrCreateDrawProgram(program, flags); } const auto vaoHandle = state.BoundVertexElements; if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record"); if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record"); const auto& vao = state.VertexElementsCsos[vaoHandle.Slot]; if (!vao.Live || vao.Gen != vaoHandle.Gen) MagmaWireFatal("vertex-elements-generation"); // P7 wave 2 package C, CONTRACT-P7 §3.2: THE `vertex-layout` ROW IS RETIRED AND SPLIT. // // A false is now ONE reason and only one - `buffer-window`, an enabled attribute the // program reads whose slot lies outside the window set_vertex_buffers published. That // is a disagreement about what crossed, not about what this device can express, so it // keeps a named Fatal through the hook (MGPipeSessionFail -> Session::Fail, which is // what rule I asks of a refusal that must stay a refusal). It is the check that caught // the Redmi VAO hash collision fixed in 376c04be, which is why it is not softened. // The P7 marker is gone from the detail string with the abort: this is a permanent // protocol error, not a shape the split path has yet to migrate, so it must not keep // counting against the census of unmigrated refusals (scripts/ci/fatal_census.py // counts the marker literally, which is also why no comment in this package spells it). // P14: resolved through the record's own memo (ResolveWireVertexInput), except in a nested // preparation, which builds into its own scratch rather than under the outer draw's feet. const VertexInputStateFactory::BackendVertexInputState* resolvedInput = nullptr; if (&scratch == &m_wireDrawScratch) { resolvedInput = m_vertexInputStateFactory->ResolveWireVertexInput( vao, state, programObject->activeVertexInputLocationMask); } else if (m_vertexInputStateFactory->BuildWireVertexInput( vao, state, programObject->activeVertexInputLocationMask, scratch.input)) { resolvedInput = &scratch.input; } if (resolvedInput == nullptr) MagmaWireFatal("vertex-layout-buffer-window"); const auto& input = *resolvedInput; // The narrow-integer variant (NarrowIntegerVertexFormats.h), chosen from two masks cached on // the program entry and on the resolved layout: a program without integer vertex inputs pays // four ANDs and a compare here. Every variant reflects the same inputs, so the layout // resolved above with the entry's active mask is the chosen variant's too. A second lookup // only when this draw needs another variant than the last one with these flags. const NarrowIntegerVertexInputs narrowIntegerInputs = ResolveNarrowIntegerVertexInputs(programObject->integerVertexInputs, input.narrowIntegerAttributes); if (narrowIntegerInputs != programObject->narrowIntegerInputs) programObject = &m_programFactory->GetOrCreateProgram(program, flags, narrowIntegerInputs); if (programObject->invalidSpirvDeclined) { MGL_WIRE_DECLINE_AT(ProgramInvalidSpirv, "a module of the program fails SPIR-V validation and was never built"); return false; } if (programObject->pointSizeCapabilityUnsupported) { MGL_WIRE_DECLINE_AT(PointSizeCapability, "the program writes gl_PointSize and the device cannot express it"); return false; } const auto& programObj = *programObject; m_currentDrawXfbCaptureDeclined = programObj.xfbCaptureDeclined; if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) { MGL_WIRE_DECLINE_AT(TextureResourcesUnprepared, "a sampled/storage image could not be made ready; see the Tex* tally"); return false; } // Shared images another session writes: acquired ahead of the recording that samples them // (WireSharedImage.cpp). AcquireNotedSharedImages(); // Lazy image uploads/promotions can submit the old recording. Resolve // ALL attachments before descriptor binding captures the current cmd - unless the open // 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); } 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(); // The seven device/format reasons are now MASKED OUT of the vertex input state, and // this is the monolith arm's pre-flight for them, statement for statement // (VulkanRenderer.cpp's SetupDraw: "Fail loudly rather than render wrong pixels"). An // enabled array with no usable VkFormat never reaches the vertex input state, which // makes it indistinguishable from a DISABLED one - and a disabled one is answered // below by uploading the current attribute value, so without this check the draw would // quietly feed the shader a constant in place of the application's vertex data. // // A DECLINE, NOT A FATAL (rule I (a)): the draw is skipped, the reason is named once, // and the observable is the monolith lane's for the same VAO on the same device. // BuildWireVertexInput only considers attributes the program reads, so a masked bit is // always an active one and this predicate is the whole of the difference. if ((input.unsupportedAttribMask & programObj.activeVertexInputLocationMask) != 0) { MGLOG_E_ONCE("Magma wire draw skipped: program=%u reads vertex attribute location " "mask 0x%x whose enabled array has no supported vertex format", program.GetExternalIndex(), input.unsupportedAttribMask & programObj.activeVertexInputLocationMask); WireDeclineTally::Count(WireDeclineSite::VertexFormatUnsupported); return false; } auto& vertexSlices = scratch.vertexSlices; vertexSlices.assign(input.bindings.size(), BufferSlice{}); for (SizeT binding = 0; binding < input.bindings.size(); ++binding) { const Uint32 location = input.bindingAttributeLocations[binding]; const auto& attr = vao.Attributes[location]; const auto& resource = state.VertexBuffers[location]; // P8-F: A RECORD INVARIANT, NOT AN UNMIGRATED VERB (it carried a P8 phase tag). // The client turns every enabled client-memory array into an owned buffer before the // draw record is built, or refuses the draw itself (MG_Impl/Pipe/OwnedDrawInputs.h), // so an attribute the program reads with no buffer handle is a foreign peer's record. if (MG_Pipe::MGPipeHandleIsNull(resource.Res)) MG_Pipe::MGPipeSessionFail( MG_Pipe::MGPipeFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"Magma:client-vertex-array\"} location=%u: an " "attribute the program reads carries no buffer handle", location); BufferSlice slice{}; if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice)) { // A buffer with no data store is legal GL to bind (glGenBuffers + glBindBuffer, no // glBufferData), and the client mints its handle at construction while the create // only crosses with the first respecify (PipeFill.cpp's M-1) - so the handle may be // one this server never saw, or one it holds with zero bytes. Either way the draw // reads no storage: it is declined, as the GL draws nothing defined from it. static Bool s_described = false; if (!s_described && !m_bufferManager.IsKnownWireBuffer(resource.Res)) { s_described = true; m_bufferManager.DescribeWireBufferHistory(resource.Res); } MGL_WIRE_DECLINE_AT(VertexAttributeWindow, "an enabled attribute's buffer has no storage"); return false; } const VkDeviceSize offset = input.bindingBaseOffsets[binding]; const auto type = static_cast(attr.Type); const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra); if (offset > slice.size || elementSize > slice.size - offset) { MGL_WIRE_DECLINE_AT(VertexAttributeWindow, "an enabled attribute's first element lies outside its buffer"); return false; } const auto conversion = input.bindingConversions[binding]; if (conversion != VertexInputStateFactory::VertexStreamConversion::None) { const SizeT stride = static_cast(attr.Stride); SizeT count = stride ? 1 + (static_cast(slice.size - offset) - elementSize) / stride : 1; // Direct non-indexed draws bound the conversion; indexed/multi-draw may // name arbitrary vertices, so preserve the entire valid source stream. So may an // indirect draw (P8-D): its vertex range is in GPU words nobody read. if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !(aspects & DrawSetupAspect::IndirectDrawBuffer) && !resource.Divisor) count = std::min(count, static_cast(drawParams.firstVertex) + drawParams.vertexCount); if (!count) { MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements"); return false; } // One conversion per (source content, layout), kept across frames until the source // changes (VkBufferManager's converted-stream cache): a multi-draw over a chunk arena // names the same stream for every chunk, frame after frame. Uint64 contentSerial = 0; const Bool cacheable = MG_Config::Features.MagmaVertexConversionCache != MG_Config::QuirkOverride::ForceOff && m_bufferManager.GetWireBufferContentSerial(resource.Res, contentSerial); const VkBufferManager::ConvertedStreamKey cacheKey{contentSerial, offset, static_cast(stride), static_cast(attr.Type), static_cast(attr.Size), static_cast(conversion), attr.Normalized != 0, attr.IsInteger != 0, attr.IsBgra != 0}; if (cacheable && m_bufferManager.LookupConvertedStream(cacheKey, count, vertexSlices[binding])) { ++m_wireStreamsReused; continue; } Vector source((count - 1) * stride + elementSize); if (!m_bufferManager.ReadWireBuffer(resource.Res, offset, source.size(), source.data())) { MGL_WIRE_DECLINE_AT(VertexStreamRead, "the vertex bytes to convert could not be read back"); return false; } Vector converted; // P7 wave 2 package C, CONTRACT-P7 §3.2 `vertex-format-conversion`: DECLINE. // The converter is asked only for a conversion BuildWireVertexInput already // chose, so a refusal here means the two disagree about what is convertible - // a MobileGL-internal defect, not a peer's malformed record and not a verb the // split path has yet to migrate. Killing the session for it told the peer // "protocol corruption" about a bug on this side; skipping the draw and naming // it once is the honest answer and is what the row asks for. The packed // converter that would make the refusal unreachable is the row's optional half // and is not taken here. if (!ConvertWireVertexStream(attr, conversion, source.data(), stride, count, converted)) { MGLOG_E_ONCE("Magma wire draw skipped: no vertex stream converter for " "location=%u type=%u size=%u conversion=%d", location, attr.Type, static_cast(attr.Size), static_cast(conversion)); WireDeclineTally::Count(WireDeclineSite::VertexStreamConverter); return false; } ++m_wireStreamsConverted; if (!(cacheable && m_bufferManager.StoreConvertedStream(cacheKey, resource.Res, count, converted.data(), converted.size(), vertexSlices[binding])) && !m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), converted.data(), converted.size(), 16, vertexSlices[binding])) { MGL_WIRE_DECLINE_AT(VertexTransientUpload, "the converted vertex stream did not fit the transient arena"); return false; } } else { slice.offset += offset; slice.size -= offset; vertexSlices[binding] = slice; } } BufferSlice indexSlice{}; VkIndexType indexType = VK_INDEX_TYPE_MAX_ENUM; if (aspects & DrawSetupAspect::IndexBuffer) { if (!indices) { MGL_WIRE_DECLINE_AT(IndexViewMissing, "an indexed draw arrived with no index view"); return false; } const SizeT width = MG_Util::GetGLTypeSize(indices->indexType); if (width != 1 && width != 2 && width != 4) { MGL_WIRE_DECLINE_AT(IndexTypeWidth, "the index type is not 1, 2 or 4 bytes wide"); return false; } indexType = width == 4 ? VK_INDEX_TYPE_UINT32 : width == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT8; const auto& restart = MG_Pipe::gPipeInputs->GetRenderStateParameters(); const Uint32 fixed = MG_Util::FixedRestartIndexForGLType(indices->indexType); const Bool replaceRestart = restart.PrimitiveRestartEnabled && !restart.PrimitiveRestartFixedIndexEnabled && restart.PrimitiveRestartIndex < fixed; const Bool expandByte = width == 1 && !m_indexTypeUint8ExtensionEnabled; const Bool cpuIndices = indices->forceClientMemory || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res); Vector copied; const void* bytes = nullptr; if (cpuIndices) bytes = reinterpret_cast(indices->indexByteOffset); else { if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice)) { // As for a vertex buffer: a bound element buffer with no data store yet. static Bool s_described = false; if (!s_described && !m_bufferManager.IsKnownWireBuffer(state.IndexBuffer.Res)) { s_described = true; m_bufferManager.DescribeWireBufferHistory(state.IndexBuffer.Res); } MGL_WIRE_DECLINE_AT(IndexWindow, "the element buffer has no storage"); return false; } const Uint64 offset = state.IndexBuffer.Offset + indices->indexByteOffset; if (offset < state.IndexBuffer.Offset || offset > indexSlice.size || indices->indexByteSize > indexSlice.size - offset) { MGL_WIRE_DECLINE_AT(IndexWindow, "the index range lies outside its buffer"); return false; } if (replaceRestart || expandByte || offset % width) { copied.resize(indices->indexByteSize); if (!m_bufferManager.ReadWireBuffer(state.IndexBuffer.Res, offset, copied.size(), copied.data())) { MGL_WIRE_DECLINE_AT(IndexStreamRead, "the indices to rewrite could not be read back"); return false; } bytes = copied.data(); } indexSlice.offset += offset; indexSlice.size = indices->indexByteSize; } if (bytes) { Vector rewritten; SizeT uploadSize = indices->indexByteSize; if (replaceRestart || expandByte) { const SizeT outWidth = expandByte ? 2 : width; rewritten.resize(uploadSize / width * outWidth); for (SizeT i = 0; i < uploadSize / width; ++i) { Uint32 value = 0; Memcpy(&value, static_cast(bytes) + i * width, width); if (replaceRestart && value == restart.PrimitiveRestartIndex) value = fixed; if (expandByte && ResolvePrimitiveRestartEnable(aspects, indices) && value == fixed) value = 0xffffu; Memcpy(rewritten.data() + i * outWidth, &value, outWidth); } bytes = rewritten.data(); uploadSize = rewritten.size(); if (expandByte) indexType = VK_INDEX_TYPE_UINT16; } if (!m_bufferManager.UploadTransient(BufferKind::Index, m_frameContext.GetCurrentFrameIndex(), bytes, uploadSize, 16, indexSlice)) { MGL_WIRE_DECLINE_AT(IndexTransientUpload, "the rewritten indices did not fit the transient arena"); return false; } } if (!indexSlice.IsValid()) { MGL_WIRE_DECLINE_AT(IndexSliceInvalid, "no usable index slice was resolved"); return false; } } if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); // Descriptor resolution can upgrade a view's root image to STORAGE usage. // Resolve it before constructing framebuffer views so both roles of that image // name the same allocation even on its first storage-image use. if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_GRAPHICS)) { MGL_WIRE_DECLINE_AT(UniformBufferBinding, "the program's uniform/storage bindings could not be bound"); return false; } // Asked again: the descriptor resolve may have closed the pass, re-created an image or // moved a layout. 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. const Uint64 compatibilityId = m_wireDrawPassCompatibilityId; const auto pipeline = GetOrCreatePipelineWithInput(mode, program, programObj, flags, input, pass, ResolvePrimitiveRestartEnable(aspects, indices), WireFramebufferIsLayered(*fbo), compatibilityId); if (pipeline == VK_NULL_HANDLE) { MGL_WIRE_DECLINE_AT(PipelineUnavailable, "no graphics pipeline could be created for this draw"); return false; } // One call for the whole run of bindings 0..n-1 rather than one per binding, and none when // the command buffer already holds exactly these (DynamicStateShadow: reset at every // recording begin and by every auxiliary pipeline; the current-attribute binds below only // ever touch bindings past n). auto& bindShadow = *g_dynamicStateShadow; const Uint64 storeEpoch = m_bufferManager.GetWireStoreDestroyEpoch(); if (bindShadow.bindStoreEpoch != storeEpoch) { bindShadow.vertexBindValid = false; bindShadow.indexBindValid = false; bindShadow.bindStoreEpoch = storeEpoch; } if (!vertexSlices.empty()) { VkBuffer buffers[kMaxVertexAttribs]; VkDeviceSize offsets[kMaxVertexAttribs]; const Uint32 count = static_cast(std::min(vertexSlices.size(), kMaxVertexAttribs)); for (Uint32 i = 0; i < count; ++i) { buffers[i] = vertexSlices[i].buffer; offsets[i] = vertexSlices[i].offset; } constexpr Uint32 kShadowed = DynamicStateShadow::kMaxShadowedVertexBindings; Bool same = bindShadow.vertexBindValid && bindShadow.vertexBindingCount == count && count <= kShadowed; for (Uint32 i = 0; same && i < count; ++i) same = bindShadow.vertexBuffers[i] == buffers[i] && bindShadow.vertexOffsets[i] == offsets[i]; if (!same) { vkCmdBindVertexBuffers(frame.commandBuffer, 0, count, buffers, offsets); bindShadow.vertexBindValid = count <= kShadowed; bindShadow.vertexBindingCount = count; for (Uint32 i = 0; i < count && i < kShadowed; ++i) { bindShadow.vertexBuffers[i] = buffers[i]; bindShadow.vertexOffsets[i] = offsets[i]; } } for (Uint32 i = count; i < vertexSlices.size(); ++i) vkCmdBindVertexBuffers(frame.commandBuffer, i, 1, &vertexSlices[i].buffer, &vertexSlices[i].offset); } Uint32 binding = static_cast(vertexSlices.size()); for (Uint32 location = 0; location < kMaxVertexAttribs; ++location) { if (!(programObj.activeVertexInputLocationMask & (1u << location)) || (input.attributeLocationMask & (1u << location))) continue; VkFormat format = VK_FORMAT_UNDEFINED; const void* data = nullptr; VkDeviceSize size = 0; if (!TryGetCurrentVertexAttributeUploadPayload(MG_Pipe::gPipeInputs->GetCurrentVertexAttribute(location), programObj.vertexInputTypes[location], format, data, size)) { MGL_WIRE_DECLINE_AT(CurrentAttributePayload, "a disabled array's current attribute value has no upload payload"); return false; } BufferSlice slice{}; if (!m_bufferManager.UploadTransient(BufferKind::Vertex, m_frameContext.GetCurrentFrameIndex(), data, size, 16, slice)) { MGL_WIRE_DECLINE_AT(CurrentAttributeUpload, "the current attribute value did not fit the transient arena"); return false; } vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset); } if (indexSlice.IsValid() && !(bindShadow.indexBindValid && bindShadow.indexBuffer == indexSlice.buffer && bindShadow.indexOffset == indexSlice.offset && bindShadow.indexType == indexType)) { vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType); bindShadow.indexBindValid = true; bindShadow.indexBuffer = indexSlice.buffer; bindShadow.indexOffset = indexSlice.offset; bindShadow.indexType = indexType; } // Still open from the previous draw (nothing closed it, same command buffer): continue. 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 && !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; } if (!bindShadow.graphicsPipelineValid || bindShadow.graphicsPipeline != pipeline) { vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); bindShadow.graphicsPipelineValid = true; bindShadow.graphicsPipeline = pipeline; } ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault, ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin)); if (m_fragmentShadingRate.Pipeline) { ShadowedSetFragmentShadingRate(frame.commandBuffer, s_vkCmdSetFragmentShadingRateKHR); } // This draw's state is set; where a discard draw then loses it (WireDiscardStateProbe.h), // the next draw must set its own rather than inherit. if (m_wireReemitStateAfterDiscard && MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) ForgetDynamicStateAfterDiscardDraw(); // Storage images this draw writes are what the next sampled-image barrier must cover. if (programObj.hasStorageImages) BumpWireImageWriteEpoch(); if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1); return true; } // P15 S0: A CLEAR INTO THE ATTACHMENTS OF THE DRAW PASS BECOMES PART OF THAT 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), so the draws after it continue that pass: // // * A clear of the whole render area that would BEGIN the pass begins it with LOAD_OP_CLEAR for // the cleared attachments (a clear-load variant of the pass's render pass). Nothing is loaded // from memory into the tile for them, which is also the cheapest form on a tiler. // * Any other clear - into the open pass, or scissored - is vkCmdClearAttachments in the pass: // ordered with the pass's draws as a draw is. // // Every write goes through the pass's attachment views (the UNORM twin of an sRGB image while // GL_FRAMEBUFFER_SRGB is off, exactly GL's raw clear - no pre-compensation), and wire attachments // live in GENERAL. What none of this expresses goes back to the standalone path, per GL buffer // bit: a partial color 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; // NOT THE DEFAULT FRAMEBUFFER (measured, not understood): on Adreno 750 the openra retrace // loses its palette-sampled sprites (SSIM 0.956) when a color+depth clear of the default // framebuffer becomes part of its draw pass - with LOAD_OP_CLEAR as well as with // vkCmdClearAttachments - while the same clear split across the two paths, or left // standalone, renders correctly. Default-framebuffer clears keep the standalone path until // that is explained (perf-p15/HANDOFF.md). if (fbo.IsDefault && MG_Config::Features.MagmaInPassClearDefault != MG_Config::QuirkOverride::ForceOn) 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) { // 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); } 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; // 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; 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 (GLScissorTestClips()) { 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)); // An empty box clears nothing, on either path. if (x0 >= x1 || y0 >= y1) return 0; 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 Bool wholeArea = area.offset.x == 0 && area.offset.y == 0 && area.extent.width == key.width && area.extent.height == key.height; const auto* active = VkRenderPassManager::GetActiveRenderPass(); Bool open = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer && active->hash == pass.hash; if (wholeArea && !open) { if (active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer); auto& begun = *m_wireDrawPass; Uint64 variantKey = 0; VkClearValue values[MG_Pipe::kMGPipeMaxColorAttachments + 1]{}; const Uint32 attachmentCount = static_cast(begun.wireAttachments.size()); if (attachmentCount == 0 || attachmentCount > MG_Pipe::kMGPipeMaxColorAttachments + 1) return remaining; // not a wire-built pass: cannot happen, but never guess for (Uint32 c = 0; c < count; ++c) { const auto& clear = clears[c]; if (clear.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) { const Uint32 a = begun.wireColors[clear.colorAttachment].attachment; variantKey |= 1ull << (2 * a); values[a].color = clear.clearValue.color; } else { const Uint32 a = begun.wireDepth.attachment; if (clear.aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) variantKey |= 1ull << (2 * a); if (clear.aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) variantKey |= 1ull << (2 * a + 1); values[a].depthStencil = clear.clearValue.depthStencil; } } if (!BeginWireDrawPassFolding(frame, begun, variantKey, values)) return remaining; return remaining & ~handled; } if (!open && active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer); 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 color 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; // Bits 2a and 2a+1 clear attachment a (color or depth, and stencil); bit kWireDiscardBit + a // marks its content undefined, so whatever is not cleared is not loaded either. for (Uint32 a = 0; a < attachmentCount; ++a) { const Bool discard = variantKey >> (kWireDiscardBit + a) & 1; if (variantKey >> (2 * a) & 1) descriptions[a].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; else if (discard) descriptions[a].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; if (variantKey >> (2 * a + 1) & 1) descriptions[a].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; else if (discard) descriptions[a].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; } 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; } // The default framebuffer's half of the deferred clears: a whole-window glClear waits for the // next pass that draws into the window, which loads it with LOAD_OP_CLEAR // (BeginWireDrawPassFolding) instead of storing a cleared window and loading it back. A pass // open on the window ends first, since the in-pass clear does not take the window // (MagmaInPassClearDefault); a present drops the depth/stencil half (Present). Returns the // buffers it did not take. GLbitfield VulkanRenderer::DeferDefaultWireClear(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; if (GLScissorTestClips()) return remaining; GLbitfield handled = 0; Bool color = false; VkImageAspectFlags depthStencil = 0; if (remaining & GL_COLOR_BUFFER_BIT) { const Int32 slot = fbo.DrawBuffers[0]; const auto mask = MG_Pipe::gPipeInputs->GetColorMaskIndexed(0); const Bool none = slot < 0 || (!mask.r() && !mask.g() && !mask.b() && !mask.a()); if (none) handled |= GL_COLOR_BUFFER_BIT; else if (mask.r() && mask.g() && mask.b() && mask.a() && payload.colorEncoding == ClearColorEncoding::Float) { handled |= GL_COLOR_BUFFER_BIT; color = true; } } // A window without a depth/stencil buffer clears nothing there. const VkFormat depthStencilFormat = m_swapchainObject.GetDepthStencilFormat(); const Bool hasDepth = depthStencilFormat != VK_FORMAT_UNDEFINED; const Bool packed = vkuFormatIsDepthAndStencil(depthStencilFormat); if (remaining & GL_DEPTH_BUFFER_BIT) { handled |= GL_DEPTH_BUFFER_BIT; if (hasDepth && MG_Pipe::gPipeInputs->GetDepthMask()) depthStencil |= VK_IMAGE_ASPECT_DEPTH_BIT; } if (remaining & GL_STENCIL_BUFFER_BIT) { const Uint32 mask = MG_Pipe::gPipeInputs->GetStencilState(StencilFace::Front).WriteMask & 255u; if (mask == 0 || !packed) handled |= GL_STENCIL_BUFFER_BIT; else if (mask == 255u) { handled |= GL_STENCIL_BUFFER_BIT; depthStencil |= VK_IMAGE_ASPECT_STENCIL_BIT; } } if (!color && depthStencil == 0) return remaining & ~handled; if (VkRenderPassManager::GetActiveRenderPass() != nullptr && m_wireDrawPass && m_wireDrawPassKey.isDefault) RetireWireDrawPass(); // The index the next drawing pass on the window will address (WireDraw.cpp's pass key). const Uint32 index = DefaultFramebufferWriteIndex(true); auto& pending = m_defaultWirePendingClear; if (pending.Pending() && (pending.swapchain != m_swapchainObject.GetHandle() || pending.swapchainImageIndex != index)) MaterializeDefaultWirePendingClear(); pending.swapchain = m_swapchainObject.GetHandle(); pending.swapchainImageIndex = index; if (color) { pending.color = true; pending.colorPayload = payload; pending.colorPayloadCompensated = payload; PreCompensateSrgbClearColor(pending.colorPayloadCompensated, m_swapchainObject.GetSurfaceFormat().format); m_swapchainObject.SetImageContentDefined(index, true); } if (depthStencil & VK_IMAGE_ASPECT_DEPTH_BIT) pending.depthStencilPayload.depth = payload.depth; if (depthStencil & VK_IMAGE_ASPECT_STENCIL_BIT) pending.depthStencilPayload.stencil = payload.stencil; if (depthStencil) { pending.depthStencil |= depthStencil; m_swapchainObject.SetDepthStencilContentDefined(index, true); } return remaining & ~handled; } void VulkanRenderer::MaterializeDefaultWirePendingClear() { if (!m_defaultWirePendingClear.Pending()) return; const DefaultWirePendingClear pending = m_defaultWirePendingClear; m_defaultWirePendingClear = {}; // A recreated swapchain took the images the clear was for. if (pending.swapchain != m_swapchainObject.GetHandle() || pending.swapchainImageIndex >= m_swapchainObject.GetImageCount()) return; const VkExtent2D extent = m_swapchainObject.GetExtent(); const VkRect2D area{{0, 0}, extent}; WireImage image{}; image.isDefault = true; image.extent = extent; image.swapchainImageIndex = pending.swapchainImageIndex; if (pending.color) { image.aspect = VK_IMAGE_ASPECT_COLOR_BIT; image.image = m_swapchainObject.GetImage(pending.swapchainImageIndex); image.format = m_swapchainObject.GetSurfaceFormat().format; image.layout = m_swapchainObject.GetImageLayout(pending.swapchainImageIndex); RecordWireImageClear(image, VK_IMAGE_ASPECT_COLOR_BIT, pending.colorPayloadCompensated, area, false, /*colorPreCompensated=*/true); } if (pending.depthStencil && m_swapchainObject.GetDepthStencilImage(pending.swapchainImageIndex)) { image.aspect = pending.depthStencil; image.image = m_swapchainObject.GetDepthStencilImage(pending.swapchainImageIndex); image.format = m_swapchainObject.GetDepthStencilFormat(); image.layout = m_swapchainObject.GetDepthStencilImageLayout(pending.swapchainImageIndex); RecordWireImageClear(image, pending.depthStencil, pending.depthStencilPayload, area, false); } } // A pending window clear (DeferDefaultWireClear) becomes the load op of the window pass that // begins: its attachments are matched by image, since a continued pass key is not walked again. // A clear the caller already put in `variantKey` is newer and wins. void VulkanRenderer::FoldDefaultWirePendingClear(RenderPassEntry& begun, Uint64& variantKey, VkClearValue* merged) { auto& pending = m_defaultWirePendingClear; if (!pending.Pending() || !m_wireDrawPassKey.isDefault || m_wireDrawPassKey.swapchain != pending.swapchain || m_wireDrawPassKey.swapchainImageIndex != pending.swapchainImageIndex) return; const Uint32 attachmentCount = static_cast(std::min(begun.wireAttachments.size(), m_wireDrawPassKey.attachments.size())); const VkImage colorImage = m_swapchainObject.GetImage(pending.swapchainImageIndex); const VkImage depthStencilImage = m_swapchainObject.GetDepthStencilImage(pending.swapchainImageIndex); for (Uint32 a = 0; a < attachmentCount && a < MG_Pipe::kMGPipeMaxColorAttachments + 1; ++a) { const auto& attachment = m_wireDrawPassKey.attachments[a]; if (pending.color && attachment.image == colorImage) { if (!(variantKey >> (2 * a) & 1)) { variantKey |= 1ull << (2 * a); merged[a].color = MakeVkClearColorValue(pending.colorPayload, false); } pending.color = false; } else if (pending.depthStencil && attachment.image == depthStencilImage) { if ((pending.depthStencil & VK_IMAGE_ASPECT_DEPTH_BIT) && !(variantKey >> (2 * a) & 1)) { variantKey |= 1ull << (2 * a); merged[a].depthStencil.depth = pending.depthStencilPayload.depth; } if ((pending.depthStencil & VK_IMAGE_ASPECT_STENCIL_BIT) && !(variantKey >> (2 * a + 1) & 1)) { variantKey |= 1ull << (2 * a + 1); merged[a].depthStencil.stencil = pending.depthStencilPayload.stencil; } pending.depthStencil = 0; } } // An aspect the pass does not attach stays pending: none of its draws write it. } // 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(); FoldDefaultWirePendingClear(begun, variantKey, merged); for (const Uint32 a : m_wireDiscardLoads) if (a < attachmentCount) variantKey |= 1ull << (kWireDiscardBit + a); m_wireDiscardLoads.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 // and MultiDrawElementsIndirectCount), with the wire store's own VkBuffer in place of the // frontend buffer's resident slice. Before this, every wire indirect draw read its commands // through ReadWireBuffer and expanded them on the CPU, and a store a shader had written made // that read a whole-GPU wait (WaitForWireBufferHostAccess) per draw. // // THE ONE FALSE: a COUNT form this device cannot issue - the monolith arm's own condition for // vkCmdDrawIndexedIndirectCount (MultiDrawElementsIndirectCount), extended to the non-indexed // twin the monolith never loads. Everything else is issued here or declined by name. Bool VulkanRenderer::DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount, GLsizei stride, Bool indexed, Bool counted, Uint64 countOffset) { if (drawcount <= 0) return true; if (counted) { const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount; if (!m_drawIndirectCountExtensionEnabled || countEntry == nullptr || (!m_multiDrawIndirectFeatureEnabled && drawcount != 1)) return false; } const auto& state = MG_Pipe::MGPipeApplier(); // GL's command sizes (GL 4.6 10.3.10), which are also Vulkan's. const Uint64 commandSize = indexed ? 5 * sizeof(Uint32) : 4 * sizeof(Uint32); if (stride == 0) stride = static_cast(commandSize); BufferSlice commands{}; if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectBuffer) || !m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectBuffer, commands)) { MGL_WIRE_DECLINE_AT(IndirectBufferUnbound, "an indirect draw names no live indirect buffer store"); return true; } const Uint64 span = static_cast(stride) * static_cast(drawcount - 1) + commandSize; // GL refuses a stride that is not a multiple of four (INVALID_VALUE), and Vulkan requires // it for drawCount > 1; a peer's record that breaks either is declined, not issued. if (stride < static_cast(commandSize) || (stride & 3) != 0 || (offset & 3u) != 0 || offset > commands.size || span > commands.size - offset) { MGL_WIRE_DECLINE_AT(IndirectCommandRange, "the indirect commands lie outside their store or are misaligned"); return true; } BufferSlice parameters{}; if (counted) { if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectParameterBuffer) || !m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectParameterBuffer, parameters) || (countOffset & 3u) != 0 || countOffset > parameters.size || parameters.size - countOffset < sizeof(Uint32)) { MGL_WIRE_DECLINE_AT(IndirectCountRange, "the indirect draw count lies outside its parameter store"); return true; } } // The index view is the WHOLE bound element store past its binding offset, as the monolith // arm binds it: the commands name their own firstIndex, which only the GPU reads. IndexBufferView indices{}; DrawCmdParam vertexRange{}; vertexRange.instanceCount = 1; Flags aspects = DrawSetupAspect::IndirectDrawBuffer; if (indexed) { const SizeT width = MG_Util::GetGLTypeSize(type); BufferSlice elements{}; if (width == 0 || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res) || !m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, elements) || state.IndexBuffer.Offset > elements.size) { MGL_WIRE_DECLINE_AT(IndirectIndexStore, "an indexed indirect draw names no usable element store"); return true; } indices.indexType = type; indices.indexByteOffset = 0; indices.indexByteSize = static_cast(elements.size - state.IndexBuffer.Offset); vertexRange.vertexCount = static_cast(indices.indexByteSize / width); aspects |= DrawSetupAspect::IndexBuffer; } // A shader write the store was marked with (MarkWireBufferGpuWritten) is made visible to // the indirect read here, once per write, so a program that omits GL_COMMAND_BARRIER_BIT // is not left racing a read the old CPU path used to take after a whole-GPU wait. The // application's own glMemoryBarrier is BuildMemoryBarrierForGlBarriers, as on the monolith // arm; transfer writes carry their own MEMORY_READ after-barrier (VkBufferManager.cpp). const Bool commandsWritten = m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectBuffer); const Bool countWritten = counted && m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectParameterBuffer); if (commandsWritten || countWritten) { RetireWireDrawPass(); auto& barrierFrame = m_frameContext.GetCurrent(); if (!barrierFrame.isCommandRecording) m_frameContext.BeginCommandRecording(); if (VkRenderPassManager::GetActiveRenderPass() != nullptr) VkRenderPassManager::EndRenderPass(barrierFrame.commandBuffer); VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT; vkCmdPipelineBarrier(barrierFrame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1); } auto& frame = m_frameContext.GetCurrent(); m_wireNativeIndirectDraw = true; const Bool ready = SetupDraw(frame, mode, aspects, vertexRange, indexed ? &indices : nullptr); m_wireNativeIndirectDraw = false; if (!ready) return true; MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__); const VkCommandBuffer commandBuffer = frame.commandBuffer; const VkDeviceSize first = commands.offset + offset; const Uint32 count = static_cast(drawcount); const Uint32 byteStride = static_cast(stride); if (counted) { const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount; countEntry(commandBuffer, commands.buffer, first, parameters.buffer, parameters.offset + countOffset, count, byteStride); } else if (count == 1 || (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled)) { // maxDrawIndirectCount bounds one call; GL's drawcount does not. const Uint32 limit = std::max(1u, m_physicalDevice.properties.limits.maxDrawIndirectCount); for (Uint32 base = 0; base < count; base += limit) { const Uint32 chunk = std::min(count - base, limit); const VkDeviceSize at = first + static_cast(base) * byteStride; if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, chunk, byteStride); else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, chunk, byteStride); } } else { // No multiDrawIndirect: one indirect draw per command, still read by the GPU // (the monolith arm's own fallback). for (Uint32 i = 0; i < count; ++i) { const VkDeviceSize at = first + static_cast(i) * byteStride; if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, 1, 0); else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, 1, 0); } } if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDraws, 1); return true; } void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) { RetireWireDrawPass(); auto& frame = m_frameContext.GetCurrent(); const auto program = WireProgramSource(MG_Pipe::MGPipeApplier().DispatchProgram); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V"); return; } const auto& programObj = m_programFactory->GetOrCreateProgram(program, {}); if (programObj.invalidSpirvDeclined) { MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built"); return; } if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) { MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch"); return; } AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.cpp) const auto pipeline = GetOrCreateComputePipeline(programObj); if (!pipeline) { MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch"); return; } if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) { MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound"); return; } vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); EndActiveRenderPassOn(frame.commandBuffer); vkCmdDispatch(frame.commandBuffer, x, y, z); } // ---- P8-SV: THE WIRE ARM'S INDIRECT DISPATCH READS ITS GROUP COUNTS ON THE GPU ---------- // // The monolith arm's shape (VulkanRenderer.cpp's DispatchComputeIndirect) with the wire // store's own VkBuffer in place of the frontend buffer's resident slice. Before this the wire // arm read the three words through ReadWireBuffer and dispatched them from the CPU, and a store // a shader had written made that read a whole-GPU wait (WaitForWireBufferHostAccess) per call. // There is no shape the device cannot issue, so there is no CPU path left: a record whose // store or words are unusable is declined by name. // // The barrier is P8-D's draw rule (DrawWireIndirectNative): a store marked shader-written gets // one MEMORY_WRITE -> INDIRECT_COMMAND_READ barrier before its next indirect read, whether or // not the application issued GL_COMMAND_BARRIER_BIT; DRAW_INDIRECT is the stage that consumes // VkDispatchIndirectCommand too. It is TAKEN before the bindings below, because binding this // dispatch's own storage buffers marks them written, and a store that is both this dispatch's // parameters and one of its outputs must keep that mark for the next reader. void VulkanRenderer::DispatchWireComputeIndirect(GLintptr offset) { RetireWireDrawPass(); const auto& state = MG_Pipe::MGPipeApplier(); constexpr Uint64 kCommandSize = 3 * sizeof(Uint32); // GL 4.6 10.3.10 / VkDispatchIndirectCommand BufferSlice parameters{}; if (MG_Pipe::MGPipeHandleIsNull(state.VerbDispatchIndirectBuffer) || !m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbDispatchIndirectBuffer, parameters)) { MGL_WIRE_DECLINE_AT(DispatchIndirectBufferUnbound, "an indirect dispatch names no live dispatch-indirect store"); return; } // GL refuses an offset that is negative or not a multiple of four (INVALID_VALUE) and a // command past the end of the buffer (INVALID_OPERATION); a peer's record that breaks // either is declined, not issued. if (offset < 0 || (static_cast(offset) & 3u) != 0 || static_cast(offset) > parameters.size || parameters.size - static_cast(offset) < kCommandSize) { MGL_WIRE_DECLINE_AT(DispatchIndirectRange, "the dispatch group counts lie outside their store or are misaligned"); return; } auto& frame = m_frameContext.GetCurrent(); if (m_bufferManager.TakeWireIndirectReadBarrier(state.VerbDispatchIndirectBuffer)) { if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); if (VkRenderPassManager::GetActiveRenderPass() != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer); VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT; vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1); } const auto program = WireProgramSource(state.DispatchProgram); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V"); return; } const auto& programObj = m_programFactory->GetOrCreateProgram(program, {}); if (programObj.invalidSpirvDeclined) { MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built"); return; } if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) { MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch"); return; } AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.cpp) const auto pipeline = GetOrCreateComputePipeline(programObj); if (!pipeline) { MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch"); return; } // PrepareWireTextureResources may have flushed and begun a new recording. auto& dispatchFrame = m_frameContext.GetCurrent(); if (!dispatchFrame.isCommandRecording) m_frameContext.BeginCommandRecording(); if (!m_uniformManager->BindProgramUniformBuffers(dispatchFrame.commandBuffer, program, programObj, m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) { MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound"); return; } vkCmdBindPipeline(dispatchFrame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline); EndActiveRenderPassOn(dispatchFrame.commandBuffer); vkCmdDispatchIndirect(dispatchFrame.commandBuffer, parameters.buffer, parameters.offset + static_cast(offset)); if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDispatches, 1); } } // namespace MobileGL::MG_Backend::DirectVulkan