From cfb243159d3b10287e4faa7b383011c517bb0761 Mon Sep 17 00:00:00 2001 From: PavelBARABANOV Date: Mon, 31 Aug 2026 02:26:30 +0300 Subject: [PATCH] [vulkan, spirv] Fall back to scalar warp intrinsics in the geometry stage when unsupported --- .../backend/spirv/emit_spirv.cpp | 3 +- .../backend/spirv/emit_spirv_warp.cpp | 65 +++++++++++++++++++ .../backend/spirv/spirv_emit_context.cpp | 14 ++-- src/shader_recompiler/profile.h | 6 ++ .../renderer_vulkan/vk_pipeline_cache.cpp | 5 ++ src/video_core/vulkan_common/vulkan_device.h | 5 ++ 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index c221888c09..4535c96e82 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -440,7 +440,8 @@ void SetupCapabilities(const Profile& profile, const Info& info, EmitContext& ct } if ((info.uses_subgroup_vote || info.uses_subgroup_invocation_id || info.uses_subgroup_shuffles) && - profile.support_vote && profile.SupportsSubgroupStage(ctx.stage)) { + profile.support_vote && + (ctx.stage != Stage::Geometry || profile.support_subgroup_in_geometry_stage)) { ctx.AddCapability(spv::Capability::GroupNonUniformBallot); ctx.AddCapability(spv::Capability::GroupNonUniformShuffle); if (!profile.warp_size_potentially_larger_than_guest) { diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp index ffd5d9c29f..bb9389f3a0 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_warp.cpp @@ -13,6 +13,18 @@ Id SubgroupScope(EmitContext& ctx) { return ctx.Const(static_cast(spv::Scope::Subgroup)); } +// Some mobile GPUs (e.g. Adreno/Turnip) only advertise subgroup ballot/shuffle support for the +// fragment and compute stages (VkPhysicalDeviceSubgroupProperties::supportedStages), even though +// they support these operations elsewhere. Guest shaders that use VOTE/SHFL in a geometry program +// would otherwise emit GroupNonUniform* SPIR-V the driver never declared support for in that +// stage. There is no barrier in the geometry stage, so a real cross-invocation emulation can't be +// made correct; instead, treat the current invocation as if it were alone in its subgroup. This is +// semantically wrong for guest code that relies on genuine cross-lane communication, but it is +// well-defined, valid SPIR-V that doesn't depend on unsupported hardware capabilities. +bool NeedsGeometrySubgroupFallback(EmitContext& ctx) { + return ctx.stage == Stage::Geometry && !ctx.profile.support_subgroup_in_geometry_stage; +} + bool StageSupportsSubgroups(EmitContext& ctx) { return ctx.profile.SupportsSubgroupStage(ctx.stage); } @@ -94,6 +106,9 @@ Id AddPartitionBase(EmitContext& ctx, Id thread_id) { } // Anonymous namespace Id EmitLaneId(EmitContext& ctx) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return ctx.u32_zero_value; + } const Id id{GetThreadId(ctx)}; if (!ctx.profile.warp_size_potentially_larger_than_guest) { return id; @@ -102,6 +117,9 @@ Id EmitLaneId(EmitContext& ctx) { } Id EmitVoteAll(EmitContext& ctx, Id pred) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return pred; + } if (!StageSupportsSubgroups(ctx)) { return pred; } @@ -118,6 +136,9 @@ Id EmitVoteAll(EmitContext& ctx, Id pred) { } Id EmitVoteAny(EmitContext& ctx, Id pred) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return pred; + } if (!StageSupportsSubgroups(ctx)) { return pred; } @@ -134,6 +155,9 @@ Id EmitVoteAny(EmitContext& ctx, Id pred) { } Id EmitVoteEqual(EmitContext& ctx, Id pred) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return ctx.true_value; + } if (!StageSupportsSubgroups(ctx)) { return ctx.true_value; } @@ -151,6 +175,16 @@ Id EmitVoteEqual(EmitContext& ctx, Id pred) { } Id EmitSubgroupBallot(EmitContext& ctx, Id pred) { + if (NeedsGeometrySubgroupFallback(ctx)) { + // Reflect only this invocation's own predicate. There is no way to observe other + // invocations' predicates without real subgroup hardware support in this stage, so this + // is a best-effort approximation: it keeps any branch gated on "did anyone match" live + // (rather than letting the SPIR-V optimizer prove it dead, which previously caused + // indirect draws fed by this shader to see indexCount=instanceCount=0), but any downstream + // math that assumes a real cross-lane population count (e.g. popcount-based compaction + // offsets) will not be correct. + return ctx.OpSelect(ctx.U32[1], pred, ctx.Const(1U), ctx.u32_zero_value); + } if (!StageSupportsSubgroups(ctx)) { return ctx.OpSelect(ctx.U32[1], pred, ctx.Const(1u), ctx.u32_zero_value); } @@ -162,6 +196,9 @@ Id EmitSubgroupBallot(EmitContext& ctx, Id pred) { } Id EmitSubgroupEqMask(EmitContext& ctx) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return ctx.Const(1U); + } if (!StageSupportsSubgroups(ctx)) { return ctx.Const(1u); } @@ -169,6 +206,9 @@ Id EmitSubgroupEqMask(EmitContext& ctx) { } Id EmitSubgroupLtMask(EmitContext& ctx) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return ctx.u32_zero_value; + } if (!StageSupportsSubgroups(ctx)) { return ctx.u32_zero_value; } @@ -176,6 +216,9 @@ Id EmitSubgroupLtMask(EmitContext& ctx) { } Id EmitSubgroupLeMask(EmitContext& ctx) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return ctx.Const(1U); + } if (!StageSupportsSubgroups(ctx)) { return ctx.Const(1u); } @@ -183,6 +226,9 @@ Id EmitSubgroupLeMask(EmitContext& ctx) { } Id EmitSubgroupGtMask(EmitContext& ctx) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return ctx.u32_zero_value; + } if (!StageSupportsSubgroups(ctx)) { return ctx.u32_zero_value; } @@ -190,6 +236,9 @@ Id EmitSubgroupGtMask(EmitContext& ctx) { } Id EmitSubgroupGeMask(EmitContext& ctx) { + if (NeedsGeometrySubgroupFallback(ctx)) { + return ctx.Const(1U); + } if (!StageSupportsSubgroups(ctx)) { return ctx.Const(1u); } @@ -198,6 +247,10 @@ Id EmitSubgroupGeMask(EmitContext& ctx) { Id EmitShuffleIndex(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp, Id segmentation_mask) { + if (NeedsGeometrySubgroupFallback(ctx)) { + SetInBoundsFlag(inst, ctx.false_value); + return value; + } const Id not_seg_mask{ctx.OpNot(ctx.U32[1], segmentation_mask)}; const Id thread_id{EmitLaneId(ctx)}; const Id min_thread_id{ComputeMinThreadId(ctx, thread_id, segmentation_mask)}; @@ -217,6 +270,10 @@ Id EmitShuffleIndex(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id cla Id EmitShuffleUp(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp, Id segmentation_mask) { + if (NeedsGeometrySubgroupFallback(ctx)) { + SetInBoundsFlag(inst, ctx.false_value); + return value; + } const Id thread_id{EmitLaneId(ctx)}; const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)}; Id src_thread_id{ctx.OpISub(ctx.U32[1], thread_id, index)}; @@ -232,6 +289,10 @@ Id EmitShuffleUp(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp, Id EmitShuffleDown(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp, Id segmentation_mask) { + if (NeedsGeometrySubgroupFallback(ctx)) { + SetInBoundsFlag(inst, ctx.false_value); + return value; + } const Id thread_id{EmitLaneId(ctx)}; const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)}; Id src_thread_id{ctx.OpIAdd(ctx.U32[1], thread_id, index)}; @@ -247,6 +308,10 @@ Id EmitShuffleDown(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clam Id EmitShuffleButterfly(EmitContext& ctx, IR::Inst* inst, Id value, Id index, Id clamp, Id segmentation_mask) { + if (NeedsGeometrySubgroupFallback(ctx)) { + SetInBoundsFlag(inst, ctx.false_value); + return value; + } const Id thread_id{EmitLaneId(ctx)}; const Id max_thread_id{GetMaxThreadId(ctx, thread_id, clamp, segmentation_mask)}; Id src_thread_id{ctx.OpBitwiseXor(ctx.U32[1], thread_id, index)}; diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index b562e82983..12aca8e449 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -1455,13 +1455,14 @@ void EmitContext::DefineInputs(const IR::Program& program) { if (info.uses_is_helper_invocation) { is_helper_invocation = DefineInput(*this, U1, false, spv::BuiltIn::HelperInvocation); } - if (info.uses_subgroup_mask && profile.SupportsSubgroupStage(stage)) { + if (info.uses_subgroup_mask && + (stage != Stage::Geometry || profile.support_subgroup_in_geometry_stage)) { subgroup_mask_eq = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupEqMaskKHR); subgroup_mask_lt = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupLtMaskKHR); subgroup_mask_le = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupLeMaskKHR); subgroup_mask_gt = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupGtMaskKHR); subgroup_mask_ge = DefineInput(*this, U32[4], false, spv::BuiltIn::SubgroupGeMaskKHR); - if (stage == Stage::Fragment) { + if (profile.support_explicit_workgroup_layout) { Decorate(subgroup_mask_eq, spv::Decoration::Flat); Decorate(subgroup_mask_lt, spv::Decoration::Flat); Decorate(subgroup_mask_le, spv::Decoration::Flat); @@ -1469,10 +1470,11 @@ void EmitContext::DefineInputs(const IR::Program& program) { Decorate(subgroup_mask_ge, spv::Decoration::Flat); } } - if ((info.uses_fswzadd || info.uses_subgroup_invocation_id || info.uses_subgroup_shuffles || - (profile.warp_size_potentially_larger_than_guest && - (info.uses_subgroup_vote || info.uses_subgroup_mask))) && - profile.SupportsSubgroupStage(stage)) { + if (info.uses_fswzadd || + ((info.uses_subgroup_invocation_id || info.uses_subgroup_shuffles || + (profile.warp_size_potentially_larger_than_guest && + (info.uses_subgroup_vote || info.uses_subgroup_mask))) && + (stage != Stage::Geometry || profile.support_subgroup_in_geometry_stage))) { AddCapability(spv::Capability::GroupNonUniform); subgroup_local_invocation_id = DefineInput(*this, U32[1], false, spv::BuiltIn::SubgroupLocalInvocationId); diff --git a/src/shader_recompiler/profile.h b/src/shader_recompiler/profile.h index d2ac480ab8..0a40d70592 100644 --- a/src/shader_recompiler/profile.h +++ b/src/shader_recompiler/profile.h @@ -41,6 +41,12 @@ struct Profile { bool support_quad_shuffles{}; bool support_vote{}; u32 supported_subgroup_stages{0x7F}; + bool support_subgroup_in_geometry_stage{}; ///< True when the device advertises subgroup + ///< ballot/shuffle support for VK_SHADER_STAGE_GEOMETRY_BIT + ///< (VkPhysicalDeviceSubgroupProperties::supportedStages). + ///< Many mobile GPUs support subgroup ops only in + ///< fragment/compute; guest shaders using VOTE/SHFL in a + ///< geometry program need a non-subgroup fallback there. bool support_viewport_index_layer_non_geometry{}; bool support_viewport_mask{}; bool support_typeless_image_loads{}; diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 642b9fa223..898085c444 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -410,6 +410,11 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, .support_quad_shuffles = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_QUAD_BIT), .support_vote = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_VOTE_BIT), .supported_subgroup_stages = supported_subgroup_stages, + .support_subgroup_in_geometry_stage = + device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_VOTE_BIT) && + device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_BALLOT_BIT) && + device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_SHUFFLE_BIT) && + device.IsSubgroupFeatureSupportedInStage(VK_SHADER_STAGE_GEOMETRY_BIT), .support_viewport_index_layer_non_geometry = device.IsExtShaderViewportIndexLayerSupported(), .support_viewport_mask = device.IsNvViewportArray2Supported(), diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 319a7b2ec3..179d255219 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -479,6 +479,11 @@ FN_MAX_LIMIT_LIST return properties.subgroup_properties.supportedStages; } + /// Returns true if the device supports subgroup ballot/shuffle in the given shader stage. + bool IsSubgroupFeatureSupportedInStage(VkShaderStageFlagBits stage) const { + return properties.subgroup_properties.supportedStages & stage; + } + /// Returns the maximum number of push descriptors. u32 MaxPushDescriptors() const { return properties.push_descriptor.maxPushDescriptors;