diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 521be1dd..ea1a162c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -3525,6 +3525,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { return current->mapped + alignedCursor; } +#if MOBILEGL_BUILD_DISAGGREGATED + Bool VkTextureManager::WireUploadsAreIdle() { + // An open batch still owns references even before it has a fence. Poll + // submitted batches without waiting; reclamation is deferred if any lives. + if (m_uploadBatchOpen) return false; + ReclaimCompletedUploads(); + return m_pendingUploadReclaims.empty(); + } +#endif + void VkTextureManager::FlushPendingUploads() { if (!m_uploadBatchOpen) { return; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 0621b10c..26163f0d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -480,6 +480,12 @@ public: // preserve-on-recreate copy are the existing callers. No-op when the // batch is empty. void FlushPendingUploads(); +#if MOBILEGL_BUILD_DISAGGREGATED + // Non-blocking idle proof for wire-object retirement. Independent texture + // submissions are not represented by VulkanRenderer's submit counter. + Bool WireUploadsAreIdle(); +#endif + // Drains every frame slot's deferred image/view releases. Only valid when // the caller has proven every queue submission complete; used by the // present-less frame-boundary drain. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 37c44260..de0ba95f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -13089,6 +13089,18 @@ void main() { return false; } +#if MOBILEGL_BUILD_DISAGGREGATED + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + // Texture uploads submit on this queue with their own fences. The + // renderer watermark alone cannot prove images/views are idle. + if (m_textureManager && !m_textureManager->WireUploadsAreIdle()) return false; + // No preparation, recording or GPU work remains. A minimized Present + // may have abandoned a recording tagged for a submission that will + // never occur, so reclaim those future-tagged objects as well. + CollectWireObjects(m_completedSubmitCounter, true); + } +#endif + // Every submission is complete and nothing recorded references the // per-frame transients. Pure-reclaim work runs on every drain: it only // releases memory that is provably dead, never invalidates anything a diff --git a/scripts/ci/magma_runahead_checks.py b/scripts/ci/magma_runahead_checks.py index 87ba4d55..772b7d23 100644 --- a/scripts/ci/magma_runahead_checks.py +++ b/scripts/ci/magma_runahead_checks.py @@ -34,6 +34,23 @@ NEGATIVE = f"{PREFIX}1.MagmaRunAheadScenario.QueuedProgramRebindsKeepEachUniform CLEAR_ASSERTION = "the clear waited for apply instead of returning while its server batch was held" ERROR = re.compile(r"VUID-[A-Za-z0-9_-]+|SYNC-HAZARD-[A-Z_]+|Validation Error|\bERROR\b", re.I) LAYER = re.compile(r'Insert instance layer\s+["\']?VK_LAYER_KHRONOS_validation', re.I) +# MobileGL's own diagnostics are not Vulkan-layer messages. Keep them visible +# separately; an embedded validation/VUID/hazard message is never classified away. +APPLICATION_ERROR = re.compile(r"^\[\d{2}:\d{2}:\d{2}(?:\.\d+)?\] \[(?:Linux|Android|Windows|macOS)[^\]]*/ERROR\]:") +VALIDATION_MARKER = re.compile(r"VUID-|SYNC-HAZARD|\bvalidation\b", re.I) + + +def split_diagnostics(stdout, private): + validation, application = [], [] + for line in (stdout + "\n" + private).splitlines(): + if not ERROR.search(line): + continue + if APPLICATION_ERROR.search(line) and not VALIDATION_MARKER.search(line): + application.append(line) + else: + validation.append(line) + return validation, application + def write_json(path, data): @@ -183,9 +200,11 @@ def validation(tests, build, out): copied = copy.deepcopy(case) copied.set("name", name) # Raw gtest.xml remains unchanged beside this aggregate. junit.append(copied) - lines = [line for line in (stdout + "\n" + private).splitlines() if ERROR.search(line)] + lines, application_lines = split_diagnostics(stdout, private) (directory / "validation-errors.txt").write_text("\n".join(lines), encoding="utf-8") - result.update(validation_error_lines=len(lines), layer_inserted=bool(LAYER.search(stdout + private))) + (directory / "application-errors.txt").write_text("\n".join(application_lines), encoding="utf-8") + result.update(validation_error_lines=len(lines), application_error_lines=len(application_lines), + layer_inserted=bool(LAYER.search(stdout + private))) if result["returncode"] or failed or not result["private_log_present"]: raise RuntimeError("GPU case did not complete with a passing result and actual private log") if not result["layer_inserted"]: