diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanCapabilityRecord.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanCapabilityRecord.cs
index 5e9d5d42..e78874a0 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanCapabilityRecord.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanCapabilityRecord.cs
@@ -84,6 +84,17 @@ internal sealed record VulkanDeviceFeatureSupport
/// Relaxed shader interface rules for the dual-legal GLSL sources.
public required bool Maintenance4 { get; init; }
+ ///
+ /// #459: the shader compiler targets Vulkan 1.3 / SPIR-V 1.6, where glslang
+ /// lowers every fragment discard to OpDemoteToHelperInvocation
+ /// (five committed modules declare the capability — sky, mesh_detail,
+ /// mesh_atmospheric and both directional-shadow cutout fragments). A module
+ /// that declares the capability without the feature enabled is invalid
+ /// usage (VUID-VkShaderModuleCreateInfo-pCode-08740), so the feature
+ /// is required and enabled, never optional.
+ ///
+ public required bool ShaderDemoteToHelperInvocation { get; init; }
+
///
/// Every feature present. The starting point for the forced-unsupported gate
/// knob and for tests that assert one specific absence at a time.
@@ -108,6 +119,7 @@ internal sealed record VulkanDeviceFeatureSupport
DynamicRendering = true,
Synchronization2 = true,
Maintenance4 = true,
+ ShaderDemoteToHelperInvocation = true,
};
///
@@ -139,6 +151,7 @@ internal sealed record VulkanDeviceFeatureSupport
var n when Is(n, nameof(DynamicRendering)) => this with { DynamicRendering = false },
var n when Is(n, nameof(Synchronization2)) => this with { Synchronization2 = false },
var n when Is(n, nameof(Maintenance4)) => this with { Maintenance4 = false },
+ var n when Is(n, nameof(ShaderDemoteToHelperInvocation)) => this with { ShaderDemoteToHelperInvocation = false },
_ => null,
};
@@ -521,6 +534,8 @@ internal static class VulkanCapabilityRequirements
failures.Add("synchronization2 is required; every barrier in the frame is a barrier2.");
if (!features.Maintenance4)
failures.Add("maintenance4 is required for the relaxed shader interface rules the shared GLSL relies on.");
+ if (!features.ShaderDemoteToHelperInvocation)
+ failures.Add("shaderDemoteToHelperInvocation is required; the SPIR-V 1.6 fragment modules lower discard to OpDemoteToHelperInvocation (#459).");
VulkanDeviceLimitSupport limits = capabilities.Limits;
if (limits.MaxPushConstantsSize < GpuBindingModel.PushConstantBytes)
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanInterop.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanInterop.cs
index 3dae155b..5788a292 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanInterop.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanInterop.cs
@@ -330,6 +330,7 @@ internal static unsafe class VulkanPhysicalDeviceInspector
DynamicRendering = vulkan13.DynamicRendering,
Synchronization2 = vulkan13.Synchronization2,
Maintenance4 = vulkan13.Maintenance4,
+ ShaderDemoteToHelperInvocation = vulkan13.ShaderDemoteToHelperInvocation,
};
}
@@ -609,6 +610,12 @@ internal sealed unsafe class VulkanLogicalDeviceFactory
DynamicRendering = true,
Synchronization2 = true,
Maintenance4 = true,
+ // #459: the SPIR-V 1.6 fragment modules declare
+ // DemoteToHelperInvocation (glslang's lowering of `discard` for a
+ // Vulkan 1.3 target); declaring it without the feature is invalid
+ // usage (VUID-VkShaderModuleCreateInfo-pCode-08740). Gated as
+ // required in VulkanCapabilityRecord, so this is never speculative.
+ ShaderDemoteToHelperInvocation = true,
};
var vulkan12 = new PhysicalDeviceVulkan12Features
{