[Fix] (Magma): re-emit dynamic state after rasterizer-discard draws

This commit is contained in:
2026-10-12 22:32:28 -04:00
parent 8c2d5019c2
commit cd9f67763e
13 changed files with 1078 additions and 0 deletions
+1
View File
@@ -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
+6
View File
@@ -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.
+1
View File
@@ -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.
@@ -52,6 +52,7 @@
#include "WireColorBlitFilter.h"
#include "WireDepthResolveArm.h"
#include "WireCopySliceProbe.h"
#include "WireDiscardStateProbe.h"
#include "WireDepthResolveProbe.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Server/ServerLoop.h>
@@ -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<Entry> s_entries;
const WireDepthResolveDeviceIdentity identity = MakeWireDepthResolveDeviceIdentity(
m_physicalDevice.properties, false, WireDepthResolveProbeKnob::Measure);
std::lock_guard<std::mutex> 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<Uint32>(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;
@@ -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.
@@ -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 <vulkan/vulkan_core.h>
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<VkImage> images;
Vector<VkImageView> views;
Vector<VkBuffer> buffers;
Vector<VkDeviceMemory> memories;
Vector<VkFramebuffer> framebuffers;
Vector<VkPipeline> pipelines;
Vector<VkShaderModule> 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<Uint32> PickMemoryType(const VkPhysicalDeviceMemoryProperties& properties, Uint32 allowed,
VkMemoryPropertyFlags wanted, VkMemoryPropertyFlags required) {
Optional<Uint32> 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<Uint32> 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<Float>(kExtent), static_cast<Float>(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, &copy);
}
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<Int>(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<Int>(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<const Uint8*>(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<PFN_vkCreateDevice>(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<Int>(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
@@ -0,0 +1,6 @@
#version 450
layout(location = 0) out vec4 color;
void main() {
color = vec4(0.0, 1.0, 0.0, 1.0);
}
@@ -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 <Config.h>
#include <Includes.h>
#include <vulkan/vulkan_core.h>
// 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
@@ -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);
}
@@ -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,
};
@@ -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);
@@ -62,6 +62,7 @@ using Uint32 = std::uint32_t;
#include <MG_Backend/DirectVulkan/Renderer/WireMultisampleResolveSpirv.h>
#include <MG_Backend/DirectVulkan/Renderer/WireYuvConvertSpirv.h>
#include <MG_Backend/DirectVulkan/Renderer/WireE5RawCopySpirv.h>
#include <MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbeSpirv.h>
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",
@@ -14,6 +14,7 @@
#include <MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.h>
#include <MG_Backend/DirectVulkan/Renderer/WireE5RawCopy.h>
#include <MG_Backend/DirectVulkan/Renderer/WireCopySliceProbe.h>
#include <MG_Backend/DirectVulkan/Renderer/WireDiscardStateProbe.h>
#include <algorithm>
#include <cstring>
@@ -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<DriverBugFinding> 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<DriverBugFinding> (*)(const VulkanDriverBugProbeContext&);
constexpr VulkanDriverBugProbeFn kVulkanDriverBugProbes[] = {
&ProbeDepthStencilResolvePassBug,
&ProbeE5TransferReadNormalizeBug,
&ProbeCrossFormatSliceCopyBug,
&ProbeDiscardStateBug,
};
} // namespace