Squashed 'third_party/SPIRV-Tools/' changes from eed5c76a5..7014be600

7014be600 Add support for SPV_KHR_subgroup_rotate (#4786)
2c7fb9707 Handle dontinline function in spread-volatile-semantics (#4776)
58dc37ea6 Add SpvBuiltInCullMaskKHR to a switch (#4790)
1295dca8e Reapply "Add folding rule to generate Fma instructions (#4783)" (#4789)
edaf51038 linker: Recalculate interface variables (#4784)
46492aa45 spirv-opt: skips if_conversion when dontflatten is set (#4770)
671f6e633 Revert "Add folding rule to generate Fma instructions (#4783)" (#4785)
2b2b0282a Add folding rule to generate Fma instructions (#4783)
cb96abbf7 Fix CMake for librt (#4773)
898ba64d2 Use cmake 3.23 on Windows. (#4782)
92c17edde Don't try to unroll loop with step count 0. (#4769)
c1bb0b941 Start SPIRV-Tools v2022.3-dev
7826e1941 Finalize SPIRV-Tools v2022.2
67fdf9409 Update CHANGES
78a0f075a Fix gen_build_version on Windows (#4780)

git-subtree-dir: third_party/SPIRV-Tools
git-subtree-split: 7014be600c7a588ef1886c1a1356f3fc0fea2ebb
Change-Id: I301663a32dfb1397cb1b1e32bb6e1e4d0ed9f93a
diff --git a/Android.mk b/Android.mk
index e7616f9..b9fbcc8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -302,7 +302,7 @@
         $(LOCAL_PATH)/utils/update_build_version.py \
         $(LOCAL_PATH)/CHANGES
 		@$(HOST_PYTHON) $(LOCAL_PATH)/utils/update_build_version.py \
-		                $(LOCAL_PATH) $(1)/build-version.inc
+		                $(LOCAL_PATH)/CHANGES $(1)/build-version.inc
 		@echo "[$(TARGET_ARCH_ABI)] Generate       : build-version.inc <= CHANGES"
 $(LOCAL_PATH)/source/software_version.cpp: $(1)/build-version.inc
 endef
diff --git a/BUILD.bazel b/BUILD.bazel
index c86ebbe..914619a 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -94,8 +94,8 @@
     name = "gen_build_version",
     srcs = ["CHANGES"],
     outs = ["build-version.inc"],
-    cmd = "SOURCE_DATE_EPOCH=0 $(location update_build_version) $$(dirname $(location CHANGES)) $(location build-version.inc)",
-    cmd_bat = "set SOURCE_DATE_EPOCH=0  && $(location //:update_build_version) \"$(location CHANGES)\\..\" $(location build-version.inc)",
+    cmd = "SOURCE_DATE_EPOCH=0 $(location update_build_version) $(location CHANGES) $(location build-version.inc)",
+    cmd_bat = "set SOURCE_DATE_EPOCH=0  && $(location //:update_build_version) $(location CHANGES) $(location build-version.inc)",
     tools = [":update_build_version"],
 )
 
diff --git a/BUILD.gn b/BUILD.gn
index 0ce6c35..ba05497 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -256,12 +256,12 @@
 action("spvtools_build_version") {
   script = "utils/update_build_version.py"
 
-  src_dir = "."
+  changes_file = "CHANGES"
   inc_file = "${target_gen_dir}/build-version.inc"
 
   outputs = [ inc_file ]
   args = [
-    rebase_path(src_dir, root_build_dir),
+    rebase_path(changes_file, root_build_dir),
     rebase_path(inc_file, root_build_dir),
   ]
 }
diff --git a/CHANGES b/CHANGES
index 27f1f88..7984461 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,9 @@
 Revision history for SPIRV-Tools
 
-v2022.2-dev 2022-04-04
+v2022.3-dev 2022-04-07
+  - Start v2022.3-dev
+
+v2022.2 2022-04-07
   - General
     - Add OpModuleProcessed to debug opcode (#4694)
   - Optimizer
diff --git a/DEPS b/DEPS
index 051b2de..abc3a76 100644
--- a/DEPS
+++ b/DEPS
@@ -6,7 +6,7 @@
   'effcee_revision': 'ddf5e2bb92957dc8a12c5392f8495333d6844133',
   'googletest_revision': '25dcdc7e8bfac8967f20fb2c0a628f5cf442188d',
   're2_revision': '0c5616df9c0aaa44c9440d87422012423d91c7d1',
-  'spirv_headers_revision': '4995a2f2723c401eb0ea3e10c81298906bf1422b',
+  'spirv_headers_revision': 'b765c355f488837ca4c77980ba69484f3ff277f5',
 }
 
 deps = {
diff --git a/kokoro/scripts/windows/build.bat b/kokoro/scripts/windows/build.bat
index 24e29cc..8c9d689 100644
--- a/kokoro/scripts/windows/build.bat
+++ b/kokoro/scripts/windows/build.bat
@@ -22,7 +22,7 @@
 set VS_VERSION=%2
 
 :: Force usage of python 3.6
-set PATH=C:\python36;"C:\Program Files\CMake\bin";%PATH%
+set PATH=C:\python36;"C:\Program Files\cmake-3.23.1-windows-x86_64\bin";%PATH%
 
 cd %SRC%
 git clone --depth=1 https://github.com/KhronosGroup/SPIRV-Headers external/spirv-headers
diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt
index f0dcadd..98559b8 100644
--- a/source/CMakeLists.txt
+++ b/source/CMakeLists.txt
@@ -197,7 +197,7 @@
 add_custom_command(OUTPUT ${SPIRV_TOOLS_BUILD_VERSION_INC}
    COMMAND ${PYTHON_EXECUTABLE}
            ${SPIRV_TOOLS_BUILD_VERSION_INC_GENERATOR}
-           ${spirv-tools_SOURCE_DIR} ${SPIRV_TOOLS_BUILD_VERSION_INC}
+           ${SPIRV_TOOLS_CHANGES_FILE} ${SPIRV_TOOLS_BUILD_VERSION_INC}
    DEPENDS ${SPIRV_TOOLS_BUILD_VERSION_INC_GENERATOR}
            ${SPIRV_TOOLS_CHANGES_FILE}
    COMMENT "Update build-version.inc in the SPIRV-Tools build directory (if necessary).")
@@ -407,7 +407,7 @@
   find_library(LIBRT rt)
   if(LIBRT)
     foreach(target ${SPIRV_TOOLS_TARGETS})
-      target_link_libraries(${target} ${LIBRT})
+      target_link_libraries(${target} rt)
     endforeach()
   endif()
 endif()
diff --git a/source/link/linker.cpp b/source/link/linker.cpp
index 76ce775..3b388cc 100644
--- a/source/link/linker.cpp
+++ b/source/link/linker.cpp
@@ -34,6 +34,7 @@
 #include "source/opt/ir_loader.h"
 #include "source/opt/pass_manager.h"
 #include "source/opt/remove_duplicates_pass.h"
+#include "source/opt/remove_unused_interface_variables_pass.h"
 #include "source/opt/type_manager.h"
 #include "source/spirv_constant.h"
 #include "source/spirv_target_env.h"
@@ -807,11 +808,16 @@
   pass_res = manager.Run(&linked_context);
   if (pass_res == opt::Pass::Status::Failure) return SPV_ERROR_INVALID_DATA;
 
-  // Phase 11: Warn if SPIR-V limits were exceeded
+  // Phase 11: Recompute EntryPoint variables
+  manager.AddPass<opt::RemoveUnusedInterfaceVariablesPass>();
+  pass_res = manager.Run(&linked_context);
+  if (pass_res == opt::Pass::Status::Failure) return SPV_ERROR_INVALID_DATA;
+
+  // Phase 12: Warn if SPIR-V limits were exceeded
   res = VerifyLimits(consumer, linked_context);
   if (res != SPV_SUCCESS) return res;
 
-  // Phase 12: Output the module
+  // Phase 13: Output the module
   linked_context.module()->ToBinary(linked_binary, true);
 
   return SPV_SUCCESS;
diff --git a/source/opcode.cpp b/source/opcode.cpp
index c9c425d..2584d51 100644
--- a/source/opcode.cpp
+++ b/source/opcode.cpp
@@ -528,6 +528,7 @@
     case SpvOpGroupNonUniformLogicalXor:
     case SpvOpGroupNonUniformQuadBroadcast:
     case SpvOpGroupNonUniformQuadSwap:
+    case SpvOpGroupNonUniformRotateKHR:
       return true;
     default:
       return false;
diff --git a/source/opt/folding_rules.cpp b/source/opt/folding_rules.cpp
index c879a0c..d15ad04 100644
--- a/source/opt/folding_rules.cpp
+++ b/source/opt/folding_rules.cpp
@@ -1430,6 +1430,64 @@
   };
 }
 
+// Replaces |inst| inplace with an FMA instruction |(x*y)+a|.
+void ReplaceWithFma(Instruction* inst, uint32_t x, uint32_t y, uint32_t a) {
+  uint32_t ext =
+      inst->context()->get_feature_mgr()->GetExtInstImportId_GLSLstd450();
+
+  if (ext == 0) {
+    inst->context()->AddExtInstImport("GLSL.std.450");
+    ext = inst->context()->get_feature_mgr()->GetExtInstImportId_GLSLstd450();
+    assert(ext != 0 &&
+           "Could not add the GLSL.std.450 extended instruction set");
+  }
+
+  std::vector<Operand> operands;
+  operands.push_back({SPV_OPERAND_TYPE_ID, {ext}});
+  operands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER, {GLSLstd450Fma}});
+  operands.push_back({SPV_OPERAND_TYPE_ID, {x}});
+  operands.push_back({SPV_OPERAND_TYPE_ID, {y}});
+  operands.push_back({SPV_OPERAND_TYPE_ID, {a}});
+
+  inst->SetOpcode(SpvOpExtInst);
+  inst->SetInOperands(std::move(operands));
+}
+
+// Folds a multiple and add into an Fma.
+//
+// Cases:
+// (x * y) + a = Fma x y a
+// a + (x * y) = Fma x y a
+bool MergeMulAddArithmetic(IRContext* context, Instruction* inst,
+                           const std::vector<const analysis::Constant*>&) {
+  assert(inst->opcode() == SpvOpFAdd);
+
+  if (!inst->IsFloatingPointFoldingAllowed()) {
+    return false;
+  }
+
+  analysis::DefUseManager* def_use_mgr = context->get_def_use_mgr();
+  for (int i = 0; i < 2; i++) {
+    uint32_t op_id = inst->GetSingleWordInOperand(i);
+    Instruction* op_inst = def_use_mgr->GetDef(op_id);
+
+    if (op_inst->opcode() != SpvOpFMul) {
+      continue;
+    }
+
+    if (!op_inst->IsFloatingPointFoldingAllowed()) {
+      continue;
+    }
+
+    uint32_t x = op_inst->GetSingleWordInOperand(0);
+    uint32_t y = op_inst->GetSingleWordInOperand(1);
+    uint32_t a = inst->GetSingleWordInOperand((i + 1) % 2);
+    ReplaceWithFma(inst, x, y, a);
+    return true;
+  }
+  return false;
+}
+
 FoldingRule IntMultipleBy1() {
   return [](IRContext*, Instruction* inst,
             const std::vector<const analysis::Constant*>& constants) {
@@ -2543,6 +2601,7 @@
   rules_[SpvOpFAdd].push_back(MergeAddSubArithmetic());
   rules_[SpvOpFAdd].push_back(MergeGenericAddSubArithmetic());
   rules_[SpvOpFAdd].push_back(FactorAddMuls());
+  rules_[SpvOpFAdd].push_back(MergeMulAddArithmetic);
 
   rules_[SpvOpFDiv].push_back(RedundantFDiv());
   rules_[SpvOpFDiv].push_back(ReciprocalFDiv());
diff --git a/source/opt/if_conversion.cpp b/source/opt/if_conversion.cpp
index 4920661..d1debd0 100644
--- a/source/opt/if_conversion.cpp
+++ b/source/opt/if_conversion.cpp
@@ -169,6 +169,8 @@
   if (branch->opcode() != SpvOpBranchConditional) return false;
   auto merge = (*common)->GetMergeInst();
   if (!merge || merge->opcode() != SpvOpSelectionMerge) return false;
+  if (merge->GetSingleWordInOperand(1) == SpvSelectionControlDontFlattenMask)
+    return false;
   if ((*common)->MergeBlockIdIfAny() != block->id()) return false;
 
   return true;
diff --git a/source/opt/ir_context.cpp b/source/opt/ir_context.cpp
index a80d4f2..c9c3f1b 100644
--- a/source/opt/ir_context.cpp
+++ b/source/opt/ir_context.cpp
@@ -926,6 +926,19 @@
   return modified;
 }
 
+void IRContext::CollectCallTreeFromRoots(unsigned entryId,
+                                         std::unordered_set<uint32_t>* funcs) {
+  std::queue<uint32_t> roots;
+  roots.push(entryId);
+  while (!roots.empty()) {
+    const uint32_t fi = roots.front();
+    roots.pop();
+    funcs->insert(fi);
+    Function* fn = GetFunction(fi);
+    AddCalls(fn, &roots);
+  }
+}
+
 void IRContext::EmitErrorMessage(std::string message, Instruction* inst) {
   if (!consumer()) {
     return;
diff --git a/source/opt/ir_context.h b/source/opt/ir_context.h
index 946f9e9..f9f5153 100644
--- a/source/opt/ir_context.h
+++ b/source/opt/ir_context.h
@@ -411,6 +411,10 @@
   void CollectNonSemanticTree(Instruction* inst,
                               std::unordered_set<Instruction*>* to_kill);
 
+  // Collect function reachable from |entryId|, returns |funcs|
+  void CollectCallTreeFromRoots(unsigned entryId,
+                                std::unordered_set<uint32_t>* funcs);
+
   // Returns true if all of the given analyses are valid.
   bool AreAnalysesValid(Analysis set) { return (set & valid_analyses_) == set; }
 
diff --git a/source/opt/loop_descriptor.cpp b/source/opt/loop_descriptor.cpp
index 9bc495e..4feb64e 100644
--- a/source/opt/loop_descriptor.cpp
+++ b/source/opt/loop_descriptor.cpp
@@ -754,6 +754,10 @@
 // |step_value| is NOT cleanly divisible then we add one to the sum.
 int64_t Loop::GetIterations(SpvOp condition, int64_t condition_value,
                             int64_t init_value, int64_t step_value) const {
+  if (step_value == 0) {
+    return 0;
+  }
+
   int64_t diff = 0;
 
   switch (condition) {
diff --git a/source/opt/loop_descriptor.h b/source/opt/loop_descriptor.h
index e88ff93..df01227 100644
--- a/source/opt/loop_descriptor.h
+++ b/source/opt/loop_descriptor.h
@@ -398,7 +398,8 @@
   // Each different loop |condition| affects how we calculate the number of
   // iterations using the |condition_value|, |init_value|, and |step_values| of
   // the induction variable. This method will return the number of iterations in
-  // a loop with those values for a given |condition|.
+  // a loop with those values for a given |condition|.  Returns 0 if the number
+  // of iterations could not be computed.
   int64_t GetIterations(SpvOp condition, int64_t condition_value,
                         int64_t init_value, int64_t step_value) const;
 
diff --git a/source/opt/spread_volatile_semantics.cpp b/source/opt/spread_volatile_semantics.cpp
index a1d3432..b61fd0f 100644
--- a/source/opt/spread_volatile_semantics.cpp
+++ b/source/opt/spread_volatile_semantics.cpp
@@ -68,38 +68,12 @@
   return decoration_manager->HasDecoration(var_id, SpvDecorationVolatile);
 }
 
-bool HasOnlyEntryPointsAsFunctions(IRContext* context, Module* module) {
-  std::unordered_set<uint32_t> entry_function_ids;
-  for (Instruction& entry_point : module->entry_points()) {
-    entry_function_ids.insert(
-        entry_point.GetSingleWordInOperand(kOpEntryPointInOperandEntryPoint));
-  }
-  for (auto& function : *module) {
-    if (entry_function_ids.find(function.result_id()) ==
-        entry_function_ids.end()) {
-      std::string message(
-          "Functions of SPIR-V for spread-volatile-semantics pass input must "
-          "be inlined except entry points");
-      message += "\n  " + function.DefInst().PrettyPrint(
-                              SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES);
-      context->consumer()(SPV_MSG_ERROR, "", {0, 0, 0}, message.c_str());
-      return false;
-    }
-  }
-  return true;
-}
-
 }  // namespace
 
 Pass::Status SpreadVolatileSemantics::Process() {
   if (HasNoExecutionModel()) {
     return Status::SuccessWithoutChange;
   }
-
-  if (!HasOnlyEntryPointsAsFunctions(context(), get_module())) {
-    return Status::Failure;
-  }
-
   const bool is_vk_memory_model_enabled =
       context()->get_feature_mgr()->HasCapability(
           SpvCapabilityVulkanMemoryModel);
@@ -142,6 +116,8 @@
     uint32_t var_id, Instruction* entry_point) {
   uint32_t entry_function_id =
       entry_point->GetSingleWordInOperand(kOpEntryPointInOperandEntryPoint);
+  std::unordered_set<uint32_t> funcs;
+  context()->CollectCallTreeFromRoots(entry_function_id, &funcs);
   return !VisitLoadsOfPointersToVariableInEntries(
       var_id,
       [](Instruction* load) {
@@ -154,7 +130,7 @@
             load->GetSingleWordInOperand(kOpLoadInOperandMemoryOperands);
         return (memory_operands & SpvMemoryAccessVolatileMask) != 0;
       },
-      {entry_function_id});
+      funcs);
 }
 
 bool SpreadVolatileSemantics::HasInterfaceInConflictOfVolatileSemantics() {
@@ -225,7 +201,7 @@
 
 bool SpreadVolatileSemantics::VisitLoadsOfPointersToVariableInEntries(
     uint32_t var_id, const std::function<bool(Instruction*)>& handle_load,
-    const std::unordered_set<uint32_t>& entry_function_ids) {
+    const std::unordered_set<uint32_t>& function_ids) {
   std::vector<uint32_t> worklist({var_id});
   auto* def_use_mgr = context()->get_def_use_mgr();
   while (!worklist.empty()) {
@@ -233,11 +209,11 @@
     worklist.pop_back();
     bool finish_traversal = !def_use_mgr->WhileEachUser(
         ptr_id, [this, &worklist, &ptr_id, handle_load,
-                 &entry_function_ids](Instruction* user) {
+                 &function_ids](Instruction* user) {
           BasicBlock* block = context()->get_instr_block(user);
           if (block == nullptr ||
-              entry_function_ids.find(block->GetParent()->result_id()) ==
-                  entry_function_ids.end()) {
+              function_ids.find(block->GetParent()->result_id()) ==
+                  function_ids.end()) {
             return true;
           }
 
@@ -266,21 +242,25 @@
     Instruction* var, const std::unordered_set<uint32_t>& entry_function_ids) {
   // Set Volatile memory operand for all load instructions if they do not have
   // it.
-  VisitLoadsOfPointersToVariableInEntries(
-      var->result_id(),
-      [](Instruction* load) {
-        if (load->NumInOperands() <= kOpLoadInOperandMemoryOperands) {
-          load->AddOperand(
-              {SPV_OPERAND_TYPE_MEMORY_ACCESS, {SpvMemoryAccessVolatileMask}});
+  for (auto entry_id : entry_function_ids) {
+    std::unordered_set<uint32_t> funcs;
+    context()->CollectCallTreeFromRoots(entry_id, &funcs);
+    VisitLoadsOfPointersToVariableInEntries(
+        var->result_id(),
+        [](Instruction* load) {
+          if (load->NumInOperands() <= kOpLoadInOperandMemoryOperands) {
+            load->AddOperand({SPV_OPERAND_TYPE_MEMORY_ACCESS,
+                              {SpvMemoryAccessVolatileMask}});
+            return true;
+          }
+          uint32_t memory_operands =
+              load->GetSingleWordInOperand(kOpLoadInOperandMemoryOperands);
+          memory_operands |= SpvMemoryAccessVolatileMask;
+          load->SetInOperand(kOpLoadInOperandMemoryOperands, {memory_operands});
           return true;
-        }
-        uint32_t memory_operands =
-            load->GetSingleWordInOperand(kOpLoadInOperandMemoryOperands);
-        memory_operands |= SpvMemoryAccessVolatileMask;
-        load->SetInOperand(kOpLoadInOperandMemoryOperands, {memory_operands});
-        return true;
-      },
-      entry_function_ids);
+        },
+        funcs);
+  }
 }
 
 bool SpreadVolatileSemantics::IsTargetForVolatileSemantics(
diff --git a/source/opt/spread_volatile_semantics.h b/source/opt/spread_volatile_semantics.h
index 531a21d..014858d 100644
--- a/source/opt/spread_volatile_semantics.h
+++ b/source/opt/spread_volatile_semantics.h
@@ -72,15 +72,14 @@
                                                  Instruction* entry_point);
 
   // Visits load instructions of pointers to variable whose result id is
-  // |var_id| if the load instructions are in entry points whose
-  // function id is one of |entry_function_ids|. |handle_load| is a function to
-  // do some actions for the load instructions. Finishes the traversal and
-  // returns false if |handle_load| returns false for a load instruction.
-  // Otherwise, returns true after running |handle_load| for all the load
-  // instructions.
+  // |var_id| if the load instructions are in reachable functions from entry
+  // points. |handle_load| is a function to do some actions for the load
+  // instructions. Finishes the traversal and returns false if |handle_load|
+  // returns false for a load instruction. Otherwise, returns true after running
+  // |handle_load| for all the load instructions.
   bool VisitLoadsOfPointersToVariableInEntries(
       uint32_t var_id, const std::function<bool(Instruction*)>& handle_load,
-      const std::unordered_set<uint32_t>& entry_function_ids);
+      const std::unordered_set<uint32_t>& function_ids);
 
   // Sets Memory Operands of OpLoad instructions that load |var| or pointers
   // of |var| as Volatile if the function id of the OpLoad instruction is
diff --git a/source/val/validate_builtins.cpp b/source/val/validate_builtins.cpp
index 57dde8a..6a2e919 100644
--- a/source/val/validate_builtins.cpp
+++ b/source/val/validate_builtins.cpp
@@ -4185,6 +4185,7 @@
     case SpvBuiltInBaryCoordNV:
     case SpvBuiltInBaryCoordNoPerspNV:
     case SpvBuiltInCurrentRayTimeNV:
+    case SpvBuiltInCullMaskKHR:
       // No validation rules (for the moment).
       break;
 
diff --git a/source/val/validate_non_uniform.cpp b/source/val/validate_non_uniform.cpp
index 2b6eb8b..6d4f8a2 100644
--- a/source/val/validate_non_uniform.cpp
+++ b/source/val/validate_non_uniform.cpp
@@ -63,6 +63,59 @@
   return SPV_SUCCESS;
 }
 
+spv_result_t ValidateGroupNonUniformRotateKHR(ValidationState_t& _,
+                                              const Instruction* inst) {
+  // Scope is already checked by ValidateExecutionScope() above.
+  const uint32_t result_type = inst->type_id();
+  if (!_.IsIntScalarOrVectorType(result_type) &&
+      !_.IsFloatScalarOrVectorType(result_type) &&
+      !_.IsBoolScalarOrVectorType(result_type)) {
+    return _.diag(SPV_ERROR_INVALID_DATA, inst)
+           << "Expected Result Type to be a scalar or vector of "
+              "floating-point, integer or boolean type.";
+  }
+
+  const uint32_t value_type = _.GetTypeId(inst->GetOperandAs<uint32_t>(3));
+  if (value_type != result_type) {
+    return _.diag(SPV_ERROR_INVALID_DATA, inst)
+           << "Result Type must be the same as the type of Value.";
+  }
+
+  const uint32_t delta_type = _.GetTypeId(inst->GetOperandAs<uint32_t>(4));
+  if (!_.IsUnsignedIntScalarType(delta_type)) {
+    return _.diag(SPV_ERROR_INVALID_DATA, inst)
+           << "Delta must be a scalar of integer type, whose Signedness "
+              "operand is 0.";
+  }
+
+  if (inst->words().size() > 6) {
+    const uint32_t cluster_size_op_id = inst->GetOperandAs<uint32_t>(5);
+    const uint32_t cluster_size_type = _.GetTypeId(cluster_size_op_id);
+    if (!_.IsUnsignedIntScalarType(cluster_size_type)) {
+      return _.diag(SPV_ERROR_INVALID_DATA, inst)
+             << "ClusterSize must be a scalar of integer type, whose "
+                "Signedness operand is 0.";
+    }
+
+    uint64_t cluster_size;
+    if (!_.GetConstantValUint64(cluster_size_op_id, &cluster_size)) {
+      return _.diag(SPV_ERROR_INVALID_DATA, inst)
+             << "ClusterSize must come from a constant instruction.";
+    }
+
+    if ((cluster_size == 0) || ((cluster_size & (cluster_size - 1)) != 0)) {
+      return _.diag(SPV_WARNING, inst)
+             << "Behavior is undefined unless ClusterSize is at least 1 and a "
+                "power of 2.";
+    }
+
+    // TODO(kpet) Warn about undefined behavior when ClusterSize is greater than
+    // the declared SubGroupSize
+  }
+
+  return SPV_SUCCESS;
+}
+
 }  // namespace
 
 // Validates correctness of non-uniform group instructions.
@@ -79,6 +132,8 @@
   switch (opcode) {
     case SpvOpGroupNonUniformBallotBitCount:
       return ValidateGroupNonUniformBallotBitCount(_, inst);
+    case SpvOpGroupNonUniformRotateKHR:
+      return ValidateGroupNonUniformRotateKHR(_, inst);
     default:
       break;
   }
diff --git a/test/link/entry_points_test.cpp b/test/link/entry_points_test.cpp
index df7ea20..edf9f42 100644
--- a/test/link/entry_points_test.cpp
+++ b/test/link/entry_points_test.cpp
@@ -104,5 +104,48 @@
                         "GLCompute, was already defined."));
 }
 
+TEST_F(EntryPoints, LinkedVariables) {
+  const std::string body1 = R"(
+               OpCapability Addresses
+OpCapability Linkage
+OpCapability Kernel
+OpMemoryModel Physical64 OpenCL
+OpDecorate %7 LinkageAttributes "foo" Export
+%1 = OpTypeInt 32 0
+%2 = OpTypeVector %1 3
+%3 = OpTypePointer Input %2
+%4 = OpVariable %3 Input
+%5 = OpTypeVoid
+%6 = OpTypeFunction %5
+%7 = OpFunction %5 None %6
+%8 = OpLabel
+%9 = OpLoad %2 %4 Aligned 32
+OpReturn
+OpFunctionEnd
+)";
+  const std::string body2 = R"(
+OpCapability Linkage
+OpCapability Kernel
+OpMemoryModel Physical64 OpenCL
+OpEntryPoint Kernel %4 "bar"
+OpDecorate %3 LinkageAttributes "foo" Import
+%1 = OpTypeVoid
+%2 = OpTypeFunction %1
+%3 = OpFunction %1 None %2
+OpFunctionEnd
+%4 = OpFunction %1 None %2
+%5 = OpLabel
+%6 = OpFunctionCall %1 %3
+OpReturn
+OpFunctionEnd
+)";
+
+  spvtest::Binary linked_binary;
+  EXPECT_EQ(SPV_SUCCESS, AssembleAndLink({body1, body2}, &linked_binary));
+  EXPECT_THAT(GetErrorMessage(), std::string());
+  EXPECT_TRUE(Validate(linked_binary));
+  EXPECT_THAT(GetErrorMessage(), std::string());
+}
+
 }  // namespace
 }  // namespace spvtools
diff --git a/test/link/linker_fixture.h b/test/link/linker_fixture.h
index 7bb1223..d005288 100644
--- a/test/link/linker_fixture.h
+++ b/test/link/linker_fixture.h
@@ -208,6 +208,10 @@
   // Returns the accumulated error messages for the test.
   std::string GetErrorMessage() const { return error_message_; }
 
+  bool Validate(const spvtest::Binary& binary) {
+    return tools_.Validate(binary);
+  }
+
  private:
   spvtools::Context context_;
   spvtools::SpirvTools
diff --git a/test/opt/fold_test.cpp b/test/opt/fold_test.cpp
index 7565ca7..2ca3256 100644
--- a/test/opt/fold_test.cpp
+++ b/test/opt/fold_test.cpp
@@ -7108,6 +7108,214 @@
         3, true)
  ));
 
+INSTANTIATE_TEST_SUITE_P(FmaGenerationMatchingTest, MatchingInstructionFoldingTest,
+::testing::Values(
+   // Test case 0: (x * y) + a = Fma(x, y, a)
+   InstructionFoldingCase<bool>(
+       Header() +
+           "; CHECK: [[ext:%\\w+]] = OpExtInstImport \"GLSL.std.450\"\n" +
+           "; CHECK: OpFunction\n" +
+           "; CHECK: [[x:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[y:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[a:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[lx:%\\w+]] = OpLoad {{%\\w+}} [[x]]\n" +
+           "; CHECK: [[ly:%\\w+]] = OpLoad {{%\\w+}} [[y]]\n" +
+           "; CHECK: [[la:%\\w+]] = OpLoad {{%\\w+}} [[a]]\n" +
+           "; CHECK: [[fma:%\\w+]] = OpExtInst {{%\\w+}} [[ext]] Fma [[lx]] [[ly]] [[la]]\n" +
+           "; CHECK: OpStore {{%\\w+}} [[fma]]\n" +
+           "%main = OpFunction %void None %void_func\n" +
+           "%main_lab = OpLabel\n" +
+           "%x = OpVariable %_ptr_float Function\n" +
+           "%y = OpVariable %_ptr_float Function\n" +
+           "%a = OpVariable %_ptr_float Function\n" +
+           "%lx = OpLoad %float %x\n" +
+           "%ly = OpLoad %float %y\n" +
+           "%mul = OpFMul %float %lx %ly\n" +
+           "%la = OpLoad %float %a\n" +
+           "%3 = OpFAdd %float %mul %la\n" +
+           "OpStore %a %3\n" +
+           "OpReturn\n" +
+           "OpFunctionEnd",
+       3, true),
+    // Test case 1:  a + (x * y) = Fma(x, y, a)
+   InstructionFoldingCase<bool>(
+       Header() +
+           "; CHECK: [[ext:%\\w+]] = OpExtInstImport \"GLSL.std.450\"\n" +
+           "; CHECK: OpFunction\n" +
+           "; CHECK: [[x:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[y:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[a:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[lx:%\\w+]] = OpLoad {{%\\w+}} [[x]]\n" +
+           "; CHECK: [[ly:%\\w+]] = OpLoad {{%\\w+}} [[y]]\n" +
+           "; CHECK: [[la:%\\w+]] = OpLoad {{%\\w+}} [[a]]\n" +
+           "; CHECK: [[fma:%\\w+]] = OpExtInst {{%\\w+}} [[ext]] Fma [[lx]] [[ly]] [[la]]\n" +
+           "; CHECK: OpStore {{%\\w+}} [[fma]]\n" +
+           "%main = OpFunction %void None %void_func\n" +
+           "%main_lab = OpLabel\n" +
+           "%x = OpVariable %_ptr_float Function\n" +
+           "%y = OpVariable %_ptr_float Function\n" +
+           "%a = OpVariable %_ptr_float Function\n" +
+           "%lx = OpLoad %float %x\n" +
+           "%ly = OpLoad %float %y\n" +
+           "%mul = OpFMul %float %lx %ly\n" +
+           "%la = OpLoad %float %a\n" +
+           "%3 = OpFAdd %float %la %mul\n" +
+           "OpStore %a %3\n" +
+           "OpReturn\n" +
+           "OpFunctionEnd",
+       3, true),
+   // Test case 2: (x * y) + a = Fma(x, y, a) with vectors
+   InstructionFoldingCase<bool>(
+       Header() +
+           "; CHECK: [[ext:%\\w+]] = OpExtInstImport \"GLSL.std.450\"\n" +
+           "; CHECK: OpFunction\n" +
+           "; CHECK: [[x:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[y:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[a:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[lx:%\\w+]] = OpLoad {{%\\w+}} [[x]]\n" +
+           "; CHECK: [[ly:%\\w+]] = OpLoad {{%\\w+}} [[y]]\n" +
+           "; CHECK: [[la:%\\w+]] = OpLoad {{%\\w+}} [[a]]\n" +
+           "; CHECK: [[fma:%\\w+]] = OpExtInst {{%\\w+}} [[ext]] Fma [[lx]] [[ly]] [[la]]\n" +
+           "; CHECK: OpStore {{%\\w+}} [[fma]]\n" +
+           "%main = OpFunction %void None %void_func\n" +
+           "%main_lab = OpLabel\n" +
+           "%x = OpVariable %_ptr_v4float Function\n" +
+           "%y = OpVariable %_ptr_v4float Function\n" +
+           "%a = OpVariable %_ptr_v4float Function\n" +
+           "%lx = OpLoad %v4float %x\n" +
+           "%ly = OpLoad %v4float %y\n" +
+           "%mul = OpFMul %v4float %lx %ly\n" +
+           "%la = OpLoad %v4float %a\n" +
+           "%3 = OpFAdd %v4float %mul %la\n" +
+           "OpStore %a %3\n" +
+           "OpReturn\n" +
+           "OpFunctionEnd",
+       3, true),
+    // Test case 3:  a + (x * y) = Fma(x, y, a) with vectors
+   InstructionFoldingCase<bool>(
+       Header() +
+           "; CHECK: [[ext:%\\w+]] = OpExtInstImport \"GLSL.std.450\"\n" +
+           "; CHECK: OpFunction\n" +
+           "; CHECK: [[x:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[y:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[a:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[lx:%\\w+]] = OpLoad {{%\\w+}} [[x]]\n" +
+           "; CHECK: [[ly:%\\w+]] = OpLoad {{%\\w+}} [[y]]\n" +
+           "; CHECK: [[la:%\\w+]] = OpLoad {{%\\w+}} [[a]]\n" +
+           "; CHECK: [[fma:%\\w+]] = OpExtInst {{%\\w+}} [[ext]] Fma [[lx]] [[ly]] [[la]]\n" +
+           "; CHECK: OpStore {{%\\w+}} [[fma]]\n" +
+           "%main = OpFunction %void None %void_func\n" +
+           "%main_lab = OpLabel\n" +
+           "%x = OpVariable %_ptr_float Function\n" +
+           "%y = OpVariable %_ptr_float Function\n" +
+           "%a = OpVariable %_ptr_float Function\n" +
+           "%lx = OpLoad %float %x\n" +
+           "%ly = OpLoad %float %y\n" +
+           "%mul = OpFMul %float %lx %ly\n" +
+           "%la = OpLoad %float %a\n" +
+           "%3 = OpFAdd %float %la %mul\n" +
+           "OpStore %a %3\n" +
+           "OpReturn\n" +
+           "OpFunctionEnd",
+       3, true),
+    // Test 5: that the OpExtInstImport instruction is generated if it is missing.
+   InstructionFoldingCase<bool>(
+           std::string() +
+           "; CHECK: [[ext:%\\w+]] = OpExtInstImport \"GLSL.std.450\"\n" +
+           "; CHECK: OpFunction\n" +
+           "; CHECK: [[x:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[y:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[a:%\\w+]] = OpVariable {{%\\w+}} Function\n" +
+           "; CHECK: [[lx:%\\w+]] = OpLoad {{%\\w+}} [[x]]\n" +
+           "; CHECK: [[ly:%\\w+]] = OpLoad {{%\\w+}} [[y]]\n" +
+           "; CHECK: [[la:%\\w+]] = OpLoad {{%\\w+}} [[a]]\n" +
+           "; CHECK: [[fma:%\\w+]] = OpExtInst {{%\\w+}} [[ext]] Fma [[lx]] [[ly]] [[la]]\n" +
+           "; CHECK: OpStore {{%\\w+}} [[fma]]\n" +
+           "OpCapability Shader\n" +
+           "OpMemoryModel Logical GLSL450\n" +
+           "OpEntryPoint Fragment %main \"main\"\n" +
+           "OpExecutionMode %main OriginUpperLeft\n" +
+           "OpSource GLSL 140\n" +
+           "OpName %main \"main\"\n" +
+           "%void = OpTypeVoid\n" +
+           "%void_func = OpTypeFunction %void\n" +
+           "%bool = OpTypeBool\n" +
+           "%float = OpTypeFloat 32\n" +
+           "%_ptr_float = OpTypePointer Function %float\n" +
+           "%main = OpFunction %void None %void_func\n" +
+           "%main_lab = OpLabel\n" +
+           "%x = OpVariable %_ptr_float Function\n" +
+           "%y = OpVariable %_ptr_float Function\n" +
+           "%a = OpVariable %_ptr_float Function\n" +
+           "%lx = OpLoad %float %x\n" +
+           "%ly = OpLoad %float %y\n" +
+           "%mul = OpFMul %float %lx %ly\n" +
+           "%la = OpLoad %float %a\n" +
+           "%3 = OpFAdd %float %mul %la\n" +
+           "OpStore %a %3\n" +
+           "OpReturn\n" +
+           "OpFunctionEnd",
+       3, true),
+   // Test 5: Don't fold if the multiple is marked no contract.
+   InstructionFoldingCase<bool>(
+       std::string() +
+           "OpCapability Shader\n" +
+           "OpMemoryModel Logical GLSL450\n" +
+           "OpEntryPoint Fragment %main \"main\"\n" +
+           "OpExecutionMode %main OriginUpperLeft\n" +
+           "OpSource GLSL 140\n" +
+           "OpName %main \"main\"\n" +
+           "OpDecorate %mul NoContraction\n" +
+           "%void = OpTypeVoid\n" +
+           "%void_func = OpTypeFunction %void\n" +
+           "%bool = OpTypeBool\n" +
+           "%float = OpTypeFloat 32\n" +
+           "%_ptr_float = OpTypePointer Function %float\n" +
+           "%main = OpFunction %void None %void_func\n" +
+           "%main_lab = OpLabel\n" +
+           "%x = OpVariable %_ptr_float Function\n" +
+           "%y = OpVariable %_ptr_float Function\n" +
+           "%a = OpVariable %_ptr_float Function\n" +
+           "%lx = OpLoad %float %x\n" +
+           "%ly = OpLoad %float %y\n" +
+           "%mul = OpFMul %float %lx %ly\n" +
+           "%la = OpLoad %float %a\n" +
+           "%3 = OpFAdd %float %mul %la\n" +
+           "OpStore %a %3\n" +
+           "OpReturn\n" +
+           "OpFunctionEnd",
+       3, false),
+       // Test 6: Don't fold if the add is marked no contract.
+       InstructionFoldingCase<bool>(
+           std::string() +
+               "OpCapability Shader\n" +
+               "OpMemoryModel Logical GLSL450\n" +
+               "OpEntryPoint Fragment %main \"main\"\n" +
+               "OpExecutionMode %main OriginUpperLeft\n" +
+               "OpSource GLSL 140\n" +
+               "OpName %main \"main\"\n" +
+               "OpDecorate %3 NoContraction\n" +
+               "%void = OpTypeVoid\n" +
+               "%void_func = OpTypeFunction %void\n" +
+               "%bool = OpTypeBool\n" +
+               "%float = OpTypeFloat 32\n" +
+               "%_ptr_float = OpTypePointer Function %float\n" +
+               "%main = OpFunction %void None %void_func\n" +
+               "%main_lab = OpLabel\n" +
+               "%x = OpVariable %_ptr_float Function\n" +
+               "%y = OpVariable %_ptr_float Function\n" +
+               "%a = OpVariable %_ptr_float Function\n" +
+               "%lx = OpLoad %float %x\n" +
+               "%ly = OpLoad %float %y\n" +
+               "%mul = OpFMul %float %lx %ly\n" +
+               "%la = OpLoad %float %a\n" +
+               "%3 = OpFAdd %float %mul %la\n" +
+               "OpStore %a %3\n" +
+               "OpReturn\n" +
+               "OpFunctionEnd",
+           3, false)
+));
+
 using MatchingInstructionWithNoResultFoldingTest =
 ::testing::TestWithParam<InstructionFoldingCase<bool>>;
 
diff --git a/test/opt/if_conversion_test.cpp b/test/opt/if_conversion_test.cpp
index dee15c3..81e9bb2 100644
--- a/test/opt/if_conversion_test.cpp
+++ b/test/opt/if_conversion_test.cpp
@@ -328,6 +328,40 @@
   SinglePassRunAndCheck<IfConversion>(text, text, true, true);
 }
 
+TEST_F(IfConversionTest, DontFlatten) {
+  const std::string text = R"(OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint Vertex %1 "func" %2
+%void = OpTypeVoid
+%bool = OpTypeBool
+%true = OpConstantTrue %bool
+%uint = OpTypeInt 32 0
+%uint_0 = OpConstant %uint 0
+%uint_1 = OpConstant %uint 1
+%v2uint = OpTypeVector %uint 2
+%10 = OpConstantComposite %v2uint %uint_0 %uint_1
+%11 = OpConstantComposite %v2uint %uint_1 %uint_0
+%_ptr_Output_v2uint = OpTypePointer Output %v2uint
+%2 = OpVariable %_ptr_Output_v2uint Output
+%13 = OpTypeFunction %void
+%1 = OpFunction %void None %13
+%14 = OpLabel
+OpSelectionMerge %15 DontFlatten
+OpBranchConditional %true %16 %17
+%16 = OpLabel
+OpBranch %15
+%17 = OpLabel
+OpBranch %15
+%15 = OpLabel
+%18 = OpPhi %v2uint %10 %16 %11 %17
+OpStore %2 %18
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndCheck<IfConversion>(text, text, true, true);
+}
+
 TEST_F(IfConversionTest, LoopUntouched) {
   const std::string text = R"(OpCapability Shader
 OpMemoryModel Logical GLSL450
diff --git a/test/opt/loop_optimizations/unroll_simple.cpp b/test/opt/loop_optimizations/unroll_simple.cpp
index b72305c..299fb2d 100644
--- a/test/opt/loop_optimizations/unroll_simple.cpp
+++ b/test/opt/loop_optimizations/unroll_simple.cpp
@@ -3789,6 +3789,40 @@
   SinglePassRunAndMatch<PartialUnrollerTestPass<2>>(text, true);
 }
 
+TEST_F(PassClassTest, DontUnrollInfiteLoop) {
+  // This is an infinite loop that because the step is 0.  We want to make sure
+  // the unroller does not try to unroll it.
+  const std::string text = R"(OpCapability Shader
+%1 = OpExtInstImport "GLSL.std.450"
+OpMemoryModel Logical GLSL450
+OpEntryPoint Fragment %2 "main"
+OpExecutionMode %2 OriginUpperLeft
+%void = OpTypeVoid
+%4 = OpTypeFunction %void
+%int = OpTypeInt 32 1
+%int_0 = OpConstant %int 0
+%int_50 = OpConstant %int 50
+%bool = OpTypeBool
+%int_0_0 = OpConstant %int 0
+%2 = OpFunction %void None %4
+%10 = OpLabel
+OpBranch %11
+%11 = OpLabel
+%12 = OpPhi %int %int_0 %10 %13 %14
+%15 = OpSLessThan %bool %12 %int_50
+OpLoopMerge %16 %14 Unroll
+OpBranchConditional %15 %14 %16
+%14 = OpLabel
+%13 = OpIAdd %int %12 %int_0_0
+OpBranch %11
+%16 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndCheck<LoopUnroller>(text, text, false);
+}
+
 }  // namespace
 }  // namespace opt
 }  // namespace spvtools
diff --git a/test/opt/spread_volatile_semantics_test.cpp b/test/opt/spread_volatile_semantics_test.cpp
index fdabd92..dbb889c 100644
--- a/test/opt/spread_volatile_semantics_test.cpp
+++ b/test/opt/spread_volatile_semantics_test.cpp
@@ -54,6 +54,7 @@
 OpSourceExtension "GL_EXT_nonuniform_qualifier"
 OpSourceExtension "GL_KHR_ray_tracing"
 OpName %main "main"
+OpName %fn "fn"
 OpName %StorageBuffer "StorageBuffer"
 OpMemberName %StorageBuffer 0 "index"
 OpMemberName %StorageBuffer 1 "red"
@@ -109,6 +110,11 @@
 %29 = OpCompositeExtract %float %27 0
 %31 = OpAccessChain %_ptr_Uniform_float %sbo %int_1
 OpStore %31 %29
+%32 = OpFunctionCall %void %fn
+OpReturn
+OpFunctionEnd
+%fn = OpFunction %void None %3
+%33 = OpLabel
 OpReturn
 OpFunctionEnd
 )");
@@ -782,12 +788,7 @@
 OpFunctionEnd
 )";
 
-  EXPECT_EQ(RunPass(text), Pass::Status::Failure);
-  const char expected_error[] =
-      "ERROR: 0: Functions of SPIR-V for spread-volatile-semantics pass "
-      "input must be inlined except entry points";
-  EXPECT_STREQ(GetErrorMessage().substr(0, sizeof(expected_error) - 1).c_str(),
-               expected_error);
+  EXPECT_EQ(RunPass(text), Pass::Status::SuccessWithoutChange);
 }
 
 TEST_F(VolatileSpreadErrorTest, VarNotUsedInEntryPointForVolatile) {
@@ -1133,6 +1134,134 @@
   EXPECT_EQ(status, Pass::Status::SuccessWithoutChange);
 }
 
+TEST_F(VolatileSpreadTest, NoInlinedfuncCalls) {
+  const std::string text = R"(
+OpCapability RayTracingNV
+OpCapability VulkanMemoryModel
+OpCapability GroupNonUniform
+OpExtension "SPV_NV_ray_tracing"
+OpExtension "SPV_KHR_vulkan_memory_model"
+OpMemoryModel Logical Vulkan
+OpEntryPoint RayGenerationNV %main "main" %SubgroupSize
+OpSource HLSL 630
+OpName %main "main"
+OpName %src_main "src.main"
+OpName %bb_entry "bb.entry"
+OpName %func0 "func0"
+OpName %bb_entry_0 "bb.entry"
+OpName %func2 "func2"
+OpName %bb_entry_1 "bb.entry"
+OpName %param_var_count "param.var.count"
+OpName %func1 "func1"
+OpName %bb_entry_2 "bb.entry"
+OpName %func3 "func3"
+OpName %count "count"
+OpName %bb_entry_3 "bb.entry"
+OpDecorate %SubgroupSize BuiltIn SubgroupSize
+%uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%void = OpTypeVoid
+%6 = OpTypeFunction %void
+%_ptr_Function_uint = OpTypePointer Function %uint
+%25 = OpTypeFunction %void %_ptr_Function_uint
+%SubgroupSize = OpVariable %_ptr_Input_uint Input
+%main = OpFunction %void None %6
+%7 = OpLabel
+%8 = OpFunctionCall %void %src_main
+OpReturn
+OpFunctionEnd
+%src_main = OpFunction %void None %6
+%bb_entry = OpLabel
+%11 = OpFunctionCall %void %func0
+OpReturn
+OpFunctionEnd
+%func0 = OpFunction %void DontInline %6
+%bb_entry_0 = OpLabel
+%14 = OpFunctionCall %void %func2
+%16 = OpFunctionCall %void %func1
+OpReturn
+OpFunctionEnd
+%func2 = OpFunction %void DontInline %6
+%bb_entry_1 = OpLabel
+%param_var_count = OpVariable %_ptr_Function_uint Function
+; CHECK: {{%\w+}} = OpLoad %uint %SubgroupSize Volatile
+%21 = OpLoad %uint %SubgroupSize
+OpStore %param_var_count %21
+%22 = OpFunctionCall %void %func3 %param_var_count
+OpReturn
+OpFunctionEnd
+%func1 = OpFunction %void DontInline %6
+%bb_entry_2 = OpLabel
+OpReturn
+OpFunctionEnd
+%func3 = OpFunction %void DontInline %25
+%count = OpFunctionParameter %_ptr_Function_uint
+%bb_entry_3 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+  SinglePassRunAndMatch<SpreadVolatileSemantics>(text, true);
+}
+
+TEST_F(VolatileSpreadErrorTest, NoInlinedMultiEntryfuncCalls) {
+  const std::string text = R"(
+OpCapability RayTracingNV
+OpCapability SubgroupBallotKHR
+OpExtension "SPV_NV_ray_tracing"
+OpExtension "SPV_KHR_shader_ballot"
+%1 = OpExtInstImport "GLSL.std.450"
+OpMemoryModel Logical GLSL450
+OpEntryPoint RayGenerationNV %main "main" %SubgroupSize
+OpEntryPoint GLCompute %main2 "main2" %gl_LocalInvocationIndex %SubgroupSize
+OpSource HLSL 630
+OpName %main "main"
+OpName %bb_entry "bb.entry"
+OpName %main2 "main2"
+OpName %bb_entry_0 "bb.entry"
+OpName %func "func"
+OpName %count "count"
+OpName %bb_entry_1 "bb.entry"
+OpDecorate %gl_LocalInvocationIndex BuiltIn LocalInvocationIndex
+OpDecorate %SubgroupSize BuiltIn SubgroupSize
+%uint = OpTypeInt 32 0
+%uint_0 = OpConstant %uint 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%float = OpTypeFloat 32
+%v4float = OpTypeVector %float 4
+%void = OpTypeVoid
+%12 = OpTypeFunction %void
+%_ptr_Function_uint = OpTypePointer Function %uint
+%_ptr_Function_v4float = OpTypePointer Function %v4float
+%29 = OpTypeFunction %void %_ptr_Function_v4float
+%34 = OpTypeFunction %void %_ptr_Function_uint
+%SubgroupSize = OpVariable %_ptr_Input_uint Input
+%gl_LocalInvocationIndex = OpVariable %_ptr_Input_uint Input
+%main = OpFunction %void None %12
+%bb_entry = OpLabel
+%20 = OpFunctionCall %void %func
+OpReturn
+OpFunctionEnd
+%main2 = OpFunction %void None %12
+%bb_entry_0 = OpLabel
+%33 = OpFunctionCall %void %func
+OpReturn
+OpFunctionEnd
+%func = OpFunction %void DontInline %12
+%bb_entry_1 = OpLabel
+%count = OpVariable %_ptr_Function_uint Function
+%35 = OpLoad %uint %SubgroupSize
+OpStore %count %35
+OpReturn
+OpFunctionEnd
+)";
+  EXPECT_EQ(RunPass(text), Pass::Status::Failure);
+  const char expected_error[] =
+      "ERROR: 0: Variable is a target for Volatile semantics for an entry "
+      "point, but it is not for another entry point";
+  EXPECT_STREQ(GetErrorMessage().substr(0, sizeof(expected_error) - 1).c_str(),
+               expected_error);
+}
+
 }  // namespace
 }  // namespace opt
 }  // namespace spvtools
diff --git a/test/text_to_binary.extension_test.cpp b/test/text_to_binary.extension_test.cpp
index 3a592a0..cf4919a 100644
--- a/test/text_to_binary.extension_test.cpp
+++ b/test/text_to_binary.extension_test.cpp
@@ -1075,5 +1075,27 @@
                              {1, 2, 3, SpvGroupOperationReduce, 4})},
         })));
 
+// SPV_KHR_subgroup_rotate
+
+INSTANTIATE_TEST_SUITE_P(
+    SPV_KHR_subgroup_rotate, ExtensionRoundTripTest,
+    Combine(Values(SPV_ENV_UNIVERSAL_1_0, SPV_ENV_UNIVERSAL_1_6,
+                   SPV_ENV_VULKAN_1_0, SPV_ENV_VULKAN_1_1, SPV_ENV_VULKAN_1_2,
+                   SPV_ENV_VULKAN_1_3, SPV_ENV_OPENCL_2_1),
+            ValuesIn(std::vector<AssemblyCase>{
+                {"OpExtension \"SPV_KHR_subgroup_rotate\"\n",
+                 MakeInstruction(SpvOpExtension,
+                                 MakeVector("SPV_KHR_subgroup_rotate"))},
+                {"OpCapability GroupNonUniformRotateKHR\n",
+                 MakeInstruction(SpvOpCapability,
+                                 {SpvCapabilityGroupNonUniformRotateKHR})},
+                {"%2 = OpGroupNonUniformRotateKHR %1 %3 %4 %5\n",
+                 MakeInstruction(SpvOpGroupNonUniformRotateKHR,
+                                 {1, 2, 3, 4, 5})},
+                {"%2 = OpGroupNonUniformRotateKHR %1 %3 %4 %5 %6\n",
+                 MakeInstruction(SpvOpGroupNonUniformRotateKHR,
+                                 {1, 2, 3, 4, 5, 6})},
+            })));
+
 }  // namespace
 }  // namespace spvtools
diff --git a/test/val/CMakeLists.txt b/test/val/CMakeLists.txt
index 64eba44..65f2791 100644
--- a/test/val/CMakeLists.txt
+++ b/test/val/CMakeLists.txt
@@ -45,6 +45,7 @@
        val_extension_spv_khr_integer_dot_product.cpp
        val_extension_spv_khr_bit_instructions.cpp
        val_extension_spv_khr_terminate_invocation.cpp
+       val_extension_spv_khr_subgroup_rotate.cpp
        val_ext_inst_test.cpp
        val_ext_inst_debug_test.cpp
        ${VAL_TEST_COMMON_SRCS}
diff --git a/test/val/val_extension_spv_khr_subgroup_rotate.cpp b/test/val/val_extension_spv_khr_subgroup_rotate.cpp
new file mode 100644
index 0000000..4f156e8
--- /dev/null
+++ b/test/val/val_extension_spv_khr_subgroup_rotate.cpp
@@ -0,0 +1,352 @@
+// Copyright (c) 2022 The Khronos Group Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "test/val/val_fixtures.h"
+
+namespace spvtools {
+namespace val {
+namespace {
+
+using ::testing::HasSubstr;
+using ::testing::Values;
+using ::testing::ValuesIn;
+
+struct Case {
+  std::vector<std::string> caps;
+  bool shader;
+  std::string result_type;
+  std::string scope;
+  std::string delta;
+  std::string cluster_size;
+  std::string expected_error;  // empty for no error.
+};
+
+inline std::ostream& operator<<(std::ostream& out, Case c) {
+  out << "\nSPV_KHR_subgroup_rotate Case{{";
+  for (auto& cap : c.caps) {
+    out << cap;
+  }
+  out << "} ";
+  out << (c.shader ? "shader " : "kernel ");
+  out << c.result_type + " ";
+  out << c.scope + " ";
+  out << c.delta + " ";
+  out << c.cluster_size + " ";
+  out << "err'" << c.expected_error << "'";
+  out << "}";
+  return out;
+}
+
+std::string AssemblyForCase(const Case& c) {
+  std::ostringstream ss;
+
+  if (c.shader) {
+    ss << "OpCapability Shader\n";
+  } else {
+    ss << "OpCapability Kernel\n";
+    ss << "OpCapability Addresses\n";
+  }
+  for (auto& cap : c.caps) {
+    ss << "OpCapability " << cap << "\n";
+  }
+  ss << "OpExtension \"SPV_KHR_subgroup_rotate\"\n";
+
+  if (c.shader) {
+    ss << "OpMemoryModel Logical GLSL450\n";
+    ss << "OpEntryPoint GLCompute %main \"main\"\n";
+  } else {
+    ss << "OpMemoryModel Physical32 OpenCL\n";
+    ss << "OpEntryPoint Kernel %main \"main\"\n";
+  }
+
+  ss << R"(
+    %void    = OpTypeVoid
+    %void_fn = OpTypeFunction %void
+    %u32 = OpTypeInt 32 0
+    %float = OpTypeFloat 32
+    %ptr = OpTypePointer Function %u32
+  )";
+
+  if (c.shader) {
+    ss << "%i32 = OpTypeInt 32 1\n";
+  }
+
+  ss << R"(
+    %u32_0 = OpConstant %u32 0
+    %u32_1 = OpConstant %u32 1
+    %u32_15 = OpConstant %u32 15
+    %u32_16 = OpConstant %u32 16
+    %u32_undef = OpUndef %u32
+    %u32_spec_1 = OpSpecConstant %u32 1
+    %u32_spec_16 = OpSpecConstant %u32 16
+    %f32_1 = OpConstant %float 1.0
+    %subgroup = OpConstant %u32 3
+    %workgroup = OpConstant %u32 2
+    %invalid_scope = OpConstant %u32 1
+    %val = OpConstant %u32 42
+  )";
+
+  if (c.shader) {
+    ss << "%i32_1 = OpConstant %i32 1\n";
+  }
+
+  ss << R"(
+    %main = OpFunction %void None %void_fn
+    %entry = OpLabel
+  )";
+
+  ss << "%unused = OpGroupNonUniformRotateKHR ";
+  ss << c.result_type + " ";
+  ss << c.scope;
+  ss << " %val ";
+  ss << c.delta;
+  ss << " " + c.cluster_size;
+  ss << "\n";
+
+  ss << R"(
+    OpReturn
+    OpFunctionEnd
+  )";
+
+  return ss.str();
+}
+
+using ValidateSpvKHRSubgroupRotate = spvtest::ValidateBase<Case>;
+
+TEST_P(ValidateSpvKHRSubgroupRotate, Base) {
+  const auto& c = GetParam();
+  const auto& assembly = AssemblyForCase(c);
+  CompileSuccessfully(assembly);
+  if (c.expected_error.empty()) {
+    EXPECT_EQ(SPV_SUCCESS, ValidateInstructions()) << getDiagnosticString();
+  } else {
+    EXPECT_NE(SPV_SUCCESS, ValidateInstructions());
+    EXPECT_THAT(getDiagnosticString(), HasSubstr(c.expected_error));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    Valid, ValidateSpvKHRSubgroupRotate,
+    ::testing::Values(
+        Case{
+            {"GroupNonUniformRotateKHR"}, false, "%u32", "%subgroup", "%u32_1"},
+        Case{{"GroupNonUniformRotateKHR"}, true, "%u32", "%subgroup", "%u32_1"},
+        Case{{"GroupNonUniformRotateKHR"},
+             false,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%u32_16"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%u32_16"},
+        Case{{"GroupNonUniformRotateKHR"},
+             false,
+             "%u32",
+             "%subgroup",
+             "%u32_spec_1",
+             "%u32_16"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%u32_spec_16"},
+        Case{{"GroupNonUniformRotateKHR"},
+             false,
+             "%u32",
+             "%workgroup",
+             "%u32_1"},
+        Case{
+            {"GroupNonUniformRotateKHR"}, true, "%u32", "%workgroup", "%u32_1"},
+        Case{{"GroupNonUniformRotateKHR"},
+             false,
+             "%u32",
+             "%workgroup",
+             "%u32_spec_1"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%workgroup",
+             "%u32_spec_1"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    RequiresCapability, ValidateSpvKHRSubgroupRotate,
+    ::testing::Values(Case{{},
+                           false,
+                           "%u32",
+                           "%subgroup",
+                           "%u32_1",
+                           "",
+                           "Opcode GroupNonUniformRotateKHR requires one of "
+                           "these capabilities: "
+                           "GroupNonUniformRotateKHR"},
+                      Case{{},
+                           true,
+                           "%u32",
+                           "%subgroup",
+                           "%u32_1",
+                           "",
+                           "Opcode GroupNonUniformRotateKHR requires one of "
+                           "these capabilities: "
+                           "GroupNonUniformRotateKHR"}));
+
+TEST_F(ValidateSpvKHRSubgroupRotate, RequiresExtension) {
+  const std::string str = R"(
+    OpCapability GroupNonUniformRotateKHR
+)";
+  CompileSuccessfully(str.c_str());
+  EXPECT_NE(SPV_SUCCESS, ValidateInstructions());
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr(
+          "1st operand of Capability: operand GroupNonUniformRotateKHR(6026) "
+          "requires one of these extensions: SPV_KHR_subgroup_rotate"));
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    InvalidExecutionScope, ValidateSpvKHRSubgroupRotate,
+    ::testing::Values(
+        Case{{"GroupNonUniformRotateKHR"},
+             false,
+             "%u32",
+             "%invalid_scope",
+             "%u32_1",
+             "",
+             "Execution scope is limited to Subgroup or Workgroup"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%invalid_scope",
+             "%u32_1",
+             "",
+             "Execution scope is limited to Subgroup or Workgroup"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    InvalidResultType, ValidateSpvKHRSubgroupRotate,
+    ::testing::Values(Case{{"GroupNonUniformRotateKHR"},
+                           false,
+                           "%ptr",
+                           "%subgroup",
+                           "%u32_1",
+                           "",
+                           "Expected Result Type to be a scalar or vector of "
+                           "floating-point, integer or boolean type"},
+                      Case{{"GroupNonUniformRotateKHR"},
+                           true,
+                           "%ptr",
+                           "%subgroup",
+                           "%u32_1",
+                           "",
+                           "Expected Result Type to be a scalar or vector of "
+                           "floating-point, integer or boolean type"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    MismatchedResultAndValueTypes, ValidateSpvKHRSubgroupRotate,
+    ::testing::Values(
+        Case{{"GroupNonUniformRotateKHR"},
+             false,
+             "%float",
+             "%subgroup",
+             "%u32_1",
+             "",
+             "Result Type must be the same as the type of Value"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%float",
+             "%subgroup",
+             "%u32_1",
+             "",
+             "Result Type must be the same as the type of Value"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    InvalidDelta, ValidateSpvKHRSubgroupRotate,
+    ::testing::Values(Case{{"GroupNonUniformRotateKHR"},
+                           false,
+                           "%u32",
+                           "%subgroup",
+                           "%f32_1",
+                           "",
+                           "Delta must be a scalar of integer type, whose "
+                           "Signedness operand is 0"},
+                      Case{{"GroupNonUniformRotateKHR"},
+                           true,
+                           "%u32",
+                           "%subgroup",
+                           "%f32_1",
+                           "",
+                           "Delta must be a scalar of integer type, whose "
+                           "Signedness operand is 0"},
+                      Case{{"GroupNonUniformRotateKHR"},
+                           true,
+                           "%u32",
+                           "%subgroup",
+                           "%i32_1",
+                           "",
+                           "Delta must be a scalar of integer type, whose "
+                           "Signedness operand is 0"}));
+
+INSTANTIATE_TEST_SUITE_P(
+    InvalidClusterSize, ValidateSpvKHRSubgroupRotate,
+    ::testing::Values(
+        Case{{"GroupNonUniformRotateKHR"},
+             false,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%f32_1",
+             "ClusterSize must be a scalar of integer type, whose Signedness "
+             "operand is 0"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%i32_1",
+             "ClusterSize must be a scalar of integer type, whose Signedness "
+             "operand is 0"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%u32_0",
+             "Behavior is undefined unless ClusterSize is at least 1 and a "
+             "power of 2"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%u32_15",
+             "Behavior is undefined unless ClusterSize is at least 1 and a "
+             "power of 2"},
+        Case{{"GroupNonUniformRotateKHR"},
+             true,
+             "%u32",
+             "%subgroup",
+             "%u32_1",
+             "%u32_undef",
+             "ClusterSize must come from a constant instruction"}));
+
+}  // namespace
+}  // namespace val
+}  // namespace spvtools
diff --git a/utils/update_build_version.py b/utils/update_build_version.py
index 321de74..2a1ca60 100755
--- a/utils/update_build_version.py
+++ b/utils/update_build_version.py
@@ -17,16 +17,16 @@
 # Updates an output file with version info unless the new content is the same
 # as the existing content.
 #
-# Args: <spirv-tools_dir> <output-file>
+# Args: <changes-file> <output-file>
 #
 # The output file will contain a line of text consisting of two C source syntax
 # string literals separated by a comma:
-#  - The software version deduced from the CHANGES file in the given directory.
+#  - The software version deduced from the given CHANGES file.
 #  - A longer string with the project name, the software version number, and
-#    git commit information for the directory.  The commit information
-#    is the output of "git describe" if that succeeds, or "git rev-parse HEAD"
-#    if that succeeds, or otherwise a message containing the phrase
-#    "unknown hash".
+#    git commit information for the CHANGES file's directory.  The commit
+#    information is the output of "git describe" if that succeeds, or "git
+#    rev-parse HEAD" if that succeeds, or otherwise a message containing the
+#    phrase "unknown hash".
 # The string contents are escaped as necessary.
 
 import datetime
@@ -73,9 +73,8 @@
     return stdout
 
 
-def deduce_software_version(directory):
-    """Returns a software version number parsed from the CHANGES file
-    in the given directory.
+def deduce_software_version(changes_file):
+    """Returns a software version number parsed from the given CHANGES file.
 
     The CHANGES file describes most recent versions first.
     """
@@ -85,7 +84,6 @@
     # unexpected carriage returns on a linefeed-only system such as
     # Linux.
     pattern = re.compile(r'^(v\d+\.\d+(-dev)?) \d\d\d\d-\d\d-\d\d\s*$')
-    changes_file = os.path.join(directory, 'CHANGES')
     with open(changes_file, mode='r') as f:
         for line in f.readlines():
             match = pattern.match(line)
@@ -125,16 +123,17 @@
 
 def main():
     if len(sys.argv) != 3:
-        print('usage: {} <spirv-tools-dir> <output-file>'.format(sys.argv[0]))
+        print('usage: {} <changes-files> <output-file>'.format(sys.argv[0]))
         sys.exit(1)
 
     output_file = sys.argv[2]
     mkdir_p(os.path.dirname(output_file))
 
     software_version = deduce_software_version(sys.argv[1])
+    directory = os.path.dirname(sys.argv[1])
     new_content = '"{}", "SPIRV-Tools {} {}"\n'.format(
         software_version, software_version,
-        describe(sys.argv[1]).replace('"', '\\"'))
+        describe(directory).replace('"', '\\"'))
 
     if os.path.isfile(output_file):
         with open(output_file, 'r') as f: