From 8e87cf5a66bb8c2d5b48f6ca4e73e030d9101dfc Mon Sep 17 00:00:00 2001 From: SethRamey <141290181+SethRamey@users.noreply.github.com> Date: Fri, 6 Nov 2026 06:56:01 -0500 Subject: [PATCH 1/7] vulkan: intel arc fixes for MKW Before these changes, intel dGPU device loss would occur soon after boot. These changes also fix several intel dGPU crashes and banding. Task shaders: payload word 1 was never set which caused Intel device loss. Now zeroed at start, followed by a barrier. Banding: shaders that previously lost a conditional barrier raced on Intel dGPU, those shaders are now forced to subgroup size 32. Pipeline cache: Intel driver crashed when cache was read during pipeline builds. Flush is now skipped for the driver. Closing the game: state tracker kept stale pointer to channel state which crashed on shutdown. ClearChannel() resets it. Size 32 is a workaround, not a real barrier Tested A770, driver 32.0.101.9150, Windows, MKW Assisted-by: Claude Opus 5.5 (debugging, code, testing) --- src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp | 7 +++++++ src/shader_recompiler/ir_opt/conditional_barrier_pass.cpp | 1 + src/shader_recompiler/shader_info.h | 1 + src/video_core/renderer_vulkan/vk_compute_pipeline.cpp | 6 +++++- src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 3 ++- src/video_core/renderer_vulkan/vk_rasterizer.cpp | 6 ++++++ src/video_core/renderer_vulkan/vk_rasterizer.h | 1 + src/video_core/renderer_vulkan/vk_state_tracker.h | 5 +++++ src/video_core/vulkan_common/vulkan_device.h | 6 ++++++ 9 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp index 6c599528cc..e0f3f437f6 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp @@ -207,6 +207,13 @@ void EmitIsbeWriteMapU32(EmitContext& ctx, const IR::Value& address, Id value) { } void EmitMeshPrologue(EmitContext& ctx) { + if (ctx.stage == Stage::Task) { + // A task that culls all its threads never writes the mesh workgroup count, and the payload + // starts undefined. Intel's garbage there launches enough workgroups to lose the device. + ctx.OpStore(PayloadWord(ctx, ctx.Const(1U)), ctx.u32_zero_value); + Barrier(ctx); + return; + } if (ctx.stage != Stage::Mesh) { return; } diff --git a/src/shader_recompiler/ir_opt/conditional_barrier_pass.cpp b/src/shader_recompiler/ir_opt/conditional_barrier_pass.cpp index c3ed27f4f2..8a792b2ff2 100644 --- a/src/shader_recompiler/ir_opt/conditional_barrier_pass.cpp +++ b/src/shader_recompiler/ir_opt/conditional_barrier_pass.cpp @@ -31,6 +31,7 @@ void ConditionalBarrierPass(IR::Program& program) { inst.GetOpcode() == IR::Opcode::Barrier) { LOG_WARNING(Shader, "Barrier within conditional control flow"); inst.ReplaceOpcode(IR::Opcode::Identity); + program.info.removed_conditional_barrier = true; } } break; diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h index 4e5fb5c6e4..d2e2c807e0 100644 --- a/src/shader_recompiler/shader_info.h +++ b/src/shader_recompiler/shader_info.h @@ -320,6 +320,7 @@ struct Info { bool uses_sparse_residency{}; bool uses_demote_to_helper_invocation{}; bool uses_subgroup_vote{}; + bool removed_conditional_barrier{}; bool uses_subgroup_mask{}; bool uses_fswzadd{}; bool uses_derivatives{}; diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index ae008b48a7..5fa13eb0fa 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -88,7 +88,11 @@ ComputePipeline::ComputePipeline(const Device& device_, Scheduler& scheduler, vk }; // Only warp-level ops observe the subgroup size. Forcing 32 on other shaders makes Intel // compile SIMD32, which can spill a register-heavy kernel for minutes (Mario Kart World). - const bool uses_warp_ops = info.uses_subgroup_vote || info.uses_subgroup_invocation_id || + // A shader that lost a conditional barrier relies on a whole warp running in step: at a + // smaller subgroup size it reads stale shared memory, which shows as banding over the image + // in Mario Kart World on Intel's Windows driver. + const bool uses_warp_ops = info.removed_conditional_barrier || + info.uses_subgroup_vote || info.uses_subgroup_invocation_id || info.uses_subgroup_shuffles || info.uses_subgroup_mask || info.uses_quad_shuffles || info.uses_fswzadd; VkPipelineCreateFlags flags{}; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 18aa74b29e..adb9f04786 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -912,7 +912,8 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading } void PipelineCache::QueueVulkanPipelineCacheFlush() { - if (!use_vulkan_pipeline_cache || vulkan_pipeline_cache_filename.empty()) { + if (!use_vulkan_pipeline_cache || vulkan_pipeline_cache_filename.empty() || + !device.CanReadPipelineCacheDuringBuilds()) { return; } if (++pipelines_since_flush < VULKAN_CACHE_FLUSH_PIPELINES) { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index bb8c7f57a3..f51182f044 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -246,6 +246,8 @@ RasterizerVulkan::RasterizerVulkan(Core::Frontend::EmuWindow& emu_window_, Tegra RasterizerVulkan::~RasterizerVulkan() { VideoCommon::GpuSplit::Get().SetIdleHook(nullptr, nullptr); + // The channels are gone by now, and submitting invalidates the state tracker's flags. + state_tracker.ClearChannel(); scheduler.WaitWorker(); scheduler.Finish(); } @@ -2553,9 +2555,13 @@ void RasterizerVulkan::BindChannel(Tegra::Control::ChannelState& channel) { query_cache.BindToChannel(channel_id); state_tracker.ChangeChannel(channel); state_tracker.InvalidateState(); + bound_channel_id = channel_id; } void RasterizerVulkan::ReleaseChannel(s32 channel_id) { + if (channel_id == bound_channel_id) { + state_tracker.ClearChannel(); + } EraseChannel(channel_id); { std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex}; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index 1e550a27ca..65de3105d2 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -224,6 +224,7 @@ class RasterizerVulkan final : public VideoCore::RasterizerInterface, const Device& device; MemoryAllocator& memory_allocator; StateTracker& state_tracker; + s32 bound_channel_id = -1; Scheduler& scheduler; StagingBufferPool staging_pool; diff --git a/src/video_core/renderer_vulkan/vk_state_tracker.h b/src/video_core/renderer_vulkan/vk_state_tracker.h index 6b47ba4176..d47fa9e0e2 100644 --- a/src/video_core/renderer_vulkan/vk_state_tracker.h +++ b/src/video_core/renderer_vulkan/vk_state_tracker.h @@ -290,6 +290,11 @@ class StateTracker { void ChangeChannel(Tegra::Control::ChannelState& channel_state); + /// Stops using the bound channel's flags, for when it goes away. + void ClearChannel() { + flags = &default_flags; + } + void InvalidateState(); private: diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index f3f9209c99..c69edb6d2d 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -369,6 +369,12 @@ class Device { id != VK_DRIVER_ID_MESA_RADV; } + /// Whether a pipeline cache can be read while pipelines build into it, as Vulkan allows. Intel's + /// Windows driver crashes in vkGetPipelineCacheData when a build is running. + bool CanReadPipelineCacheDuringBuilds() const { + return GetDriverID() != VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS; + } + /// Returns true for tile-based deferred renderers. bool IsTiler() const { switch (GetDriverID()) { From f9de7d8551bcb3e6ce8a3c80a4caebec4d74da91 Mon Sep 17 00:00:00 2001 From: SethRamey <141290181+SethRamey@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:44:01 -0500 Subject: [PATCH 2/7] vulkan: wait for shader builds before the cache save on close Pipeline Cache now waits for shader build workers before saving cache on drivers where CanReadPipelineCacheDuringBuilds() is false. Tested on A770, MKW. Closing during shader builds closed cleanly. Assisted-by: Claude Opus 5.5 (debugging, code, testing) --- src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index adb9f04786..7be731e3bc 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -661,6 +661,9 @@ bool PreloadAllPipelines() { PipelineCache::~PipelineCache() { SaveUsedPipelines(); if (use_vulkan_pipeline_cache && !vulkan_pipeline_cache_filename.empty()) { + if (!device.CanReadPipelineCacheDuringBuilds()) { + workers.WaitForRequests(); + } SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache, CACHE_VERSION); } From 37a3ad3ad95de36f372fbe14732cadd6a3987225 Mon Sep 17 00:00:00 2001 From: SethRamey <141290181+SethRamey@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:24:32 -0500 Subject: [PATCH 3/7] vulkan: drop queued shader builds on close New ClearRequests() in ThreadWorker, close waits only for running builds Tested A770, MKW, closing during shader builds Assisted-by: Claude Opus 5.5 (debugging, code, testing) --- src/common/thread_worker.h | 7 +++++++ src/video_core/renderer_vulkan/vk_pipeline_cache.cpp | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/common/thread_worker.h b/src/common/thread_worker.h index ec3ca0af49..a88713665d 100644 --- a/src/common/thread_worker.h +++ b/src/common/thread_worker.h @@ -117,6 +117,13 @@ class StatefulThreadWorker { }); } + // Drops tasks no worker has started. Running tasks still finish. + void ClearRequests() { + std::scoped_lock lock{queue_mutex}; + work_scheduled -= requests.size(); + requests = std::queue{}; + } + private: std::queue requests; std::mutex queue_mutex; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 7be731e3bc..e926a0a1f8 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -662,6 +662,8 @@ PipelineCache::~PipelineCache() { SaveUsedPipelines(); if (use_vulkan_pipeline_cache && !vulkan_pipeline_cache_filename.empty()) { if (!device.CanReadPipelineCacheDuringBuilds()) { + // Drop queued builds, as destroying the workers would; running ones must end first. + workers.ClearRequests(); workers.WaitForRequests(); } SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache, From 70a841058e9aed182d188d33087cf00d666dd919 Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:49:41 +0100 Subject: [PATCH 4/7] shader: share the warp-op check between pipeline stages Info::UsesWarpOps() replaces the copy in the compute pipeline so graphics mesh/task stages can use the same condition, including shaders that lost a conditional barrier. --- src/shader_recompiler/shader_info.h | 8 ++++++++ .../renderer_vulkan/vk_compute_pipeline.cpp | 11 ++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h index d2e2c807e0..3e38f37751 100644 --- a/src/shader_recompiler/shader_info.h +++ b/src/shader_recompiler/shader_info.h @@ -320,6 +320,7 @@ struct Info { bool uses_sparse_residency{}; bool uses_demote_to_helper_invocation{}; bool uses_subgroup_vote{}; + /// Set when a barrier in conditional control flow was dropped, see ConditionalBarrierPass. bool removed_conditional_barrier{}; bool uses_subgroup_mask{}; bool uses_fswzadd{}; @@ -375,6 +376,13 @@ struct Info { /// and storage buffer words (see EvaluateHandleProgram), evaluated for each draw. std::vector> handle_programs; ImageDescriptors image_descriptors; + + /// True when the shader only runs correctly if a subgroup spans a whole guest warp. + [[nodiscard]] bool UsesWarpOps() const noexcept { + return removed_conditional_barrier || uses_subgroup_vote || uses_subgroup_invocation_id || + uses_subgroup_shuffles || uses_subgroup_mask || uses_quad_shuffles || + uses_fswzadd; + } }; template diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp index 5fa13eb0fa..682360e07e 100644 --- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp @@ -88,13 +88,10 @@ ComputePipeline::ComputePipeline(const Device& device_, Scheduler& scheduler, vk }; // Only warp-level ops observe the subgroup size. Forcing 32 on other shaders makes Intel // compile SIMD32, which can spill a register-heavy kernel for minutes (Mario Kart World). - // A shader that lost a conditional barrier relies on a whole warp running in step: at a - // smaller subgroup size it reads stale shared memory, which shows as banding over the image - // in Mario Kart World on Intel's Windows driver. - const bool uses_warp_ops = info.removed_conditional_barrier || - info.uses_subgroup_vote || info.uses_subgroup_invocation_id || - info.uses_subgroup_shuffles || info.uses_subgroup_mask || - info.uses_quad_shuffles || info.uses_fswzadd; + // A shader that lost a conditional barrier counts too: it relies on a whole warp running + // in step, and at a smaller subgroup size it reads stale shared memory, which shows as + // banding over the image in Mario Kart World on Intel's Windows driver. + const bool uses_warp_ops = info.UsesWarpOps(); VkPipelineCreateFlags flags{}; if (device.IsKhrPipelineExecutablePropertiesEnabled() && Settings::values.renderer_debug.GetValue()) { flags |= VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR; From ff72fed972400b814743e4d7ac8743d5f3057b68 Mon Sep 17 00:00:00 2001 From: Andreas Heider Date: Sat, 7 Nov 2026 10:49:22 +0000 Subject: [PATCH 5/7] vulkan: mesh and task stages now require the guest warp size Graphics pipelines built the required-subgroup-size struct but never attached it (it was [[maybe_unused]]). Compute pipelines already require 32 when they use warp ops. Mainly applies to intel, because there we have 16-wide pipelines. Foliage rendered as glowing white on Intel ANV. Its leaf mesh shaders compact visible leaves with Ballot + BitCount(ballot & LtMask). ANV compiled the large colour-pass shader 16-wide, so the two halves of each workgroup wrote the same slots, while the small pre-pass shader ran 32-wide. The colour pass then failed its depth-EQUAL test against the pre-pass, and the sky showed through. Signed-off-by: Andreas Heider Assisted-by: Claude Opus 5.5 (debugging with RenderDoc, fix) --- .../renderer_vulkan/vk_graphics_pipeline.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index d3ffc661c2..906724d6c5 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -1138,7 +1138,7 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) { .dynamicStateCount = static_cast(dynamic_states.size()), .pDynamicStates = dynamic_states.data(), }; - [[maybe_unused]] const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup_size_ci{ + const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup_size_ci{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT, .pNext = nullptr, .requiredSubgroupSize = GuestWarpSize, @@ -1148,7 +1148,7 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) { if (!spv_modules[stage]) { continue; } - [[maybe_unused]] auto& stage_ci = + auto& stage_ci = shader_stages.emplace_back(VkPipelineShaderStageCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .pNext = nullptr, @@ -1159,6 +1159,16 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) { .pName = "main", .pSpecializationInfo = nullptr, }); + const Shader::Info& info{stage_infos[stage]}; + const bool uses_warp_ops = info.uses_subgroup_vote || info.uses_subgroup_invocation_id || + info.uses_subgroup_shuffles || info.uses_subgroup_mask || + info.uses_quad_shuffles || info.uses_fswzadd; + if ((stage_ci.stage == VK_SHADER_STAGE_MESH_BIT_EXT || + stage_ci.stage == VK_SHADER_STAGE_TASK_BIT_EXT) && + uses_warp_ops && device.IsExtSubgroupSizeControlSupported() && + device.IsGuestWarpSizeSupported(stage_ci.stage)) { + stage_ci.pNext = &subgroup_size_ci; + } } VkPipelineCreateFlags flags{}; if (device.IsKhrPipelineExecutablePropertiesEnabled() && Settings::values.renderer_debug.GetValue()) { From 0e8205682015c316cf35175cb5c8047c9ec44b04 Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Sat, 7 Nov 2026 15:52:07 +0100 Subject: [PATCH 6/7] vulkan: use Info::UsesWarpOps for mesh and task stages Shaders that lost a conditional barrier now get the guest warp size on mesh and task stages too, matching compute. --- src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index 906724d6c5..e742e268c4 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -1160,12 +1160,9 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) { .pSpecializationInfo = nullptr, }); const Shader::Info& info{stage_infos[stage]}; - const bool uses_warp_ops = info.uses_subgroup_vote || info.uses_subgroup_invocation_id || - info.uses_subgroup_shuffles || info.uses_subgroup_mask || - info.uses_quad_shuffles || info.uses_fswzadd; if ((stage_ci.stage == VK_SHADER_STAGE_MESH_BIT_EXT || stage_ci.stage == VK_SHADER_STAGE_TASK_BIT_EXT) && - uses_warp_ops && device.IsExtSubgroupSizeControlSupported() && + info.UsesWarpOps() && device.IsExtSubgroupSizeControlSupported() && device.IsGuestWarpSizeSupported(stage_ci.stage)) { stage_ci.pNext = &subgroup_size_ci; } From 77c41e14dbfb34a0d214901efcf7abd465338f1d Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Sat, 7 Nov 2026 17:16:33 +0100 Subject: [PATCH 7/7] add relower_words.cpp for instruction lowering from hex input --- relower_words.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 relower_words.cpp diff --git a/relower_words.cpp b/relower_words.cpp new file mode 100644 index 0000000000..71045b4139 --- /dev/null +++ b/relower_words.cpp @@ -0,0 +1,19 @@ +// Reads "w0 w1 w2 w3" hex lines on stdin; lowers each instruction followed by EXIT. +// Build: c++ -std=c++20 -O1 -w -I /src relower_words.cpp -lfmt -o relower +#include +#include +#include "shader_recompiler/frontend/ampere/lower_to_maxwell.h" + +using namespace Shader::Ampere; + +int main(int argc, char**) { + const bool tensor = argc > 1; // any argument: emulate HMMA as EDEN_NX2_TENSOR would + unsigned w0, w1, w2, w3; + while (std::scanf("%x %x %x %x", &w0, &w1, &w2, &w3) == 4) { + const std::vector code{Instruction{{w0, w1, w2, w3}}, + Instruction{{0x0000794d, 0, 0x03800000, 0}}}; + const auto lowered = LowerToMaxwell(code, LoweringOptions{.tensor = tensor}); + std::printf("%08x %08x %08x %08x: %s%.*s\n", w0, w1, w2, w3, lowered ? "OK" : "FAIL ", + static_cast(lowered.error.size()), lowered.error.data()); + } +}