From 73559d15aa0e1bbdb60d8d7624c17ac4fb5936aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 17:45:44 +0100 Subject: [PATCH] Vulkan: Work around a PowerVR crash when a vertex shader doesn't read the lights UBO On a Moto G54 (PowerVR BXM-8-256), F1 2009 and MX vs. ATV Untamed crash every few seconds in vkCmdDrawIndexed, with a write fault at the start of a page inside vulkan.mtk.so. At draw time, the driver writes the address and range of every dynamic UBO in the bound descriptor set into a table, but sizes the table for the ones the shader stage reads. Our layout has two dynamic UBOs, base and lights (vertex only), and a vertex shader without lighting reads only base, so each such draw writes 16 bytes past the table. That's usually harmless, and faults when the table ends a page. With the new PVR_UNUSED_DYNAMIC_UBO_OVERRUN set on PowerVR, Vulkan vertex shaders always declare and read the lights UBO. Declaring it isn't enough. On the G54, F1 went from crashing in 5 of 5 10-second runs to 0 of 5, and MX ran 3 of 3. The SPIR-V cache is keyed by source, so no cache version bump is needed. Co-Authored-By: Claude Opus 5.5 --- Common/GPU/Vulkan/thin3d_vulkan.cpp | 4 ++++ Common/GPU/thin3d.cpp | 1 + Common/GPU/thin3d.h | 1 + GPU/Common/VertexShaderGenerator.cpp | 8 +++++++- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Common/GPU/Vulkan/thin3d_vulkan.cpp b/Common/GPU/Vulkan/thin3d_vulkan.cpp index 1f5554d2f4..238f6a158e 100644 --- a/Common/GPU/Vulkan/thin3d_vulkan.cpp +++ b/Common/GPU/Vulkan/thin3d_vulkan.cpp @@ -1140,6 +1140,10 @@ VKContext::VKContext(VulkanContext *vulkan, bool useRenderThread) } else if (caps_.vendor == GPUVendor::VENDOR_IMGTEC) { // Not sure about driver versions, so let's just ban, impact is tiny. bugs_.Infest(Bugs::PVR_BAD_16BIT_TEXFORMATS); + // At draw time, the driver writes the address of every dynamic UBO in the bound descriptor set into a + // table it sized for the ones the shader stage uses, so a stage that skips one runs off the end of it. + // Seen on a BXM-8-256 (Moto G54): a page fault in vkCmdDrawIndexed every few seconds in F1 2009. + bugs_.Infest(Bugs::PVR_UNUSED_DYNAMIC_UBO_OVERRUN); } if (!vulkan->Extensions().KHR_depth_stencil_resolve) { diff --git a/Common/GPU/thin3d.cpp b/Common/GPU/thin3d.cpp index 224a26507f..fd82f17a33 100644 --- a/Common/GPU/thin3d.cpp +++ b/Common/GPU/thin3d.cpp @@ -538,6 +538,7 @@ const char *Bugs::GetBugName(uint32_t bug) { case ADRENO_RESOURCE_DEADLOCK: return "ADRENO_RESOURCE_DEADLOCK"; case PVR_BAD_16BIT_TEXFORMATS: return "PVR_BAD_16BIT_TEXFORMATS"; case EMPTY_RENDERPASS_BROKEN_MALI: return "EMPTY_RENDERPASS_BROKEN_MALI"; + case PVR_UNUSED_DYNAMIC_UBO_OVERRUN: return "PVR_UNUSED_DYNAMIC_UBO_OVERRUN"; default: return "(N/A)"; } } diff --git a/Common/GPU/thin3d.h b/Common/GPU/thin3d.h index b45e23ca0c..71455c1dfd 100644 --- a/Common/GPU/thin3d.h +++ b/Common/GPU/thin3d.h @@ -348,6 +348,7 @@ public: PVR_BAD_16BIT_TEXFORMATS = 14, NO_DEPTH_CANNOT_DISCARD_STENCIL_MALI = 15, EMPTY_RENDERPASS_BROKEN_MALI = 16, + PVR_UNUSED_DYNAMIC_UBO_OVERRUN = 17, MAX_BUG, }; diff --git a/GPU/Common/VertexShaderGenerator.cpp b/GPU/Common/VertexShaderGenerator.cpp index ae0278bb66..4b5464d9ef 100644 --- a/GPU/Common/VertexShaderGenerator.cpp +++ b/GPU/Common/VertexShaderGenerator.cpp @@ -133,6 +133,8 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag int ls0 = id.Bits(VS_BIT_LS0, 2); int ls1 = id.Bits(VS_BIT_LS1, 2); bool enableLighting = id.Bit(VS_BIT_LIGHTING_ENABLE); + // See PVR_UNUSED_DYNAMIC_UBO_OVERRUN: every vertex shader reads the lights UBO, which is bound as dynamic either way. + const bool readUnusedLights = compat.shaderLanguage == GLSL_VULKAN && !enableLighting && !doShadeMapping && bugs.Has(Draw::Bugs::PVR_UNUSED_DYNAMIC_UBO_OVERRUN); int matUpdate = id.Bits(VS_BIT_MATERIAL_UPDATE, 3); bool lightUberShader = id.Bit(VS_BIT_LIGHT_UBERSHADER) && enableLighting; // checking lighting here for the shader test's benefit, in reality if ubershader is set, lighting is set. @@ -177,7 +179,7 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag WRITE(p, "\n"); WRITE(p, "layout (std140, set = 0, binding = %d) uniform baseVars {\n%s};\n", DRAW_BINDING_DYNUBO_BASE, ub_baseStr); - if (enableLighting || doShadeMapping) + if (enableLighting || doShadeMapping || readUnusedLights) WRITE(p, "layout (std140, set = 0, binding = %d) uniform lightVars {\n%s};\n", DRAW_BINDING_DYNUBO_LIGHT, ub_vs_lightsStr); if (useHWTransform) @@ -1005,6 +1007,10 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag WRITE(p, " outPos.y *= -1.0;\n"); } + if (readUnusedLights) { + // Declaring the block isn't enough, it has to be read. The ambient alpha is never below zero. + WRITE(p, " if (u_ambient.w < -1.0) outPos.x += 1.0;\n"); + } // We've named the output gl_Position in HLSL as well. WRITE(p, " %sgl_Position = outPos;\n", compat.vsOutPrefix);