diff --git a/CMakeLists.txt b/CMakeLists.txt index c0aacfcd..41be723c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -735,6 +735,7 @@ list(APPEND SOURCE_FILES MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/WireE5RawCopy.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/WireCopySliceProbe.cpp + MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.cpp # P5 b1: the block-granularity persistent-map push the record arm makes mandatory (T2). MobileGL/MG_State/GLState/BufferState/PersistentMapTracker.cpp # P13 W5: the verb emitters and the monolith verb port, and the client GPU-write set diff --git a/MobileGL/Config.h b/MobileGL/Config.h index a3443441..097c6128 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -397,6 +397,12 @@ namespace MobileGL::MG_Config { // a device probe at renderer bring-up (one region vs. per-slice regions); ForceOn splits // on any device; ForceOff is the negative control that replays the dropped slices. QuirkOverride MagmaSplitSliceCopies = QuirkOverride::Auto; + // MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD: DirectVulkan sets the dynamic state again after + // every rasterizer-discard draw (WireDiscardStateProbe.h). On Adreno 750 a draw with a + // rasterizerDiscardEnable pipeline drops the dynamic viewport/scissor, and later draws that + // rely on it draw nothing. Auto defers to a device probe at renderer bring-up; ForceOn + // re-emits on any device; ForceOff is the negative control that replays the lost state. + QuirkOverride MagmaReemitStateAfterDiscard = QuirkOverride::Auto; // --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) --- // MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend // PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext. diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index a3315a3b..05684b1d 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -422,6 +422,7 @@ namespace MobileGL::MG_ConfigLoader { features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE"); features.MagmaE5RawReads = QueryEnvQuirkOverride("MOBILEGL_MAGMA_E5_RAW_READS"); features.MagmaSplitSliceCopies = QueryEnvQuirkOverride("MOBILEGL_MAGMA_SPLIT_SLICE_COPIES"); + features.MagmaReemitStateAfterDiscard = QueryEnvQuirkOverride("MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD"); // MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables // accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name // that starts with MOBILEGL_ is visible to these queries by construction. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 010e7371..a4296d9c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -52,6 +52,7 @@ #include "WireColorBlitFilter.h" #include "WireDepthResolveArm.h" #include "WireCopySliceProbe.h" +#include "WireDiscardStateProbe.h" #include "WireDepthResolveProbe.h" #if MOBILEGL_BUILD_DISAGGREGATED #include @@ -3523,6 +3524,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { shadow.dynamicTailValid = false; } + // A rasterizer-discard draw consumes the dynamic state it was recorded with on some drivers + // (WireDiscardStateProbe.h): every dynamic value is forgotten, so the next draw's tail sets its own + // again instead of trusting what the shadow says the command buffer holds. Binds stay valid. + void VulkanRenderer::ForgetDynamicStateAfterDiscardDraw() { + auto& shadow = *g_dynamicStateShadow; + shadow.viewportValid = false; + shadow.scissorValid = false; + shadow.blendConstantsValid = false; + shadow.depthBiasValid = false; + shadow.lineWidthValid = false; + shadow.stencilValid = false; + shadow.fragmentShadingRateValid = false; + shadow.dynamicTailValid = false; + } + void VulkanRenderer::ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo, Uint32 viewportCount) { auto& shadow = *g_dynamicStateShadow; @@ -8528,6 +8544,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Same terms again. ArmWireE5RawReads(); ArmWireCopySliceSplit(); + ArmWireDiscardStateReemit(); } // P7 gate 5 (g5-msprobe): THE ARM ORDER OF THE MULTISAMPLE DEPTH/STENCIL RESOLVE IS A MEASURED @@ -8709,6 +8726,47 @@ namespace MobileGL::MG_Backend::DirectVulkan { armed ? "are split one slice per region" : "go as one region"); } + // THE RE-EMIT AFTER A DISCARD DRAW IS ARMED BY MEASUREMENT (WireDiscardStateProbe.h), once per + // device identity, or forced by MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD. + void VulkanRenderer::ArmWireDiscardStateReemit() { + const MG_Config::QuirkOverride setting = MG_Config::Features.MagmaReemitStateAfterDiscard; + Bool armed = setting == MG_Config::QuirkOverride::ForceOn; + if (setting == MG_Config::QuirkOverride::Auto) { + struct Entry { + WireDepthResolveDeviceIdentity identity; + WireDiscardStateProbeVerdict verdict; + }; + static std::mutex s_mutex; + static Vector s_entries; + const WireDepthResolveDeviceIdentity identity = MakeWireDepthResolveDeviceIdentity( + m_physicalDevice.properties, false, WireDepthResolveProbeKnob::Measure); + std::lock_guard lock(s_mutex); + const Entry* found = nullptr; + for (const Entry& entry : s_entries) + if (entry.identity == identity) found = &entry; + if (found == nullptr) { + WireDiscardStateProbeContext context; + context.physicalDevice = m_physicalDevice.handle; + context.device = m_device; + context.queue = m_graphicsQueue; + context.queueFamilyIndex = static_cast(m_physicalDevice.queueFamilies.graphicsFamily); + const WireDiscardStateProbeMeasurement measurement = RunWireDiscardStateProbe(context); + const WireDiscardStateProbeVerdict verdict = EvaluateWireDiscardStateProbe(measurement); + if (measurement.fenceWaitTimedOut) + MGLOG_W("DirectVulkan: the discard state probe timed out waiting on its own submission; its " + "Vulkan objects are deliberately leaked"); + MGLOG_I("DirectVulkan: discard state probe verdict=%s: %s", WireDiscardStateProbeVerdictName(verdict), + DescribeWireDiscardStateProbe(measurement).c_str()); + s_entries.push_back({identity, verdict}); + found = &s_entries.back(); + } + armed = found->verdict == WireDiscardStateProbeVerdict::DiscardDrawDropsState; + } + m_wireReemitStateAfterDiscard = armed; + MGLOG_I("DirectVulkan: dynamic state %s", + armed ? "is set again after every rasterizer-discard draw" : "carries across discard draws"); + } + void VulkanRenderer::ArmPrimGenReroute() { using namespace MG_Util::SelfTest; m_primGenRerouteKind = PrimGenRerouteKind::None; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index b93cb9ab..09cff6e9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -799,6 +799,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { // CopyImageSubData then records one region per slice. Set by ArmWireCopySliceSplit at bring-up. Bool m_wireSplitCrossFormatSliceCopies = false; void ArmWireCopySliceSplit(); + // A rasterizer-discard draw drops the dynamic viewport/scissor it was recorded with on some + // drivers (WireDiscardStateProbe.h): there the draw path forgets the dynamic-state shadow after + // each discard draw (ForgetDynamicStateAfterDiscardDraw). Set by ArmWireDiscardStateReemit. + Bool m_wireReemitStateAfterDiscard = false; + void ArmWireDiscardStateReemit(); + void ForgetDynamicStateAfterDiscardDraw(); void ArmWireE5RawReads(); public: // The kit when the route is armed, else nullptr. VkTextureManager asks before a level copy. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.cpp new file mode 100644 index 00000000..a20c2576 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.cpp @@ -0,0 +1,408 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.cpp +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// See WireDiscardStateProbe.h. Plumbing as in WireDepthResolveProbe.cpp: one throwaway command +// buffer, a bounded fence wait that leaks rather than idle-waits a hung GPU. + +#include "WireDiscardStateProbe.h" + +#include + +namespace MobileGL::MG_Backend::DirectVulkan { +#include "WireDiscardStateProbeSpirv.h" + + String DescribeWireDiscardStateProbe(const WireDiscardStateProbeMeasurement& measurement) { + if (!measurement.ran) + return format("not run ({})", measurement.failureReason.empty() ? "?" : measurement.failureReason); + return format("a draw inheriting viewport/scissor after a discard draw: {}/{} pixels; after a clipped " + "rasterizing draw: {}/{}", + measurement.subjectGreen, measurement.pixels, measurement.controlGreen, measurement.pixels); + } + + namespace { + constexpr Uint32 kExtent = 16; + constexpr Uint32 kPixels = kExtent * kExtent; + constexpr VkFormat kColorFormat = VK_FORMAT_R8G8B8A8_UNORM; + constexpr Uint64 kFenceTimeoutNs = 5ull * 1000ull * 1000ull * 1000ull; + + struct ProbeObjects { + VkDevice device = VK_NULL_HANDLE; + Vector images; + Vector views; + Vector buffers; + Vector memories; + Vector framebuffers; + Vector pipelines; + Vector modules; + VkRenderPass pass = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkCommandPool commandPool = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + + void Destroy() { + for (VkPipeline pipeline : pipelines) vkDestroyPipeline(device, pipeline, nullptr); + for (VkShaderModule module : modules) vkDestroyShaderModule(device, module, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + for (VkFramebuffer framebuffer : framebuffers) vkDestroyFramebuffer(device, framebuffer, nullptr); + vkDestroyRenderPass(device, pass, nullptr); + for (VkImageView view : views) vkDestroyImageView(device, view, nullptr); + for (VkImage image : images) vkDestroyImage(device, image, nullptr); + for (VkBuffer buffer : buffers) vkDestroyBuffer(device, buffer, nullptr); + for (VkDeviceMemory memory : memories) vkFreeMemory(device, memory, nullptr); + vkDestroyCommandPool(device, commandPool, nullptr); + vkDestroyFence(device, fence, nullptr); + } + }; + + Optional PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed, + VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) { + Optional fallback; + for (Uint32 i = 0; i < properties.memoryTypeCount; ++i) { + if (!(allowed & (1u << i))) continue; + const VkMemoryPropertyFlags flags = properties.memoryTypes[i].propertyFlags; + if (flags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) continue; + if ((flags & required) != required) continue; + if ((flags & wanted) == wanted) return i; + if (!fallback) fallback = i; + } + return fallback; + } + } // namespace + + WireDiscardStateProbeMeasurement RunWireDiscardStateProbe(const WireDiscardStateProbeContext& context) { + WireDiscardStateProbeMeasurement measurement; + const VkDevice device = context.device; + if (device == VK_NULL_HANDLE || context.queue == VK_NULL_HANDLE) { + measurement.failureReason = "no device or queue"; + return measurement; + } + + ProbeObjects objects; + objects.device = device; + Bool leak = false; + struct Teardown { + ProbeObjects& objects; + Bool& leak; + ~Teardown() { + if (!leak) objects.Destroy(); + } + } teardown{objects, leak}; + const auto fail = [&](String reason) { + measurement.ran = false; + measurement.failureReason = Move(reason); + return measurement; + }; + + VkPhysicalDeviceMemoryProperties memoryProperties{}; + vkGetPhysicalDeviceMemoryProperties(context.physicalDevice, &memoryProperties); + const auto allocate = [&](const VkMemoryRequirements& requirements, VkMemoryPropertyFlags wanted, + VkMemoryPropertyFlags required, VkDeviceMemory& out, Bool* coherent) -> Bool { + const Optional type = + PickMemoryType(memoryProperties, requirements.memoryTypeBits, wanted, required); + if (!type) return false; + if (coherent) + *coherent = (memoryProperties.memoryTypes[*type].propertyFlags & + VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) != 0; + VkMemoryAllocateInfo info{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + info.allocationSize = requirements.size; + info.memoryTypeIndex = *type; + if (vkAllocateMemory(device, &info, nullptr, &out) != VK_SUCCESS) return false; + objects.memories.push_back(out); + return true; + }; + + // One render pass for both lanes: clear, draw twice, hand the target to the readback copy. + { + VkAttachmentDescription attachment{}; + attachment.format = kColorFormat; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + // The readback copy waits on the pass's attachment writes. + VkSubpassDependency dependency{}; + dependency.srcSubpass = 0; + dependency.dstSubpass = VK_SUBPASS_EXTERNAL; + dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + dependency.dstStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; + dependency.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + dependency.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + info.dependencyCount = 1; + info.pDependencies = &dependency; + if (vkCreateRenderPass(device, &info, nullptr, &objects.pass) != VK_SUCCESS) + return fail("vkCreateRenderPass failed"); + } + + // Two lanes, SUBJECT (the first draw discarded) and CONTROL (the first draw clipped away): + // each has its own target, framebuffer and readback. + struct Lane { + VkImage target = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkBuffer readback = VK_NULL_HANDLE; + VkDeviceMemory readbackMemory = VK_NULL_HANDLE; + Bool readbackCoherent = false; + VkPipeline first = VK_NULL_HANDLE; + } lanes[2]; + for (Lane& lane : lanes) { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = kColorFormat; + imageInfo.extent = {kExtent, kExtent, 1}; + imageInfo.mipLevels = imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if (vkCreateImage(device, &imageInfo, nullptr, &lane.target) != VK_SUCCESS) + return fail("vkCreateImage failed"); + objects.images.push_back(lane.target); + VkMemoryRequirements requirements{}; + vkGetImageMemoryRequirements(device, lane.target, &requirements); + VkDeviceMemory imageMemory = VK_NULL_HANDLE; + if (!allocate(requirements, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, imageMemory, nullptr) || + vkBindImageMemory(device, lane.target, imageMemory, 0) != VK_SUCCESS) + return fail("image memory failed"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = lane.target; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = kColorFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageView view = VK_NULL_HANDLE; + if (vkCreateImageView(device, &viewInfo, nullptr, &view) != VK_SUCCESS) + return fail("vkCreateImageView failed"); + objects.views.push_back(view); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = objects.pass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = framebufferInfo.height = kExtent; + framebufferInfo.layers = 1; + if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &lane.framebuffer) != VK_SUCCESS) + return fail("vkCreateFramebuffer failed"); + objects.framebuffers.push_back(lane.framebuffer); + + VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufferInfo.size = kPixels * 4; + bufferInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + if (vkCreateBuffer(device, &bufferInfo, nullptr, &lane.readback) != VK_SUCCESS) + return fail("vkCreateBuffer failed"); + objects.buffers.push_back(lane.readback); + vkGetBufferMemoryRequirements(device, lane.readback, &requirements); + if (!allocate(requirements, VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, + lane.readbackMemory, &lane.readbackCoherent) || + vkBindBufferMemory(device, lane.readback, lane.readbackMemory, 0) != VK_SUCCESS) + return fail("readback memory failed"); + } + + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + if (vkCreatePipelineLayout(device, &layoutInfo, nullptr, &objects.pipelineLayout) != VK_SUCCESS) + return fail("vkCreatePipelineLayout failed"); + const auto shader = [&](const Uint32* code, SizeT bytes, VkShaderModule& out) -> Bool { + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = bytes; + info.pCode = code; + if (vkCreateShaderModule(device, &info, nullptr, &out) != VK_SUCCESS) return false; + objects.modules.push_back(out); + return true; + }; + VkShaderModule vertex = VK_NULL_HANDLE, fragment = VK_NULL_HANDLE; + if (!shader(kWireDiscardStateProbeVertexSpirv, sizeof(kWireDiscardStateProbeVertexSpirv), vertex) || + !shader(kWireDiscardStateProbeFragmentSpirv, sizeof(kWireDiscardStateProbeFragmentSpirv), fragment)) + return fail("vkCreateShaderModule failed"); + + // Every pipeline declares viewport and scissor dynamic; only `discard` differs. + const auto makePipeline = [&](Bool discard, VkPipeline& out) -> Bool { + VkPipelineShaderStageCreateInfo stages[2]{}; + for (auto& stage : stages) { + stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stage.pName = "main"; + } + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + VkPipelineViewportStateCreateInfo viewportState{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewportState.viewportCount = viewportState.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.rasterizerDiscardEnable = discard ? VK_TRUE : VK_FALSE; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + raster.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + constexpr VkDynamicState kDynamic[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = kDynamic; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertexInput; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewportState; + info.pRasterizationState = &raster; + info.pMultisampleState = &samples; + info.pColorBlendState = &blend; + info.pDynamicState = &dynamic; + info.layout = objects.pipelineLayout; + info.renderPass = objects.pass; + if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &info, nullptr, &out) != VK_SUCCESS) return false; + objects.pipelines.push_back(out); + return true; + }; + VkPipeline second = VK_NULL_HANDLE; + if (!makePipeline(true, lanes[0].first) || !makePipeline(false, lanes[1].first) || !makePipeline(false, second)) + return fail("pipeline creation failed"); + + VkCommandPoolCreateInfo commandPoolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + commandPoolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + commandPoolInfo.queueFamilyIndex = context.queueFamilyIndex; + if (vkCreateCommandPool(device, &commandPoolInfo, nullptr, &objects.commandPool) != VK_SUCCESS) + return fail("vkCreateCommandPool failed"); + VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + commandInfo.commandPool = objects.commandPool; + commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + commandInfo.commandBufferCount = 1; + VkCommandBuffer cmd = VK_NULL_HANDLE; + if (vkAllocateCommandBuffers(device, &commandInfo, &cmd) != VK_SUCCESS) + return fail("vkAllocateCommandBuffers failed"); + VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if (vkBeginCommandBuffer(cmd, &beginInfo) != VK_SUCCESS) return fail("vkBeginCommandBuffer failed"); + + const VkClearValue black{{{0.0f, 0.0f, 0.0f, 1.0f}}}; + const VkViewport viewport{0.0f, 0.0f, static_cast(kExtent), static_cast(kExtent), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, {kExtent, kExtent}}; + for (Uint32 i = 0; i < 2; ++i) { + const Lane& lane = lanes[i]; + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = objects.pass; + begin.framebuffer = lane.framebuffer; + begin.renderArea = scissor; + begin.clearValueCount = 1; + begin.pClearValues = &black; + vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE); + // The first draw sets the state: discarded (SUBJECT) or a strip the vertex shader puts + // outside the clip volume (CONTROL, vertices 4-7). + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, lane.first); + vkCmdSetViewport(cmd, 0, 1, &viewport); + vkCmdSetScissor(cmd, 0, 1, &scissor); + vkCmdDraw(cmd, 4, 1, i == 0 ? 0 : 4, 0); + // The second draw covers the target with what it inherits. + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, second); + vkCmdDraw(cmd, 4, 1, 0, 0); + vkCmdEndRenderPass(cmd); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {kExtent, kExtent, 1}; + vkCmdCopyImageToBuffer(cmd, lane.target, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, lane.readback, 1, ©); + } + VkMemoryBarrier hostBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER}; + hostBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + hostBarrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &hostBarrier, 0, + nullptr, 0, nullptr); + if (vkEndCommandBuffer(cmd) != VK_SUCCESS) return fail("vkEndCommandBuffer failed"); + + VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + if (vkCreateFence(device, &fenceInfo, nullptr, &objects.fence) != VK_SUCCESS) + return fail("vkCreateFence failed"); + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &cmd; + if (const VkResult result = vkQueueSubmit(context.queue, 1, &submit, objects.fence); result != VK_SUCCESS) + return fail(format("vkQueueSubmit failed (VkResult {})", static_cast(result))); + const VkResult waited = vkWaitForFences(device, 1, &objects.fence, VK_TRUE, kFenceTimeoutNs); + if (waited == VK_TIMEOUT) { + leak = true; + measurement.fenceWaitTimedOut = true; + return fail("the probe's fence did not signal within 5 s"); + } + if (waited != VK_SUCCESS) return fail(format("vkWaitForFences failed (VkResult {})", static_cast(waited))); + + Uint32* green[2] = {&measurement.subjectGreen, &measurement.controlGreen}; + for (Uint32 i = 0; i < 2; ++i) { + const Lane& lane = lanes[i]; + void* mapped = nullptr; + if (vkMapMemory(device, lane.readbackMemory, 0, VK_WHOLE_SIZE, 0, &mapped) != VK_SUCCESS || !mapped) + return fail("vkMapMemory failed"); + if (!lane.readbackCoherent) { + VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; + range.memory = lane.readbackMemory; + range.size = VK_WHOLE_SIZE; + vkInvalidateMappedMemoryRanges(device, 1, &range); + } + const Uint8* pixels = static_cast(mapped); + for (Uint32 p = 0; p < kPixels; ++p) + if (pixels[p * 4] < 16 && pixels[p * 4 + 1] > 240) ++*green[i]; + vkUnmapMemory(device, lane.readbackMemory); + } + measurement.pixels = kPixels; + measurement.ran = true; + return measurement; + } + + WireDiscardStateProbeMeasurement RunWireDiscardStateProbeOnThrowawayDevice( + PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice, + Uint32 graphicsQueueFamilyIndex) { + WireDiscardStateProbeMeasurement measurement; + const auto createDevice = + reinterpret_cast(getInstanceProcAddr(instance, "vkCreateDevice")); + if (createDevice == nullptr) { + measurement.failureReason = "no vkCreateDevice"; + return measurement; + } + const Float priority = 1.0f; + VkDeviceQueueCreateInfo queueInfo{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queueInfo.queueFamilyIndex = graphicsQueueFamilyIndex; + queueInfo.queueCount = 1; + queueInfo.pQueuePriorities = &priority; + VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + deviceInfo.queueCreateInfoCount = 1; + deviceInfo.pQueueCreateInfos = &queueInfo; + VkDevice device = VK_NULL_HANDLE; + if (const VkResult result = createDevice(physicalDevice, &deviceInfo, nullptr, &device); + result != VK_SUCCESS || device == VK_NULL_HANDLE) { + measurement.failureReason = format("vkCreateDevice failed (VkResult {})", static_cast(result)); + return measurement; + } + WireDiscardStateProbeContext context; + context.physicalDevice = physicalDevice; + context.device = device; + vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &context.queue); + context.queueFamilyIndex = graphicsQueueFamilyIndex; + measurement = RunWireDiscardStateProbe(context); + if (!measurement.fenceWaitTimedOut) vkDestroyDevice(device, nullptr); + return measurement; + } +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.frag b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.frag new file mode 100644 index 00000000..46827c76 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.frag @@ -0,0 +1,6 @@ +#version 450 +layout(location = 0) out vec4 color; + +void main() { + color = vec4(0.0, 1.0, 0.0, 1.0); +} diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.h new file mode 100644 index 00000000..43eadb42 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.h @@ -0,0 +1,94 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.h +// Copyright (c) 2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +#pragma once + +#include +#include + +#include + +// DYNAMIC STATE ACROSS A RASTERIZER-DISCARD DRAW, and the probe that decides whether a device needs it +// set again. +// +// Dynamic viewport and scissor are command-buffer state: set once, they hold for every later draw +// whose pipeline declares them dynamic, across pipeline binds and render passes. Magma's draw path +// relies on that - its dynamic-state shadow skips a vkCmdSetViewport/vkCmdSetScissor whose value the +// command buffer already holds. On Adreno 750 a DRAW with a rasterizerDiscardEnable pipeline drops +// that state: the next draw that inherits it instead of setting it again draws nothing, in the same +// render pass or a later one. Binding the discard pipeline and setting the state without drawing +// with it is harmless, and so is the same draw with a rasterizing pipeline. +// +// The GL pattern is a vertex shader that writes with imageStore under GL_RASTERIZER_DISCARD, then a +// draw that reads what it wrote (KHR-GL4x.shader_image_load_store.advanced-sync-imageAccess and +// -vertexArray): the second draw has the same viewport and scissor, so the shadow sets neither, and +// it is lost. +// +// Where the probe below measured the defect, the draw path forgets the dynamic-state shadow after +// every discard draw (VulkanRenderer::ForgetDynamicStateAfterDiscardDraw), so the next draw sets its +// own. Armed by VulkanRenderer::ArmWireDiscardStateReemit at bring-up and reported in the POST's +// "Known Driver Bugs" (MG_Util/SelfTest/DriverBugProbes.cpp); MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD +// forces it. +namespace MobileGL::MG_Backend::DirectVulkan { + struct WireDiscardStateProbeMeasurement { + Bool ran = false; + Bool fenceWaitTimedOut = false; + String failureReason; + // Pixels of each lane's 16x16 target. + Uint32 pixels = 0; + // SUBJECT: a discard draw, then a full-target draw that inherits its viewport and scissor. + // Green pixels (all of them, when correct). + Uint32 subjectGreen = 0; + // CONTROL: the same with the first draw rasterizing (clipped away). Must be all green. + Uint32 controlGreen = 0; + }; + + enum class WireDiscardStateProbeVerdict : Uint8 { + NotRun, + Inconclusive, + Clean, + // The defect: the draw after the discard draw lost the state, the one after the clipped draw + // did not. + DiscardDrawDropsState, + }; + + inline const char* WireDiscardStateProbeVerdictName(WireDiscardStateProbeVerdict verdict) { + switch (verdict) { + case WireDiscardStateProbeVerdict::NotRun: return "not-run"; + case WireDiscardStateProbeVerdict::Inconclusive: return "inconclusive"; + case WireDiscardStateProbeVerdict::Clean: return "clean"; + case WireDiscardStateProbeVerdict::DiscardDrawDropsState: return "discard-draw-drops-state"; + } + return "?"; + } + + inline WireDiscardStateProbeVerdict EvaluateWireDiscardStateProbe(const WireDiscardStateProbeMeasurement& m) { + if (!m.ran || m.fenceWaitTimedOut || m.pixels == 0) return WireDiscardStateProbeVerdict::NotRun; + if (m.controlGreen != m.pixels) return WireDiscardStateProbeVerdict::Inconclusive; + return m.subjectGreen != m.pixels ? WireDiscardStateProbeVerdict::DiscardDrawDropsState + : WireDiscardStateProbeVerdict::Clean; + } + + String DescribeWireDiscardStateProbe(const WireDiscardStateProbeMeasurement& measurement); + + struct WireDiscardStateProbeContext { + VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; + VkDevice device = VK_NULL_HANDLE; + // Must be otherwise idle for the probe's duration. + VkQueue queue = VK_NULL_HANDLE; + Uint32 queueFamilyIndex = 0; + }; + + // One submission on its own command pool, a bounded fence wait (on timeout every object is + // leaked and the measurement says so), teardown on every other path. + WireDiscardStateProbeMeasurement RunWireDiscardStateProbe(const WireDiscardStateProbeContext& context); + + // The POST's entry: a throwaway device with one graphics queue and no features. + WireDiscardStateProbeMeasurement RunWireDiscardStateProbeOnThrowawayDevice( + PFN_vkGetInstanceProcAddr getInstanceProcAddr, VkInstance instance, VkPhysicalDevice physicalDevice, + Uint32 graphicsQueueFamilyIndex); +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.vert b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.vert new file mode 100644 index 00000000..1430d2d7 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.vert @@ -0,0 +1,8 @@ +#version 450 +// The discard state probe's draws (WireDiscardStateProbe.h): vertices 0-3 are a full-viewport strip, +// vertices 4-7 the same strip far outside the clip volume (the control's first draw). +void main() { + const vec2 corners[4] = vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0)); + const vec2 offset = gl_VertexIndex >= 4 ? vec2(8.0) : vec2(0.0); + gl_Position = vec4(corners[gl_VertexIndex & 3] + offset, 0.0, 1.0); +} diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbeSpirv.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbeSpirv.h new file mode 100644 index 00000000..d33d5343 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbeSpirv.h @@ -0,0 +1,447 @@ +// Generated from WireDiscardStateProbe.vert / WireDiscardStateProbe.frag (Vulkan 1.0 / SPIR-V 1.0) by +// scripts/bake_internal_shaders.py, which runs MG_Test's BakedInternalShadersTest with the IN-TREE +// glslang and splices the words it emits into the arrays below. Do not edit the words by hand: +// MOBILEGL_BAKED_INTERNAL_SHADERS (the same test, run as a gate) recompiles every source in its table +// and reds on the first word that disagrees. +#pragma once + +static constexpr Uint32 kWireDiscardStateProbeVertexSpirv[] = { + 0x07230203u, + 0x00010000u, + 0x0008000bu, + 0x00000037u, + 0x00000000u, + 0x00020011u, + 0x00000001u, + 0x0006000bu, + 0x00000001u, + 0x4c534c47u, + 0x6474732eu, + 0x3035342eu, + 0x00000000u, + 0x0003000eu, + 0x00000000u, + 0x00000001u, + 0x0007000fu, + 0x00000000u, + 0x00000004u, + 0x6e69616du, + 0x00000000u, + 0x0000000cu, + 0x0000001eu, + 0x00030003u, + 0x00000002u, + 0x000001c2u, + 0x00040005u, + 0x00000004u, + 0x6e69616du, + 0x00000000u, + 0x00040005u, + 0x00000009u, + 0x7366666fu, + 0x00007465u, + 0x00060005u, + 0x0000000cu, + 0x565f6c67u, + 0x65747265u, + 0x646e4978u, + 0x00007865u, + 0x00060005u, + 0x0000001cu, + 0x505f6c67u, + 0x65567265u, + 0x78657472u, + 0x00000000u, + 0x00060006u, + 0x0000001cu, + 0x00000000u, + 0x505f6c67u, + 0x7469736fu, + 0x006e6f69u, + 0x00070006u, + 0x0000001cu, + 0x00000001u, + 0x505f6c67u, + 0x746e696fu, + 0x657a6953u, + 0x00000000u, + 0x00070006u, + 0x0000001cu, + 0x00000002u, + 0x435f6c67u, + 0x4470696cu, + 0x61747369u, + 0x0065636eu, + 0x00070006u, + 0x0000001cu, + 0x00000003u, + 0x435f6c67u, + 0x446c6c75u, + 0x61747369u, + 0x0065636eu, + 0x00030005u, + 0x0000001eu, + 0x00000000u, + 0x00050005u, + 0x0000002du, + 0x65646e69u, + 0x6c626178u, + 0x00000065u, + 0x00040047u, + 0x0000000cu, + 0x0000000bu, + 0x0000002au, + 0x00030047u, + 0x0000001cu, + 0x00000002u, + 0x00050048u, + 0x0000001cu, + 0x00000000u, + 0x0000000bu, + 0x00000000u, + 0x00050048u, + 0x0000001cu, + 0x00000001u, + 0x0000000bu, + 0x00000001u, + 0x00050048u, + 0x0000001cu, + 0x00000002u, + 0x0000000bu, + 0x00000003u, + 0x00050048u, + 0x0000001cu, + 0x00000003u, + 0x0000000bu, + 0x00000004u, + 0x00020013u, + 0x00000002u, + 0x00030021u, + 0x00000003u, + 0x00000002u, + 0x00030016u, + 0x00000006u, + 0x00000020u, + 0x00040017u, + 0x00000007u, + 0x00000006u, + 0x00000002u, + 0x00040020u, + 0x00000008u, + 0x00000007u, + 0x00000007u, + 0x00040015u, + 0x0000000au, + 0x00000020u, + 0x00000001u, + 0x00040020u, + 0x0000000bu, + 0x00000001u, + 0x0000000au, + 0x0004003bu, + 0x0000000bu, + 0x0000000cu, + 0x00000001u, + 0x0004002bu, + 0x0000000au, + 0x0000000eu, + 0x00000004u, + 0x00020014u, + 0x0000000fu, + 0x0004002bu, + 0x00000006u, + 0x00000011u, + 0x41000000u, + 0x0005002cu, + 0x00000007u, + 0x00000012u, + 0x00000011u, + 0x00000011u, + 0x0004002bu, + 0x00000006u, + 0x00000013u, + 0x00000000u, + 0x0005002cu, + 0x00000007u, + 0x00000014u, + 0x00000013u, + 0x00000013u, + 0x00040017u, + 0x00000015u, + 0x0000000fu, + 0x00000002u, + 0x00040017u, + 0x00000018u, + 0x00000006u, + 0x00000004u, + 0x00040015u, + 0x00000019u, + 0x00000020u, + 0x00000000u, + 0x0004002bu, + 0x00000019u, + 0x0000001au, + 0x00000001u, + 0x0004001cu, + 0x0000001bu, + 0x00000006u, + 0x0000001au, + 0x0006001eu, + 0x0000001cu, + 0x00000018u, + 0x00000006u, + 0x0000001bu, + 0x0000001bu, + 0x00040020u, + 0x0000001du, + 0x00000003u, + 0x0000001cu, + 0x0004003bu, + 0x0000001du, + 0x0000001eu, + 0x00000003u, + 0x0004002bu, + 0x0000000au, + 0x0000001fu, + 0x00000000u, + 0x0004002bu, + 0x00000019u, + 0x00000020u, + 0x00000004u, + 0x0004001cu, + 0x00000021u, + 0x00000007u, + 0x00000020u, + 0x0004002bu, + 0x00000006u, + 0x00000022u, + 0xbf800000u, + 0x0005002cu, + 0x00000007u, + 0x00000023u, + 0x00000022u, + 0x00000022u, + 0x0004002bu, + 0x00000006u, + 0x00000024u, + 0x3f800000u, + 0x0005002cu, + 0x00000007u, + 0x00000025u, + 0x00000024u, + 0x00000022u, + 0x0005002cu, + 0x00000007u, + 0x00000026u, + 0x00000022u, + 0x00000024u, + 0x0005002cu, + 0x00000007u, + 0x00000027u, + 0x00000024u, + 0x00000024u, + 0x0007002cu, + 0x00000021u, + 0x00000028u, + 0x00000023u, + 0x00000025u, + 0x00000026u, + 0x00000027u, + 0x0004002bu, + 0x0000000au, + 0x0000002au, + 0x00000003u, + 0x00040020u, + 0x0000002cu, + 0x00000007u, + 0x00000021u, + 0x00040020u, + 0x00000035u, + 0x00000003u, + 0x00000018u, + 0x00050036u, + 0x00000002u, + 0x00000004u, + 0x00000000u, + 0x00000003u, + 0x000200f8u, + 0x00000005u, + 0x0004003bu, + 0x00000008u, + 0x00000009u, + 0x00000007u, + 0x0004003bu, + 0x0000002cu, + 0x0000002du, + 0x00000007u, + 0x0004003du, + 0x0000000au, + 0x0000000du, + 0x0000000cu, + 0x000500afu, + 0x0000000fu, + 0x00000010u, + 0x0000000du, + 0x0000000eu, + 0x00050050u, + 0x00000015u, + 0x00000016u, + 0x00000010u, + 0x00000010u, + 0x000600a9u, + 0x00000007u, + 0x00000017u, + 0x00000016u, + 0x00000012u, + 0x00000014u, + 0x0003003eu, + 0x00000009u, + 0x00000017u, + 0x0004003du, + 0x0000000au, + 0x00000029u, + 0x0000000cu, + 0x000500c7u, + 0x0000000au, + 0x0000002bu, + 0x00000029u, + 0x0000002au, + 0x0003003eu, + 0x0000002du, + 0x00000028u, + 0x00050041u, + 0x00000008u, + 0x0000002eu, + 0x0000002du, + 0x0000002bu, + 0x0004003du, + 0x00000007u, + 0x0000002fu, + 0x0000002eu, + 0x0004003du, + 0x00000007u, + 0x00000030u, + 0x00000009u, + 0x00050081u, + 0x00000007u, + 0x00000031u, + 0x0000002fu, + 0x00000030u, + 0x00050051u, + 0x00000006u, + 0x00000032u, + 0x00000031u, + 0x00000000u, + 0x00050051u, + 0x00000006u, + 0x00000033u, + 0x00000031u, + 0x00000001u, + 0x00070050u, + 0x00000018u, + 0x00000034u, + 0x00000032u, + 0x00000033u, + 0x00000013u, + 0x00000024u, + 0x00050041u, + 0x00000035u, + 0x00000036u, + 0x0000001eu, + 0x0000001fu, + 0x0003003eu, + 0x00000036u, + 0x00000034u, + 0x000100fdu, + 0x00010038u, +}; + +static constexpr Uint32 kWireDiscardStateProbeFragmentSpirv[] = { + 0x07230203u, + 0x00010000u, + 0x0008000bu, + 0x0000000du, + 0x00000000u, + 0x00020011u, + 0x00000001u, + 0x0006000bu, + 0x00000001u, + 0x4c534c47u, + 0x6474732eu, + 0x3035342eu, + 0x00000000u, + 0x0003000eu, + 0x00000000u, + 0x00000001u, + 0x0006000fu, + 0x00000004u, + 0x00000004u, + 0x6e69616du, + 0x00000000u, + 0x00000009u, + 0x00030010u, + 0x00000004u, + 0x00000007u, + 0x00030003u, + 0x00000002u, + 0x000001c2u, + 0x00040005u, + 0x00000004u, + 0x6e69616du, + 0x00000000u, + 0x00040005u, + 0x00000009u, + 0x6f6c6f63u, + 0x00000072u, + 0x00040047u, + 0x00000009u, + 0x0000001eu, + 0x00000000u, + 0x00020013u, + 0x00000002u, + 0x00030021u, + 0x00000003u, + 0x00000002u, + 0x00030016u, + 0x00000006u, + 0x00000020u, + 0x00040017u, + 0x00000007u, + 0x00000006u, + 0x00000004u, + 0x00040020u, + 0x00000008u, + 0x00000003u, + 0x00000007u, + 0x0004003bu, + 0x00000008u, + 0x00000009u, + 0x00000003u, + 0x0004002bu, + 0x00000006u, + 0x0000000au, + 0x00000000u, + 0x0004002bu, + 0x00000006u, + 0x0000000bu, + 0x3f800000u, + 0x0007002cu, + 0x00000007u, + 0x0000000cu, + 0x0000000au, + 0x0000000bu, + 0x0000000au, + 0x0000000bu, + 0x00050036u, + 0x00000002u, + 0x00000004u, + 0x00000000u, + 0x00000003u, + 0x000200f8u, + 0x00000005u, + 0x0003003eu, + 0x00000009u, + 0x0000000cu, + 0x000100fdu, + 0x00010038u, +}; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc index ef762a3c..1ab70ae1 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireDraw.inc @@ -982,6 +982,11 @@ if (m_fragmentShadingRate.Pipeline) { ShadowedSetFragmentShadingRate(frame.commandBuffer, s_vkCmdSetFragmentShadingRateKHR); } + // This draw's state is set; where a discard draw then loses it (WireDiscardStateProbe.h), + // the next draw must set its own rather than inherit. + if (m_wireReemitStateAfterDiscard && + MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::RasterizerDiscard)) + ForgetDynamicStateAfterDiscardDraw(); // Storage images this draw writes are what the next sampled-image barrier must cover. if (programObj.hasStorageImages) BumpWireImageWriteEpoch(); if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1); diff --git a/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp b/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp index a35ec77d..0baf9dd1 100644 --- a/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp +++ b/MobileGL/MG_Test/SelfTest/BakedInternalShadersTest.cpp @@ -62,6 +62,7 @@ using Uint32 = std::uint32_t; #include #include #include +#include struct BakedShader { const char* name; // ctest entry name, and the bake file's stem @@ -107,6 +108,19 @@ const BakedShader kBakedShaders[] = { "kWireE5RawCopy3DSpirv", EShLangCompute, glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0, kWireE5RawCopy3DSpirv), + // The rasterizer-discard state probe (WireDiscardStateProbe.h). + MGL_BAKED_ROW("WireDiscardStateProbeVertex", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.vert", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbeSpirv.h", + "kWireDiscardStateProbeVertexSpirv", EShLangVertex, + glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0, + kWireDiscardStateProbeVertexSpirv), + MGL_BAKED_ROW("WireDiscardStateProbeFragment", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.frag", + "MobileGL/MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbeSpirv.h", + "kWireDiscardStateProbeFragmentSpirv", EShLangFragment, + glslang::EShTargetVulkan_1_0, glslang::EShTargetSpv_1_0, + kWireDiscardStateProbeFragmentSpirv), MGL_BAKED_ROW("WireYuvConvertVertex", "MobileGL/MG_Backend/DirectVulkan/Renderer/WireYuvConvert.vert", "MobileGL/MG_Backend/DirectVulkan/Renderer/WireYuvConvertSpirv.h", diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index bb9ff00d..c864031c 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -2578,11 +2579,34 @@ namespace MobileGL::MG_Util::SelfTest { return DriverBugFinding{"Cross-format 2D-array <-> 3D image copies drop slices", DriverBugVerdict::Fixed, Move(detail)}; } + + // The same probe the Magma renderer runs at bring-up (VulkanRenderer::ArmWireDiscardStateReemit). + Optional ProbeDiscardStateBug(const VulkanDriverBugProbeContext& context) { + using namespace MG_Backend::DirectVulkan; + if (context.getInstanceProcAddr == nullptr) return std::nullopt; + const WireDiscardStateProbeMeasurement measurement = RunWireDiscardStateProbeOnThrowawayDevice( + context.getInstanceProcAddr, context.instance, context.physicalDevice, + context.graphicsQueueFamilyIndex); + const WireDiscardStateProbeVerdict verdict = EvaluateWireDiscardStateProbe(measurement); + MGLOG_I("Driver POST: discard state probe verdict=%s: %s", WireDiscardStateProbeVerdictName(verdict), + DescribeWireDiscardStateProbe(measurement).c_str()); + if (verdict != WireDiscardStateProbeVerdict::DiscardDrawDropsState) return std::nullopt; + String detail = + "a draw with a rasterizerDiscardEnable pipeline drops the dynamic viewport/scissor, so a later " + "draw that inherits them draws nothing, while after a rasterizing draw it does: "; + detail += DescribeWireDiscardStateProbe(measurement); + detail += + ". Magma sets the dynamic state again after every discard draw " + "(MOBILEGL_MAGMA_REEMIT_STATE_AFTER_DISCARD=0 turns it off)"; + return DriverBugFinding{"Rasterizer-discard draws drop dynamic viewport/scissor", + DriverBugVerdict::Fixed, Move(detail)}; + } using VulkanDriverBugProbeFn = Optional (*)(const VulkanDriverBugProbeContext&); constexpr VulkanDriverBugProbeFn kVulkanDriverBugProbes[] = { &ProbeDepthStencilResolvePassBug, &ProbeE5TransferReadNormalizeBug, &ProbeCrossFormatSliceCopyBug, + &ProbeDiscardStateBug, }; } // namespace