spirv-val: Require motion blur and position fetch capabilities for SER (#7007)

Enforces the requirements clarified in KhronosGroup/SPIRV-Registry#463
(KhronosGroup/SPIRV-Registry#462):

- The SER motion instructions and OpHitObjectGetCurrentTime{NV,EXT}
require
  the RayTracingMotionBlurNV capability.
- OpHitObjectGetIntersectionTriangleVertexPositionsEXT requires the
  RayTracingPositionFetchKHR capability.

These were never enforced. The grammar listed some of them as additional
enabling capabilities, but any one enabling capability is sufficient,
and
the hit object type already requires the shader invocation reorder
capability.

The check is done up front in the NV and EXT reorder passes, in the same
style as the existing OpHitObjectRecordFromQueryEXT / RayQueryKHR check.
OpHitObjectRecordHitMotionNV, OpHitObjectRecordHitWithIndexMotionNV and
OpHitObjectRecordMissMotionNV weren't otherwise handled in the NV pass.

Tests: the existing tests that use these instructions now declare the
required capability and also check the new error when it's missing. A
test
for OpHitObjectGetCurrentTimeNV is added.

This doesn't depend on the SPIRV-Headers change
(KhronosGroup/SPIRV-Headers#643). test_val_rstuvw passes against the
currently pinned headers, and all validator test suites pass against the
updated grammar.

▎ GenAI disclosure: Draft assisted by Claude (claude-opus-5-5,
Anthropic, commercial SaaS — Claude Code CLI).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/source/val/validate_ray_tracing_reorder.cpp b/source/val/validate_ray_tracing_reorder.cpp
index cf83dac..b8bf241 100644
--- a/source/val/validate_ray_tracing_reorder.cpp
+++ b/source/val/validate_ray_tracing_reorder.cpp
@@ -317,6 +317,23 @@
     return;
   };
 
+  // Instructions that also require a capability other than the one that
+  // enables them.
+  switch (opcode) {
+    case spv::Op::OpHitObjectTraceRayMotionNV:
+    case spv::Op::OpHitObjectRecordHitMotionNV:
+    case spv::Op::OpHitObjectRecordHitWithIndexMotionNV:
+    case spv::Op::OpHitObjectRecordMissMotionNV:
+    case spv::Op::OpHitObjectGetCurrentTimeNV:
+      if (!_.HasCapability(spv::Capability::RayTracingMotionBlurNV))
+        return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
+               << spvOpcodeString(opcode)
+               << ": requires RayTracingMotionBlurNV capability";
+      break;
+    default:
+      break;
+  }
+
   switch (opcode) {
     case spv::Op::OpHitObjectIsMissNV:
     case spv::Op::OpHitObjectIsHitNV:
@@ -792,6 +809,28 @@
     return;
   };
 
+  // Instructions that also require a capability other than the one that
+  // enables them.
+  switch (opcode) {
+    case spv::Op::OpHitObjectTraceRayMotionEXT:
+    case spv::Op::OpHitObjectRecordMissMotionEXT:
+    case spv::Op::OpHitObjectTraceMotionReorderExecuteEXT:
+    case spv::Op::OpHitObjectGetCurrentTimeEXT:
+      if (!_.HasCapability(spv::Capability::RayTracingMotionBlurNV))
+        return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
+               << spvOpcodeString(opcode)
+               << ": requires RayTracingMotionBlurNV capability";
+      break;
+    case spv::Op::OpHitObjectGetIntersectionTriangleVertexPositionsEXT:
+      if (!_.HasCapability(spv::Capability::RayTracingPositionFetchKHR))
+        return _.diag(SPV_ERROR_INVALID_CAPABILITY, inst)
+               << spvOpcodeString(opcode)
+               << ": requires RayTracingPositionFetchKHR capability";
+      break;
+    default:
+      break;
+  }
+
   switch (opcode) {
     case spv::Op::OpHitObjectIsMissEXT:
     case spv::Op::OpHitObjectIsHitEXT:
diff --git a/test/val/val_ray_tracing_reorder_test.cpp b/test/val/val_ray_tracing_reorder_test.cpp
index 7071096..b0f1dec 100644
--- a/test/val/val_ray_tracing_reorder_test.cpp
+++ b/test/val/val_ray_tracing_reorder_test.cpp
@@ -30,6 +30,15 @@
 
 using ValidateRayTracingReorderNV = spvtest::ValidateBase<bool>;
 
+const std::string kMotionBlurCapability =
+    "OpCapability RayTracingMotionBlurNV\n";
+const std::string kMotionBlurExtension =
+    "OpExtension \"SPV_NV_ray_tracing_motion_blur\"\n";
+const std::string kPositionFetchCapability =
+    "OpCapability RayTracingPositionFetchKHR\n";
+const std::string kPositionFetchExtension =
+    "OpExtension \"SPV_KHR_ray_tracing_position_fetch\"\n";
+
 std::string GenerateReorderThreadCode(const std::string& body = "",
                                       const std::string& declarations = "",
                                       const std::string& extensions = "",
@@ -108,13 +117,19 @@
 
 std::string GenerateReorderShaderCode(
     const std::string& body = "", const std::string& declarations = "",
-    const std::string execution_model = "RayGenerationKHR") {
+    const std::string execution_model = "RayGenerationKHR",
+    const std::string& extensions = "", const std::string& capabilities = "") {
   std::ostringstream ss;
   ss << R"(
                OpCapability RayTracingKHR
                OpCapability ShaderInvocationReorderNV
-               OpExtension "SPV_KHR_ray_tracing"
+         )";
+  ss << capabilities;
+  ss << R"(    OpExtension "SPV_KHR_ray_tracing"
                OpExtension "SPV_NV_shader_invocation_reorder"
+         )";
+  ss << extensions;
+  ss << R"(
           %1 = OpExtInstImport "GLSL.std.450"
                OpMemoryModel Logical GLSL450
                OpEntryPoint )"
@@ -233,9 +248,47 @@
       OpHitObjectTraceRayMotionNV %hObj %26 %uint_1 %uint_1 %uint_1 %uint_1 %uint_1 %31 %float_0_5 %32 %float_1 %float_10 %__0
   )";
 
+  CompileSuccessfully(
+      GenerateReorderShaderCode(body, declarations, "RayGenerationKHR",
+                                kMotionBlurExtension, kMotionBlurCapability)
+          .c_str(),
+      SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+
   CompileSuccessfully(GenerateReorderShaderCode(body, declarations).c_str(),
                       SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+            ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("HitObjectTraceRayMotionNV: requires "
+                        "RayTracingMotionBlurNV capability"));
+}
+
+TEST_F(ValidateRayTracingReorderNV, HitObjectGetCurrentTimeNV) {
+  const std::string declarations = R"(
+        %_ptr_Function_float = OpTypePointer Function %float
+  )";
+
+  const std::string body = R"(
+      %time = OpVariable %_ptr_Function_float Function
+      %12 = OpHitObjectGetCurrentTimeNV %float %hObj
+      OpStore %time %12
+  )";
+
+  CompileSuccessfully(
+      GenerateReorderShaderCode(body, declarations, "RayGenerationKHR",
+                                kMotionBlurExtension, kMotionBlurCapability)
+          .c_str(),
+      SPV_ENV_VULKAN_1_2);
   EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+
+  CompileSuccessfully(GenerateReorderShaderCode(body, declarations).c_str(),
+                      SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+            ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("HitObjectGetCurrentTimeNV: requires "
+                        "RayTracingMotionBlurNV capability"));
 }
 
 TEST_F(ValidateRayTracingReorderNV, HitObjectRecordHitNV) {
@@ -1003,9 +1056,20 @@
       OpHitObjectTraceRayMotionEXT %hObj %26 %uint_1 %uint_1 %uint_1 %uint_1 %uint_1 %31 %float_0_5 %32 %float_1 %float_10 %__0
   )";
 
+  CompileSuccessfully(
+      GenerateReorderShaderCodeEXT(body, declarations, kMotionBlurExtension,
+                                   kMotionBlurCapability)
+          .c_str(),
+      SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+
   CompileSuccessfully(GenerateReorderShaderCodeEXT(body, declarations).c_str(),
                       SPV_ENV_VULKAN_1_2);
-  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+            ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("HitObjectTraceRayMotionEXT: requires "
+                        "RayTracingMotionBlurNV capability"));
 }
 
 TEST_F(ValidateRayTracingReorderEXT, HitObjectRecordEmptyEXT) {
@@ -1197,9 +1261,20 @@
       OpStore %time %12
   )";
 
+  CompileSuccessfully(
+      GenerateReorderShaderCodeEXT(body, declarations, kMotionBlurExtension,
+                                   kMotionBlurCapability)
+          .c_str(),
+      SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+
   CompileSuccessfully(GenerateReorderShaderCodeEXT(body, declarations).c_str(),
                       SPV_ENV_VULKAN_1_2);
-  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+            ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("HitObjectGetCurrentTimeEXT: requires "
+                        "RayTracingMotionBlurNV capability"));
 }
 
 TEST_F(ValidateRayTracingReorderEXT, HitObjectGetObjectRayOriginEXT) {
@@ -1442,9 +1517,21 @@
       OpStore %vertices %result
   )";
 
+  CompileSuccessfully(
+      GenerateReorderShaderCodeEXT(body, declarations, kPositionFetchExtension,
+                                   kPositionFetchCapability)
+          .c_str(),
+      SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+
   CompileSuccessfully(GenerateReorderShaderCodeEXT(body, declarations).c_str(),
                       SPV_ENV_VULKAN_1_2);
-  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+            ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("HitObjectGetIntersectionTriangleVertexPositionsEXT: requires "
+                "RayTracingPositionFetchKHR capability"));
 }
 
 TEST_F(ValidateRayTracingReorderEXT,
@@ -1544,9 +1631,20 @@
       OpHitObjectRecordMissMotionEXT %hObj %uint_1 %uint_1 %29 %float_2 %31 %float_5 %float_10
   )";
 
+  CompileSuccessfully(
+      GenerateReorderShaderCodeEXT(body, declarations, kMotionBlurExtension,
+                                   kMotionBlurCapability)
+          .c_str(),
+      SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+
   CompileSuccessfully(GenerateReorderShaderCodeEXT(body, declarations).c_str(),
                       SPV_ENV_VULKAN_1_2);
-  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+            ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("HitObjectRecordMissMotionEXT: requires "
+                        "RayTracingMotionBlurNV capability"));
 }
 
 // Fused Hit Object Instructions Tests
@@ -1614,9 +1712,20 @@
       OpHitObjectTraceMotionReorderExecuteEXT %hObj %26 %uint_1 %uint_1 %uint_1 %uint_1 %uint_1 %31 %float_0_5 %32 %float_1 %float_10 %__0 %uint_4 %uint_2
   )";
 
+  CompileSuccessfully(
+      GenerateReorderShaderCodeEXT(body, declarations, kMotionBlurExtension,
+                                   kMotionBlurCapability)
+          .c_str(),
+      SPV_ENV_VULKAN_1_2);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+
   CompileSuccessfully(GenerateReorderShaderCodeEXT(body, declarations).c_str(),
                       SPV_ENV_VULKAN_1_2);
-  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+            ValidateInstructions(SPV_ENV_VULKAN_1_2));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("HitObjectTraceMotionReorderExecuteEXT: requires "
+                        "RayTracingMotionBlurNV capability"));
 }
 
 TEST_F(ValidateRayTracingReorderNV, HitObjectTraceRayNVMinMoreThanMax) {