[MG_Backend] (Disaggregated): order Magma GPU work and defer wire object retirement

This commit is contained in:
2026-09-20 04:11:36 -04:00
parent c1960fdfaa
commit 51e61ddabe
14 changed files with 277 additions and 69 deletions
@@ -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<Int>(element);
if (baseLocation < 0 || !program.UniformLocationsAliasSameUniform(baseLocation, location))
return false;
const Int unit = program.GetUniformSamplerOrImageUnitIndex(static_cast<Uint>(location));
if (unit < 0 || static_cast<Uint32>(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,
@@ -100,6 +100,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<SamplerImageFeedbackBinding>& 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
@@ -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<const Uint8*>(resource->buffer.GetMappedData()) + offset, static_cast<SizeT>(size));
return true;
@@ -22,6 +22,7 @@
// (shape) and the server's staged-texture store (texels).
#include <MG_Pipe/PipeApply.h>
#include <MG_Remote/Server/StagedTextureStore.h>
#include "../DirectVulkan.h"
#endif
#include <algorithm>
#include <cstdio>
@@ -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.
@@ -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
@@ -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<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams, const IndexBufferView* indices);
void DestroyWireDrawPass();
void RetireWireDrawPass();
struct WireRetiredObjects {
Uint64 submitIndex = 0;
UniquePtr<RenderPassEntry> drawPass;
Vector<VkFramebuffer> framebuffers;
Vector<VkRenderPass> renderPasses;
Vector<VkImageView> imageViews;
Vector<VkDescriptorPool> descriptorPools;
};
void RetireWireObjects(WireRetiredObjects objects);
void CollectWireObjects(Uint64 completedSubmit, Bool all = false);
Vector<WireRetiredObjects> m_wireRetiredObjects;
Uint32 m_wirePreparationDepth = 0;
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
UniquePtr<RenderPassEntry> m_wireDrawPass;
Vector<VkImageView> m_wireDrawViews;
@@ -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<Target> 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;
}
@@ -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<DrawSetupAspect> 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();
@@ -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;
+7 -16
View File
@@ -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());
+5 -10
View File
@@ -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<Uint64>(kCapRunAheadApply)
: static_cast<Uint64>(kCapNone);
return (ready && (type == BackendType::DirectGLES || type == BackendType::DirectVulkan))
? static_cast<Uint64>(kCapRunAheadApply) : static_cast<Uint64>(kCapNone);
}
struct MGPCaps {
@@ -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.
+4
View File
@@ -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
+2 -3
View File
@@ -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;
}