diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index deb1c70f..5d45111b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -2763,6 +2763,41 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool UniformManager::PrepareWireTextureResources(const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj) { + if (!program.IsWire()) return true; + if (programObj.declinedDescriptors || m_textureManager == nullptr) return false; + const auto& state = MG_Pipe::MGPipeApplier(); + // Resolve writable images first: aliases sampled by the same draw must + // see the final STORAGE-capable allocation before any view is built. + for (const Bool storage : {true, false}) { + const auto wanted = storage ? ProgramFactory::DescriptorBindingKind::StorageImage + : ProgramFactory::DescriptorBindingKind::CombinedImageSampler; + for (const Uint32 binding : programObj.activeBindings) { + if (binding >= m_maxBindings) break; + if (programObj.bindingKinds[binding] != wanted) continue; + const Int baseLocation = programObj.samplerUniformLocationByBinding[binding]; + const Uint32 count = BindingDescriptorCount(programObj, binding); + for (Uint32 element = 0; element < count; ++element) { + const Int location = baseLocation + static_cast(element); + if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location)) + return false; + const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast(location)); + if (unit < 0 || static_cast(unit) >= + (storage ? MG_Pipe::kMGPipeMaxImageUnits : MG_Pipe::kMGPipeMaxTextureUnits)) return false; + const auto handle = storage ? state.BoundShaderImages[unit].Res + : state.BoundSamplerViews[unit].Texture; + if (MG_Pipe::MGPipeHandleIsNull(handle)) WireDescriptorFatal("unbound-image-placeholder@P7"); + if (!m_textureManager->SyncTextureResourceByHandle(handle, false, storage)) return false; + } + } + } + m_textureManager->FlushPendingUploads(); + return true; + } +#endif + Bool UniformManager::BindProgramUniformBuffers(VkCommandBuffer commandBuffer, const MagmaProgramSource& program, const ProgramFactory::VkProgramObject& programObj, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h index 32cf0132..a3b3fd24 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.h @@ -100,6 +100,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector& outBindings) const; static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, GLint imageLevel, GLenum imageAccess); +#if MOBILEGL_BUILD_DISAGGREGATED + // Resolve lazy texture uploads/promotions before the caller captures the + // command buffer passed to BindProgramUniformBuffers. Preparation may + // submit older work, but descriptor recording must never rotate it. + Bool PrepareWireTextureResources(const MagmaProgramSource& program, + const ProgramFactory::VkProgramObject& programObj); +#endif // samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that // every input of every combined-image-sampler resolution is unchanged since the // previous draw's resolve - same (texture, sampler) per binding, texture params diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index f72b60fd..4c6f28c2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -316,7 +316,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Concurrent GPU reads do not prevent a CPU read. Only staged copies or // shader writes need a host visibility barrier and a submission wait; // ordinary index/vertex inspection must not stall once per draw. - if (resource->gpuWritesPending && !WaitForWireBufferHostAccess(*resource)) return false; + if (resource->gpuWritesPending) { + if (!WaitForWireBufferHostAccess(*resource)) return false; + // A draw may already hold a native slice of this store while another + // attribute needs a CPU conversion. The wait completes previous GPU + // work, not the draw that will bind that previously acquired slice. + // Preserve its reservation even if the idle drain advanced the frame. + resource->lastUseSerial = m_frameSerial; + } if (!resource->buffer.Invalidate(size, offset)) return false; Memcpy(dst, static_cast(resource->buffer.GetMappedData()) + offset, static_cast(size)); return true; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 9f6e2ebb..521be1dd 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -22,6 +22,7 @@ // (shape) and the server's staged-texture store (texels). #include #include +#include "../DirectVulkan.h" #endif #include #include @@ -2187,6 +2188,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.sampleCount == VK_SAMPLE_COUNT_1_BIT && resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && resource.layout != VK_IMAGE_LAYOUT_UNDEFINED; + // Preservation submits its copy separately. Earlier draws/clears of the + // old image must reach the queue first; waiting only for the preservation + // fence cannot order work that is still in the renderer's open recording. + // The descriptor caller preflights this before capturing its command buffer. + if (preserve && pVulkanRenderer && !pVulkanRenderer->FlushWirePendingCommandsForTextureUpdate()) return false; TextureResource replacement; VkImageCreateInfo imageInfo{}; @@ -2429,6 +2435,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { return true; } + // This upload will be independently submitted by FlushPendingUploads. + // Submit older renderer work before adding the new bytes to the batch, + // otherwise Draw(old T), TexSubImage(T), Draw(new T) can upload before + // the first draw. A clean texture never reaches this submission boundary. + if (pVulkanRenderer && !pVulkanRenderer->FlushWirePendingCommandsForTextureUpdate()) return false; + // UploadDirtyMipLevels' batching rules, verbatim: flush an open batch that already // writes this image and was touched by the open recording, and bound the staging bytes // a batch pins. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e9399334..5da391db 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3431,6 +3431,7 @@ void main() { } #if MOBILEGL_BUILD_DISAGGREGATED DestroyWireDrawPass(); + CollectWireObjects(m_submitCounter, true); DestroyWireColorBlitResources(); #endif OnSubmitsCompletedUpTo(m_submitCounter); @@ -13051,9 +13052,15 @@ void main() { // Mid-frame-flushed command buffers whose submission just completed can // be freed now; present-less flush loops have no other reclaim point. m_frameContext.FreeRetiredCommandBuffersCompletedUpTo(m_completedSubmitCounter); +#if MOBILEGL_BUILD_DISAGGREGATED + CollectWireObjects(m_completedSubmitCounter); +#endif } Bool VulkanRenderer::TryDrainFrameTransients() { +#if MOBILEGL_BUILD_DISAGGREGATED + if (m_wirePreparationDepth != 0) return false; +#endif if (m_device == VK_NULL_HANDLE || m_frameContext.GetFrameCount() == 0) { return false; } @@ -13170,6 +13177,9 @@ void main() { } Bool VulkanRenderer::SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence) { +#if MOBILEGL_BUILD_DISAGGREGATED + RetireWireDrawPass(); +#endif // Batched texture uploads must reach the queue before the frame's // commands: the recording being submitted may sample images whose // texels only exist in the texture manager's open upload batch. @@ -15414,6 +15424,12 @@ void main() { vkDeviceWaitIdle(m_device); OnSubmitsCompletedUpTo(m_submitCounter); +#if MOBILEGL_BUILD_DISAGGREGATED + // The old swapchain will be destroyed below, including recordings that + // are being abandoned instead of submitted. Release all their views now. + DestroyWireDrawPass(); + CollectWireObjects(m_submitCounter, true); +#endif if (m_timerQueryManager) { // The in-progress command buffer is abandoned below (its recording diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 36f30774..4f1b2712 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -143,6 +143,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkCommandBuffer AcquireBufferCopyCommandBuffer() override; #if MOBILEGL_BUILD_DISAGGREGATED VkBufferManager& GetWireBufferManager() { return m_bufferManager; } + Bool FlushWirePendingCommandsForTextureUpdate() { + return !HasPendingRecordedWork() || FlushPendingCommands(); + } #endif // FrameContext::IRecordingObserver: prepares the frame's timer-query @@ -470,6 +473,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices); void DestroyWireDrawPass(); + void RetireWireDrawPass(); + struct WireRetiredObjects { + Uint64 submitIndex = 0; + UniquePtr drawPass; + Vector framebuffers; + Vector renderPasses; + Vector imageViews; + Vector descriptorPools; + }; + void RetireWireObjects(WireRetiredObjects objects); + void CollectWireObjects(Uint64 completedSubmit, Bool all = false); + Vector m_wireRetiredObjects; + Uint32 m_wirePreparationDepth = 0; void DispatchWireCompute(GLuint x, GLuint y, GLuint z); UniquePtr m_wireDrawPass; Vector m_wireDrawViews; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc index 742aad0b..a4930ed2 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlit.inc @@ -11,8 +11,6 @@ }; VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE; VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; - VkDescriptorPool descriptorPool = VK_NULL_HANDLE; - VkDescriptorSet descriptorSet = VK_NULL_HANDLE; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE; Vector targets; @@ -20,8 +18,8 @@ void VulkanRenderer::DestroyWireColorBlitResources() { if (!m_wireColorBlitResources) return; - // Shutdown calls this after vkDeviceWaitIdle; every individual blit also - // waits before recycling its descriptor set or destroying temporary views. + // Shutdown calls this after vkDeviceWaitIdle and after all per-submit + // descriptor pools/framebuffers/views have been collected. auto& cache = *m_wireColorBlitResources; for (const auto& target : cache.targets) { vkDestroyPipeline(m_device, target.pipeline, nullptr); @@ -29,7 +27,6 @@ } vkDestroySampler(m_device, cache.nearest, nullptr); vkDestroySampler(m_device, cache.linear, nullptr); - vkDestroyDescriptorPool(m_device, cache.descriptorPool, nullptr); vkDestroyPipelineLayout(m_device, cache.pipelineLayout, nullptr); vkDestroyDescriptorSetLayout(m_device, cache.descriptorLayout, nullptr); vkDestroyShaderModule(m_device, cache.vertex, nullptr); @@ -90,17 +87,6 @@ pipelineLayout.pushConstantRangeCount = 1; pipelineLayout.pPushConstantRanges = &push; VK_VERIFY(vkCreatePipelineLayout(m_device, &pipelineLayout, nullptr, &cache.pipelineLayout)); - const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; - VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - pool.maxSets = 1; - pool.poolSizeCount = 1; - pool.pPoolSizes = &poolSize; - VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &cache.descriptorPool)); - VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - alloc.descriptorPool = cache.descriptorPool; - alloc.descriptorSetCount = 1; - alloc.pSetLayouts = &cache.descriptorLayout; - VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &cache.descriptorSet)); VkSamplerCreateInfo sampler{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; sampler.addressModeU = sampler.addressModeV = sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; @@ -211,10 +197,25 @@ framebufferInfo.layers = 1; VkFramebuffer framebuffer = VK_NULL_HANDLE; VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferInfo, nullptr, &framebuffer)); + // Each in-flight blit owns its descriptor set. Updating a cached set + // would rewrite the source view of an older queued blit. + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkDescriptorSet descriptorSet = VK_NULL_HANDLE; + const VkDescriptorPoolSize poolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; + VkDescriptorPoolCreateInfo pool{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + pool.maxSets = 1; + pool.poolSizeCount = 1; + pool.pPoolSizes = &poolSize; + VK_VERIFY(vkCreateDescriptorPool(m_device, &pool, nullptr, &descriptorPool)); + VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + alloc.descriptorPool = descriptorPool; + alloc.descriptorSetCount = 1; + alloc.pSetLayouts = &cache.descriptorLayout; + VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet)); const VkDescriptorImageInfo sampled{filter == GL_LINEAR ? cache.linear : cache.nearest, sourceView, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL}; VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = cache.descriptorSet; + write.dstSet = descriptorSet; write.dstBinding = 0; write.descriptorCount = 1; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; @@ -244,17 +245,21 @@ vkCmdSetViewport(frame.commandBuffer, 0, 1, &viewport); vkCmdSetScissor(frame.commandBuffer, 0, 1, &scissor); vkCmdBindDescriptorSets(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - cache.pipelineLayout, 0, 1, &cache.descriptorSet, 0, nullptr); + cache.pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); vkCmdPushConstants(frame.commandBuffer, cache.pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(constants), &constants); vkCmdDraw(frame.commandBuffer, 6, 1, 0, 0); vkCmdEndRenderPass(frame.commandBuffer); ResetDynamicStateShadow(); InvalidatePipelineMemo(); - if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("color-blit-submit"); - vkDestroyFramebuffer(m_device, framebuffer, nullptr); - vkDestroyImageView(m_device, sourceView, nullptr); - vkDestroyImageView(m_device, destinationView, nullptr); + // This native pass binds its own set/layout without going through the + // application's descriptor binder. Its next fast path must rebind. + if (m_uniformManager) m_uniformManager->OnCommandBufferBoundary(); + WireRetiredObjects retired{}; + retired.framebuffers.push_back(framebuffer); + retired.imageViews = {sourceView, destinationView}; + retired.descriptorPools.push_back(descriptorPool); + RetireWireObjects(Move(retired)); m_swapchainObject.SetImageContentDefined(m_imageIndexAcquired, true); return true; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index fa8be3f7..d94cb14e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -53,14 +53,55 @@ } void VulkanRenderer::DestroyWireDrawPass() { - // FlushPendingCommands only submits. These objects may still be referenced by - // that submission, so retire this temporary pass after the queue completes. - if (m_wireDrawPass || !m_wireDrawViews.empty()) VK_VERIFY(vkQueueWaitIdle(m_graphicsQueue)); + // 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() == m_wireDrawPass.get()) + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + } m_wireDrawPass.reset(); for (const auto view : m_wireDrawViews) vkDestroyImageView(m_device, view, nullptr); m_wireDrawViews.clear(); } + 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() { + if (!m_wireDrawPass) return; + auto& frame = m_frameContext.GetCurrent(); + if (frame.isCommandRecording && VkRenderPassManager::GetActiveRenderPass()) + VkRenderPassManager::EndRenderPass(frame.commandBuffer); + WireRetiredObjects retired{}; + retired.drawPass = Move(m_wireDrawPass); + retired.imageViews = Move(m_wireDrawViews); + RetireWireObjects(Move(retired)); + } + + void VulkanRenderer::CollectWireObjects(Uint64 completedSubmit, Bool all) { + auto it = m_wireRetiredObjects.begin(); + while (it != m_wireRetiredObjects.end()) { + if (!all && it->submitIndex > completedSubmit) { ++it; continue; } + // Framebuffers/descriptor sets stop naming their views first. + it->drawPass.reset(); + for (const auto pool : it->descriptorPools) vkDestroyDescriptorPool(m_device, pool, nullptr); + for (const auto framebuffer : it->framebuffers) vkDestroyFramebuffer(m_device, framebuffer, nullptr); + for (const auto pass : it->renderPasses) vkDestroyRenderPass(m_device, pass, nullptr); + for (const auto view : it->imageViews) vkDestroyImageView(m_device, view, nullptr); + it = m_wireRetiredObjects.erase(it); + } + } + + struct WirePreparationScope { + Uint32& depth; + explicit WirePreparationScope(Uint32& value) : depth(value) { ++depth; } + ~WirePreparationScope() { --depth; } + }; + Bool VulkanRenderer::SetupWireDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* indices) { auto& state = MG_Pipe::MGPipeApplier(); @@ -70,8 +111,10 @@ WireBufferLegacyFatal(); const auto* fbo = state.DrawFramebuffer(); if (!fbo) MagmaWireFatal("draw-framebuffer-record"); - if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("draw-flush"); - DestroyWireDrawPass(); + RetireWireDrawPass(); + // 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); ProgramFactory::CompileOptionFlags flags = ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(fbo->IsDefault)); @@ -82,6 +125,18 @@ } if (programObject->pointSizeCapabilityUnsupported) return false; const auto& programObj = *programObject; + if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) return false; + // Lazy image uploads/promotions can submit the old recording. Resolve + // ALL attachments before descriptor binding captures the current cmd. + 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(); const auto vaoHandle = state.BoundVertexElements; if (MG_Pipe::MGPipeHandleIsNull(vaoHandle)) MagmaWireFatal("vertex-elements-record"); if (vaoHandle.Slot >= state.VertexElementsCsos.size()) MagmaWireFatal("vertex-elements-record"); @@ -301,11 +356,12 @@ } void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) { - if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("dispatch-flush"); + RetireWireDrawPass(); auto& frame = m_frameContext.GetCurrent(); const auto program = WireProgramSource(MG_Pipe::MGPipeApplier().DispatchProgram); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) return; const auto& programObj = m_programFactory->GetOrCreateProgram(program, {}); + if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) return; const auto pipeline = GetOrCreateComputePipeline(programObj); if (!pipeline) return; if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 8ea97670..ad454de7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -67,6 +67,16 @@ VkImageAspectFlags barrierAspect = image.aspect; if (image.format == VK_FORMAT_D24_UNORM_S8_UINT || image.format == VK_FORMAT_D32_SFLOAT_S8_UINT) barrierAspect = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + if (image.layout == layout) { + // GENERAL->GENERAL is not a layout transition, but the next load, + // sample or blit still depends on earlier attachment/shader writes. + // The old per-verb queue idle must not be its only ordering edge. + VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT; + memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT; + vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &memory, 0, nullptr, 0, nullptr); + } if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer, image.image, image.layout, layout, stage, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, access, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, barrierAspect, 0, image.levels)) @@ -81,7 +91,7 @@ void VulkanRenderer::ClearWireFramebuffer(const MG_Pipe::MGPFramebufferState& fbo, const ClearAttachmentPayload& payload, GLint drawbuffer) { - if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + RetireWireDrawPass(); if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) return; const auto clearSurface = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect, Uint32 colorIndex) { @@ -171,12 +181,11 @@ vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE); vkCmdClearAttachments(frame.commandBuffer, 1, &clear, 1, &rect); vkCmdEndRenderPass(frame.commandBuffer); - // P5f changes ownership, not Magma's wait contract. Synchronous retirement keeps - // these temporary server resources bounded and makes their lifetime explicit. - if (!SubmitReadbackCommandsAndWait(frame)) MagmaWireFatal("clear-submit"); - vkDestroyFramebuffer(m_device, fb, nullptr); - vkDestroyRenderPass(m_device, pass, nullptr); - vkDestroyImageView(m_device, view, nullptr); + WireRetiredObjects retired{}; + retired.framebuffers.push_back(fb); + retired.renderPasses.push_back(pass); + retired.imageViews.push_back(view); + RetireWireObjects(Move(retired)); if (!image.isDefault && surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) m_textureManager->MarkWireTextureGpuWritten(image.storage, image.level, image.layer, image.layers); if (image.isDefault) { @@ -253,7 +262,7 @@ void VulkanRenderer::BlitWireFramebuffers(GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1, GLbitfield mask, GLenum filter) { - if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + RetireWireDrawPass(); // The scissor test clips blit writes: intersect the destination rectangle with // the scissor box and shrink the source proportionally. if (MGB_CTX->IsCapabilityEnabled(CapabilityInput::ScissorTest)) { @@ -377,7 +386,7 @@ "CopyTexSubImage2D currently only supports GL_TEXTURE_2D destinations."); return; } - if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + RetireWireDrawPass(); auto& state = MG_Pipe::MGPipeApplier(); const auto* read = state.ReadFramebuffer(); if (!read) MagmaWireFatal("copy-framebuffer-record"); @@ -415,7 +424,7 @@ } void VulkanRenderer::GenerateWireMipmap() { - if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("wire-verb-flush"); + RetireWireDrawPass(); auto& state = MG_Pipe::MGPipeApplier(); Uint32 base = state.VerbMipBaseLevel, baseLayer = 0, layerCount = 1; const auto handle = m_textureManager->ResolveWireTextureStorage(state.VerbMipRes, base, baseLayer, @@ -423,7 +432,7 @@ auto* resource = m_textureManager->SyncTextureResourceByHandle(handle); if (!resource) MagmaWireFatal("mipmap-resource"); m_textureManager->FlushPendingUploads(); - if (HasPendingRecordedWork() && !FlushPendingCommands()) MagmaWireFatal("mipmap-flush"); + RetireWireDrawPass(); auto& frame = m_frameContext.GetCurrent(); if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording(); const Uint32 count = state.VerbMipLevelCount; diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index 8c7ee090..e43607e3 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -96,11 +96,12 @@ namespace MobileGL::MG_Backend { // the apply thread reading client memory that has already moved, which renders wrong // rather than aborting. constexpr Bool kMGPipeP5eRunAheadReady = true; + constexpr Bool kMGPipeMagmaRunAheadReady = false; // The Magma transport now owns buffer stores and consumes their vertex, // index and shader binding records. Both backends publish the resource // family only alongside a real op table (checked below). This does not - // publish kCapRunAheadApply: Magma keeps its existing lockstep contract. + // itself establish run-ahead readiness: that has its separate gate below. Uint64 ConsumedSubsystemsFor(BackendType type) { switch (type) { case BackendType::DirectGLES: return MG_Pipe::kMGPipeSubsystemsMigratedAtP5e; @@ -186,21 +187,11 @@ namespace MobileGL::MG_Backend { serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) { capBits |= MG_Pipe::kCapBackendOwnsXfbCapture; } - // P5e (CONTRACT-P5E.md §1, §6): kCapRunAheadApply, THE DIRECTGLES ARM AND ONLY IT. - // - // Magma is deliberately absent and is not an omission: it keeps the lockstep for - // the whole of P5e, its four apply-thread allocator sites are real debt P7 retires, - // and MGPipeApplierCurrentRecordIsBarriered() answers true for every record on a - // server that does not publish this bit - which is exactly what keeps those probes - // and its BARRIER_PULLED reads inside P5C's semantics and its rsp accounting - // honest. Publishing the bit here for DirectVulkan would turn accounted pulls into - // torn ones, so MagmaPipeIdentityTest pins its absence. - // - // The Espryt arm is gated on kMGPipeP5eRunAheadReady, which is false until the - // integration commit: the bit is what ARMS the client, so every package before it - // lands inert. - capBits |= MG_Pipe::MGPipeRunAheadCapBitsFor(MG_Config::ActiveBackendType, - kMGPipeP5eRunAheadReady); + // Each backend has an independent implementation-readiness gate. + // The runtime RunAhead knob can decline the feature, never create it. + const Bool runAheadReady = MG_Config::ActiveBackendType == BackendType::DirectVulkan + ? kMGPipeMagmaRunAheadReady : kMGPipeP5eRunAheadReady; + capBits |= MG_Pipe::MGPipeRunAheadCapBitsFor(MG_Config::ActiveBackendType, runAheadReady); session.SetCapabilityBits(capBits); session.SetBackend(loop.Backend()); diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index 7f015d37..ddee8eb4 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -152,17 +152,12 @@ namespace MobileGL::MG_Pipe { kCapRunAheadApply = 1ull << 10, }; - // THE RUN-AHEAD ARM, AS A PURE FUNCTION, so that "Magma never" is something a unit case can - // hold rather than a line inside MG_Backend/Init.cpp's InitSplitRoles that only a live - // split session reaches. `ready` is Init.cpp's kMGPipeP5eRunAheadReady - false until the - // P5e integration commit - and the BACKEND TYPE is the half that must not depend on it: - // whatever `ready` says, a DirectVulkan server publishes no bit 10, because Magma keeps the - // lockstep for the whole of P5e (CONTRACT-P5E.md §6) and its apply thread really does read - // client memory inside MagmaP7AllocatorDebtScope. MG_Test/Pipe/MagmaPipeIdentityTest.cpp - // pins both halves; reverting the type test there is the phase's named red. + // Readiness is supplied independently by each backend's integration gate. + // The capability names client-memory independence, not merely a config knob. + // See CONTRACT-MAGMA-RUNAHEAD.md for the Vulkan resource/submission contract. inline constexpr Uint64 MGPipeRunAheadCapBitsFor(BackendType type, Bool ready) { - return (ready && type == BackendType::DirectGLES) ? static_cast(kCapRunAheadApply) - : static_cast(kCapNone); + return (ready && (type == BackendType::DirectGLES || type == BackendType::DirectVulkan)) + ? static_cast(kCapRunAheadApply) : static_cast(kCapNone); } struct MGPCaps { diff --git a/MobileGL/MG_Remote/CONTRACT-MAGMA-RUNAHEAD.md b/MobileGL/MG_Remote/CONTRACT-MAGMA-RUNAHEAD.md new file mode 100644 index 00000000..3a540126 --- /dev/null +++ b/MobileGL/MG_Remote/CONTRACT-MAGMA-RUNAHEAD.md @@ -0,0 +1,56 @@ +# CONTRACT — Magma run-ahead + +This extends CONTRACT-P5E to DirectVulkan after P5f and the Magma application-buffer +migration. The P5e rule F, wait-class table, reply ownership, event flow control, +present credit and one-way run-ahead latch remain the protocol. This document +supersedes the historical requirement that Magma never publish `kCapRunAheadApply`. +It does not make Magma a multi-process transport or complete every P7/P8 GL shape. + +## Readiness and waits + +Magma has an independent implementation-readiness constant in `MG_Backend/Init.cpp`. +Only the integration commit may enable it after the queued-state tests pass. A +ready server advertises the existing bit 10; `MOBILEGL_IPC_RUN_AHEAD=0` remains the +paired lockstep control. Unsupported backends/readiness=false advertise no bit. +The knob alone never arms run-ahead. A later caps snapshot may disarm, not arm it. + +Unbarriered apply reads only records, server storage and immutable capabilities. +No frontend object/registry or legacy pointer getter is admitted by a barrier, +scope, or backend choice. Unsupported GL shapes keep named refusals. Reply rows, +explicit synchronization, control operations and present-credit exhaustion retain +their existing waits; a test that removes all waits would violate the protocol. + +## GPU resource and submission order + +1. Wire buffer updates use exact owned bytes. A busy store is updated by ordered + GPU copy or a justified host-access wait; CPU readback does not revoke a slice + already reserved for a following draw. Store identity includes generation. +2. Lazy texture upload/preserve submissions are ordered after earlier renderer + recordings. Descriptor resolution prepares all active texture/image resources + and framebuffer attachments before capturing the current command buffer; a + later lazy upload cannot silently move bindings into an already submitted one. +3. CPU draw preparation can own future transient slices even before a draw is + recorded. Idle-drain/arena rewind must not reclaim them during that interval. +4. Wire draw, clear and color blit retain every native framebuffer, render pass, + view and descriptor pool until the submission that names it completes. Retirement + is keyed to submit indices, not an assumed number of frames or client progress. + Swapchain replacement/shutdown drains these after device idle, including abandoned + recordings. Independent blits never overwrite a descriptor set still in flight. +5. GPU writes retain their reverse notifications; readback waits for actual GPU + completion and returns owned bytes. Removing a per-draw queue idle does not + remove explicit GL synchronization or image/buffer memory dependencies. + +## Evidence required before activation + +Real Magma tests must hold apply at a known boundary while the client publishes +later commands, observe real sequence/ack counters, then verify pixels/bytes from +each queued state. Include resource rewrite/respecify/death-reuse, program/uniform +rebinding, GPU write/readback, texture ordering/promotion, and present credits 1/3. +The dedicated discovery set must execute completely without skips. A real +run-ahead-off control must fail the queued-progress assertion, then restore green. + +Keep strict/dual-block and zero-residual gates; preserve pull G1 and test-name +ratchets. On Redmi, verify the installed library identity, actual `run-ahead ARMED`, +both roles, world rendering and liveness, and record a paired run-ahead-off arm. +Performance numbers are recorded with their workload/clock limits, not substituted +for correctness or inferred from the capability log alone. diff --git a/MobileGL/MG_Remote/CONTRACT-P5E.md b/MobileGL/MG_Remote/CONTRACT-P5E.md index 241f3267..406f7f24 100644 --- a/MobileGL/MG_Remote/CONTRACT-P5E.md +++ b/MobileGL/MG_Remote/CONTRACT-P5E.md @@ -1,5 +1,9 @@ # CONTRACT-P5E — the client runs ahead of apply on the Espryt draw path +> Historical P5e backend scope: the later [Magma run-ahead contract](CONTRACT-MAGMA-RUNAHEAD.md) +> supersedes this document's "Magma never publishes bit 10" restriction after P5f and the +> server-buffer migration. The wire wait classes, events and present-credit protocol remain. + Authority: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, field ownership, R-1…R-17), `CONTRACT-P5B.md` (the class-C slots) and `CONTRACT-P5C.md` (rule E, the two named exemptions, SEG_EVENT, the guards). Where it disagrees with any of them this file is newer and wins; §8 lists diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index 815e4370..ba4a3767 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -1150,9 +1150,8 @@ namespace MobileGL::MG_Remote::Client { // told that, or an A/B arm that measured nothing looks like an A/B arm that // measured no difference (Config.h's own argument for parsing it everywhere). MGLOG_W_ONCE("MG_Remote client: run-ahead requested, server does not publish " - "kCapRunAheadApply - running lockstep. This is Magma always, and " - "Espryt until the P5e integration commit flips " - "kMGPipeP5eRunAheadReady"); + "kCapRunAheadApply - running lockstep. The selected backend has " + "not enabled its implementation-readiness gate"); } return; }