mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Backend] (Disaggregated): order Magma GPU work and defer wire object retirement
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user