Add tooling support for SPV_KHR_maximal_reconvergence (#5542)

* Validation for SPV_KHR_maximal_reconvergence
* Add pass to add/remove maximal reconvergence execution mode
---------

Co-authored-by: David Neto <dneto@google.com>
diff --git a/Android.mk b/Android.mk
index afa0403..3cf3f19 100644
--- a/Android.mk
+++ b/Android.mk
@@ -157,6 +157,7 @@
 		source/opt/loop_utils.cpp \
 		source/opt/mem_pass.cpp \
 		source/opt/merge_return_pass.cpp \
+		source/opt/modify_maximal_reconvergence.cpp \
 		source/opt/module.cpp \
 		source/opt/optimizer.cpp \
 		source/opt/pass.cpp \
diff --git a/BUILD.gn b/BUILD.gn
index 9ff36d8..7c42a99 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -737,6 +737,8 @@
     "source/opt/mem_pass.h",
     "source/opt/merge_return_pass.cpp",
     "source/opt/merge_return_pass.h",
+    "source/opt/modify_maximal_reconvergence.cpp",
+    "source/opt/modify_maximal_reconvergence.h",
     "source/opt/module.cpp",
     "source/opt/module.h",
     "source/opt/null_pass.h",
diff --git a/DEPS b/DEPS
index 866f98e..7ba5521 100644
--- a/DEPS
+++ b/DEPS
@@ -13,7 +13,7 @@
   'protobuf_revision': 'v21.12',
 
   're2_revision': '264e71e88e1c8a4b5ec326e70e9cf1d476f58a58',
-  'spirv_headers_revision': '7b0309708da5126b89e4ce6f19835f36dc912f2f',
+  'spirv_headers_revision': 'ae6a8b39717523d96683bc0d20b541944e28072f',
 }
 
 deps = {
diff --git a/include/spirv-tools/optimizer.hpp b/include/spirv-tools/optimizer.hpp
index bc4f5be..926e438 100644
--- a/include/spirv-tools/optimizer.hpp
+++ b/include/spirv-tools/optimizer.hpp
@@ -1003,6 +1003,10 @@
 // OpBeginInterlockInvocationEXT and one OpEndInterlockInvocationEXT, in that
 // order.
 Optimizer::PassToken CreateInvocationInterlockPlacementPass();
+
+// Creates a pass to add/remove maximal reconvergence execution mode.
+// This pass either adds or removes maximal reconvergence from all entry points.
+Optimizer::PassToken CreateModifyMaximalReconvergencePass(bool add);
 }  // namespace spvtools
 
 #endif  // INCLUDE_SPIRV_TOOLS_OPTIMIZER_HPP_
diff --git a/source/opt/CMakeLists.txt b/source/opt/CMakeLists.txt
index 6ebbfbf..4e7d92d 100644
--- a/source/opt/CMakeLists.txt
+++ b/source/opt/CMakeLists.txt
@@ -94,6 +94,7 @@
   loop_unswitch_pass.h
   mem_pass.h
   merge_return_pass.h
+  modify_maximal_reconvergence.h
   module.h
   null_pass.h
   passes.h
@@ -214,6 +215,7 @@
   loop_unswitch_pass.cpp
   mem_pass.cpp
   merge_return_pass.cpp
+  modify_maximal_reconvergence.cpp
   module.cpp
   optimizer.cpp
   pass.cpp
diff --git a/source/opt/modify_maximal_reconvergence.cpp b/source/opt/modify_maximal_reconvergence.cpp
new file mode 100644
index 0000000..dd79b62
--- /dev/null
+++ b/source/opt/modify_maximal_reconvergence.cpp
@@ -0,0 +1,103 @@
+// Copyright (c) 2024 Google LLC
+//
+// 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 "modify_maximal_reconvergence.h"
+
+#include "source/opt/ir_context.h"
+#include "source/util/make_unique.h"
+
+namespace spvtools {
+namespace opt {
+
+Pass::Status ModifyMaximalReconvergence::Process() {
+  bool changed = false;
+  if (add_) {
+    changed = AddMaximalReconvergence();
+  } else {
+    changed = RemoveMaximalReconvergence();
+  }
+  return changed ? Pass::Status::SuccessWithChange
+                 : Pass::Status::SuccessWithoutChange;
+}
+
+bool ModifyMaximalReconvergence::AddMaximalReconvergence() {
+  bool changed = false;
+  bool has_extension = false;
+  bool has_shader =
+      context()->get_feature_mgr()->HasCapability(spv::Capability::Shader);
+  for (auto extension : context()->extensions()) {
+    if (extension.GetOperand(0).AsString() == "SPV_KHR_maximal_reconvergence") {
+      has_extension = true;
+      break;
+    }
+  }
+
+  std::unordered_set<uint32_t> entry_points_with_mode;
+  for (auto mode : get_module()->execution_modes()) {
+    if (spv::ExecutionMode(mode.GetSingleWordInOperand(1)) ==
+        spv::ExecutionMode::MaximallyReconvergesKHR) {
+      entry_points_with_mode.insert(mode.GetSingleWordInOperand(0));
+    }
+  }
+
+  for (auto entry_point : get_module()->entry_points()) {
+    const uint32_t id = entry_point.GetSingleWordInOperand(1);
+    if (!entry_points_with_mode.count(id)) {
+      changed = true;
+      if (!has_extension) {
+        context()->AddExtension("SPV_KHR_maximal_reconvergence");
+        has_extension = true;
+      }
+      if (!has_shader) {
+        context()->AddCapability(spv::Capability::Shader);
+        has_shader = true;
+      }
+      context()->AddExecutionMode(MakeUnique<Instruction>(
+          context(), spv::Op::OpExecutionMode, 0, 0,
+          std::initializer_list<Operand>{
+              {SPV_OPERAND_TYPE_ID, {id}},
+              {SPV_OPERAND_TYPE_EXECUTION_MODE,
+               {static_cast<uint32_t>(
+                   spv::ExecutionMode::MaximallyReconvergesKHR)}}}));
+      entry_points_with_mode.insert(id);
+    }
+  }
+
+  return changed;
+}
+
+bool ModifyMaximalReconvergence::RemoveMaximalReconvergence() {
+  bool changed = false;
+  std::vector<Instruction*> to_remove;
+  Instruction* mode = &*get_module()->execution_mode_begin();
+  while (mode) {
+    if (mode->opcode() != spv::Op::OpExecutionMode &&
+        mode->opcode() != spv::Op::OpExecutionModeId) {
+      break;
+    }
+    if (spv::ExecutionMode(mode->GetSingleWordInOperand(1)) ==
+        spv::ExecutionMode::MaximallyReconvergesKHR) {
+      mode = context()->KillInst(mode);
+      changed = true;
+    } else {
+      mode = mode->NextNode();
+    }
+  }
+
+  changed |=
+      context()->RemoveExtension(Extension::kSPV_KHR_maximal_reconvergence);
+  return changed;
+}
+}  // namespace opt
+}  // namespace spvtools
diff --git a/source/opt/modify_maximal_reconvergence.h b/source/opt/modify_maximal_reconvergence.h
new file mode 100644
index 0000000..8d9a698
--- /dev/null
+++ b/source/opt/modify_maximal_reconvergence.h
@@ -0,0 +1,53 @@
+// Copyright (c) 2024 Google LLC
+//
+// 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.
+
+#ifndef LIBSPIRV_OPT_MODIFY_MAXIMAL_RECONVERGENCE_H_
+#define LIBSPIRV_OPT_MODIFY_MAXIMAL_RECONVERGENCE_H_
+
+#include "pass.h"
+
+namespace spvtools {
+namespace opt {
+
+// Modifies entry points to either add or remove MaximallyReconvergesKHR
+//
+// This pass will either add or remove MaximallyReconvergesKHR to all entry
+// points in the module. When adding the execution mode, it does not attempt to
+// determine whether any ray tracing invocation repack instructions might be
+// executed because it is a runtime restriction. That is left to the user.
+class ModifyMaximalReconvergence : public Pass {
+ public:
+  const char* name() const override { return "modify-maximal-reconvergence"; }
+  Status Process() override;
+
+  explicit ModifyMaximalReconvergence(bool add = true) : Pass(), add_(add) {}
+
+  IRContext::Analysis GetPreservedAnalyses() override {
+    return IRContext::kAnalysisDefUse |
+           IRContext::kAnalysisInstrToBlockMapping |
+           IRContext::kAnalysisDecorations | IRContext::kAnalysisCombinators |
+           IRContext::kAnalysisCFG | IRContext::kAnalysisNameMap |
+           IRContext::kAnalysisConstants | IRContext::kAnalysisTypes;
+  }
+
+ private:
+  bool AddMaximalReconvergence();
+  bool RemoveMaximalReconvergence();
+
+  bool add_;
+};
+}  // namespace opt
+}  // namespace spvtools
+
+#endif  // LIBSPIRV_OPT_MODIFY_MAXIMAL_RECONVERGENCE_H_
diff --git a/source/opt/optimizer.cpp b/source/opt/optimizer.cpp
index 7cfc89f..4d089f6 100644
--- a/source/opt/optimizer.cpp
+++ b/source/opt/optimizer.cpp
@@ -606,6 +606,23 @@
       return false;
     }
     RegisterPass(CreateSwitchDescriptorSetPass(from_set, to_set));
+  } else if (pass_name == "modify-maximal-reconvergence") {
+    if (pass_args.size() == 0) {
+      Error(consumer(), nullptr, {},
+            "--modify-maximal-reconvergence requires an argument");
+      return false;
+    }
+    if (pass_args == "add") {
+      RegisterPass(CreateModifyMaximalReconvergencePass(true));
+    } else if (pass_args == "remove") {
+      RegisterPass(CreateModifyMaximalReconvergencePass(false));
+    } else {
+      Errorf(consumer(), nullptr, {},
+             "Invalid argument for --modify-maximal-reconvergence: %s (must be "
+             "'add' or 'remove')",
+             pass_args.c_str());
+      return false;
+    }
   } else {
     Errorf(consumer(), nullptr, {},
            "Unknown flag '--%s'. Use --help for a list of valid flags",
@@ -1141,6 +1158,11 @@
   return MakeUnique<Optimizer::PassToken::Impl>(
       MakeUnique<opt::InvocationInterlockPlacementPass>());
 }
+
+Optimizer::PassToken CreateModifyMaximalReconvergencePass(bool add) {
+  return MakeUnique<Optimizer::PassToken::Impl>(
+      MakeUnique<opt::ModifyMaximalReconvergence>(add));
+}
 }  // namespace spvtools
 
 extern "C" {
diff --git a/source/opt/passes.h b/source/opt/passes.h
index 305f578..9d027fb 100644
--- a/source/opt/passes.h
+++ b/source/opt/passes.h
@@ -65,6 +65,7 @@
 #include "source/opt/loop_unroller.h"
 #include "source/opt/loop_unswitch_pass.h"
 #include "source/opt/merge_return_pass.h"
+#include "source/opt/modify_maximal_reconvergence.h"
 #include "source/opt/null_pass.h"
 #include "source/opt/private_to_local_pass.h"
 #include "source/opt/reduce_load_size.h"
diff --git a/source/val/validate_cfg.cpp b/source/val/validate_cfg.cpp
index 7b3f748..9b7161f 100644
--- a/source/val/validate_cfg.cpp
+++ b/source/val/validate_cfg.cpp
@@ -190,6 +190,8 @@
               "ID of an OpLabel instruction";
   }
 
+  // A similar requirement for SPV_KHR_maximal_reconvergence is deferred until
+  // entry point call trees have been reconrded.
   if (_.version() >= SPV_SPIRV_VERSION_WORD(1, 6) && true_id == false_id) {
     return _.diag(SPV_ERROR_INVALID_ID, inst)
            << "In SPIR-V 1.6 or later, True Label and False Label must be "
@@ -875,6 +877,95 @@
   return SPV_SUCCESS;
 }
 
+spv_result_t MaximalReconvergenceChecks(ValidationState_t& _) {
+  // Find all the entry points with the MaximallyReconvergencesKHR execution
+  // mode.
+  std::unordered_set<uint32_t> maximal_funcs;
+  std::unordered_set<uint32_t> maximal_entry_points;
+  for (auto entry_point : _.entry_points()) {
+    const auto* exec_modes = _.GetExecutionModes(entry_point);
+    if (exec_modes &&
+        exec_modes->count(spv::ExecutionMode::MaximallyReconvergesKHR)) {
+      maximal_entry_points.insert(entry_point);
+      maximal_funcs.insert(entry_point);
+    }
+  }
+
+  if (maximal_entry_points.empty()) {
+    return SPV_SUCCESS;
+  }
+
+  // Find all the functions reachable from a maximal reconvergence entry point.
+  for (const auto& func : _.functions()) {
+    const auto& entry_points = _.EntryPointReferences(func.id());
+    for (auto id : entry_points) {
+      if (maximal_entry_points.count(id)) {
+        maximal_funcs.insert(func.id());
+        break;
+      }
+    }
+  }
+
+  // Check for conditional branches with the same true and false targets.
+  for (const auto& inst : _.ordered_instructions()) {
+    if (inst.opcode() == spv::Op::OpBranchConditional) {
+      const auto true_id = inst.GetOperandAs<uint32_t>(1);
+      const auto false_id = inst.GetOperandAs<uint32_t>(2);
+      if (true_id == false_id && maximal_funcs.count(inst.function()->id())) {
+        return _.diag(SPV_ERROR_INVALID_ID, &inst)
+               << "In entry points using the MaximallyReconvergesKHR execution "
+                  "mode, True Label and False Label must be different labels";
+      }
+    }
+  }
+
+  // Check for invalid multiple predecessors. Only loop headers, continue
+  // targets, merge targets or switch targets or defaults may have multiple
+  // unique predecessors.
+  for (const auto& func : _.functions()) {
+    if (!maximal_funcs.count(func.id())) continue;
+
+    for (const auto* block : func.ordered_blocks()) {
+      std::unordered_set<uint32_t> unique_preds;
+      const auto* preds = block->predecessors();
+      if (!preds) continue;
+
+      for (const auto* pred : *preds) {
+        unique_preds.insert(pred->id());
+      }
+      if (unique_preds.size() < 2) continue;
+
+      const auto* terminator = block->terminator();
+      const auto index = terminator - &_.ordered_instructions()[0];
+      const auto* pre_terminator = &_.ordered_instructions()[index - 1];
+      if (pre_terminator->opcode() == spv::Op::OpLoopMerge) continue;
+
+      const auto* label = _.FindDef(block->id());
+      bool ok = false;
+      for (const auto& pair : label->uses()) {
+        const auto* use_inst = pair.first;
+        switch (use_inst->opcode()) {
+          case spv::Op::OpSelectionMerge:
+          case spv::Op::OpLoopMerge:
+          case spv::Op::OpSwitch:
+            ok = true;
+            break;
+          default:
+            break;
+        }
+      }
+      if (!ok) {
+        return _.diag(SPV_ERROR_INVALID_CFG, label)
+               << "In entry points using the MaximallyReconvergesKHR "
+                  "execution mode, this basic block must not have multiple "
+                  "unique predecessors";
+      }
+    }
+  }
+
+  return SPV_SUCCESS;
+}
+
 spv_result_t PerformCfgChecks(ValidationState_t& _) {
   for (auto& function : _.functions()) {
     // Check all referenced blocks are defined within a function
@@ -999,6 +1090,11 @@
         return error;
     }
   }
+
+  if (auto error = MaximalReconvergenceChecks(_)) {
+    return error;
+  }
+
   return SPV_SUCCESS;
 }
 
diff --git a/test/operand_capabilities_test.cpp b/test/operand_capabilities_test.cpp
index 4872228..6c4042d 100644
--- a/test/operand_capabilities_test.cpp
+++ b/test/operand_capabilities_test.cpp
@@ -486,7 +486,8 @@
                 CASE1(DECORATION, Decoration::XfbBuffer, TransformFeedback),
                 CASE1(DECORATION, Decoration::XfbStride, TransformFeedback),
                 CASE1(DECORATION, Decoration::FuncParamAttr, Kernel),
-                CASE1(DECORATION, Decoration::FPFastMathMode, Kernel),
+                CASE2(DECORATION, Decoration::FPFastMathMode, Kernel,
+                      FloatControls2),
                 CASE1(DECORATION, Decoration::LinkageAttributes, Linkage),
                 CASE1(DECORATION, Decoration::NoContraction, Shader),
                 CASE1(DECORATION, Decoration::InputAttachmentIndex,
diff --git a/test/opt/CMakeLists.txt b/test/opt/CMakeLists.txt
index ceada13..92d266b 100644
--- a/test/opt/CMakeLists.txt
+++ b/test/opt/CMakeLists.txt
@@ -78,6 +78,7 @@
        local_single_block_elim.cpp
        local_single_store_elim_test.cpp
        local_ssa_elim_test.cpp
+       modify_maximal_reconvergence_test.cpp
        module_test.cpp
        module_utils.h
        optimizer_test.cpp
diff --git a/test/opt/modify_maximal_reconvergence_test.cpp b/test/opt/modify_maximal_reconvergence_test.cpp
new file mode 100644
index 0000000..bef9237
--- /dev/null
+++ b/test/opt/modify_maximal_reconvergence_test.cpp
@@ -0,0 +1,312 @@
+// Copyright (c) 2024 Google LLC
+//
+// 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 "assembly_builder.h"
+#include "pass_fixture.h"
+#include "pass_utils.h"
+
+namespace {
+
+using namespace spvtools;
+
+using ModifyMaximalReconvergenceTest = opt::PassTest<::testing::Test>;
+
+TEST_F(ModifyMaximalReconvergenceTest, AddNoEntryPoint) {
+  const std::string text = R"(
+; CHECK-NOT: OpExtension
+OpCapability Kernel
+OpCapability Linkage
+OpMemoryModel Logical OpenCL
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, true);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, AddSingleEntryPoint) {
+  const std::string text = R"(
+; CHECK: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %main MaximallyReconvergesKHR
+
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpName %main "main"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, true);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, AddExtensionExists) {
+  const std::string text = R"(
+; CHECK: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK-NOT: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %main MaximallyReconvergesKHR
+
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpName %main "main"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, true);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, AddExecutionModeExists) {
+  const std::string text = R"(
+; CHECK: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK-NOT: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %main LocalSize 1 1 1
+; CHECK-NEXT: OpExecutionMode %main MaximallyReconvergesKHR
+; CHECK-NOT: OpExecutionMode %main MaximallyReconvergesKHR
+
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+OpName %main "main"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, true);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, AddTwoEntryPoints) {
+  const std::string text = R"(
+; CHECK: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %comp MaximallyReconvergesKHR
+; CHECK: OpExecutionMode %frag MaximallyReconvergesKHR
+
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %comp "main"
+OpEntryPoint Fragment %frag "main"
+OpExecutionMode %comp LocalSize 1 1 1
+OpExecutionMode %frag OriginUpperLeft
+OpName %comp "comp"
+OpName %frag "frag"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%comp = OpFunction %void None %void_fn
+%entry1 = OpLabel
+OpReturn
+OpFunctionEnd
+%frag = OpFunction %void None %void_fn
+%entry2 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, true);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, AddTwoEntryPointsOneFunc) {
+  const std::string text = R"(
+; CHECK: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %comp MaximallyReconvergesKHR
+; CHECK-NOT: OpExecutionMode %comp MaximallyReconvergesKHR
+
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %comp "main1"
+OpEntryPoint GLCompute %comp "main2"
+OpExecutionMode %comp LocalSize 1 1 1
+OpName %comp "comp"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%comp = OpFunction %void None %void_fn
+%entry1 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, true);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, AddTwoEntryPointsOneExecutionMode) {
+  const std::string text = R"(
+; CHECK: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %comp MaximallyReconvergesKHR
+; CHECK-NOT: OpExecutionMode %comp MaximallyReconvergesKHR
+; CHECK: OpExecutionMode %frag MaximallyReconvergesKHR
+; CHECK-NOT: OpExecutionMode %comp MaximallyReconvergesKHR
+
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %comp "main"
+OpEntryPoint Fragment %frag "main"
+OpExecutionMode %comp LocalSize 1 1 1
+OpExecutionMode %frag OriginUpperLeft
+OpExecutionMode %comp MaximallyReconvergesKHR
+OpName %comp "comp"
+OpName %frag "frag"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%comp = OpFunction %void None %void_fn
+%entry1 = OpLabel
+OpReturn
+OpFunctionEnd
+%frag = OpFunction %void None %void_fn
+%entry2 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, true);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, RemoveNoEntryPoint) {
+  const std::string text = R"(OpCapability Kernel
+OpCapability Linkage
+OpMemoryModel Logical OpenCL
+)";
+
+  SinglePassRunAndCheck<opt::ModifyMaximalReconvergence>(text, text, false,
+                                                         true, false);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, RemoveOnlyExtension) {
+  const std::string text = R"(
+; CHECK-NOT: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %main LocalSize 1 1 1
+
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpName %main "main"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, false);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, RemoveSingleEntryPoint) {
+  const std::string text = R"(
+; CHECK-NOT: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %main LocalSize 1 1 1
+; CHECK-NOT: OpExecutionMode %main MaximallyReconvergesKHR
+
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+OpName %main "main"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, false);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, RemoveTwoEntryPointsOneExecutionMode) {
+  const std::string text = R"(
+; CHECK-NOT: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %comp LocalSize 1 1 1
+; CHECK-NEXT: OpExecutionMode %frag OriginUpperLeft
+; CHECK-NOT: OpExecutionMode %comp MaximallyReconvergesKHR
+
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %comp "main"
+OpEntryPoint Fragment %frag "main"
+OpExecutionMode %comp LocalSize 1 1 1
+OpExecutionMode %comp MaximallyReconvergesKHR
+OpExecutionMode %frag OriginUpperLeft
+OpName %comp "comp"
+OpName %frag "frag"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%comp = OpFunction %void None %void_fn
+%entry1 = OpLabel
+OpReturn
+OpFunctionEnd
+%frag = OpFunction %void None %void_fn
+%entry2 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, false);
+}
+
+TEST_F(ModifyMaximalReconvergenceTest, RemoveTwoEntryPoints) {
+  const std::string text = R"(
+; CHECK-NOT: OpExtension "SPV_KHR_maximal_reconvergence"
+; CHECK: OpExecutionMode %comp LocalSize 1 1 1
+; CHECK-NEXT: OpExecutionMode %frag OriginUpperLeft
+; CHECK-NOT: OpExecutionMode {{%\w}} MaximallyReconvergesKHR
+
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %comp "main"
+OpEntryPoint Fragment %frag "main"
+OpExecutionMode %comp LocalSize 1 1 1
+OpExecutionMode %comp MaximallyReconvergesKHR
+OpExecutionMode %frag OriginUpperLeft
+OpExecutionMode %frag MaximallyReconvergesKHR
+OpName %comp "comp"
+OpName %frag "frag"
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%comp = OpFunction %void None %void_fn
+%entry1 = OpLabel
+OpReturn
+OpFunctionEnd
+%frag = OpFunction %void None %void_fn
+%entry2 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  SinglePassRunAndMatch<opt::ModifyMaximalReconvergence>(text, true, false);
+}
+
+}  // namespace
diff --git a/test/text_to_binary.extension_test.cpp b/test/text_to_binary.extension_test.cpp
index 55f8466..17adc55 100644
--- a/test/text_to_binary.extension_test.cpp
+++ b/test/text_to_binary.extension_test.cpp
@@ -1247,5 +1247,22 @@
              MakeInstruction(spv::Op::OpStencilAttachmentReadEXT, {1, 2, 3})},
         })));
 
+// SPV_KHR_maximal_reconvergence
+
+INSTANTIATE_TEST_SUITE_P(
+    SPV_KHR_maximal_reconvergence, ExtensionRoundTripTest,
+    Combine(
+        Values(SPV_ENV_UNIVERSAL_1_0, SPV_ENV_UNIVERSAL_1_5, SPV_ENV_VULKAN_1_0,
+               SPV_ENV_VULKAN_1_1, SPV_ENV_VULKAN_1_2, SPV_ENV_VULKAN_1_3),
+        ValuesIn(std::vector<AssemblyCase>{
+            {"OpExtension \"SPV_KHR_maximal_reconvergence\"\n",
+             MakeInstruction(spv::Op::OpExtension,
+                             MakeVector("SPV_KHR_maximal_reconvergence"))},
+            {"OpExecutionMode %1 MaximallyReconvergesKHR\n",
+             MakeInstruction(
+                 spv::Op::OpExecutionMode,
+                 {1, (uint32_t)spv::ExecutionMode::MaximallyReconvergesKHR})},
+        })));
+
 }  // namespace
 }  // namespace spvtools
diff --git a/test/val/val_cfg_test.cpp b/test/val/val_cfg_test.cpp
index 3953057..233aee6 100644
--- a/test/val/val_cfg_test.cpp
+++ b/test/val/val_cfg_test.cpp
@@ -4803,6 +4803,321 @@
                         "via a structured exit"));
 }
 
+TEST_F(ValidateCFG,
+       MaximalReconvergenceBranchConditionalSameTargetNotInCallTree) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%func_entry = OpLabel
+OpBranchConditional %cond %func_exit %func_exit
+%func_exit = OpLabel
+OpReturn
+OpFunctionEnd
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceBranchConditionalSameTargetInCallTree) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%func_entry = OpLabel
+OpBranchConditional %cond %func_exit %func_exit
+%func_exit = OpLabel
+OpReturn
+OpFunctionEnd
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+%call = OpFunctionCall %void %func
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("In entry points using the MaximallyReconvergesKHR "
+                        "execution mode, True "
+                        "Label and False Label must be different labels"));
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceEarlyReconvergenceNotInCallTree) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%func_entry = OpLabel
+OpSelectionMerge %func_exit None
+OpBranchConditional %cond %then %else
+%then = OpLabel
+OpBranch %merge
+%else = OpLabel
+OpBranch %merge
+%merge = OpLabel
+OpBranch %func_exit
+%func_exit = OpLabel
+OpReturn
+OpFunctionEnd
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceEarlyReconvergenceInCallTree) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%func_entry = OpLabel
+OpSelectionMerge %func_exit None
+OpBranchConditional %cond %then %else
+%then = OpLabel
+OpBranch %merge
+%else = OpLabel
+OpBranch %merge
+%merge = OpLabel
+OpBranch %func_exit
+%func_exit = OpLabel
+OpReturn
+OpFunctionEnd
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+%call = OpFunctionCall %void %func
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_ERROR_INVALID_CFG, ValidateInstructions());
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr(
+          "In entry points using the MaximallyReconvergesKHR execution mode, "
+          "this basic block must not have multiple unique predecessors"));
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceLoopMultiplePredsOk) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpBranch %loop
+%loop = OpLabel
+OpLoopMerge %merge %loop None
+OpBranchConditional %cond %loop %merge
+%merge = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceLoopMultiplePredsOk2) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpBranch %loop
+%loop = OpLabel
+OpLoopMerge %merge %cont None
+OpBranch %body
+%body = OpLabel
+OpBranch %cont
+%cont = OpLabel
+OpBranchConditional %cond %loop %merge
+%merge = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceSelectionMergeMultiplePredsOk) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpSelectionMerge %merge None
+OpBranchConditional %cond %then %else
+%then = OpLabel
+OpBranch %merge
+%else = OpLabel
+OpBranch %merge
+%merge = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceSelectionMergeMultiplePredsOk2) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+OpName %merge "merge"
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpSelectionMerge %merge None
+OpBranchConditional %cond %then %else
+%then = OpLabel
+OpBranch %merge
+%else = OpLabel
+OpBranch %merge
+%merge = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceLoopMergeMultiplePredsOk) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpBranch %loop
+%loop = OpLabel
+OpLoopMerge %merge %continue None
+OpBranchConditional %cond %merge %continue
+%continue = OpLabel
+OpBranchConditional %cond %loop %merge
+%merge = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
+TEST_F(ValidateCFG, MaximalReconvergenceCaseFallthroughMultiplePredsOk) {
+  const std::string text = R"(
+OpCapability Shader
+OpExtension "SPV_KHR_maximal_reconvergence"
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%bool = OpTypeBool
+%cond = OpUndef %bool
+%int = OpTypeInt 32 0
+%val = OpUndef %int
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%main_entry = OpLabel
+OpSelectionMerge %merge None
+OpSwitch %val %merge 0 %case1 1 %case2
+%case1 = OpLabel
+OpBranch %case2
+%case2 = OpLabel
+OpBranch %merge
+%merge = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_modes_test.cpp b/test/val/val_modes_test.cpp
index 8dc0fbc..97d8c39 100644
--- a/test/val/val_modes_test.cpp
+++ b/test/val/val_modes_test.cpp
@@ -1306,6 +1306,28 @@
   EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_3));
 }
 
+TEST_F(ValidateMode, MaximalReconvergenceRequiresExtension) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpExecutionMode %main MaximallyReconvergesKHR
+%void = OpTypeVoid
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv);
+  EXPECT_EQ(SPV_ERROR_MISSING_EXTENSION, ValidateInstructions());
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("(6023) requires one of these extensions: "
+                        "SPV_KHR_maximal_reconvergence "));
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools