From 43bcd03dca1204f9b1837485140e942e7af0d0f2 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 11 Aug 2026 19:42:47 -0400 Subject: [PATCH] [Fix] (MG_Backend/DirectVulkan, MG_Impl): a malformed graphics pipeline shape skips the draw instead of faulting inside the driver --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 37 +++++++++++++++++++ .../MG_Impl/GLImpl/Drawing/GL_Drawing.cpp | 7 ++++ 2 files changed, 44 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e2645985..14281e58 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4623,6 +4623,43 @@ void main() { } } + // Shape gate. Behind the memo probe deliberately: only a pipeline that was created + // successfully is ever memoized, so a program refused here can never be sitting in the + // memo, and the steady-state draw keeps paying nothing for the check. + // + // vkCreateGraphicsPipelines is not a validating entry point: a stage set that a + // conformant implementation would reject with VK_ERROR_* is, on Adreno 830, a SIGSEGV + // inside the driver - process death instead of a failed draw. The separable-program path + // is what made these shapes reachable at all (a monolithic glUseProgram program cannot + // hold a compute stage together with graphics ones, a pipeline object can), so the three + // it can produce are named and refused here. Same philosophy as the VK_NULL_HANDLE gate + // in SetupDraw: hostile input degrades to a broken draw, never to a dead process. GL + // leaves all three undefined for a draw, so nothing legal is being turned away. + // MGLOG_I because the INFO builds CTS runs against keep only I and F. + { + Bool hasVertexStage = false; + for (const auto& stage : programObj.stages) { + if (stage.module == VK_NULL_HANDLE) { + MGLOG_I("GetOrCreatePipeline skipped: program=%u has a null shader module for stage 0x%x", + program.GetExternalIndex(), static_cast(stage.stage)); + return VK_NULL_HANDLE; + } + if (stage.stage == VK_SHADER_STAGE_COMPUTE_BIT) { + MGLOG_I("GetOrCreatePipeline skipped: program=%u carries a compute stage, which no graphics " + "pipeline may contain", + program.GetExternalIndex()); + return VK_NULL_HANDLE; + } + if (stage.stage == VK_SHADER_STAGE_VERTEX_BIT) { + hasVertexStage = true; + } + } + if (!hasVertexStage) { + MGLOG_I("GetOrCreatePipeline skipped: program=%u has no vertex stage", program.GetExternalIndex()); + return VK_NULL_HANDLE; + } + } + #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG const auto& limits = m_physicalDevice.properties.limits; if (programObj.fragmentInputComponentCount != 0) { diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index eaa1c6e0..493b5520 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -670,6 +670,11 @@ namespace MobileGL::MG_Impl::GLImpl { 5 * sizeof(Uint32), __func__)) { return; } + // The only two draw entry points that were missing this. Every backend draw path + // dereferences GetProgramForDraw() unconditionally, so "no current program" has to be + // stopped here or it is a null dereference rather than the INVALID_OPERATION the spec + // asks for - reachable through a bound pipeline that supplies no graphics stage. + if (!ValidateCurrentProgramForExecution(__func__)) return; auto multiDrawElementsIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawElementsIndirectCount; if (!multiDrawElementsIndirectCount) { MG_State::pGLContext->RecordError( @@ -689,6 +694,8 @@ namespace MobileGL::MG_Impl::GLImpl { 4 * sizeof(Uint32), __func__)) { return; } + // See MultiDrawElementsIndirectCount. + if (!ValidateCurrentProgramForExecution(__func__)) return; auto multiDrawArraysIndirectCount = MG_Backend::gBackendFunctionsTable.GL.MultiDrawArraysIndirectCount; if (!multiDrawArraysIndirectCount) { MG_State::pGLContext->RecordError(