Support SPIR-V 1.4 (#2550)

* SPIR-V 1.4 headers, add SPV_ENV_UNIVERSAL_1_4

* Support --target-env spv1.4 in help for command line tools

* Support asm/dis of UniformId decoration

* Validate UniformId decoration

* Fix version check on instructions and operands

Also register decorations used with OpDecorateId

* Extension lists can differ between enums that match

Example: SubgroupMaskEq vs SubgroupMaskEqKHR

* Validate scope value for Uniform decoration, for SPIR-V 1.4

* More unioning of exts

* Preserve grammar order within an enum value

* 1.4: Validate OpSelect over composites

* Tools default to 1.4

* Add asm/dis test for OpCopyLogical

* 1.4: asm/dis tests for PtrEqual, PtrNotEqual, PtrDiff

* Basic asm/Dis test for OpCopyMemory

* Test asm/dis OpCopyMemory with 2-memory access

Add asm/dis tests for OpCopyMemorySized

Requires grammar update to add second optional memory access operand
to OpCopyMemory and OpCopyMemorySized

* Validate one or two memory accesses on OpCopyMemory*

* Check av/vis on CopyMemory source and target memory access

This is a proposed rule. See
https://gitlab.khronos.org/spirv/SPIR-V/issues/413

* Validate operation for OpSpecConstantOp

* Validate NonWritable decoration

Also permit NonWritable on members of UBO and SSBO.

* SPIR-V 1.4: NonWrtiable can decorate Function and Private vars

* Update optimizer CLI tests for SPIR-V 1.4

* Testing tools: Give expected SPIR-V version in message

* SPIR-V 1.4 validation for entry point interfaces

* Allow only unique interfaces
* Allow all global variables
* Check that all statically used global variables are listed
* new tests

* Add validation fixture CompileFailure

* Add 1.4 validation for pointer comparisons

* New tests

* Validate with image operands SignExtend, ZeroExtend

Since we don't actually know the image texel format, we can't fully
validate.  We need more context.

But we can make sure we allow the new image operands in known-good
cases.

* Validate OpCopyLogical

* Recursively checks subtypes
* new tests

* Add SPIR-V 1.4 tests for NoSignedWrap, NoUnsignedWrap

* Allow scalar conditions in 1.4 with OpSelect

* Allows scalar conditions with vector operands
* new tests

* Validate uniform id scope as an execution scope

* Validate the values of memory and execution scopes are valid scope
values
* new test

* Remove SPIR-V 1.4 Vulkan 1.0 environment

* SPIR-V 1.4 requires Vulkan 1.1

* FIX: include string for spvLog

* FIX: validate nonwritable

* FIX: test case suite for member decorate string

* FIX: test case for hlsl functionality1

* Validation test fixture: ease debugging

* Use binary version for SPIR-V 1.4 specific features

* Switch checks based on the SPIR-V version from the target environment
to instead use the version from the binary
* Moved header parsing into the ValidationState_t constructor (where
version based features are set)
* Added new versions of tests that assemble a 1.3 binary and validate a
1.4 environment

* Fix test for update to SPIR-V 1.4 headers

* Fix formatting

* Ext inst lookup: Add Vulkan 1.1 env with SPIR-V 1.4

* Update spirv-val help

* Operand version checks should use module version

Use the module version instead of the target environment version.

* Fix comment about two-access form of OpCopyMemory
diff --git a/include/spirv-tools/libspirv.h b/include/spirv-tools/libspirv.h
index da59037..b5b49ba 100644
--- a/include/spirv-tools/libspirv.h
+++ b/include/spirv-tools/libspirv.h
@@ -427,6 +427,8 @@
   SPV_ENV_UNIVERSAL_1_3,  // SPIR-V 1.3 latest revision, no other restrictions.
   SPV_ENV_VULKAN_1_1,     // Vulkan 1.1 latest revision.
   SPV_ENV_WEBGPU_0,       // Work in progress WebGPU 1.0.
+  SPV_ENV_UNIVERSAL_1_4,  // SPIR-V 1.4 latest revision, no other restrictions.
+  SPV_ENV_VULKAN_1_1_SPIRV_1_4,  // Vulkan 1.1 with SPIR-V 1.4 binary.
 } spv_target_env;
 
 // SPIR-V Validator can be parameterized with the following Universal Limits.
diff --git a/source/ext_inst.cpp b/source/ext_inst.cpp
index 08c775e..1198f76 100644
--- a/source/ext_inst.cpp
+++ b/source/ext_inst.cpp
@@ -30,6 +30,7 @@
 #include "glsl.std.450.insts.inc"
 #include "opencl.std.insts.inc"
 
+#include "spirv-tools/libspirv.h"
 #include "spv-amd-gcn-shader.insts.inc"
 #include "spv-amd-shader-ballot.insts.inc"
 #include "spv-amd-shader-explicit-vertex-parameter.insts.inc"
@@ -80,7 +81,9 @@
     case SPV_ENV_OPENGL_4_5:
     case SPV_ENV_UNIVERSAL_1_3:
     case SPV_ENV_VULKAN_1_1:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
     case SPV_ENV_WEBGPU_0:
+    case SPV_ENV_UNIVERSAL_1_4:
       *pExtInstTable = &kTable_1_0;
       return SPV_SUCCESS;
     default:
diff --git a/source/spirv_target_env.cpp b/source/spirv_target_env.cpp
index 320b306..09a1219 100644
--- a/source/spirv_target_env.cpp
+++ b/source/spirv_target_env.cpp
@@ -15,6 +15,7 @@
 #include "source/spirv_target_env.h"
 
 #include <cstring>
+#include <string>
 
 #include "source/spirv_constant.h"
 #include "spirv-tools/libspirv.h"
@@ -61,6 +62,10 @@
       return "SPIR-V 1.3 (under Vulkan 1.1 semantics)";
     case SPV_ENV_WEBGPU_0:
       return "SPIR-V 1.3 (under WIP WebGPU semantics)";
+    case SPV_ENV_UNIVERSAL_1_4:
+      return "SPIR-V 1.4";
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
+      return "SPIR-V 1.4 (under Vulkan 1.1 semantics)";
   }
   return "";
 }
@@ -91,6 +96,9 @@
     case SPV_ENV_VULKAN_1_1:
     case SPV_ENV_WEBGPU_0:
       return SPV_SPIRV_VERSION_WORD(1, 3);
+    case SPV_ENV_UNIVERSAL_1_4:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
+      return SPV_SPIRV_VERSION_WORD(1, 4);
   }
   return SPV_SPIRV_VERSION_WORD(0, 0);
 }
@@ -99,7 +107,10 @@
   auto match = [s](const char* b) {
     return s && (0 == strncmp(s, b, strlen(b)));
   };
-  if (match("vulkan1.0")) {
+  if (match("vulkan1.1spv1.4")) {
+    if (env) *env = SPV_ENV_VULKAN_1_1_SPIRV_1_4;
+    return true;
+  } else if (match("vulkan1.0")) {
     if (env) *env = SPV_ENV_VULKAN_1_0;
     return true;
   } else if (match("vulkan1.1")) {
@@ -117,6 +128,9 @@
   } else if (match("spv1.3")) {
     if (env) *env = SPV_ENV_UNIVERSAL_1_3;
     return true;
+  } else if (match("spv1.4")) {
+    if (env) *env = SPV_ENV_UNIVERSAL_1_4;
+    return true;
   } else if (match("opencl1.2embedded")) {
     if (env) *env = SPV_ENV_OPENCL_EMBEDDED_1_2;
     return true;
@@ -185,9 +199,11 @@
     case SPV_ENV_OPENCL_EMBEDDED_2_2:
     case SPV_ENV_UNIVERSAL_1_3:
     case SPV_ENV_WEBGPU_0:
+    case SPV_ENV_UNIVERSAL_1_4:
       return false;
     case SPV_ENV_VULKAN_1_0:
     case SPV_ENV_VULKAN_1_1:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
       return true;
   }
   return false;
@@ -207,6 +223,8 @@
     case SPV_ENV_UNIVERSAL_1_3:
     case SPV_ENV_VULKAN_1_1:
     case SPV_ENV_WEBGPU_0:
+    case SPV_ENV_UNIVERSAL_1_4:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
       return false;
     case SPV_ENV_OPENCL_1_2:
     case SPV_ENV_OPENCL_EMBEDDED_1_2:
@@ -242,6 +260,8 @@
     case SPV_ENV_OPENCL_EMBEDDED_2_2:
     case SPV_ENV_OPENCL_2_1:
     case SPV_ENV_OPENCL_2_2:
+    case SPV_ENV_UNIVERSAL_1_4:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
       return false;
     case SPV_ENV_WEBGPU_0:
       return true;
@@ -273,7 +293,8 @@
       return "OpenGL";
     }
     case SPV_ENV_VULKAN_1_0:
-    case SPV_ENV_VULKAN_1_1: {
+    case SPV_ENV_VULKAN_1_1:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4: {
       return "Vulkan";
     }
     case SPV_ENV_WEBGPU_0: {
@@ -282,7 +303,8 @@
     case SPV_ENV_UNIVERSAL_1_0:
     case SPV_ENV_UNIVERSAL_1_1:
     case SPV_ENV_UNIVERSAL_1_2:
-    case SPV_ENV_UNIVERSAL_1_3: {
+    case SPV_ENV_UNIVERSAL_1_3:
+    case SPV_ENV_UNIVERSAL_1_4: {
       return "Universal";
     }
   }
diff --git a/source/table.cpp b/source/table.cpp
index b10d776..7890305 100644
--- a/source/table.cpp
+++ b/source/table.cpp
@@ -37,7 +37,9 @@
     case SPV_ENV_UNIVERSAL_1_2:
     case SPV_ENV_UNIVERSAL_1_3:
     case SPV_ENV_VULKAN_1_1:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
     case SPV_ENV_WEBGPU_0:
+    case SPV_ENV_UNIVERSAL_1_4:
       break;
     default:
       return nullptr;
diff --git a/source/val/validate.cpp b/source/val/validate.cpp
index 9e8d7fb..4a730da 100644
--- a/source/val/validate.cpp
+++ b/source/val/validate.cpp
@@ -14,10 +14,9 @@
 
 #include "source/val/validate.h"
 
+#include <algorithm>
 #include <cassert>
 #include <cstdio>
-
-#include <algorithm>
 #include <functional>
 #include <iterator>
 #include <memory>
@@ -52,21 +51,6 @@
 namespace val {
 namespace {
 
-// TODO(umar): Validate header
-// TODO(umar): The binary parser validates the magic word, and the length of the
-// header, but nothing else.
-spv_result_t setHeader(void* user_data, spv_endianness_t, uint32_t,
-                       uint32_t version, uint32_t generator, uint32_t id_bound,
-                       uint32_t) {
-  // Record the ID bound so that the validator can ensure no ID is out of bound.
-  ValidationState_t& _ = *(reinterpret_cast<ValidationState_t*>(user_data));
-  _.setIdBound(id_bound);
-  _.setGenerator(generator);
-  _.setVersion(version);
-
-  return SPV_SUCCESS;
-}
-
 // Parses OpExtension instruction and registers extension.
 void RegisterExtension(ValidationState_t& _,
                        const spv_parsed_instruction_t* inst) {
@@ -282,7 +266,8 @@
 
   // Parse the module and perform inline validation checks. These checks do
   // not require the the knowledge of the whole module.
-  if (auto error = spvBinaryParse(&context, vstate, words, num_words, setHeader,
+  if (auto error = spvBinaryParse(&context, vstate, words, num_words,
+                                  /*parsed_header =*/nullptr,
                                   ProcessInstruction, pDiagnostic)) {
     return error;
   }
diff --git a/source/val/validate_annotation.cpp b/source/val/validate_annotation.cpp
index 5963b24..4e8a2cd 100644
--- a/source/val/validate_annotation.cpp
+++ b/source/val/validate_annotation.cpp
@@ -185,6 +185,21 @@
   return "Unknown";
 }
 
+// Returns true if the decoration takes ID parameters.
+// TODO(dneto): This can be generated from the grammar.
+bool DecorationTakesIdParameters(uint32_t type) {
+  switch (static_cast<SpvDecoration>(type)) {
+    case SpvDecorationUniformId:
+    case SpvDecorationAlignmentId:
+    case SpvDecorationMaxByteOffsetId:
+    case SpvDecorationHlslCounterBufferGOOGLE:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
 spv_result_t ValidateDecorate(ValidationState_t& _, const Instruction* inst) {
   const auto decoration = inst->GetOperandAs<uint32_t>(1);
   if (decoration == SpvDecorationSpecId) {
@@ -205,10 +220,27 @@
            << "' is not valid for the WebGPU execution environment.";
   }
 
+  if (DecorationTakesIdParameters(decoration)) {
+    return _.diag(SPV_ERROR_INVALID_ID, inst)
+           << "Decorations taking ID parameters may not be used with "
+              "OpDecorateId";
+  }
   // TODO: Add validations for all decorations.
   return SPV_SUCCESS;
 }
 
+spv_result_t ValidateDecorateId(ValidationState_t& _, const Instruction* inst) {
+  const auto decoration = inst->GetOperandAs<uint32_t>(1);
+  if (!DecorationTakesIdParameters(decoration)) {
+    return _.diag(SPV_ERROR_INVALID_ID, inst)
+           << "Decorations that don't take ID parameters may not be used with "
+              "OpDecorateId";
+  }
+  // TODO: Add validations for these decorations.
+  // UniformId is covered elsewhere.
+  return SPV_SUCCESS;
+}
+
 spv_result_t ValidateMemberDecorate(ValidationState_t& _,
                                     const Instruction* inst) {
   const auto struct_type_id = inst->GetOperandAs<uint32_t>(0);
@@ -340,7 +372,8 @@
 spv_result_t RegisterDecorations(ValidationState_t& _,
                                  const Instruction* inst) {
   switch (inst->opcode()) {
-    case SpvOpDecorate: {
+    case SpvOpDecorate:
+    case SpvOpDecorateId: {
       const uint32_t target_id = inst->word(1);
       const SpvDecoration dec_type = static_cast<SpvDecoration>(inst->word(2));
       std::vector<uint32_t> dec_params;
@@ -415,6 +448,11 @@
     case SpvOpDecorate:
       if (auto error = ValidateDecorate(_, inst)) return error;
       break;
+    case SpvOpDecorateId:
+      if (auto error = ValidateDecorateId(_, inst)) return error;
+      break;
+    // TODO(dneto): SpvOpDecorateStringGOOGLE
+    // See https://github.com/KhronosGroup/SPIRV-Tools/issues/2253
     case SpvOpMemberDecorate:
       if (auto error = ValidateMemberDecorate(_, inst)) return error;
       break;
diff --git a/source/val/validate_barriers.cpp b/source/val/validate_barriers.cpp
index 4fbe9c9..b499c8c 100644
--- a/source/val/validate_barriers.cpp
+++ b/source/val/validate_barriers.cpp
@@ -14,8 +14,6 @@
 
 // Validates correctness of barrier SPIR-V instructions.
 
-#include "source/val/validate.h"
-
 #include <string>
 
 #include "source/diagnostic.h"
@@ -24,6 +22,7 @@
 #include "source/spirv_target_env.h"
 #include "source/util/bitutils.h"
 #include "source/val/instruction.h"
+#include "source/val/validate.h"
 #include "source/val/validate_memory_semantics.h"
 #include "source/val/validate_scopes.h"
 #include "source/val/validation_state.h"
@@ -38,8 +37,7 @@
 
   switch (opcode) {
     case SpvOpControlBarrier: {
-      if (spvVersionForTargetEnv(_.context()->target_env) <
-          SPV_SPIRV_VERSION_WORD(1, 3)) {
+      if (_.version() < SPV_SPIRV_VERSION_WORD(1, 3)) {
         _.function(inst->function()->id())
             ->RegisterExecutionModelLimitation(
                 [](SpvExecutionModel model, std::string* message) {
diff --git a/source/val/validate_composites.cpp b/source/val/validate_composites.cpp
index de3210e..1c1e77c 100644
--- a/source/val/validate_composites.cpp
+++ b/source/val/validate_composites.cpp
@@ -513,6 +513,92 @@
   return SPV_SUCCESS;
 }
 
+// Returns true if |lhs| and |rhs| logically match.
+// 1. Must both be either OpTypeArray or OpTypeStruct
+// 2. If OpTypeArray, then
+//  * Length must be the same
+//  * Element type must match or logically match
+// 3. If OpTypeStruct, then
+//  * Both have same number of elements
+//  * Element N for both structs must match or logically match
+bool LogicallyMatch(ValidationState_t& _, const Instruction* lhs,
+                    const Instruction* rhs) {
+  if (lhs->opcode() != rhs->opcode()) {
+    return false;
+  }
+
+  if (lhs->opcode() == SpvOpTypeArray) {
+    // Size operands must match.
+    if (lhs->GetOperandAs<uint32_t>(2u) != rhs->GetOperandAs<uint32_t>(2u)) {
+      return false;
+    }
+
+    // Elements must match or logically match.
+    const auto lhs_ele_id = lhs->GetOperandAs<uint32_t>(1u);
+    const auto rhs_ele_id = rhs->GetOperandAs<uint32_t>(1u);
+    if (lhs_ele_id == rhs_ele_id) {
+      return true;
+    }
+
+    const auto lhs_ele = _.FindDef(lhs_ele_id);
+    const auto rhs_ele = _.FindDef(rhs_ele_id);
+    if (!lhs_ele || !rhs_ele) {
+      return false;
+    }
+    return LogicallyMatch(_, lhs_ele, rhs_ele);
+  } else if (lhs->opcode() == SpvOpTypeStruct) {
+    // Number of elements must match.
+    if (lhs->operands().size() != rhs->operands().size()) {
+      return false;
+    }
+
+    for (size_t i = 1u; i < lhs->operands().size(); ++i) {
+      const auto lhs_ele_id = lhs->GetOperandAs<uint32_t>(i);
+      const auto rhs_ele_id = rhs->GetOperandAs<uint32_t>(i);
+      // Elements must match or logically match.
+      if (lhs_ele_id == rhs_ele_id) {
+        continue;
+      }
+
+      const auto lhs_ele = _.FindDef(lhs_ele_id);
+      const auto rhs_ele = _.FindDef(rhs_ele_id);
+      if (!lhs_ele || !rhs_ele) {
+        return false;
+      }
+
+      if (!LogicallyMatch(_, lhs_ele, rhs_ele)) {
+        return false;
+      }
+    }
+
+    // All checks passed.
+    return true;
+  }
+
+  // No other opcodes are acceptable at this point. Arrays and structs are
+  // caught above and if they're elements are not arrays or structs they are
+  // required to match exactly.
+  return false;
+}
+
+spv_result_t ValidateCopyLogical(ValidationState_t& _,
+                                 const Instruction* inst) {
+  const auto result_type = _.FindDef(inst->type_id());
+  const auto source = _.FindDef(inst->GetOperandAs<uint32_t>(2u));
+  const auto source_type = _.FindDef(source->type_id());
+  if (!source_type || !result_type || source_type == result_type) {
+    return _.diag(SPV_ERROR_INVALID_ID, inst)
+           << "Result Type must not equal the Operand type";
+  }
+
+  if (!LogicallyMatch(_, source_type, result_type)) {
+    return _.diag(SPV_ERROR_INVALID_ID, inst)
+           << "Result Type does not logically match the Operand type";
+  }
+
+  return SPV_SUCCESS;
+}
+
 }  // anonymous namespace
 
 // Validates correctness of composite instructions.
@@ -534,6 +620,8 @@
       return ValidateCopyObject(_, inst);
     case SpvOpTranspose:
       return ValidateTranspose(_, inst);
+    case SpvOpCopyLogical:
+      return ValidateCopyLogical(_, inst);
     default:
       break;
   }
diff --git a/source/val/validate_constants.cpp b/source/val/validate_constants.cpp
index c413b4f..191344e 100644
--- a/source/val/validate_constants.cpp
+++ b/source/val/validate_constants.cpp
@@ -364,7 +364,8 @@
       if (!_.features().uconvert_spec_constant_op &&
           !_.HasCapability(SpvCapabilityKernel)) {
         return _.diag(SPV_ERROR_INVALID_ID, inst)
-               << "UConvert requires Kernel capability or extension "
+               << "Prior to SPIR-V 1.4, specialization constant operation "
+                  "UConvert requires Kernel capability or extension "
                   "SPV_AMD_gpu_shader_int16";
       }
       break;
diff --git a/source/val/validate_decorations.cpp b/source/val/validate_decorations.cpp
index 1eef64d..8828672 100644
--- a/source/val/validate_decorations.cpp
+++ b/source/val/validate_decorations.cpp
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "source/val/validate.h"
-
 #include <algorithm>
 #include <cassert>
 #include <string>
@@ -25,8 +23,10 @@
 
 #include "source/diagnostic.h"
 #include "source/opcode.h"
+#include "source/spirv_constant.h"
 #include "source/spirv_target_env.h"
 #include "source/spirv_validator_options.h"
+#include "source/val/validate_scopes.h"
 #include "source/val/validation_state.h"
 
 namespace spvtools {
@@ -683,6 +683,7 @@
     const auto& descs = vstate.entry_point_descriptions(entry_point);
     int num_builtin_inputs = 0;
     int num_builtin_outputs = 0;
+    std::unordered_set<Instruction*> seen_vars;
     for (const auto& desc : descs) {
       for (auto interface : desc.interfaces) {
         Instruction* var_instr = vstate.FindDef(interface);
@@ -694,14 +695,30 @@
         }
         const SpvStorageClass storage_class =
             var_instr->GetOperandAs<SpvStorageClass>(2);
-        if (storage_class != SpvStorageClassInput &&
-            storage_class != SpvStorageClassOutput) {
-          return vstate.diag(SPV_ERROR_INVALID_ID, var_instr)
-                 << "OpEntryPoint interfaces must be OpVariables with "
-                    "Storage Class of Input(1) or Output(3). Found Storage "
-                    "Class "
-                 << storage_class << " for Entry Point id " << entry_point
-                 << ".";
+        if (vstate.version() >= SPV_SPIRV_VERSION_WORD(1, 4)) {
+          // Starting in 1.4, OpEntryPoint must list all global variables
+          // it statically uses and those interfaces must be unique.
+          if (storage_class == SpvStorageClassFunction) {
+            return vstate.diag(SPV_ERROR_INVALID_ID, var_instr)
+                   << "OpEntryPoint interfaces should only list global "
+                      "variables";
+          }
+
+          if (!seen_vars.insert(var_instr).second) {
+            return vstate.diag(SPV_ERROR_INVALID_ID, var_instr)
+                   << "Non-unique OpEntryPoint interface "
+                   << vstate.getIdName(interface) << " is disallowed";
+          }
+        } else {
+          if (storage_class != SpvStorageClassInput &&
+              storage_class != SpvStorageClassOutput) {
+            return vstate.diag(SPV_ERROR_INVALID_ID, var_instr)
+                   << "OpEntryPoint interfaces must be OpVariables with "
+                      "Storage Class of Input(1) or Output(3). Found Storage "
+                      "Class "
+                   << storage_class << " for Entry Point id " << entry_point
+                   << ".";
+          }
         }
 
         const uint32_t ptr_id = var_instr->word(1);
@@ -964,10 +981,12 @@
             vstate.RegisterPointerToUniformBlock(ptrInst->id());
             vstate.RegisterStructForUniformBlock(id);
           }
-          if ((uniform && bufferDeco) || (storage_buffer && blockDeco)) {
+          if ((uniform && bufferDeco) ||
+              ((storage_buffer || phys_storage_buffer) && blockDeco)) {
             vstate.RegisterPointerToStorageBuffer(ptrInst->id());
             vstate.RegisterStructForStorageBuffer(id);
           }
+
           if (blockRules || bufferRules) {
             const char* deco_str = blockDeco ? "Block" : "BufferBlock";
             spv_result_t recursive_status = SPV_SUCCESS;
@@ -1257,34 +1276,51 @@
   if (decoration.struct_member_index() == Decoration::kInvalidMember) {
     // The target must be a memory object declaration.
     // First, it must be a variable or function parameter.
-    if (inst.opcode() != SpvOpVariable &&
-        inst.opcode() != SpvOpFunctionParameter) {
+    const auto opcode = inst.opcode();
+    const auto type_id = inst.type_id();
+    if (opcode != SpvOpVariable && opcode != SpvOpFunctionParameter) {
       return vstate.diag(SPV_ERROR_INVALID_ID, &inst)
              << "Target of NonWritable decoration must be a memory object "
                 "declaration (a variable or a function parameter)";
     }
-    // Second, it must point to a UBO, SSBO, or storage image.
-    const auto type_id = inst.type_id();
-    if (!vstate.IsPointerToUniformBlock(type_id) &&
-        !vstate.IsPointerToStorageBuffer(type_id) &&
-        !vstate.IsPointerToStorageImage(type_id)) {
+    const auto var_storage_class = opcode == SpvOpVariable
+                                       ? inst.GetOperandAs<SpvStorageClass>(2)
+                                       : SpvStorageClassMax;
+    if ((var_storage_class == SpvStorageClassFunction ||
+         var_storage_class == SpvStorageClassPrivate) &&
+        vstate.features().nonwritable_var_in_function_or_private) {
+      // New permitted feature in SPIR-V 1.4.
+    } else if (
+        // It may point to a UBO, SSBO, or storage image.
+        vstate.IsPointerToUniformBlock(type_id) ||
+        vstate.IsPointerToStorageBuffer(type_id) ||
+        vstate.IsPointerToStorageImage(type_id)) {
+    } else {
       return vstate.diag(SPV_ERROR_INVALID_ID, &inst)
              << "Target of NonWritable decoration is invalid: must point to a "
-                "storage image, uniform block, or storage buffer";
+                "storage image, uniform block, "
+             << (vstate.features().nonwritable_var_in_function_or_private
+                     ? "storage buffer, or variable in Private or Function "
+                       "storage class"
+                     : "or storage buffer");
     }
   }
 
   return SPV_SUCCESS;
 }
 
-// Returns SPV_SUCCESS if validation rules are satisfied for Uniform
-// decorations. Otherwise emits a diagnostic and returns something other than
-// SPV_SUCCESS. Assumes each decoration on a group has been propagated down to
-// the group members.
+// Returns SPV_SUCCESS if validation rules are satisfied for Uniform or
+// UniformId decorations. Otherwise emits a diagnostic and returns something
+// other than SPV_SUCCESS. Assumes each decoration on a group has been
+// propagated down to the group members.  The |inst| parameter is the object
+// being decorated.
 spv_result_t CheckUniformDecoration(ValidationState_t& vstate,
                                     const Instruction& inst,
-                                    const Decoration&) {
-  // Uniform must decorate an "object"
+                                    const Decoration& decoration) {
+  const char* const dec_name =
+      decoration.dec_type() == SpvDecorationUniform ? "Uniform" : "UniformId";
+
+  // Uniform or UniformId must decorate an "object"
   //  - has a result ID
   //  - is an instantiation of a non-void type.  So it has a type ID, and that
   //  type is not void.
@@ -1293,19 +1329,33 @@
 
   if (inst.type_id() == 0) {
     return vstate.diag(SPV_ERROR_INVALID_ID, &inst)
-           << "Uniform decoration applied to a non-object";
+           << dec_name << " decoration applied to a non-object";
   }
   if (Instruction* type_inst = vstate.FindDef(inst.type_id())) {
     if (type_inst->opcode() == SpvOpTypeVoid) {
       return vstate.diag(SPV_ERROR_INVALID_ID, &inst)
-             << "Uniform decoration applied to a value with void type";
+             << dec_name << " decoration applied to a value with void type";
     }
   } else {
     // We might never get here because this would have been rejected earlier in
     // the flow.
     return vstate.diag(SPV_ERROR_INVALID_ID, &inst)
-           << "Uniform decoration applied to an object with invalid type";
+           << dec_name << " decoration applied to an object with invalid type";
   }
+
+  // Use of Uniform with OpDecorate is checked elsewhere.
+  // Use of UniformId with OpDecorateId is checked elsewhere.
+
+  if (decoration.dec_type() == SpvDecorationUniformId) {
+    assert(decoration.params().size() == 1 &&
+           "Grammar ensures UniformId has one parameter");
+
+    // The scope id is an execution scope.
+    if (auto error =
+            ValidateExecutionScope(vstate, &inst, decoration.params()[0]))
+      return error;
+  }
+
   return SPV_SUCCESS;
 }
 
@@ -1365,7 +1415,6 @@
     // been propagated down to the group members.
     if (inst->opcode() == SpvOpDecorationGroup) continue;
 
-    // Validates FPRoundingMode decoration
     for (const auto& decoration : decorations) {
       switch (decoration.dec_type()) {
         case SpvDecorationFPRoundingMode:
@@ -1376,6 +1425,7 @@
           PASS_OR_BAIL(CheckNonWritableDecoration(vstate, *inst, decoration));
           break;
         case SpvDecorationUniform:
+        case SpvDecorationUniformId:
           PASS_OR_BAIL(CheckUniformDecoration(vstate, *inst, decoration));
           break;
         case SpvDecorationNoSignedWrap:
diff --git a/source/val/validate_image.cpp b/source/val/validate_image.cpp
index a103693..ebf9ae0 100644
--- a/source/val/validate_image.cpp
+++ b/source/val/validate_image.cpp
@@ -63,6 +63,8 @@
     case SpvImageOperandsMakeTexelVisibleKHRMask:
     case SpvImageOperandsNonPrivateTexelKHRMask:
     case SpvImageOperandsVolatileTexelKHRMask:
+    case SpvImageOperandsSignExtendMask:
+    case SpvImageOperandsZeroExtendMask:
       return true;
   }
   return false;
@@ -218,10 +220,12 @@
   const SpvOp opcode = inst->opcode();
   const size_t num_words = inst->words().size();
 
-  // NonPrivate and Volatile take no operand words.
+  // NonPrivate, Volatile, SignExtend, ZeroExtend take no operand words.
   const uint32_t mask_bits_having_operands =
       mask & ~uint32_t(SpvImageOperandsNonPrivateTexelKHRMask |
-                       SpvImageOperandsVolatileTexelKHRMask);
+                       SpvImageOperandsVolatileTexelKHRMask |
+                       SpvImageOperandsSignExtendMask |
+                       SpvImageOperandsZeroExtendMask);
   size_t expected_num_image_operand_words =
       spvtools::utils::CountSetBits(mask_bits_having_operands);
   if (mask & SpvImageOperandsGradMask) {
@@ -541,6 +545,32 @@
     if (auto error = ValidateMemoryScope(_, inst, visible_scope)) return error;
   }
 
+  if (mask & SpvImageOperandsSignExtendMask) {
+    // Checked elsewhere: SPIR-V 1.4 version or later.
+
+    // "The texel value is converted to the target value via sign extension.
+    // Only valid when the texel type is a scalar or vector of integer type."
+    //
+    // We don't have enough information to know what the texel type is.
+    // In OpenCL, knowledge is deferred until runtime: the image SampledType is
+    // void, and the Format is Unknown.
+    // In Vulkan, the texel type is only known in all cases by the pipeline
+    // setup.
+  }
+
+  if (mask & SpvImageOperandsZeroExtendMask) {
+    // Checked elsewhere: SPIR-V 1.4 version or later.
+
+    // "The texel value is converted to the target value via zero extension.
+    // Only valid when the texel type is a scalar or vector of integer type."
+    //
+    // We don't have enough information to know what the texel type is.
+    // In OpenCL, knowledge is deferred until runtime: the image SampledType is
+    // void, and the Format is Unknown.
+    // In Vulkan, the texel type is only known in all cases by the pipeline
+    // setup.
+  }
+
   return SPV_SUCCESS;
 }
 
diff --git a/source/val/validate_instruction.cpp b/source/val/validate_instruction.cpp
index a626103..655cb02 100644
--- a/source/val/validate_instruction.cpp
+++ b/source/val/validate_instruction.cpp
@@ -14,10 +14,9 @@
 
 // Performs validation on instructions that appear inside of a SPIR-V block.
 
-#include "source/val/validate.h"
-
 #include <algorithm>
 #include <cassert>
+#include <iomanip>
 #include <sstream>
 #include <string>
 #include <vector>
@@ -35,6 +34,7 @@
 #include "source/spirv_validator_options.h"
 #include "source/util/string_utils.h"
 #include "source/val/function.h"
+#include "source/val/validate.h"
 #include "source/val/validation_state.h"
 
 namespace spvtools {
@@ -98,21 +98,59 @@
   return CapabilitySet();
 }
 
+// Returns SPV_SUCCESS if, for the given operand, the target environment
+// satsifies minimum version requirements, or if the module declares an
+// enabling extension for the operand.  Otherwise emit a diagnostic and
+// return an error code.
+spv_result_t OperandVersionExtensionCheck(
+    ValidationState_t& _, const Instruction* inst, size_t which_operand,
+    const spv_operand_desc_t& operand_desc, uint32_t word) {
+  const uint32_t module_version = _.version();
+  const uint32_t operand_min_version = operand_desc.minVersion;
+  const bool reserved = operand_min_version == 0xffffffffu;
+  const bool version_satisfied =
+      !reserved && (operand_min_version <= module_version);
+
+  if (version_satisfied) {
+    return SPV_SUCCESS;
+  }
+  if (!reserved && operand_desc.numExtensions == 0) {
+    return _.diag(SPV_ERROR_WRONG_VERSION, inst)
+           << spvtools::utils::CardinalToOrdinal(which_operand)
+           << " operand of " << spvOpcodeString(inst->opcode()) << ": operand "
+           << operand_desc.name << "(" << word << ") requires SPIR-V version "
+           << SPV_SPIRV_VERSION_MAJOR_PART(operand_min_version) << "."
+           << SPV_SPIRV_VERSION_MINOR_PART(operand_min_version) << " or later";
+  } else {
+    ExtensionSet required_extensions(operand_desc.numExtensions,
+                                     operand_desc.extensions);
+    if (!_.HasAnyOfExtensions(required_extensions)) {
+      return _.diag(SPV_ERROR_MISSING_EXTENSION, inst)
+             << spvtools::utils::CardinalToOrdinal(which_operand)
+             << " operand of " << spvOpcodeString(inst->opcode())
+             << ": operand " << operand_desc.name << "(" << word
+             << ") requires one of these extensions: "
+             << ExtensionSetToString(required_extensions);
+    }
+  }
+  return SPV_SUCCESS;
+}
+
 // Returns SPV_SUCCESS if the given operand is enabled by capabilities declared
 // in the module.  Otherwise issues an error message and returns
 // SPV_ERROR_INVALID_CAPABILITY.
 spv_result_t CheckRequiredCapabilities(ValidationState_t& state,
                                        const Instruction* inst,
                                        size_t which_operand,
-                                       spv_operand_type_t type,
-                                       uint32_t operand) {
+                                       const spv_parsed_operand_t& operand,
+                                       uint32_t word) {
   // Mere mention of PointSize, ClipDistance, or CullDistance in a Builtin
   // decoration does not require the associated capability.  The use of such
   // a variable value should trigger the capability requirement, but that's
   // not implemented yet.  This rule is independent of target environment.
   // See https://github.com/KhronosGroup/SPIRV-Tools/issues/365
-  if (type == SPV_OPERAND_TYPE_BUILT_IN) {
-    switch (operand) {
+  if (operand.type == SPV_OPERAND_TYPE_BUILT_IN) {
+    switch (word) {
       case SpvBuiltInPointSize:
       case SpvBuiltInClipDistance:
       case SpvBuiltInCullDistance:
@@ -120,14 +158,14 @@
       default:
         break;
     }
-  } else if (type == SPV_OPERAND_TYPE_FP_ROUNDING_MODE) {
+  } else if (operand.type == SPV_OPERAND_TYPE_FP_ROUNDING_MODE) {
     // Allow all FP rounding modes if requested
     if (state.features().free_fp_rounding_mode) {
       return SPV_SUCCESS;
     }
-  } else if (type == SPV_OPERAND_TYPE_GROUP_OPERATION &&
+  } else if (operand.type == SPV_OPERAND_TYPE_GROUP_OPERATION &&
              state.features().group_ops_reduce_and_scans &&
-             (operand <= uint32_t(SpvGroupOperationExclusiveScan))) {
+             (word <= uint32_t(SpvGroupOperationExclusiveScan))) {
     // Allow certain group operations if requested.
     return SPV_SUCCESS;
   }
@@ -135,10 +173,10 @@
   CapabilitySet enabling_capabilities;
   spv_operand_desc operand_desc = nullptr;
   const auto lookup_result =
-      state.grammar().lookupOperand(type, operand, &operand_desc);
+      state.grammar().lookupOperand(operand.type, word, &operand_desc);
   if (lookup_result == SPV_SUCCESS) {
     // Allow FPRoundingMode decoration if requested.
-    if (type == SPV_OPERAND_TYPE_DECORATION &&
+    if (operand.type == SPV_OPERAND_TYPE_DECORATION &&
         operand_desc->value == SpvDecorationFPRoundingMode) {
       if (state.features().free_fp_rounding_mode) return SPV_SUCCESS;
 
@@ -161,29 +199,13 @@
              << " requires one of these capabilities: "
              << ToString(enabling_capabilities, state.grammar());
     }
+    return OperandVersionExtensionCheck(state, inst, which_operand,
+                                        *operand_desc, word);
   }
 
   return SPV_SUCCESS;
 }
 
-// Returns operand's required extensions.
-ExtensionSet RequiredExtensions(const ValidationState_t& state,
-                                spv_operand_type_t type, uint32_t operand) {
-  spv_operand_desc operand_desc;
-  if (state.grammar().lookupOperand(type, operand, &operand_desc) ==
-      SPV_SUCCESS) {
-    assert(operand_desc);
-    // If this operand is incorporated into core SPIR-V before or in the current
-    // target environment, we don't require extensions anymore.
-    if (spvVersionForTargetEnv(state.grammar().target_env()) >=
-        operand_desc->minVersion)
-      return {};
-    return {operand_desc->numExtensions, operand_desc->extensions};
-  }
-
-  return {};
-}
-
 // Returns SPV_ERROR_INVALID_BINARY and emits a diagnostic if the instruction
 // is explicitly reserved in the SPIR-V core spec.  Otherwise return
 // SPV_SUCCESS.
@@ -244,7 +266,7 @@
       for (uint32_t mask_bit = 0x80000000; mask_bit; mask_bit >>= 1) {
         if (word & mask_bit) {
           spv_result_t status =
-              CheckRequiredCapabilities(_, inst, i + 1, operand.type, mask_bit);
+              CheckRequiredCapabilities(_, inst, i + 1, operand, mask_bit);
           if (status != SPV_SUCCESS) return status;
         }
       }
@@ -255,34 +277,13 @@
     } else {
       // Check the operand word as a whole.
       spv_result_t status =
-          CheckRequiredCapabilities(_, inst, i + 1, operand.type, word);
+          CheckRequiredCapabilities(_, inst, i + 1, operand, word);
       if (status != SPV_SUCCESS) return status;
     }
   }
   return SPV_SUCCESS;
 }
 
-// Checks that all extensions required by the given instruction's operands were
-// declared in the module.
-spv_result_t ExtensionCheck(ValidationState_t& _, const Instruction* inst) {
-  const SpvOp opcode = inst->opcode();
-  for (size_t operand_index = 0; operand_index < inst->operands().size();
-       ++operand_index) {
-    const auto& operand = inst->operand(operand_index);
-    const uint32_t word = inst->word(operand.offset);
-    const ExtensionSet required_extensions =
-        RequiredExtensions(_, operand.type, word);
-    if (!_.HasAnyOfExtensions(required_extensions)) {
-      return _.diag(SPV_ERROR_MISSING_EXTENSION, inst)
-             << spvtools::utils::CardinalToOrdinal(operand_index + 1)
-             << " operand of " << spvOpcodeString(opcode) << ": operand "
-             << word << " requires one of these extensions: "
-             << ExtensionSetToString(required_extensions);
-    }
-  }
-  return SPV_SUCCESS;
-}
-
 // Checks that the instruction can be used in this target environment's base
 // version. Assumes that CapabilityCheck has checked direct capability
 // dependencies for the opcode.
@@ -301,24 +302,27 @@
     return SPV_SUCCESS;
   }
 
+  const auto module_version = _.version();
+
   ExtensionSet exts(inst_desc->numExtensions, inst_desc->extensions);
   if (exts.IsEmpty()) {
-    // If no extensions can enable this instruction, then emit error messages
-    // only concerning core SPIR-V versions if errors happen.
+    // If no extensions can enable this instruction, then emit error
+    // messages only concerning core SPIR-V versions if errors happen.
     if (min_version == ~0u) {
       return _.diag(SPV_ERROR_WRONG_VERSION, inst)
              << spvOpcodeString(opcode) << " is reserved for future use.";
     }
 
-    if (spvVersionForTargetEnv(_.grammar().target_env()) < min_version) {
+    if (module_version < min_version) {
       return _.diag(SPV_ERROR_WRONG_VERSION, inst)
              << spvOpcodeString(opcode) << " requires "
              << spvTargetEnvDescription(
                     static_cast<spv_target_env>(min_version))
              << " at minimum.";
     }
-  // Otherwise, we only error out when no enabling extensions are registered.
   } else if (!_.HasAnyOfExtensions(exts)) {
+    // Otherwise, we only error out when no enabling extensions are
+    // registered.
     if (min_version == ~0u) {
       return _.diag(SPV_ERROR_MISSING_EXTENSION, inst)
              << spvOpcodeString(opcode)
@@ -326,11 +330,11 @@
              << ExtensionSetToString(exts);
     }
 
-    if (static_cast<uint32_t>(_.grammar().target_env()) < min_version) {
+    if (module_version < min_version) {
       return _.diag(SPV_ERROR_WRONG_VERSION, inst)
-             << spvOpcodeString(opcode) << " requires "
-             << spvTargetEnvDescription(
-                    static_cast<spv_target_env>(min_version))
+             << spvOpcodeString(opcode) << " requires SPIR-V version "
+             << SPV_SPIRV_VERSION_MAJOR_PART(min_version) << "."
+             << SPV_SPIRV_VERSION_MINOR_PART(min_version)
              << " at minimum or one of the following extensions: "
              << ExtensionSetToString(exts);
     }
@@ -367,11 +371,10 @@
 
   // Section 2.17 of SPIRV Spec specifies that the "Structure Nesting Depth"
   // must be less than or equal to 255.
-  // This is interpreted as structures including other structures as members.
-  // The code does not follow pointers or look into arrays to see if we reach a
-  // structure downstream.
-  // The nesting depth of a struct is 1+(largest depth of any member).
-  // Scalars are at depth 0.
+  // This is interpreted as structures including other structures as
+  // members. The code does not follow pointers or look into arrays to see
+  // if we reach a structure downstream. The nesting depth of a struct is
+  // 1+(largest depth of any member). Scalars are at depth 0.
   uint32_t max_member_depth = 0;
   // Struct members start at word 2 of OpTypeStruct instruction.
   for (size_t word_i = 2; word_i < inst->words().size(); ++word_i) {
@@ -394,8 +397,8 @@
   return SPV_SUCCESS;
 }
 
-// Checks that the number of (literal, label) pairs in OpSwitch is within the
-// limit.
+// Checks that the number of (literal, label) pairs in OpSwitch is within
+// the limit.
 spv_result_t LimitCheckSwitch(ValidationState_t& _, const Instruction* inst) {
   if (SpvOpSwitch == inst->opcode()) {
     // The instruction syntax is as follows:
@@ -414,7 +417,8 @@
   return SPV_SUCCESS;
 }
 
-// Ensure the number of variables of the given class does not exceed the limit.
+// Ensure the number of variables of the given class does not exceed the
+// limit.
 spv_result_t LimitCheckNumVars(ValidationState_t& _, const uint32_t var_id,
                                const SpvStorageClass storage_class) {
   if (SpvStorageClassFunction == storage_class) {
@@ -472,7 +476,8 @@
     if (_.memory_model() != SpvMemoryModelVulkanKHR &&
         _.HasCapability(SpvCapabilityVulkanMemoryModelKHR)) {
       return _.diag(SPV_ERROR_INVALID_DATA, inst)
-             << "VulkanMemoryModelKHR capability must only be specified if the "
+             << "VulkanMemoryModelKHR capability must only be specified if "
+                "the "
                 "VulkanKHR memory model is used.";
     }
 
@@ -548,7 +553,6 @@
               "capability is used.";
   }
 
-  if (auto error = ExtensionCheck(_, inst)) return error;
   if (auto error = ReservedCheck(_, inst)) return error;
   if (auto error = EnvironmentCheck(_, inst)) return error;
   if (auto error = CapabilityCheck(_, inst)) return error;
diff --git a/source/val/validate_interfaces.cpp b/source/val/validate_interfaces.cpp
index fffc6da..c85b673 100644
--- a/source/val/validate_interfaces.cpp
+++ b/source/val/validate_interfaces.cpp
@@ -12,14 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "source/val/validate.h"
-
 #include <algorithm>
 #include <vector>
 
 #include "source/diagnostic.h"
+#include "source/spirv_constant.h"
+#include "source/spirv_target_env.h"
 #include "source/val/function.h"
 #include "source/val/instruction.h"
+#include "source/val/validate.h"
 #include "source/val/validation_state.h"
 
 namespace spvtools {
@@ -27,10 +28,16 @@
 namespace {
 
 // Returns true if \c inst is an input or output variable.
-bool is_interface_variable(const Instruction* inst) {
-  return inst->opcode() == SpvOpVariable &&
-         (inst->word(3u) == SpvStorageClassInput ||
-          inst->word(3u) == SpvStorageClassOutput);
+bool is_interface_variable(const Instruction* inst, bool is_spv_1_4) {
+  if (is_spv_1_4) {
+    // Starting in SPIR-V 1.4, all global variables are interface variables.
+    return inst->opcode() == SpvOpVariable &&
+           inst->word(3u) != SpvStorageClassFunction;
+  } else {
+    return inst->opcode() == SpvOpVariable &&
+           (inst->word(3u) == SpvStorageClassInput ||
+            inst->word(3u) == SpvStorageClassOutput);
+  }
 }
 
 // Checks that \c var is listed as an interface in all the entry points that use
@@ -85,9 +92,8 @@
       }
       if (!found) {
         return _.diag(SPV_ERROR_INVALID_ID, var)
-               << (var->word(3u) == SpvStorageClassInput ? "Input" : "Output")
-               << " variable id <" << var->id() << "> is used by entry point '"
-               << desc.name << "' id <" << id
+               << "Interface variable id <" << var->id()
+               << "> is used by entry point '" << desc.name << "' id <" << id
                << ">, but is not listed as an interface";
       }
     }
@@ -99,8 +105,9 @@
 }  // namespace
 
 spv_result_t ValidateInterfaces(ValidationState_t& _) {
+  bool is_spv_1_4 = _.version() >= SPV_SPIRV_VERSION_WORD(1, 4);
   for (auto& inst : _.ordered_instructions()) {
-    if (is_interface_variable(&inst)) {
+    if (is_interface_variable(&inst, is_spv_1_4)) {
       if (auto error = check_interface_variable(_, &inst)) {
         return error;
       }
diff --git a/source/val/validate_logicals.cpp b/source/val/validate_logicals.cpp
index a25460b..5886dbf 100644
--- a/source/val/validate_logicals.cpp
+++ b/source/val/validate_logicals.cpp
@@ -151,6 +151,14 @@
         const Instruction* type_inst = _.FindDef(result_type);
         assert(type_inst);
 
+        const auto composites = _.features().select_between_composites;
+        auto fail = [&_, composites, inst, opcode]() -> spv_result_t {
+          return _.diag(SPV_ERROR_INVALID_DATA, inst)
+                 << "Expected scalar or "
+                 << (composites ? "composite" : "vector")
+                 << " type as Result Type: " << spvOpcodeString(opcode);
+        };
+
         const SpvOp type_opcode = type_inst->opcode();
         switch (type_opcode) {
           case SpvOpTypePointer: {
@@ -174,35 +182,48 @@
             break;
           }
 
-          default: {
+          // Not RuntimeArray because of other rules.
+          case SpvOpTypeArray:
+          case SpvOpTypeMatrix:
+          case SpvOpTypeStruct: {
+            if (!composites) return fail();
+            break;
+          };
+
+          default:
+            return fail();
+        }
+
+        const uint32_t condition_type = _.GetOperandTypeId(inst, 2);
+        const uint32_t left_type = _.GetOperandTypeId(inst, 3);
+        const uint32_t right_type = _.GetOperandTypeId(inst, 4);
+
+        if (!condition_type || (!_.IsBoolScalarType(condition_type) &&
+                                !_.IsBoolVectorType(condition_type)))
+          return _.diag(SPV_ERROR_INVALID_DATA, inst)
+                 << "Expected bool scalar or vector type as condition: "
+                 << spvOpcodeString(opcode);
+
+        if (_.GetDimension(condition_type) != dimension) {
+          // If the condition is a vector type, then the result must also be a
+          // vector with matching dimensions. In SPIR-V 1.4, a scalar condition
+          // can be used to select between vector types. |composites| is a
+          // proxy for SPIR-V 1.4 functionality.
+          if (!composites || _.IsBoolVectorType(condition_type)) {
             return _.diag(SPV_ERROR_INVALID_DATA, inst)
-                   << "Expected scalar or vector type as Result Type: "
+                   << "Expected vector sizes of Result Type and the condition "
+                      "to be equal: "
                    << spvOpcodeString(opcode);
           }
         }
+
+        if (result_type != left_type || result_type != right_type)
+          return _.diag(SPV_ERROR_INVALID_DATA, inst)
+                 << "Expected both objects to be of Result Type: "
+                 << spvOpcodeString(opcode);
+
+        break;
       }
-
-      const uint32_t condition_type = _.GetOperandTypeId(inst, 2);
-      const uint32_t left_type = _.GetOperandTypeId(inst, 3);
-      const uint32_t right_type = _.GetOperandTypeId(inst, 4);
-
-      if (!condition_type || (!_.IsBoolScalarType(condition_type) &&
-                              !_.IsBoolVectorType(condition_type)))
-        return _.diag(SPV_ERROR_INVALID_DATA, inst)
-               << "Expected bool scalar or vector type as condition: "
-               << spvOpcodeString(opcode);
-
-      if (_.GetDimension(condition_type) != dimension)
-        return _.diag(SPV_ERROR_INVALID_DATA, inst)
-               << "Expected vector sizes of Result Type and the condition to be"
-               << " equal: " << spvOpcodeString(opcode);
-
-      if (result_type != left_type || result_type != right_type)
-        return _.diag(SPV_ERROR_INVALID_DATA, inst)
-               << "Expected both objects to be of Result Type: "
-               << spvOpcodeString(opcode);
-
-      break;
     }
 
     case SpvOpIEqual:
diff --git a/source/val/validate_memory.cpp b/source/val/validate_memory.cpp
index f6127a1..e141309 100644
--- a/source/val/validate_memory.cpp
+++ b/source/val/validate_memory.cpp
@@ -263,59 +263,40 @@
   return std::make_pair(dst_sc, src_sc);
 }
 
+// Returns the number of instruction words taken up by a memory access
+// argument and its implied operands.
+int MemoryAccessNumWords(uint32_t mask) {
+  int result = 1;  // Count the mask
+  if (mask & SpvMemoryAccessAlignedMask) ++result;
+  if (mask & SpvMemoryAccessMakePointerAvailableKHRMask) ++result;
+  if (mask & SpvMemoryAccessMakePointerVisibleKHRMask) ++result;
+  return result;
+}
+
+// Returns the scope ID operand for MakeAvailable memory access with mask
+// at the given operand index.
 // This function is only called for OpLoad, OpStore, OpCopyMemory and
 // OpCopyMemorySized, OpCooperativeMatrixLoadNV, and
 // OpCooperativeMatrixStoreNV.
-uint32_t GetMakeAvailableScope(const Instruction* inst, uint32_t mask) {
-  uint32_t offset = 1;
-  if (mask & SpvMemoryAccessAlignedMask) ++offset;
-
-  uint32_t scope_id = 0;
-  switch (inst->opcode()) {
-    case SpvOpLoad:
-    case SpvOpCopyMemorySized:
-      return inst->GetOperandAs<uint32_t>(3 + offset);
-    case SpvOpStore:
-    case SpvOpCopyMemory:
-      return inst->GetOperandAs<uint32_t>(2 + offset);
-    case SpvOpCooperativeMatrixLoadNV:
-      return inst->GetOperandAs<uint32_t>(5 + offset);
-    case SpvOpCooperativeMatrixStoreNV:
-      return inst->GetOperandAs<uint32_t>(4 + offset);
-    default:
-      assert(false && "unexpected opcode");
-      break;
-  }
-
-  return scope_id;
+uint32_t GetMakeAvailableScope(const Instruction* inst, uint32_t mask,
+                               uint32_t mask_index) {
+  assert(mask & SpvMemoryAccessMakePointerAvailableKHRMask);
+  uint32_t this_bit = uint32_t(SpvMemoryAccessMakePointerAvailableKHRMask);
+  uint32_t index =
+      mask_index - 1 + MemoryAccessNumWords(mask & (this_bit | (this_bit - 1)));
+  return inst->GetOperandAs<uint32_t>(index);
 }
 
 // This function is only called for OpLoad, OpStore, OpCopyMemory,
 // OpCopyMemorySized, OpCooperativeMatrixLoadNV, and
 // OpCooperativeMatrixStoreNV.
-uint32_t GetMakeVisibleScope(const Instruction* inst, uint32_t mask) {
-  uint32_t offset = 1;
-  if (mask & SpvMemoryAccessAlignedMask) ++offset;
-  if (mask & SpvMemoryAccessMakePointerAvailableKHRMask) ++offset;
-
-  uint32_t scope_id = 0;
-  switch (inst->opcode()) {
-    case SpvOpLoad:
-    case SpvOpCopyMemorySized:
-      return inst->GetOperandAs<uint32_t>(3 + offset);
-    case SpvOpStore:
-    case SpvOpCopyMemory:
-      return inst->GetOperandAs<uint32_t>(2 + offset);
-    case SpvOpCooperativeMatrixLoadNV:
-      return inst->GetOperandAs<uint32_t>(5 + offset);
-    case SpvOpCooperativeMatrixStoreNV:
-      return inst->GetOperandAs<uint32_t>(4 + offset);
-    default:
-      assert(false && "unexpected opcode");
-      break;
-  }
-
-  return scope_id;
+uint32_t GetMakeVisibleScope(const Instruction* inst, uint32_t mask,
+                             uint32_t mask_index) {
+  assert(mask & SpvMemoryAccessMakePointerVisibleKHRMask);
+  uint32_t this_bit = uint32_t(SpvMemoryAccessMakePointerVisibleKHRMask);
+  uint32_t index =
+      mask_index - 1 + MemoryAccessNumWords(mask & (this_bit | (this_bit - 1)));
+  return inst->GetOperandAs<uint32_t>(index);
 }
 
 bool DoesStructContainRTA(const ValidationState_t& _, const Instruction* inst) {
@@ -342,7 +323,7 @@
     return SPV_SUCCESS;
   }
 
-  uint32_t mask = inst->GetOperandAs<uint32_t>(index);
+  const uint32_t mask = inst->GetOperandAs<uint32_t>(index);
   if (mask & SpvMemoryAccessMakePointerAvailableKHRMask) {
     if (inst->opcode() == SpvOpLoad ||
         inst->opcode() == SpvOpCooperativeMatrixLoadNV) {
@@ -357,7 +338,7 @@
     }
 
     // Check the associated scope for MakeAvailableKHR.
-    const auto available_scope = GetMakeAvailableScope(inst, mask);
+    const auto available_scope = GetMakeAvailableScope(inst, mask, index);
     if (auto error = ValidateMemoryScope(_, inst, available_scope))
       return error;
   }
@@ -376,7 +357,7 @@
     }
 
     // Check the associated scope for MakeVisibleKHR.
-    const auto visible_scope = GetMakeVisibleScope(inst, mask);
+    const auto visible_scope = GetMakeVisibleScope(inst, mask, index);
     if (auto error = ValidateMemoryScope(_, inst, visible_scope)) return error;
   }
 
@@ -863,6 +844,51 @@
   return SPV_SUCCESS;
 }
 
+spv_result_t ValidateCopyMemoryMemoryAccess(ValidationState_t& _,
+                                            const Instruction* inst) {
+  assert(inst->opcode() == SpvOpCopyMemory ||
+         inst->opcode() == SpvOpCopyMemorySized);
+  const uint32_t first_access_index = inst->opcode() == SpvOpCopyMemory ? 2 : 3;
+  if (inst->operands().size() > first_access_index) {
+    if (auto error = CheckMemoryAccess(_, inst, first_access_index))
+      return error;
+
+    const auto first_access = inst->GetOperandAs<uint32_t>(first_access_index);
+    const uint32_t second_access_index =
+        first_access_index + MemoryAccessNumWords(first_access);
+    if (inst->operands().size() > second_access_index) {
+      if (_.features().copy_memory_permits_two_memory_accesses) {
+        if (auto error = CheckMemoryAccess(_, inst, second_access_index))
+          return error;
+
+        // In the two-access form in SPIR-V 1.4 and later:
+        //  - the first is the target (write) access and it can't have
+        //  make-visible.
+        //  - the second is the source (read) access and it can't have
+        //  make-available.
+        if (first_access & SpvMemoryAccessMakePointerVisibleKHRMask) {
+          return _.diag(SPV_ERROR_INVALID_DATA, inst)
+                 << "Target memory access must not include "
+                    "MakePointerVisibleKHR";
+        }
+        const auto second_access =
+            inst->GetOperandAs<uint32_t>(second_access_index);
+        if (second_access & SpvMemoryAccessMakePointerAvailableKHRMask) {
+          return _.diag(SPV_ERROR_INVALID_DATA, inst)
+                 << "Source memory access must not include "
+                    "MakePointerAvailableKHR";
+        }
+      } else {
+        return _.diag(SPV_ERROR_INVALID_DATA, inst)
+               << spvOpcodeString(static_cast<SpvOp>(inst->opcode()))
+               << " with two memory access operands requires SPIR-V 1.4 or "
+                  "later";
+      }
+    }
+  }
+  return SPV_SUCCESS;
+}
+
 spv_result_t ValidateCopyMemory(ValidationState_t& _, const Instruction* inst) {
   const auto target_index = 0;
   const auto target_id = inst->GetOperandAs<uint32_t>(target_index);
@@ -968,7 +994,7 @@
 
     if (auto error = CheckMemoryAccess(_, inst, 3)) return error;
   }
-  return SPV_SUCCESS;
+  return ValidateCopyMemoryMemoryAccess(_, inst);
 }
 
 spv_result_t ValidateAccessChain(ValidationState_t& _,
@@ -1326,6 +1352,57 @@
   return SPV_SUCCESS;
 }
 
+spv_result_t ValidatePtrComparison(ValidationState_t& _,
+                                   const Instruction* inst) {
+  if (_.addressing_model() == SpvAddressingModelLogical &&
+      !_.features().variable_pointers_storage_buffer) {
+    return _.diag(SPV_ERROR_INVALID_ID, inst)
+           << "Instruction cannot be used without a variable pointers "
+              "capability";
+  }
+
+  const auto result_type = _.FindDef(inst->type_id());
+  if (inst->opcode() == SpvOpPtrDiff) {
+    if (!result_type || result_type->opcode() != SpvOpTypeInt) {
+      return _.diag(SPV_ERROR_INVALID_ID, inst)
+             << "Result Type must be an integer scalar";
+    }
+  } else {
+    if (!result_type || result_type->opcode() != SpvOpTypeBool) {
+      return _.diag(SPV_ERROR_INVALID_ID, inst)
+             << "Result Type must be OpTypeBool";
+    }
+  }
+
+  const auto op1 = _.FindDef(inst->GetOperandAs<uint32_t>(2u));
+  const auto op2 = _.FindDef(inst->GetOperandAs<uint32_t>(3u));
+  if (!op1 || !op2 || op1->type_id() != op2->type_id()) {
+    return _.diag(SPV_ERROR_INVALID_ID, inst)
+           << "The types of Operand 1 and Operand 2 must match";
+  }
+  const auto op1_type = _.FindDef(op1->type_id());
+  if (!op1_type || op1_type->opcode() != SpvOpTypePointer) {
+    return _.diag(SPV_ERROR_INVALID_ID, inst)
+           << "Operand type must be a pointer";
+  }
+
+  if (_.addressing_model() == SpvAddressingModelLogical) {
+    SpvStorageClass sc = op1_type->GetOperandAs<SpvStorageClass>(1u);
+    if (sc != SpvStorageClassWorkgroup && sc != SpvStorageClassStorageBuffer) {
+      return _.diag(SPV_ERROR_INVALID_ID, inst)
+             << "Invalid pointer storage class";
+    }
+
+    if (sc == SpvStorageClassWorkgroup && !_.features().variable_pointers) {
+      return _.diag(SPV_ERROR_INVALID_ID, inst)
+             << "Workgroup storage class pointer requires VariablePointers "
+                "capability to be specified";
+    }
+  }
+
+  return SPV_SUCCESS;
+}
+
 }  // namespace
 
 spv_result_t MemoryPass(ValidationState_t& _, const Instruction* inst) {
@@ -1362,6 +1439,11 @@
     case SpvOpCooperativeMatrixLengthNV:
       if (auto error = ValidateCooperativeMatrixLengthNV(_, inst)) return error;
       break;
+    case SpvOpPtrEqual:
+    case SpvOpPtrNotEqual:
+    case SpvOpPtrDiff:
+      if (auto error = ValidatePtrComparison(_, inst)) return error;
+      break;
     case SpvOpImageTexelPointer:
     case SpvOpGenericPtrMemSemantics:
     default:
diff --git a/source/val/validate_scopes.cpp b/source/val/validate_scopes.cpp
index 2223a77..7e396c0 100644
--- a/source/val/validate_scopes.cpp
+++ b/source/val/validate_scopes.cpp
@@ -22,6 +22,23 @@
 namespace spvtools {
 namespace val {
 
+bool IsValidScope(uint32_t scope) {
+  // Deliberately avoid a default case so we have to update the list when the
+  // scopes list changes.
+  switch (static_cast<SpvScope>(scope)) {
+    case SpvScopeCrossDevice:
+    case SpvScopeDevice:
+    case SpvScopeWorkgroup:
+    case SpvScopeSubgroup:
+    case SpvScopeInvocation:
+    case SpvScopeQueueFamilyKHR:
+      return true;
+    case SpvScopeMax:
+      break;
+  }
+  return false;
+}
+
 spv_result_t ValidateExecutionScope(ValidationState_t& _,
                                     const Instruction* inst, uint32_t scope) {
   SpvOp opcode = inst->opcode();
@@ -52,6 +69,11 @@
     return SPV_SUCCESS;
   }
 
+  if (is_const_int32 && !IsValidScope(value)) {
+    return _.diag(SPV_ERROR_INVALID_DATA, inst)
+           << "Invalid scope value:\n " << _.Disassemble(*_.FindDef(scope));
+  }
+
   // Vulkan specific rules
   if (spvIsVulkanEnv(_.context()->target_env)) {
     // Vulkan 1.1 specific rules
@@ -154,6 +176,11 @@
     return SPV_SUCCESS;
   }
 
+  if (is_const_int32 && !IsValidScope(value)) {
+    return _.diag(SPV_ERROR_INVALID_DATA, inst)
+           << "Invalid scope value:\n " << _.Disassemble(*_.FindDef(scope));
+  }
+
   if (value == SpvScopeQueueFamilyKHR) {
     if (_.HasCapability(SpvCapabilityVulkanMemoryModelKHR)) {
       return SPV_SUCCESS;
diff --git a/source/val/validate_type.cpp b/source/val/validate_type.cpp
index edfede8..e3c7662 100644
--- a/source/val/validate_type.cpp
+++ b/source/val/validate_type.cpp
@@ -319,6 +319,7 @@
   }
   return SPV_SUCCESS;
 }
+}  // namespace
 
 spv_result_t ValidateTypeFunction(ValidationState_t& _,
                                   const Instruction* inst) {
@@ -437,8 +438,6 @@
   return SPV_SUCCESS;
 }
 
-}  // namespace
-
 spv_result_t TypePass(ValidationState_t& _, const Instruction* inst) {
   if (!spvOpcodeGeneratesType(inst->opcode()) &&
       inst->opcode() != SpvOpTypeForwardPointer) {
diff --git a/source/val/validation_state.cpp b/source/val/validation_state.cpp
index e542904..325a3e8 100644
--- a/source/val/validation_state.cpp
+++ b/source/val/validation_state.cpp
@@ -19,6 +19,7 @@
 #include <utility>
 
 #include "source/opcode.h"
+#include "source/spirv_constant.h"
 #include "source/spirv_target_env.h"
 #include "source/val/basic_block.h"
 #include "source/val/construct.h"
@@ -146,6 +147,30 @@
   return SPV_SUCCESS;
 }
 
+spv_result_t setHeader(void* user_data, spv_endianness_t, uint32_t,
+                       uint32_t version, uint32_t generator, uint32_t id_bound,
+                       uint32_t) {
+  ValidationState_t& vstate =
+      *(reinterpret_cast<ValidationState_t*>(user_data));
+  vstate.setIdBound(id_bound);
+  vstate.setGenerator(generator);
+  vstate.setVersion(version);
+
+  return SPV_SUCCESS;
+}
+
+// Add features based on SPIR-V core version number.
+void UpdateFeaturesBasedOnSpirvVersion(ValidationState_t::Feature* features,
+                                       uint32_t version) {
+  assert(features);
+  if (version >= SPV_SPIRV_VERSION_WORD(1, 4)) {
+    features->select_between_composites = true;
+    features->copy_memory_permits_two_memory_accesses = true;
+    features->uconvert_spec_constant_op = true;
+    features->nonwritable_var_in_function_or_private = true;
+  }
+}
+
 }  // namespace
 
 ValidationState_t::ValidationState_t(const spv_const_context ctx,
@@ -205,11 +230,12 @@
     spv_context_t hijacked_context = *ctx;
     hijacked_context.consumer = [](spv_message_level_t, const char*,
                                    const spv_position_t&, const char*) {};
-    spvBinaryParse(&hijacked_context, this, words, num_words,
-                   /* parsed_header = */ nullptr, CountInstructions,
+    spvBinaryParse(&hijacked_context, this, words, num_words, setHeader,
+                   CountInstructions,
                    /* diagnostic = */ nullptr);
     preallocateStorage();
   }
+  UpdateFeaturesBasedOnSpirvVersion(&features_, version_);
 
   friendly_mapper_ = spvtools::MakeUnique<spvtools::FriendlyNameMapper>(
       context_, words_, num_words_);
@@ -447,7 +473,7 @@
       pointer_size_and_alignment_ = 4;
       break;
     default:
-      // fall through
+    // fall through
     case SpvAddressingModelPhysical64:
     case SpvAddressingModelPhysicalStorageBuffer64EXT:
       pointer_size_and_alignment_ = 8;
diff --git a/source/val/validation_state.h b/source/val/validation_state.h
index 54d5139..5e84e24 100644
--- a/source/val/validation_state.h
+++ b/source/val/validation_state.h
@@ -106,9 +106,20 @@
     // Members need not be listed in offset order
     bool scalar_block_layout = false;
 
-    // Permit UConvert as an OpSpecConstantOp operation.
+    // SPIR-V 1.4 allows us to select between any two composite values
+    // of the same type.
+    bool select_between_composites = false;
+
+    // SPIR-V 1.4 allows two memory access operands for OpCopyMemory and
+    // OpCopyMemorySized.
+    bool copy_memory_permits_two_memory_accesses = false;
+
+    // SPIR-V 1.4 allows UConvert as a spec constant op in any environment.
     // The Kernel capability already enables it, separately from this flag.
     bool uconvert_spec_constant_op = false;
+
+    // SPIR-V 1.4 allows Function and Private variables to be NonWritable
+    bool nonwritable_var_in_function_or_private = false;
   };
 
   ValidationState_t(const spv_const_context context,
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 9226ea7..bf0792c 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -129,6 +129,7 @@
   text_start_new_inst_test.cpp
   text_to_binary.annotation_test.cpp
   text_to_binary.barrier_test.cpp
+  text_to_binary.composite_test.cpp
   text_to_binary.constant_test.cpp
   text_to_binary.control_flow_test.cpp
   text_to_binary_test.cpp
diff --git a/test/binary_header_get_test.cpp b/test/binary_header_get_test.cpp
index e771f1a..dcaf992 100644
--- a/test/binary_header_get_test.cpp
+++ b/test/binary_header_get_test.cpp
@@ -51,7 +51,7 @@
   ASSERT_EQ(SPV_SUCCESS, spvBinaryHeaderGet(&const_bin, endian, &header));
 
   ASSERT_EQ(static_cast<uint32_t>(SpvMagicNumber), header.magic);
-  ASSERT_EQ(0x00010300u, header.version);
+  ASSERT_EQ(0x00010400u, header.version);
   ASSERT_EQ(static_cast<uint32_t>(SPV_GENERATOR_CODEPLAY), header.generator);
   ASSERT_EQ(1u, header.bound);
   ASSERT_EQ(0u, header.schema);
diff --git a/test/opt/decoration_manager_test.cpp b/test/opt/decoration_manager_test.cpp
index f85ff6a..3ae6458 100644
--- a/test/opt/decoration_manager_test.cpp
+++ b/test/opt/decoration_manager_test.cpp
@@ -709,8 +709,8 @@
   EXPECT_THAT(GetErrorMessage(), "");
 
   std::string expected_decorations =
-      R"(OpDecorateStringGOOGLE %5 HlslSemanticGOOGLE "blah"
-OpDecorateId %5 HlslCounterBufferGOOGLE %2
+      R"(OpDecorateString %5 UserSemantic "blah"
+OpDecorateId %5 CounterBuffer %2
 OpDecorate %5 Aliased
 )";
   EXPECT_THAT(ToText(decorations), expected_decorations);
@@ -720,11 +720,11 @@
 OpExtension "SPV_GOOGLE_hlsl_functionality1"
 OpExtension "SPV_GOOGLE_decorate_string"
 OpMemoryModel Logical GLSL450
-OpDecorateStringGOOGLE %1 HlslSemanticGOOGLE "blah"
-OpDecorateId %1 HlslCounterBufferGOOGLE %2
+OpDecorateString %1 UserSemantic "blah"
+OpDecorateId %1 CounterBuffer %2
 OpDecorate %1 Aliased
-OpDecorateStringGOOGLE %5 HlslSemanticGOOGLE "blah"
-OpDecorateId %5 HlslCounterBufferGOOGLE %2
+OpDecorateString %5 UserSemantic "blah"
+OpDecorateId %5 CounterBuffer %2
 OpDecorate %5 Aliased
 %3 = OpTypeInt 32 0
 %4 = OpTypePointer Uniform %3
diff --git a/test/text_to_binary.annotation_test.cpp b/test/text_to_binary.annotation_test.cpp
index 712209e..69a4861 100644
--- a/test/text_to_binary.annotation_test.cpp
+++ b/test/text_to_binary.annotation_test.cpp
@@ -60,6 +60,29 @@
       Eq(input.str()));
 }
 
+// Like above, but parameters to the decoration are IDs.
+using OpDecorateSimpleIdTest =
+    spvtest::TextToBinaryTestBase<::testing::TestWithParam<
+        std::tuple<spv_target_env, EnumCase<SpvDecoration>>>>;
+
+TEST_P(OpDecorateSimpleIdTest, AnySimpleDecoration) {
+  // This string should assemble, but should not validate.
+  std::stringstream input;
+  input << "OpDecorateId %1 " << std::get<1>(GetParam()).name();
+  for (auto operand : std::get<1>(GetParam()).operands())
+    input << " %" << operand;
+  input << std::endl;
+  EXPECT_THAT(CompiledInstructions(input.str(), std::get<0>(GetParam())),
+              Eq(MakeInstruction(SpvOpDecorateId,
+                                 {1, uint32_t(std::get<1>(GetParam()).value())},
+                                 std::get<1>(GetParam()).operands())));
+  // Also check disassembly.
+  EXPECT_THAT(
+      EncodeAndDecodeSuccessfully(input.str(), SPV_BINARY_TO_TEXT_OPTION_NONE,
+                                  std::get<0>(GetParam())),
+      Eq(input.str()));
+}
+
 #define CASE(NAME) SpvDecoration##NAME, #NAME
 INSTANTIATE_TEST_SUITE_P(
     TextToBinaryDecorateSimple, OpDecorateSimpleTest,
@@ -112,6 +135,21 @@
                          Combine(Values(SPV_ENV_UNIVERSAL_1_1),
                                  Values(EnumCase<SpvDecoration>{
                                      CASE(MaxByteOffset), {128}})));
+
+INSTANTIATE_TEST_SUITE_P(TextToBinaryDecorateSimpleV14, OpDecorateSimpleTest,
+                         Combine(Values(SPV_ENV_UNIVERSAL_1_4),
+                                 ValuesIn(std::vector<EnumCase<SpvDecoration>>{
+                                     {CASE(Uniform), {}},
+                                 })));
+
+INSTANTIATE_TEST_SUITE_P(TextToBinaryDecorateSimpleIdV14,
+                         OpDecorateSimpleIdTest,
+                         Combine(Values(SPV_ENV_UNIVERSAL_1_4),
+                                 ValuesIn(std::vector<EnumCase<SpvDecoration>>{
+                                     // In 1.4, UniformId decoration takes a
+                                     // scope Id.
+                                     {CASE(UniformId), {1}},
+                                 })));
 #undef CASE
 
 TEST_F(OpDecorateSimpleTest, WrongDecoration) {
diff --git a/test/text_to_binary.composite_test.cpp b/test/text_to_binary.composite_test.cpp
new file mode 100644
index 0000000..6ae1cd3
--- /dev/null
+++ b/test/text_to_binary.composite_test.cpp
@@ -0,0 +1,49 @@
+// Copyright (c) 2018 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.
+
+// Assembler tests for instructions in the "Group Instrucions" section of the
+// SPIR-V spec.
+
+#include <string>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "test/test_fixture.h"
+#include "test/unit_spirv.h"
+
+using ::testing::Eq;
+using ::testing::HasSubstr;
+
+namespace spvtools {
+namespace {
+
+using spvtest::Concatenate;
+
+using CompositeRoundTripTest = RoundTripTest;
+
+TEST_F(CompositeRoundTripTest, Good) {
+  std::string spirv = "%2 = OpCopyLogical %1 %3\n";
+  std::string disassembly = EncodeAndDecodeSuccessfully(
+      spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(CompositeRoundTripTest, V13Bad) {
+  std::string spirv = "%2 = OpCopyLogical %1 %3\n";
+  std::string err = CompileFailure(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_THAT(err, HasSubstr("Invalid Opcode name 'OpCopyLogical'"));
+}
+
+}  // namespace
+}  // namespace spvtools
diff --git a/test/text_to_binary.extension_test.cpp b/test/text_to_binary.extension_test.cpp
index 4c31545..84552b5 100644
--- a/test/text_to_binary.extension_test.cpp
+++ b/test/text_to_binary.extension_test.cpp
@@ -276,11 +276,11 @@
                 {"OpCapability StorageBuffer16BitAccess\n",
                  MakeInstruction(SpvOpCapability,
                                  {SpvCapabilityStorageBuffer16BitAccess})},
-                {"OpCapability StorageUniform16\n",
+                {"OpCapability UniformAndStorageBuffer16BitAccess\n",
                  MakeInstruction(
                      SpvOpCapability,
                      {SpvCapabilityUniformAndStorageBuffer16BitAccess})},
-                {"OpCapability StorageUniform16\n",
+                {"OpCapability UniformAndStorageBuffer16BitAccess\n",
                  MakeInstruction(SpvOpCapability,
                                  {SpvCapabilityStorageUniform16})},
                 {"OpCapability StoragePushConstant16\n",
@@ -656,6 +656,10 @@
 
 // SPV_GOOGLE_decorate_string
 
+// Now that OpDecorateString is the preferred spelling for
+// OpDecorateStringGOOGLE use that name in round trip tests, and the GOOGLE
+// name in an assembly-only test.
+
 INSTANTIATE_TEST_SUITE_P(
     SPV_GOOGLE_decorate_string, ExtensionRoundTripTest,
     Combine(
@@ -664,6 +668,32 @@
         Values(SPV_ENV_UNIVERSAL_1_0, SPV_ENV_UNIVERSAL_1_1,
                SPV_ENV_UNIVERSAL_1_2, SPV_ENV_VULKAN_1_0),
         ValuesIn(std::vector<AssemblyCase>{
+            {"OpDecorateString %1 UserSemantic \"ABC\"\n",
+             MakeInstruction(SpvOpDecorateStringGOOGLE,
+                             {1, SpvDecorationHlslSemanticGOOGLE},
+                             MakeVector("ABC"))},
+            {"OpDecorateString %1 UserSemantic \"ABC\"\n",
+             MakeInstruction(SpvOpDecorateString,
+                             {1, SpvDecorationUserSemantic},
+                             MakeVector("ABC"))},
+            {"OpMemberDecorateString %1 3 UserSemantic \"DEF\"\n",
+             MakeInstruction(SpvOpMemberDecorateStringGOOGLE,
+                             {1, 3, SpvDecorationUserSemantic},
+                             MakeVector("DEF"))},
+            {"OpMemberDecorateString %1 3 UserSemantic \"DEF\"\n",
+             MakeInstruction(SpvOpMemberDecorateString,
+                             {1, 3, SpvDecorationUserSemantic},
+                             MakeVector("DEF"))},
+        })));
+
+INSTANTIATE_TEST_SUITE_P(
+    SPV_GOOGLE_decorate_string, ExtensionAssemblyTest,
+    Combine(
+        // We'll get coverage over operand tables by trying the universal
+        // environments, and at least one specific environment.
+        Values(SPV_ENV_UNIVERSAL_1_0, SPV_ENV_UNIVERSAL_1_1,
+               SPV_ENV_UNIVERSAL_1_2, SPV_ENV_VULKAN_1_0),
+        ValuesIn(std::vector<AssemblyCase>{
             {"OpDecorateStringGOOGLE %1 HlslSemanticGOOGLE \"ABC\"\n",
              MakeInstruction(SpvOpDecorateStringGOOGLE,
                              {1, SpvDecorationHlslSemanticGOOGLE},
@@ -676,6 +706,9 @@
 
 // SPV_GOOGLE_hlsl_functionality1
 
+// Now that CounterBuffer is the preferred spelling for HlslCounterBufferGOOGLE,
+// use that name in round trip tests, and the GOOGLE name in an assembly-only
+// test.
 INSTANTIATE_TEST_SUITE_P(
     SPV_GOOGLE_hlsl_functionality1, ExtensionRoundTripTest,
     Combine(
@@ -686,6 +719,24 @@
         // HlslSemanticGOOGLE is tested in SPV_GOOGLE_decorate_string, since
         // they are coupled together.
         ValuesIn(std::vector<AssemblyCase>{
+            {"OpDecorateId %1 CounterBuffer %2\n",
+             MakeInstruction(SpvOpDecorateId,
+                             {1, SpvDecorationHlslCounterBufferGOOGLE, 2})},
+            {"OpDecorateId %1 CounterBuffer %2\n",
+             MakeInstruction(SpvOpDecorateId,
+                             {1, SpvDecorationCounterBuffer, 2})},
+        })));
+
+INSTANTIATE_TEST_SUITE_P(
+    SPV_GOOGLE_hlsl_functionality1, ExtensionAssemblyTest,
+    Combine(
+        // We'll get coverage over operand tables by trying the universal
+        // environments, and at least one specific environment.
+        Values(SPV_ENV_UNIVERSAL_1_0, SPV_ENV_UNIVERSAL_1_1,
+               SPV_ENV_UNIVERSAL_1_2, SPV_ENV_VULKAN_1_0),
+        // HlslSemanticGOOGLE is tested in SPV_GOOGLE_decorate_string, since
+        // they are coupled together.
+        ValuesIn(std::vector<AssemblyCase>{
             {"OpDecorateId %1 HlslCounterBufferGOOGLE %2\n",
              MakeInstruction(SpvOpDecorateId,
                              {1, SpvDecorationHlslCounterBufferGOOGLE, 2})},
diff --git a/test/text_to_binary.memory_test.cpp b/test/text_to_binary.memory_test.cpp
index 6d08432..c83c847 100644
--- a/test/text_to_binary.memory_test.cpp
+++ b/test/text_to_binary.memory_test.cpp
@@ -30,6 +30,7 @@
 using spvtest::MakeInstruction;
 using spvtest::TextToBinaryTest;
 using ::testing::Eq;
+using ::testing::HasSubstr;
 
 // Test assembly of Memory Access masks
 
@@ -95,12 +96,326 @@
 #undef CASE
 // clang-format on
 
+using MemoryRoundTripTest = RoundTripTest;
+
+// OpPtrEqual appeared in SPIR-V 1.4
+
+TEST_F(MemoryRoundTripTest, OpPtrEqualGood) {
+  std::string spirv = "%2 = OpPtrEqual %1 %3 %4\n";
+  EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
+              Eq(MakeInstruction(SpvOpPtrEqual, {1, 2, 3, 4})));
+  std::string disassembly = EncodeAndDecodeSuccessfully(
+      spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpPtrEqualV13Bad) {
+  std::string spirv = "%2 = OpPtrEqual %1 %3 %4\n";
+  std::string err = CompileFailure(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_THAT(err, HasSubstr("Invalid Opcode name 'OpPtrEqual'"));
+}
+
+// OpPtrNotEqual appeared in SPIR-V 1.4
+
+TEST_F(MemoryRoundTripTest, OpPtrNotEqualGood) {
+  std::string spirv = "%2 = OpPtrNotEqual %1 %3 %4\n";
+  EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
+              Eq(MakeInstruction(SpvOpPtrNotEqual, {1, 2, 3, 4})));
+  std::string disassembly = EncodeAndDecodeSuccessfully(
+      spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpPtrNotEqualV13Bad) {
+  std::string spirv = "%2 = OpPtrNotEqual %1 %3 %4\n";
+  std::string err = CompileFailure(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_THAT(err, HasSubstr("Invalid Opcode name 'OpPtrNotEqual'"));
+}
+
+// OpPtrDiff appeared in SPIR-V 1.4
+
+TEST_F(MemoryRoundTripTest, OpPtrDiffGood) {
+  std::string spirv = "%2 = OpPtrDiff %1 %3 %4\n";
+  EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
+              Eq(MakeInstruction(SpvOpPtrDiff, {1, 2, 3, 4})));
+  std::string disassembly = EncodeAndDecodeSuccessfully(
+      spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpPtrDiffV13Good) {
+  // OpPtrDiff is enabled by a capability as well, so we can assemble
+  // it even in older SPIR-V environments.  We do that so we can
+  // write tests.
+  std::string spirv = "%2 = OpPtrDiff %1 %3 %4\n";
+  std::string disassembly = EncodeAndDecodeSuccessfully(
+      spirv, SPV_BINARY_TO_TEXT_OPTION_NONE, SPV_ENV_UNIVERSAL_1_4);
+}
+
+// OpCopyMemory
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryNoMemAccessGood) {
+  std::string spirv = "OpCopyMemory %1 %2\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryTooFewArgsBad) {
+  std::string spirv = "OpCopyMemory %1\n";
+  std::string err = CompileFailure(spirv);
+  EXPECT_THAT(err, HasSubstr("Expected operand, found end of stream"));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryTooManyArgsBad) {
+  std::string spirv = "OpCopyMemory %1 %2 %3\n";
+  std::string err = CompileFailure(spirv);
+  EXPECT_THAT(err, HasSubstr("Invalid memory access operand '%3'"));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessNoneGood) {
+  std::string spirv = "OpCopyMemory %1 %2 None\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 0})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessVolatileGood) {
+  std::string spirv = "OpCopyMemory %1 %2 Volatile\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 1})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessAligned8Good) {
+  std::string spirv = "OpCopyMemory %1 %2 Aligned 8\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 2, 8})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessNontemporalGood) {
+  std::string spirv = "OpCopyMemory %1 %2 Nontemporal\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 4})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessAvGood) {
+  std::string spirv = "OpCopyMemory %1 %2 MakePointerAvailableKHR %3\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 8, 3})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessVisGood) {
+  std::string spirv = "OpCopyMemory %1 %2 MakePointerVisibleKHR %3\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 16, 3})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessNonPrivateGood) {
+  std::string spirv = "OpCopyMemory %1 %2 NonPrivatePointerKHR\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 32})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryAccessMixedGood) {
+  std::string spirv =
+      "OpCopyMemory %1 %2 "
+      "Volatile|Aligned|Nontemporal|MakePointerAvailableKHR|"
+      "MakePointerVisibleKHR|NonPrivatePointerKHR 16 %3 %4\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 63, 16, 3, 4})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryTwoAccessV13Good) {
+  std::string spirv = "OpCopyMemory %1 %2 Volatile Volatile\n";
+  // Note: This will assemble but should not validate for SPIR-V 1.3
+  EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_3),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 1, 1})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryTwoAccessV14Good) {
+  std::string spirv = "OpCopyMemory %1 %2 Volatile Volatile\n";
+  EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 1, 1})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemoryTwoAccessMixedV14Good) {
+  std::string spirv =
+      "OpCopyMemory %1 %2 Volatile|Nontemporal|"
+      "MakePointerVisibleKHR %3 "
+      "Aligned|MakePointerAvailableKHR|NonPrivatePointerKHR 16 %4\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemory, {1, 2, 21, 3, 42, 16, 4})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+// OpCopyMemorySized
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedNoMemAccessGood) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTooFewArgsBad) {
+  std::string spirv = "OpCopyMemorySized %1 %2\n";
+  std::string err = CompileFailure(spirv);
+  EXPECT_THAT(err, HasSubstr("Expected operand, found end of stream"));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTooManyArgsBad) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 %4\n";
+  std::string err = CompileFailure(spirv);
+  EXPECT_THAT(err, HasSubstr("Invalid memory access operand '%4'"));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessNoneGood) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 None\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 0})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessVolatileGood) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 Volatile\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 1})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessAligned8Good) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 Aligned 8\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 2, 8})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessNontemporalGood) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 Nontemporal\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 4})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessAvGood) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 MakePointerAvailableKHR %4\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 8, 4})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessVisGood) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 MakePointerVisibleKHR %4\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 16, 4})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessNonPrivateGood) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 NonPrivatePointerKHR\n";
+  EXPECT_THAT(CompiledInstructions(spirv),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 32})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedAccessMixedGood) {
+  std::string spirv =
+      "OpCopyMemorySized %1 %2 %3 "
+      "Volatile|Aligned|Nontemporal|MakePointerAvailableKHR|"
+      "MakePointerVisibleKHR|NonPrivatePointerKHR 16 %4 %5\n";
+  EXPECT_THAT(
+      CompiledInstructions(spirv),
+      Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 63, 16, 4, 5})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTwoAccessV13Good) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 Volatile Volatile\n";
+  // Note: This will assemble but should not validate for SPIR-V 1.3
+  EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_3),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 1, 1})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTwoAccessV14Good) {
+  std::string spirv = "OpCopyMemorySized %1 %2 %3 Volatile Volatile\n";
+  EXPECT_THAT(CompiledInstructions(spirv, SPV_ENV_UNIVERSAL_1_4),
+              Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 1, 1})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
+TEST_F(MemoryRoundTripTest, OpCopyMemorySizedTwoAccessMixedV14Good) {
+  std::string spirv =
+      "OpCopyMemorySized %1 %2 %3 Volatile|Nontemporal|"
+      "MakePointerVisibleKHR %4 "
+      "Aligned|MakePointerAvailableKHR|NonPrivatePointerKHR 16 %5\n";
+  EXPECT_THAT(
+      CompiledInstructions(spirv),
+      Eq(MakeInstruction(SpvOpCopyMemorySized, {1, 2, 3, 21, 4, 42, 16, 5})));
+  std::string disassembly =
+      EncodeAndDecodeSuccessfully(spirv, SPV_BINARY_TO_TEXT_OPTION_NONE);
+  EXPECT_THAT(disassembly, Eq(spirv));
+}
+
 // TODO(dneto): OpVariable with initializers
 // TODO(dneto): OpImageTexelPointer
 // TODO(dneto): OpLoad
 // TODO(dneto): OpStore
-// TODO(dneto): OpCopyMemory
-// TODO(dneto): OpCopyMemorySized
 // TODO(dneto): OpAccessChain
 // TODO(dneto): OpInBoundsAccessChain
 // TODO(dneto): OpPtrAccessChain
diff --git a/test/tools/expect.py b/test/tools/expect.py
index 5844882..56ddaec 100755
--- a/test/tools/expect.py
+++ b/test/tools/expect.py
@@ -26,6 +26,8 @@
 from spirv_test_framework import SpirvTest
 from builtins import bytes
 
+DEFAULT_SPIRV_VERSION = 0x010000
+
 def convert_to_unix_line_endings(source):
   """Converts all line endings in source to be unix line endings."""
   result = source.replace('\r\n', '\n').replace('\r', '\n')
@@ -172,7 +174,7 @@
     # profile
 
     if version != spv_version and version != 0:
-      return False, 'Incorrect SPV binary: wrong version number'
+      return False, 'Incorrect SPV binary: wrong version number: ' + hex(version) + ' expected ' + hex(spv_version)
     # Shaderc-over-Glslang (0x000d....) or
     # SPIRV-Tools (0x0007....) generator number
     if read_word(preamble, 2, little_endian) != 0x000d0007 and \
@@ -188,7 +190,9 @@
 class CorrectObjectFilePreamble(CorrectBinaryLengthAndPreamble):
   """Provides methods for verifying preamble for a SPV object file."""
 
-  def verify_object_file_preamble(self, filename, spv_version=0x10000):
+  def verify_object_file_preamble(self,
+                                  filename,
+                                  spv_version=DEFAULT_SPIRV_VERSION):
     """Checks that the given SPIR-V binary file has correct preamble."""
 
     success, message = verify_file_non_empty(filename)
@@ -257,6 +261,21 @@
     return True, ''
 
 
+class ValidObjectFile1_4(ReturnCodeIsZero, CorrectObjectFilePreamble):
+  """Mixin class for checking that every input file generates a valid SPIR-V 1.4
+    object file following the object file naming rule, and there is no output on
+    stdout/stderr."""
+
+  def check_object_file_preamble(self, status):
+    for input_filename in status.input_filenames:
+      object_filename = get_object_filename(input_filename)
+      success, message = self.verify_object_file_preamble(
+          os.path.join(status.directory, object_filename), 0x10400)
+      if not success:
+        return False, message
+    return True, ''
+
+
 class ValidObjectFileWithAssemblySubstr(SuccessfulReturn,
                                         CorrectObjectFilePreamble):
   """Mixin class for checking that every input file generates a valid object
diff --git a/test/tools/opt/flags.py b/test/tools/opt/flags.py
index 411b000..ca873c7 100644
--- a/test/tools/opt/flags.py
+++ b/test/tools/opt/flags.py
@@ -34,7 +34,7 @@
 
 
 @inside_spirv_testsuite('SpirvOptBase')
-class TestAssemblyFileAsOnlyParameter(expect.ValidObjectFile1_3):
+class TestAssemblyFileAsOnlyParameter(expect.ValidObjectFile1_4):
   """Tests that spirv-opt accepts a SPIR-V object file."""
 
   shader = placeholder.FileSPIRVShader(empty_main_assembly(), '.spvasm')
@@ -52,7 +52,7 @@
 
 
 @inside_spirv_testsuite('SpirvOptFlags')
-class TestValidPassFlags(expect.ValidObjectFile1_3,
+class TestValidPassFlags(expect.ValidObjectFile1_4,
                          expect.ExecutedListOfPasses):
   """Tests that spirv-opt accepts all valid optimization flags."""
 
@@ -129,7 +129,7 @@
 
 
 @inside_spirv_testsuite('SpirvOptFlags')
-class TestPerformanceOptimizationPasses(expect.ValidObjectFile1_3,
+class TestPerformanceOptimizationPasses(expect.ValidObjectFile1_4,
                                         expect.ExecutedListOfPasses):
   """Tests that spirv-opt schedules all the passes triggered by -O."""
 
@@ -176,7 +176,7 @@
 
 
 @inside_spirv_testsuite('SpirvOptFlags')
-class TestSizeOptimizationPasses(expect.ValidObjectFile1_3,
+class TestSizeOptimizationPasses(expect.ValidObjectFile1_4,
                                  expect.ExecutedListOfPasses):
   """Tests that spirv-opt schedules all the passes triggered by -Os."""
 
@@ -215,7 +215,7 @@
 
 
 @inside_spirv_testsuite('SpirvOptFlags')
-class TestLegalizationPasses(expect.ValidObjectFile1_3,
+class TestLegalizationPasses(expect.ValidObjectFile1_4,
                              expect.ExecutedListOfPasses):
   """Tests that spirv-opt schedules all the passes triggered by --legalize-hlsl.
   """
diff --git a/test/val/val_barriers_test.cpp b/test/val/val_barriers_test.cpp
index 0b28c45..cf81e42 100644
--- a/test/val/val_barriers_test.cpp
+++ b/test/val/val_barriers_test.cpp
@@ -954,8 +954,8 @@
 OpMemoryBarrier %u32 %u32_0
 )";
 
-  CompileSuccessfully(GenerateKernelCode(body));
-  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
+  CompileSuccessfully(GenerateKernelCode(body), SPV_ENV_UNIVERSAL_1_1);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_1));
   EXPECT_THAT(getDiagnosticString(), HasSubstr("Operand 5[%uint] cannot be a "
                                                "type"));
 }
diff --git a/test/val/val_capability_test.cpp b/test/val/val_capability_test.cpp
index a1f1d3d..4fb2a7c 100644
--- a/test/val/val_capability_test.cpp
+++ b/test/val/val_capability_test.cpp
@@ -2265,8 +2265,8 @@
   EXPECT_EQ(SPV_ERROR_MISSING_EXTENSION,
             ValidateInstructions(SPV_ENV_UNIVERSAL_1_0));
   EXPECT_THAT(getDiagnosticString(),
-              HasSubstr("operand 5255 requires one of these extensions: "
-                        "SPV_NV_viewport_array2"));
+              HasSubstr("operand ShaderViewportMaskNV(5255) requires one of "
+                        "these extensions: SPV_NV_viewport_array2"));
 }
 
 TEST_F(ValidateCapability,
@@ -2347,8 +2347,8 @@
   EXPECT_EQ(SPV_ERROR_MISSING_EXTENSION,
             ValidateInstructions(SPV_ENV_UNIVERSAL_1_0));
   EXPECT_THAT(getDiagnosticString(),
-              HasSubstr("operand 5568 requires one of these extensions: "
-                        "SPV_INTEL_subgroups"));
+              HasSubstr("operand SubgroupShuffleINTEL(5568) requires one of "
+                        "these extensions: SPV_INTEL_subgroups"));
 }
 
 TEST_F(ValidateCapability,
@@ -2407,6 +2407,33 @@
                         "specified if the VulkanKHR memory model is used"));
 }
 
+// In the grammar, SubgroupEqMask and SubgroupMaskKHR have different enabling
+// lists of extensions.
+TEST_F(ValidateCapability, SubgroupEqMaskEnabledByExtension) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability SubgroupBallotKHR
+OpExtension "SPV_KHR_shader_ballot"
+OpMemoryModel Logical Simple
+OpEntryPoint GLCompute %main "main"
+OpDecorate %var BuiltIn SubgroupEqMask
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%ptr_uint = OpTypePointer Private %uint
+%var = OpVariable %ptr_uint Private
+%fn = OpTypeFunction %void
+%main = OpFunction %void None %fn
+%entry = OpLabel
+%val = OpLoad %uint %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_0);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_0))
+      << getDiagnosticString();
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_composites_test.cpp b/test/val/val_composites_test.cpp
index db6ff5b..309c194 100644
--- a/test/val/val_composites_test.cpp
+++ b/test/val/val_composites_test.cpp
@@ -1568,6 +1568,209 @@
               HasSubstr("Expected Index to be int scalar"));
 }
 
+TEST_F(ValidateComposites, CopyLogicalSameType) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%struct = OpTypeStruct
+%const_struct = OpConstantComposite %struct
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %struct %const_struct
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Result Type must not equal the Operand type"));
+}
+
+TEST_F(ValidateComposites, CopyLogicalSameStructDifferentId) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%struct1 = OpTypeStruct
+%struct2 = OpTypeStruct
+%const_struct = OpConstantComposite %struct1
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %struct2 %const_struct
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+}
+
+TEST_F(ValidateComposites, CopyLogicalArrayDifferentLength) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_4 = OpConstant %int 4
+%int_5 = OpConstant %int 5
+%array1 = OpTypeArray %int %int_4
+%array2 = OpTypeArray %int %int_5
+%const_array = OpConstantComposite %array1 %int_4 %int_4 %int_4 %int_4
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %array2 %const_array
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Result Type does not logically match the Operand type"));
+}
+
+TEST_F(ValidateComposites, CopyLogicalArrayDifferentElement) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%float = OpTypeFloat 32
+%int = OpTypeInt 32 0
+%int_4 = OpConstant %int 4
+%array1 = OpTypeArray %int %int_4
+%array2 = OpTypeArray %float %int_4
+%const_array = OpConstantComposite %array1 %int_4 %int_4 %int_4 %int_4
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %array2 %const_array
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Result Type does not logically match the Operand type"));
+}
+
+TEST_F(ValidateComposites, CopyLogicalArrayLogicallyMatchedElement) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%float = OpTypeFloat 32
+%int = OpTypeInt 32 0
+%int_1 = OpConstant %int 1
+%inner1 = OpTypeArray %int %int_1
+%inner2 = OpTypeArray %int %int_1
+%array1 = OpTypeArray %inner1 %int_1
+%array2 = OpTypeArray %inner2 %int_1
+%const_inner = OpConstantComposite %inner1 %int_1
+%const_array = OpConstantComposite %array1 %const_inner
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %array2 %const_array
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+}
+
+TEST_F(ValidateComposites, CopyLogicalStructDifferentNumberElements) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%struct1 = OpTypeStruct
+%struct2 = OpTypeStruct %int
+%const_struct = OpConstantComposite %struct1
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %struct2 %const_struct
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Result Type does not logically match the Operand type"));
+}
+
+TEST_F(ValidateComposites, CopyLogicalStructDifferentElement) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%int = OpTypeInt 32 1
+%int_0 = OpConstant %int 0
+%uint_0 = OpConstant %uint 0
+%struct1 = OpTypeStruct %int %uint
+%struct2 = OpTypeStruct %int %int
+%const_struct = OpConstantComposite %struct1 %int_0 %uint_0
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %struct2 %const_struct
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Result Type does not logically match the Operand type"));
+}
+
+TEST_F(ValidateComposites, CopyLogicalStructLogicallyMatch) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_1 = OpConstant %int 1
+%array1 = OpTypeArray %int %int_1
+%array2 = OpTypeArray %int %int_1
+%struct1 = OpTypeStruct %int %array1
+%struct2 = OpTypeStruct %int %array2
+%const_array = OpConstantComposite %array1 %int_1
+%const_struct = OpConstantComposite %struct1 %int_1 %const_array
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%copy = OpCopyLogical %struct2 %const_struct
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_constants_test.cpp b/test/val/val_constants_test.cpp
index 4bced25..0c26d14 100644
--- a/test/val/val_constants_test.cpp
+++ b/test/val/val_constants_test.cpp
@@ -199,6 +199,60 @@
     }));
 
 INSTANTIATE_TEST_SUITE_P(
+    UConvert, ValidateConstantOp,
+    ValuesIn(std::vector<ConstantOpCase>{
+        // TODO(dneto): Conversions must change width.
+        {SPV_ENV_UNIVERSAL_1_0,
+         kKernelPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         true, ""},
+        {SPV_ENV_UNIVERSAL_1_1,
+         kKernelPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         true, ""},
+        {SPV_ENV_UNIVERSAL_1_3,
+         kKernelPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         true, ""},
+        {SPV_ENV_UNIVERSAL_1_3,
+         kKernelPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         true, ""},
+        {SPV_ENV_UNIVERSAL_1_4,
+         kKernelPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         true, ""},
+        {SPV_ENV_UNIVERSAL_1_0,
+         kShaderPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         false,
+         "Prior to SPIR-V 1.4, specialization constant operation "
+         "UConvert requires Kernel capability"},
+        {SPV_ENV_UNIVERSAL_1_1,
+         kShaderPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         false,
+         "Prior to SPIR-V 1.4, specialization constant operation "
+         "UConvert requires Kernel capability"},
+        {SPV_ENV_UNIVERSAL_1_3,
+         kShaderPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         false,
+         "Prior to SPIR-V 1.4, specialization constant operation "
+         "UConvert requires Kernel capability"},
+        {SPV_ENV_UNIVERSAL_1_3,
+         kShaderPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         false,
+         "Prior to SPIR-V 1.4, specialization constant operation "
+         "UConvert requires Kernel capability"},
+        {SPV_ENV_UNIVERSAL_1_4,
+         kShaderPreamble kBasicTypes
+         "%v = OpSpecConstantOp %uint UConvert %uint_0",
+         true, ""},
+    }));
+
+INSTANTIATE_TEST_SUITE_P(
     KernelInKernel, ValidateConstantOp,
     ValuesIn(std::vector<ConstantOpCase>{
         // TODO(dneto): Conversions must change width.
@@ -294,6 +348,25 @@
          true, ""},
     }));
 
+TEST_F(ValidateConstant, SpecConstantUConvert1p3Binary1p4EnvBad) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%int = OpTypeInt 32 0
+%int0 = OpConstant %int 0
+%const = OpSpecConstantOp %int UConvert %int0
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr(
+          "Prior to SPIR-V 1.4, specialization constant operation UConvert "
+          "requires Kernel capability or extension SPV_AMD_gpu_shader_int16"));
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_data_test.cpp b/test/val/val_data_test.cpp
index d9413bc..7dd0269 100644
--- a/test/val/val_data_test.cpp
+++ b/test/val/val_data_test.cpp
@@ -778,9 +778,10 @@
       ASSERT_EQ(SPV_ERROR_INVALID_CAPABILITY, ValidateInstructions(env));
       EXPECT_THAT(
           getDiagnosticString(),
-          HasSubstr("Operand 2 of Decorate requires one of these capabilities: "
-                    "StorageBuffer16BitAccess StorageUniform16 "
-                    "StoragePushConstant16 StorageInputOutput16"));
+          HasSubstr(
+              "Operand 2 of Decorate requires one of these capabilities: "
+              "StorageBuffer16BitAccess UniformAndStorageBuffer16BitAccess "
+              "StoragePushConstant16 StorageInputOutput16"));
     }
   }
 }
diff --git a/test/val/val_decoration_test.cpp b/test/val/val_decoration_test.cpp
index 764ee08..95ae3a2 100644
--- a/test/val/val_decoration_test.cpp
+++ b/test/val/val_decoration_test.cpp
@@ -19,6 +19,7 @@
 
 #include "gmock/gmock.h"
 #include "source/val/decoration.h"
+#include "test/test_fixture.h"
 #include "test/unit_spirv.h"
 #include "test/val/val_code_generator.h"
 #include "test/val/val_fixtures.h"
@@ -4708,7 +4709,7 @@
   EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
 }
 
-// Uniform decoration
+// Uniform and UniformId decorations
 
 TEST_F(ValidateDecorations, UniformDecorationGood) {
   const std::string spirv = R"(
@@ -4737,49 +4738,96 @@
   EXPECT_THAT(getDiagnosticString(), Eq(""));
 }
 
-TEST_F(ValidateDecorations, UniformDecorationTargetsTypeBad) {
-  const std::string spirv = R"(
+// Returns SPIR-V assembly for a shader that uses a given decoration
+// instruction.
+std::string ShaderWithUniformLikeDecoration(const std::string& inst) {
+  return std::string(R"(
 OpCapability Shader
 OpMemoryModel Logical Simple
 OpEntryPoint GLCompute %main "main"
 OpExecutionMode %main LocalSize 1 1 1
-OpDecorate %fn Uniform
-%void = OpTypeVoid
-%fn = OpTypeFunction %void
-%main = OpFunction %void None %fn
-%entry = OpLabel
-OpReturn
-OpFunctionEnd
-)";
-
-  CompileSuccessfully(spirv);
-  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
-  EXPECT_THAT(getDiagnosticString(),
-              HasSubstr("Uniform decoration applied to a non-object"));
-  EXPECT_THAT(getDiagnosticString(), HasSubstr("%2 = OpTypeFunction %void"));
-}
-
-TEST_F(ValidateDecorations, UniformDecorationTargetsVoidValueBad) {
-  const std::string spirv = R"(
-OpCapability Shader
-OpMemoryModel Logical Simple
-OpEntryPoint GLCompute %main "main"
-OpExecutionMode %main LocalSize 1 1 1
+OpName %subgroupscope "subgroupscope"
 OpName %call "call"
 OpName %myfunc "myfunc"
-OpDecorate %call Uniform
+OpName %int0 "int0"
+OpName %float0 "float0"
+OpName %fn "fn"
+)") + inst +
+         R"(
 %void = OpTypeVoid
-%fnty = OpTypeFunction %void
-%myfunc = OpFunction %void None %fnty
+%float = OpTypeFloat 32
+%int = OpTypeInt 32 1
+%int0 = OpConstantNull %int
+%int_99 = OpConstant %int 99
+%subgroupscope = OpConstant %int 3
+%float0 = OpConstantNull %float
+%fn = OpTypeFunction %void
+%myfunc = OpFunction %void None %fn
 %myfuncentry = OpLabel
 OpReturn
 OpFunctionEnd
-%main = OpFunction %void None %fnty
+%main = OpFunction %void None %fn
 %entry = OpLabel
 %call = OpFunctionCall %void %myfunc
 OpReturn
 OpFunctionEnd
 )";
+}
+
+TEST_F(ValidateDecorations, UniformIdDecorationWithScopeIdV13Bad) {
+  const std::string spirv = ShaderWithUniformLikeDecoration(
+      "OpDecorateId %int0 UniformId %subgroupscope");
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_WRONG_VERSION,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("requires SPIR-V version 1.4 or later\n"
+                        "  OpDecorateId %int0 UniformId %subgroupscope"));
+}
+
+TEST_F(ValidateDecorations, UniformIdDecorationWithScopeIdV13BadTargetV14) {
+  const std::string spirv = ShaderWithUniformLikeDecoration(
+      "OpDecorateId %int0 UniformId %subgroupscope");
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_WRONG_VERSION,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("requires SPIR-V version 1.4 or later"));
+}
+
+TEST_F(ValidateDecorations, UniformIdDecorationWithScopeIdV14Good) {
+  const std::string spirv = ShaderWithUniformLikeDecoration(
+      "OpDecorateId %int0 UniformId %subgroupscope");
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateDecorations, UniformDecorationTargetsTypeBad) {
+  const std::string spirv =
+      ShaderWithUniformLikeDecoration("OpDecorate %fn Uniform");
+
+  CompileSuccessfully(spirv);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Uniform decoration applied to a non-object"));
+  EXPECT_THAT(getDiagnosticString(), HasSubstr("%fn = OpTypeFunction %void"));
+}
+
+TEST_F(ValidateDecorations, UniformIdDecorationTargetsTypeBad) {
+  const std::string spirv = ShaderWithUniformLikeDecoration(
+      "OpDecorateId %fn UniformId %subgroupscope");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("UniformId decoration applied to a non-object"));
+  EXPECT_THAT(getDiagnosticString(), HasSubstr("%fn = OpTypeFunction %void"));
+}
+
+TEST_F(ValidateDecorations, UniformDecorationTargetsVoidValueBad) {
+  const std::string spirv =
+      ShaderWithUniformLikeDecoration("OpDecorate %call Uniform");
 
   CompileSuccessfully(spirv);
   EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
@@ -4788,6 +4836,82 @@
                         "  %call = OpFunctionCall %void %myfunc"));
 }
 
+TEST_F(ValidateDecorations, UniformIdDecorationTargetsVoidValueBad) {
+  const std::string spirv = ShaderWithUniformLikeDecoration(
+      "OpDecorateId %call UniformId %subgroupscope");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4))
+      << spirv;
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("UniformId decoration applied to a value with void type\n"
+                "  %call = OpFunctionCall %void %myfunc"));
+}
+
+TEST_F(ValidateDecorations,
+       UniformDecorationWithScopeIdV14IdIsFloatValueIsBad) {
+  const std::string spirv =
+      ShaderWithUniformLikeDecoration("OpDecorateId %int0 UniformId %float0");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("ConstantNull: expected Execution Scope to be a 32-bit int"));
+}
+
+TEST_F(ValidateDecorations,
+       UniformDecorationWithScopeIdV14IdIsInvalidIntValueBad) {
+  const std::string spirv =
+      ShaderWithUniformLikeDecoration("OpDecorateId %int0 UniformId %int_99");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Invalid scope value:\n %int_99 = OpConstant %int 99\n"));
+}
+
+TEST_F(ValidateDecorations, UniformDecorationWithScopeIdV14VulkanEnv) {
+  const std::string spirv =
+      ShaderWithUniformLikeDecoration("OpDecorateId %int0 UniformId %int0");
+
+  CompileSuccessfully(spirv, SPV_ENV_VULKAN_1_1_SPIRV_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_VULKAN_1_1_SPIRV_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr(": in Vulkan environment Execution Scope is limited to "
+                        "Workgroup and Subgroup"));
+}
+
+TEST_F(ValidateDecorations, UniformDecorationWithWrongInstructionBad) {
+  const std::string spirv =
+      ShaderWithUniformLikeDecoration("OpDecorateId %int0 Uniform");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_2);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_2));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Decorations that don't take ID parameters may not be "
+                        "used with OpDecorateId\n"
+                        "  OpDecorateId %int0 Uniform"));
+}
+
+TEST_F(ValidateDecorations, UniformIdDecorationWithWrongInstructionBad) {
+  const std::string spirv = ShaderWithUniformLikeDecoration(
+      "OpDecorate %int0 UniformId %subgroupscope");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr(
+          "Decorations taking ID parameters may not be used with OpDecorateId\n"
+          "  OpDecorate %int0 UniformId %subgroupscope"));
+}
+
 TEST_F(ValidateDecorations, MultipleOffsetDecorationsOnSameID) {
   std::string spirv = R"(
             OpCapability Shader
@@ -5070,6 +5194,26 @@
                         "SPV_KHR_no_integer_wrap_decoration"));
 }
 
+TEST_F(ValidateDecorations, NoSignedWrapRequiresExtensionV13Bad) {
+  std::string spirv = MakeIntegerShader("OpDecorate %val NoSignedWrap",
+                                        "%val = OpIAdd %int %zero %zero", "");
+
+  CompileSuccessfully(spirv);
+  EXPECT_NE(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("requires one of these extensions: "
+                        "SPV_KHR_no_integer_wrap_decoration"));
+}
+
+TEST_F(ValidateDecorations, NoSignedWrapOkInSPV14Good) {
+  std::string spirv = MakeIntegerShader("OpDecorate %val NoSignedWrap",
+                                        "%val = OpIAdd %int %zero %zero", "");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
 TEST_F(ValidateDecorations, NoSignedWrapIAddGood) {
   std::string spirv = MakeIntegerShader("OpDecorate %val NoSignedWrap",
                                         "%val = OpIAdd %int %zero %zero");
@@ -5192,6 +5336,26 @@
                         "SPV_KHR_no_integer_wrap_decoration"));
 }
 
+TEST_F(ValidateDecorations, NoUnsignedWrapRequiresExtensionV13Bad) {
+  std::string spirv = MakeIntegerShader("OpDecorate %val NoUnsignedWrap",
+                                        "%val = OpIAdd %int %zero %zero", "");
+
+  CompileSuccessfully(spirv);
+  EXPECT_NE(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("requires one of these extensions: "
+                        "SPV_KHR_no_integer_wrap_decoration"));
+}
+
+TEST_F(ValidateDecorations, NoUnsignedWrapOkInSPV14Good) {
+  std::string spirv = MakeIntegerShader("OpDecorate %val NoUnsignedWrap",
+                                        "%val = OpIAdd %int %zero %zero", "");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
 TEST_F(ValidateDecorations, NoUnsignedWrapIAddGood) {
   std::string spirv = MakeIntegerShader("OpDecorate %val NoUnsignedWrap",
                                         "%val = OpIAdd %int %zero %zero");
@@ -5987,6 +6151,18 @@
                         "buffer\n  %var_wg"));
 }
 
+TEST_F(ValidateDecorations, NonWritableVarWorkgroupV14Bad) {
+  std::string spirv = ShaderWithNonWritableTarget("%var_wg");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Target of NonWritable decoration is invalid: must "
+                        "point to a storage image, uniform block, storage "
+                        "buffer, or variable in Private or Function storage "
+                        "class\n  %var_wg"));
+}
+
 TEST_F(ValidateDecorations, NonWritableVarPrivateBad) {
   std::string spirv = ShaderWithNonWritableTarget("%var_priv");
 
@@ -5998,6 +6174,36 @@
                         "buffer\n  %var_priv"));
 }
 
+TEST_F(ValidateDecorations, NonWritableVarPrivateV13Bad) {
+  std::string spirv = ShaderWithNonWritableTarget("%var_priv");
+
+  CompileSuccessfully(spirv);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Target of NonWritable decoration is invalid: must "
+                        "point to a storage image, uniform block, or storage "
+                        "buffer\n  %var_priv"));
+}
+
+TEST_F(ValidateDecorations, NonWritableVarPrivateV14Good) {
+  std::string spirv = ShaderWithNonWritableTarget("%var_priv");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateDecorations, NonWritableVarPrivateV13TargetV14Bad) {
+  std::string spirv = ShaderWithNonWritableTarget("%var_priv");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Target of NonWritable decoration is invalid: must "
+                        "point to a storage image, uniform block, or storage "
+                        "buffer\n  %var_priv"));
+}
+
 TEST_F(ValidateDecorations, NonWritableVarFunctionBad) {
   std::string spirv = ShaderWithNonWritableTarget("%var_func");
 
@@ -6084,6 +6290,36 @@
                 SPV_ERROR_INVALID_ID,
                 "is not valid for the WebGPU execution environment."))));
 
+TEST_F(ValidateDecorations, NonWritableVarFunctionV13Bad) {
+  std::string spirv = ShaderWithNonWritableTarget("%var_func");
+
+  CompileSuccessfully(spirv);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Target of NonWritable decoration is invalid: must "
+                        "point to a storage image, uniform block, or storage "
+                        "buffer\n  %var_func"));
+}
+
+TEST_F(ValidateDecorations, NonWritableVarFunctionV14Good) {
+  std::string spirv = ShaderWithNonWritableTarget("%var_func");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateDecorations, NonWritableVarFunctionV13TargetV14Bad) {
+  std::string spirv = ShaderWithNonWritableTarget("%var_func");
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Target of NonWritable decoration is invalid: must "
+                        "point to a storage image, uniform block, or storage "
+                        "buffer\n  %var_func"));
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_extensions_test.cpp b/test/val/val_extensions_test.cpp
index 52ce382..682c321 100644
--- a/test/val/val_extensions_test.cpp
+++ b/test/val/val_extensions_test.cpp
@@ -288,9 +288,12 @@
 
   CompileSuccessfully(code.c_str(), GetParam().env);
   if (GetParam().success) {
-    ASSERT_EQ(SPV_SUCCESS, ValidateInstructions(GetParam().env));
+    ASSERT_EQ(SPV_SUCCESS, ValidateInstructions(GetParam().env))
+        << getDiagnosticString();
   } else {
-    ASSERT_NE(SPV_SUCCESS, ValidateInstructions(GetParam().env));
+    ASSERT_NE(SPV_SUCCESS, ValidateInstructions(GetParam().env))
+        << " in " << spvTargetEnvDescription(GetParam().env) << ":\n"
+        << code;
     const std::string message = getDiagnosticString();
     if (spvIsVulkanEnv(GetParam().env)) {
       EXPECT_THAT(message, HasSubstr(std::string(GetParam().cap) +
diff --git a/test/val/val_fixtures.h b/test/val/val_fixtures.h
index fd5368b..79ad725 100644
--- a/test/val/val_fixtures.h
+++ b/test/val/val_fixtures.h
@@ -37,6 +37,11 @@
   // Returns the a spv_const_binary struct
   spv_const_binary get_const_binary();
 
+  // Assembles the given SPIR-V text, checks that it fails to assemble,
+  // and returns resulting diagnostic.  No internal state is updated.
+  std::string CompileFailure(std::string code,
+                             spv_target_env env = SPV_ENV_UNIVERSAL_1_0);
+
   // Checks that 'code' is valid SPIR-V text representation and stores the
   // binary version for further method calls.
   void CompileSuccessfully(std::string code,
@@ -101,16 +106,29 @@
 }
 
 template <typename T>
+std::string ValidateBase<T>::CompileFailure(std::string code,
+                                            spv_target_env env) {
+  spv_diagnostic diagnostic = nullptr;
+  EXPECT_NE(SPV_SUCCESS,
+            spvTextToBinary(ScopedContext(env).context, code.c_str(),
+                            code.size(), &binary_, &diagnostic));
+  std::string result(diagnostic->error);
+  return result;
+}
+
+template <typename T>
 void ValidateBase<T>::CompileSuccessfully(std::string code,
                                           spv_target_env env) {
   DestroyBinary();
   spv_diagnostic diagnostic = nullptr;
-  ASSERT_EQ(SPV_SUCCESS,
-            spvTextToBinary(ScopedContext(env).context, code.c_str(),
-                            code.size(), &binary_, &diagnostic))
+  ScopedContext context(env);
+  auto status = spvTextToBinary(context.context, code.c_str(), code.size(),
+                                &binary_, &diagnostic);
+  EXPECT_EQ(SPV_SUCCESS, status)
       << "ERROR: " << diagnostic->error
       << "\nSPIR-V could not be compiled into binary:\n"
       << code;
+  ASSERT_EQ(SPV_SUCCESS, status);
   spvDiagnosticDestroy(diagnostic);
 }
 
diff --git a/test/val/val_image_test.cpp b/test/val/val_image_test.cpp
index 44e1783..c2fcb4f 100644
--- a/test/val/val_image_test.cpp
+++ b/test/val/val_image_test.cpp
@@ -25,6 +25,7 @@
 namespace val {
 namespace {
 
+using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Not;
 
@@ -54,9 +55,33 @@
     ss << "OpCapability SampledRect\n";
   }
 
+  // In 1.4, the entry point must list all module-scope variables used.  Just
+  // list all of them.
+  std::string interface_vars = (env != SPV_ENV_UNIVERSAL_1_4) ? "" :
+                                                              R"(
+%uniform_image_f32_1d_0001
+%uniform_image_f32_1d_0002_rgba32f
+%uniform_image_f32_2d_0001
+%uniform_image_f32_2d_0010
+%uniform_image_u32_2d_0001
+%uniform_image_u32_2d_0000
+%uniform_image_s32_3d_0001
+%uniform_image_f32_2d_0002
+%uniform_image_s32_2d_0002
+%uniform_image_f32_spd_0002
+%uniform_image_f32_3d_0111
+%uniform_image_f32_cube_0101
+%uniform_image_f32_cube_0102_rgba32f
+%uniform_sampler
+%private_image_u32_buffer_0002_r32ui
+%private_image_u32_spd_0002
+%private_image_f32_buffer_0002_r32ui
+)";
+
   ss << capabilities_and_extensions;
   ss << "OpMemoryModel Logical " << memory_model << "\n";
-  ss << "OpEntryPoint " << execution_model << " %main \"main\"\n";
+  ss << "OpEntryPoint " << execution_model
+     << " %main \"main\" " + interface_vars + "\n";
   if (execution_model == "Fragment") {
     ss << "OpExecutionMode %main OriginUpperLeft\n";
   }
@@ -79,6 +104,8 @@
 OpDecorate %uniform_image_s32_3d_0001 Binding 2
 OpDecorate %uniform_image_f32_2d_0002 DescriptorSet 1
 OpDecorate %uniform_image_f32_2d_0002 Binding 3
+OpDecorate %uniform_image_s32_2d_0002 DescriptorSet 1
+OpDecorate %uniform_image_s32_2d_0002 Binding 4
 OpDecorate %uniform_image_f32_spd_0002 DescriptorSet 2
 OpDecorate %uniform_image_f32_spd_0002 Binding 0
 OpDecorate %uniform_image_f32_3d_0111 DescriptorSet 2
@@ -222,6 +249,11 @@
 %uniform_image_f32_2d_0002 = OpVariable %ptr_image_f32_2d_0002 UniformConstant
 %type_sampled_image_f32_2d_0002 = OpTypeSampledImage %type_image_f32_2d_0002
 
+%type_image_s32_2d_0002 = OpTypeImage %s32 2D 0 0 0 2 Unknown
+%ptr_image_s32_2d_0002 = OpTypePointer UniformConstant %type_image_s32_2d_0002
+%uniform_image_s32_2d_0002 = OpVariable %ptr_image_s32_2d_0002 UniformConstant
+%type_sampled_image_s32_2d_0002 = OpTypeSampledImage %type_image_s32_2d_0002
+
 %type_image_f32_spd_0002 = OpTypeImage %f32 SubpassData 0 0 0 2 Unknown
 %ptr_image_f32_spd_0002 = OpTypePointer UniformConstant %type_image_f32_spd_0002
 %uniform_image_f32_spd_0002 = OpVariable %ptr_image_f32_spd_0002 UniformConstant
@@ -703,7 +735,7 @@
 
   CompileSuccessfully(GenerateShaderCode(body).c_str());
   ASSERT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
-  EXPECT_THAT(getDiagnosticString(), HasSubstr("Operand 136[%136] cannot be a "
+  EXPECT_THAT(getDiagnosticString(), HasSubstr("Operand 140[%140] cannot be a "
                                                "type"));
 }
 
@@ -4571,6 +4603,150 @@
                         "for OpReturnValue"));
 }
 
+TEST_F(ValidateImage, SignExtendV13Bad) {
+  const std::string body = R"(
+%img = OpLoad %type_image_u32_2d_0000 %uniform_image_u32_2d_0000
+%res1 = OpImageRead %u32vec4 %img %u32vec2_01 SignExtend
+)";
+
+  EXPECT_THAT(CompileFailure(GenerateShaderCode(body, "", "Fragment",
+                                                SPV_ENV_UNIVERSAL_1_3)),
+              HasSubstr("Invalid image operand 'SignExtend'"));
+}
+
+TEST_F(ValidateImage, ZeroExtendV13Bad) {
+  const std::string body = R"(
+%img = OpLoad %type_image_u32_2d_0000 %uniform_image_u32_2d_0000
+%res1 = OpImageRead %u32vec4 %img %u32vec2_01 ZeroExtend
+)";
+
+  EXPECT_THAT(CompileFailure(GenerateShaderCode(body, "", "Fragment",
+                                                SPV_ENV_UNIVERSAL_1_3)),
+              HasSubstr("Invalid image operand 'ZeroExtend'"));
+}
+
+TEST_F(ValidateImage, SignExtendScalarUIntTexelV14Good) {
+  // Unsigned int sampled type
+  const std::string body = R"(
+%img = OpLoad %type_image_u32_2d_0000 %uniform_image_u32_2d_0000
+%res1 = OpImageRead %u32 %img %u32vec2_01 SignExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateImage, SignExtendScalarSIntTexelV14Good) {
+  // Signed int sampled type
+  const std::string body = R"(
+%img = OpLoad %type_image_s32_2d_0002 %uniform_image_s32_2d_0002
+%res1 = OpImageRead %s32 %img %u32vec2_01 SignExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateImage, SignExtendScalarVectorUIntTexelV14Good) {
+  const std::string body = R"(
+%img = OpLoad %type_image_u32_2d_0000 %uniform_image_u32_2d_0000
+%res1 = OpImageRead %u32vec4 %img %u32vec2_01 SignExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateImage, SignExtendVectorSIntTexelV14Good) {
+  const std::string body = R"(
+%img = OpLoad %type_image_s32_2d_0002 %uniform_image_s32_2d_0002
+%res1 = OpImageRead %s32vec4 %img %u32vec2_01 SignExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+// No negative tests for SignExtend since we don't truly know the
+// texel format.
+
+TEST_F(ValidateImage, ZeroExtendScalarUIntTexelV14Good) {
+  // Unsigned int sampled type
+  const std::string body = R"(
+%img = OpLoad %type_image_u32_2d_0000 %uniform_image_u32_2d_0000
+%res1 = OpImageRead %u32 %img %u32vec2_01 ZeroExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateImage, ZeroExtendScalarSIntTexelV14Good) {
+  // Zeroed int sampled type
+  const std::string body = R"(
+%img = OpLoad %type_image_s32_2d_0002 %uniform_image_s32_2d_0002
+%res1 = OpImageRead %s32 %img %u32vec2_01 ZeroExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateImage, ZeroExtendScalarVectorUIntTexelV14Good) {
+  const std::string body = R"(
+%img = OpLoad %type_image_u32_2d_0000 %uniform_image_u32_2d_0000
+%res1 = OpImageRead %u32vec4 %img %u32vec2_01 ZeroExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateImage, ZeroExtendVectorSIntTexelV14Good) {
+  const std::string body = R"(
+%img = OpLoad %type_image_s32_2d_0002 %uniform_image_s32_2d_0002
+%res1 = OpImageRead %s32vec4 %img %u32vec2_01 ZeroExtend
+)";
+  const std::string extra = "\nOpCapability StorageImageReadWithoutFormat\n";
+
+  CompileSuccessfully(
+      GenerateShaderCode(body, extra, "Fragment", SPV_ENV_UNIVERSAL_1_4),
+      SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+// No negative tests for ZeroExtend since we don't truly know the
+// texel format.
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_interfaces_test.cpp b/test/val/val_interfaces_test.cpp
index ce430f6..2455173 100644
--- a/test/val/val_interfaces_test.cpp
+++ b/test/val/val_interfaces_test.cpp
@@ -48,8 +48,9 @@
   ASSERT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
   EXPECT_THAT(
       getDiagnosticString(),
-      HasSubstr("Input variable id <5> is used by entry point 'func' id <1>, "
-                "but is not listed as an interface"));
+      HasSubstr(
+          "Interface variable id <5> is used by entry point 'func' id <1>, "
+          "but is not listed as an interface"));
 }
 
 TEST_F(ValidateInterfacesTest, EntryPointMissingOutput) {
@@ -74,8 +75,9 @@
   ASSERT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
   EXPECT_THAT(
       getDiagnosticString(),
-      HasSubstr("Output variable id <5> is used by entry point 'func' id <1>, "
-                "but is not listed as an interface"));
+      HasSubstr(
+          "Interface variable id <5> is used by entry point 'func' id <1>, "
+          "but is not listed as an interface"));
 }
 
 TEST_F(ValidateInterfacesTest, InterfaceMissingUseInSubfunction) {
@@ -105,8 +107,9 @@
   ASSERT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
   EXPECT_THAT(
       getDiagnosticString(),
-      HasSubstr("Input variable id <5> is used by entry point 'func' id <1>, "
-                "but is not listed as an interface"));
+      HasSubstr(
+          "Interface variable id <5> is used by entry point 'func' id <1>, "
+          "but is not listed as an interface"));
 }
 
 TEST_F(ValidateInterfacesTest, TwoEntryPointsOneFunction) {
@@ -132,8 +135,9 @@
   ASSERT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
   EXPECT_THAT(
       getDiagnosticString(),
-      HasSubstr("Input variable id <2> is used by entry point 'func2' id <1>, "
-                "but is not listed as an interface"));
+      HasSubstr(
+          "Interface variable id <2> is used by entry point 'func2' id <1>, "
+          "but is not listed as an interface"));
 }
 
 TEST_F(ValidateInterfacesTest, MissingInterfaceThroughInitializer) {
@@ -160,8 +164,215 @@
   ASSERT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
   EXPECT_THAT(
       getDiagnosticString(),
-      HasSubstr("Input variable id <6> is used by entry point 'func' id <1>, "
-                "but is not listed as an interface"));
+      HasSubstr(
+          "Interface variable id <6> is used by entry point 'func' id <1>, "
+          "but is not listed as an interface"));
+}
+
+TEST_F(ValidateInterfacesTest, NonUniqueInterfacesSPV1p3) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main" %var %var
+OpExecutionMode %main LocalSize 1 1 1
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%uint3 = OpTypeVector %uint 3
+%struct = OpTypeStruct %uint3
+%ptr_struct = OpTypePointer Input %struct
+%var = OpVariable %ptr_struct Input
+%func_ty = OpTypeFunction %void
+%main = OpFunction %void None %func_ty
+%1 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+}
+
+TEST_F(ValidateInterfacesTest, NonUniqueInterfacesSPV1p4) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main" %var %var
+OpExecutionMode %main LocalSize 1 1 1
+OpName %main "main"
+OpName %var "var"
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%uint3 = OpTypeVector %uint 3
+%struct = OpTypeStruct %uint3
+%ptr_struct = OpTypePointer Input %struct
+%var = OpVariable %ptr_struct Input
+%func_ty = OpTypeFunction %void
+%main = OpFunction %void None %func_ty
+%1 = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Non-unique OpEntryPoint interface 2[%var] is disallowed"));
+}
+
+TEST_F(ValidateInterfacesTest, MissingGlobalVarSPV1p3) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%uint3 = OpTypeVector %uint 3
+%struct = OpTypeStruct %uint3
+%ptr_struct = OpTypePointer StorageBuffer %struct
+%var = OpVariable %ptr_struct StorageBuffer
+%func_ty = OpTypeFunction %void
+%main = OpFunction %void None %func_ty
+%1 = OpLabel
+%ld = OpLoad %struct %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+}
+
+TEST_F(ValidateInterfacesTest, MissingGlobalVarSPV1p4) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpName %var "var"
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%uint3 = OpTypeVector %uint 3
+%struct = OpTypeStruct %uint3
+%ptr_struct = OpTypePointer StorageBuffer %struct
+%var = OpVariable %ptr_struct StorageBuffer
+%func_ty = OpTypeFunction %void
+%main = OpFunction %void None %func_ty
+%1 = OpLabel
+%ld = OpLoad %struct %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Interface variable id <2> is used by entry point "
+                        "'main' id <1>, but is not listed as an interface"));
+}
+
+TEST_F(ValidateInterfacesTest, FunctionInterfaceVarSPV1p3) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main" %var
+OpExecutionMode %main LocalSize 1 1 1
+OpName %var "var"
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%uint3 = OpTypeVector %uint 3
+%struct = OpTypeStruct %uint3
+%ptr_struct = OpTypePointer Function %struct
+%func_ty = OpTypeFunction %void
+%main = OpFunction %void None %func_ty
+%1 = OpLabel
+%var = OpVariable %ptr_struct Function
+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("OpEntryPoint interfaces must be OpVariables with "
+                        "Storage Class of Input(1) or Output(3). Found Storage "
+                        "Class 7 for Entry Point id 1."));
+}
+
+TEST_F(ValidateInterfacesTest, FunctionInterfaceVarSPV1p4) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main" %var
+OpExecutionMode %main LocalSize 1 1 1
+OpName %var "var"
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%uint3 = OpTypeVector %uint 3
+%struct = OpTypeStruct %uint3
+%ptr_struct = OpTypePointer Function %struct
+%func_ty = OpTypeFunction %void
+%main = OpFunction %void None %func_ty
+%1 = OpLabel
+%var = OpVariable %ptr_struct Function
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("OpEntryPoint interfaces should only list global variables"));
+}
+
+TEST_F(ValidateInterfacesTest, ModuleSPV1p3ValidateSPV1p4_NotAllUsedGlobals) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main"
+OpExecutionMode %main LocalSize 1 1 1
+OpName %var "var"
+%void = OpTypeVoid
+%uint = OpTypeInt 32 0
+%uint3 = OpTypeVector %uint 3
+%struct = OpTypeStruct %uint3
+%ptr_struct = OpTypePointer StorageBuffer %struct
+%var = OpVariable %ptr_struct StorageBuffer
+%func_ty = OpTypeFunction %void
+%main = OpFunction %void None %func_ty
+%1 = OpLabel
+%ld = OpLoad %struct %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+}
+
+TEST_F(ValidateInterfacesTest, ModuleSPV1p3ValidateSPV1p4_DuplicateInterface) {
+  const std::string text = R"(
+OpCapability Shader
+OpMemoryModel Logical GLSL450
+OpEntryPoint GLCompute %main "main" %gid %gid
+OpExecutionMode %main LocalSize 1 1 1
+OpDecorate %gid BuiltIn GlobalInvocationId
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int3 = OpTypeVector %int 3
+%ptr_input_int3 = OpTypePointer Input %int3
+%gid = OpVariable %ptr_input_int3 Input
+%void_fn = OpTypeFunction %void
+%main = OpFunction %void None %void_fn
+%entry = OpLabel
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(text, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
 }
 
 }  // namespace
diff --git a/test/val/val_logicals_test.cpp b/test/val/val_logicals_test.cpp
index da8e7d9..b57c743 100644
--- a/test/val/val_logicals_test.cpp
+++ b/test/val/val_logicals_test.cpp
@@ -24,6 +24,7 @@
 namespace val {
 namespace {
 
+using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::Not;
 
@@ -144,6 +145,18 @@
 %boolvec3_tft = OpConstantComposite %boolvec3 %true %false %true
 %boolvec4_tftf = OpConstantComposite %boolvec4 %true %false %true %false
 
+%arr_u32_2 = OpTypeArray %u32 %u32_2
+%st_u32_u32 = OpTypeStruct %u32 %u32
+%mat_f32_2_2 = OpTypeMatrix %f32vec2 2
+
+%nul_arr_u32_2 = OpConstantNull %arr_u32_2
+%nul_st_u32_u32 = OpConstantNull %st_u32_u32
+%nul_mat_f32_2_2 = OpConstantNull %mat_f32_2_2
+
+%arr_u32_2_1_2 = OpConstantComposite %arr_u32_2 %u32_1 %u32_2
+%st_u32_u32_1_2 = OpConstantComposite %st_u32_u32 %u32_1 %u32_2
+%mat_f32_2_2_01_12 = OpConstantComposite %mat_f32_2_2 %f32vec2_01 %f32vec2_12
+
 %f32vec4ptr = OpTypePointer Function %f32vec4
 
 %main = OpFunction %void None %func
@@ -585,6 +598,20 @@
       HasSubstr("Expected scalar or vector type as Result Type: Select"));
 }
 
+TEST_F(ValidateLogicals, OpSelectWrongTypeIdV14) {
+  // In 1.4, the message changes to allow composites.
+  const std::string body = R"(
+%val1 = OpSelect %void %true %u32_0 %u32_1
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str(), SPV_ENV_UNIVERSAL_1_4);
+  ASSERT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Expected scalar or composite type as Result Type: Select"));
+}
+
 TEST_F(ValidateLogicals, OpSelectPointerNoCapability) {
   const std::string body = R"(
 %x = OpVariable %f32vec4ptr Function
@@ -687,6 +714,111 @@
               HasSubstr("Expected both objects to be of Result Type: Select"));
 }
 
+TEST_F(ValidateLogicals, OpSelectArrayV13Bad) {
+  const std::string body = R"(
+%val1 = OpSelect %arr_u32_2 %true %nul_arr_u32_2 %arr_u32_2_1_2
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str());
+  ASSERT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Expected scalar or vector type as Result Type: Select"));
+}
+
+TEST_F(ValidateLogicals, OpSelectArrayV13TargetV14Bad) {
+  const std::string body = R"(
+%val1 = OpSelect %arr_u32_2 %true %nul_arr_u32_2 %arr_u32_2_1_2
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str());
+  ASSERT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Expected scalar or vector type as Result Type"));
+}
+
+TEST_F(ValidateLogicals, OpSelectArrayV14Good) {
+  const std::string body = R"(
+%val1 = OpSelect %arr_u32_2 %true %nul_arr_u32_2 %arr_u32_2_1_2
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str(), SPV_ENV_UNIVERSAL_1_4);
+  ASSERT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateLogicals, OpSelectStructV13Bad) {
+  const std::string body = R"(
+%val1 = OpSelect %st_u32_u32 %true %nul_st_u32_u32 %st_u32_u32_1_2
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str());
+  ASSERT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Expected scalar or vector type as Result Type: Select"));
+}
+
+TEST_F(ValidateLogicals, OpSelectStructV13TargetV14Bad) {
+  const std::string body = R"(
+%val1 = OpSelect %st_u32_u32 %true %nul_st_u32_u32 %st_u32_u32_1_2
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str());
+  ASSERT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Expected scalar or vector type as Result Type"));
+}
+
+TEST_F(ValidateLogicals, OpSelectStructV14Good) {
+  const std::string body = R"(
+%val1 = OpSelect %st_u32_u32 %true %nul_st_u32_u32 %st_u32_u32_1_2
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str(), SPV_ENV_UNIVERSAL_1_4);
+  ASSERT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateLogicals, OpSelectMatrixV13Bad) {
+  const std::string body = R"(
+%val1 = OpSelect %mat_f32_2_2 %true %nul_mat_f32_2_2 %mat_f32_2_2_01_12
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str());
+  ASSERT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("Expected scalar or vector type as Result Type: Select"));
+}
+
+TEST_F(ValidateLogicals, OpSelectMatrixV13TargetV14Bad) {
+  const std::string body = R"(
+%val1 = OpSelect %mat_f32_2_2 %true %nul_mat_f32_2_2 %mat_f32_2_2_01_12
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str());
+  ASSERT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Expected scalar or vector type as Result Type"));
+}
+
+TEST_F(ValidateLogicals, OpSelectMatrixV14Good) {
+  const std::string body = R"(
+%val1 = OpSelect %mat_f32_2_2 %true %nul_mat_f32_2_2 %mat_f32_2_2_01_12
+)";
+
+  CompileSuccessfully(GenerateShaderCode(body).c_str(), SPV_ENV_UNIVERSAL_1_4);
+  ASSERT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
 TEST_F(ValidateLogicals, OpIEqualSuccess) {
   const std::string body = R"(
 %val1 = OpIEqual %bool %u32_0 %s32_1
@@ -949,6 +1081,84 @@
   ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
 }
 
+TEST_F(ValidateLogicals, SelectVectorsScalarCondition) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%int4 = OpTypeVector %int 4
+%int4_0 = OpConstantNull %int4
+%true = OpConstantTrue %bool
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%select = OpSelect %int4 %true %int4_0 %int4_0
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Expected vector sizes of Result Type and the "
+                        "condition to be equal: Select"));
+}
+
+TEST_F(ValidateLogicals, SelectVectorsScalarCondition1p4) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%int4 = OpTypeVector %int 4
+%int4_0 = OpConstantNull %int4
+%true = OpConstantTrue %bool
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%select = OpSelect %int4 %true %int4_0 %int4_0
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+}
+
+TEST_F(ValidateLogicals, SelectVectorsVectorConditionMismatchedDimensions1p4) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%bool3 = OpTypeVector %bool 3
+%int = OpTypeInt 32 0
+%int4 = OpTypeVector %int 4
+%int4_0 = OpConstantNull %int4
+%bool3_null = OpConstantNull %bool3
+%void_fn = OpTypeFunction %void
+%func = OpFunction %void None %void_fn
+%1 = OpLabel
+%select = OpSelect %int4 %bool3_null %int4_0 %int4_0
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Expected vector sizes of Result Type and the "
+                        "condition to be equal: Select"));
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_memory_test.cpp b/test/val/val_memory_test.cpp
index 246b85e..490ffa8 100644
--- a/test/val/val_memory_test.cpp
+++ b/test/val/val_memory_test.cpp
@@ -27,6 +27,7 @@
 
 using ::testing::Eq;
 using ::testing::HasSubstr;
+using ::testing::Values;
 
 using ValidateMemory = spvtest::ValidateBase<bool>;
 
@@ -1342,32 +1343,6 @@
                 "VulkanMemoryModelDeviceScopeKHR capability"));
 }
 
-TEST_F(ValidateMemory, VulkanMemoryModelDeviceScopeCopyMemoryGood1) {
-  const std::string spirv = R"(
-OpCapability Shader
-OpCapability VulkanMemoryModelKHR
-OpCapability VulkanMemoryModelDeviceScopeKHR
-OpCapability Linkage
-OpExtension "SPV_KHR_vulkan_memory_model"
-OpMemoryModel Logical VulkanKHR
-%void = OpTypeVoid
-%int = OpTypeInt 32 0
-%device = OpConstant %int 1
-%int_ptr_ssbo = OpTypePointer StorageBuffer %int
-%var1 = OpVariable %int_ptr_ssbo StorageBuffer
-%var2 = OpVariable %int_ptr_ssbo StorageBuffer
-%voidfn = OpTypeFunction %void
-%func = OpFunction %void None %voidfn
-%entry = OpLabel
-OpCopyMemory %var1 %var2 MakePointerAvailableKHR|NonPrivatePointerKHR %device
-OpReturn
-OpFunctionEnd
-)";
-
-  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
-  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
-}
-
 TEST_F(ValidateMemory, VulkanMemoryModelDeviceScopeCopyMemoryGood2) {
   const std::string spirv = R"(
 OpCapability Shader
@@ -1422,6 +1397,138 @@
   EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
 }
 
+TEST_F(ValidateMemory, VulkanMemoryModelCopyMemoryTwoAccessAvVisBadBinaryV13) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability VulkanMemoryModelKHR
+OpCapability VulkanMemoryModelDeviceScopeKHR
+OpCapability Linkage
+OpExtension "SPV_KHR_vulkan_memory_model"
+OpMemoryModel Logical VulkanKHR
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%device = OpConstant %int 1
+%int_ptr_ssbo = OpTypePointer StorageBuffer %int
+%var1 = OpVariable %int_ptr_ssbo StorageBuffer
+%var2 = OpVariable %int_ptr_ssbo StorageBuffer
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2
+  MakePointerAvailableKHR|NonPrivatePointerKHR %device
+  MakePointerVisibleKHR|NonPrivatePointerKHR %device
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr(
+          "with two memory access operands requires SPIR-V 1.4 or later"));
+}
+
+TEST_F(ValidateMemory, VulkanMemoryModelCopyMemoryTwoAccessAvVisGood) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability VulkanMemoryModelKHR
+OpCapability VulkanMemoryModelDeviceScopeKHR
+OpCapability Linkage
+OpExtension "SPV_KHR_vulkan_memory_model"
+OpMemoryModel Logical VulkanKHR
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%device = OpConstant %int 1
+%int_ptr_ssbo = OpTypePointer StorageBuffer %int
+%var1 = OpVariable %int_ptr_ssbo StorageBuffer
+%var2 = OpVariable %int_ptr_ssbo StorageBuffer
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2
+  MakePointerAvailableKHR|NonPrivatePointerKHR %device
+  MakePointerVisibleKHR|NonPrivatePointerKHR %device
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateMemory, VulkanMemoryModelCopyMemoryTwoAccessFirstWithAvBad) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability VulkanMemoryModelKHR
+OpCapability VulkanMemoryModelDeviceScopeKHR
+OpCapability Linkage
+OpExtension "SPV_KHR_vulkan_memory_model"
+OpMemoryModel Logical VulkanKHR
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%device = OpConstant %int 1
+%int_ptr_ssbo = OpTypePointer StorageBuffer %int
+%var1 = OpVariable %int_ptr_ssbo StorageBuffer
+%var2 = OpVariable %int_ptr_ssbo StorageBuffer
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2
+  MakePointerAvailableKHR|NonPrivatePointerKHR %device
+  MakePointerAvailableKHR|NonPrivatePointerKHR %device
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr(
+          "Source memory access must not include MakePointerAvailableKHR\n"
+          "  OpCopyMemory %5 %6 MakePointerAvailableKHR|NonPrivatePointerKHR"
+          " %uint_1 MakePointerAvailableKHR|NonPrivatePointerKHR %uint_1"));
+}
+
+TEST_F(ValidateMemory, VulkanMemoryModelCopyMemoryTwoAccessSecondWithVisBad) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability VulkanMemoryModelKHR
+OpCapability VulkanMemoryModelDeviceScopeKHR
+OpCapability Linkage
+OpExtension "SPV_KHR_vulkan_memory_model"
+OpMemoryModel Logical VulkanKHR
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%device = OpConstant %int 1
+%int_ptr_ssbo = OpTypePointer StorageBuffer %int
+%var1 = OpVariable %int_ptr_ssbo StorageBuffer
+%var2 = OpVariable %int_ptr_ssbo StorageBuffer
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2
+  MakePointerVisibleKHR|NonPrivatePointerKHR %device
+  MakePointerVisibleKHR|NonPrivatePointerKHR %device
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr(
+          "Target memory access must not include MakePointerVisibleKHR\n"
+          "  OpCopyMemory %5 %6 MakePointerVisibleKHR|NonPrivatePointerKHR"
+          " %uint_1 MakePointerVisibleKHR|NonPrivatePointerKHR %uint_1"));
+}
+
 TEST_F(ValidateMemory, VulkanMemoryModelDeviceScopeCopyMemorySizedBad1) {
   const std::string spirv = R"(
 OpCapability Shader
@@ -1767,8 +1874,8 @@
 OpFunctionEnd
 )";
 
-  CompileSuccessfully(body.c_str());
-  ASSERT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
+  CompileSuccessfully(body);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions());
   EXPECT_THAT(
       getDiagnosticString(),
       HasSubstr("PhysicalStorageBufferEXT must not be used with OpVariable"));
@@ -2934,6 +3041,508 @@
   EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_VULKAN_1_1));
 }
 
+TEST_F(ValidateMemory, CopyMemoryNoAccessGood) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateMemory, CopyMemorySimpleMixedAccessGood) {
+  // Test one memory access operand using features that don't require the
+  // Vulkan memory model.
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2 Volatile|Aligned|Nontemporal 4
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateMemory, CopyMemorySimpleTwoMixedAccessV13Bad) {
+  // Two memory access operands is invalid up to SPIR-V 1.3
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2 Volatile Volatile
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("CopyMemory with two memory access operands requires "
+                        "SPIR-V 1.4 or later"));
+}
+
+TEST_F(ValidateMemory, CopyMemorySimpleTwoMixedAccessV14Good) {
+  // Two memory access operands is valid in SPIR-V 1.4
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemory %var1 %var2 Volatile Volatile
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateMemory, CopyMemorySizedNoAccessGood) {
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability Addresses
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_16 = OpConstant %int 16
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemorySized %var1 %var2 %int_16
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions());
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+TEST_F(ValidateMemory, CopyMemorySizedSimpleMixedAccessGood) {
+  // Test one memory access operand using features that don't require the
+  // Vulkan memory model.
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability Addresses
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_16 = OpConstant %int 16
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemorySized %var1 %var2 %int_16 Volatile|Aligned|Nontemporal 4
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv);
+  ASSERT_EQ(SPV_SUCCESS, ValidateInstructions());
+}
+
+TEST_F(ValidateMemory, CopyMemorySizedSimpleTwoMixedAccessV13Bad) {
+  // Two memory access operands is invalid up to SPIR-V 1.3
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability Addresses
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_16 = OpConstant %int 16
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemorySized %var1 %var2 %int_16 Volatile Volatile
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_3);
+  EXPECT_EQ(SPV_ERROR_INVALID_DATA,
+            ValidateInstructions(SPV_ENV_UNIVERSAL_1_3));
+  EXPECT_THAT(
+      getDiagnosticString(),
+      HasSubstr("CopyMemorySized with two memory access operands requires "
+                "SPIR-V 1.4 or later"));
+}
+
+TEST_F(ValidateMemory, CopyMemorySizedSimpleTwoMixedAccessV14Good) {
+  // Two memory access operands is valid in SPIR-V 1.4
+  const std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability Addresses
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%int = OpTypeInt 32 0
+%int_16 = OpConstant %int 16
+%int_ptr_priv = OpTypePointer Private %int
+%var1 = OpVariable %int_ptr_priv Private
+%var2 = OpVariable %int_ptr_priv Private
+%voidfn = OpTypeFunction %void
+%func = OpFunction %void None %voidfn
+%entry = OpLabel
+OpCopyMemorySized %var1 %var2 %int_16 Volatile Volatile
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(), Eq(""));
+}
+
+using ValidatePointerComparisons = spvtest::ValidateBase<std::string>;
+
+TEST_P(ValidatePointerComparisons, Good) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability VariablePointersStorageBuffer
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer StorageBuffer %int
+%var = OpVariable %ptr_int StorageBuffer
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %int ";
+  } else {
+    spirv += " %bool ";
+  }
+
+  spirv += R"(%var %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+}
+
+TEST_P(ValidatePointerComparisons, GoodWorkgroup) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability VariablePointers
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer Workgroup %int
+%var = OpVariable %ptr_int Workgroup
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %int ";
+  } else {
+    spirv += " %bool ";
+  }
+
+  spirv += R"(%var %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+}
+
+TEST_P(ValidatePointerComparisons, BadResultType) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability VariablePointersStorageBuffer
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer StorageBuffer %int
+%var = OpVariable %ptr_int StorageBuffer
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %bool ";
+  } else {
+    spirv += " %int ";
+  }
+
+  spirv += R"(%var %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  if (operation == "OpPtrDiff") {
+    EXPECT_THAT(getDiagnosticString(),
+                HasSubstr("Result Type must be an integer scalar"));
+  } else {
+    EXPECT_THAT(getDiagnosticString(),
+                HasSubstr("Result Type must be OpTypeBool"));
+  }
+}
+
+TEST_P(ValidatePointerComparisons, BadCapabilities) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer StorageBuffer %int
+%var = OpVariable %ptr_int StorageBuffer
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %int ";
+  } else {
+    spirv += " %bool ";
+  }
+
+  spirv += R"(%var %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  if (operation == "OpPtrDiff") {
+    // Gets caught by the grammar.
+    EXPECT_EQ(SPV_ERROR_INVALID_CAPABILITY,
+              ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  } else {
+    EXPECT_EQ(SPV_ERROR_INVALID_ID,
+              ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+    EXPECT_THAT(getDiagnosticString(),
+                HasSubstr("Instruction cannot be used without a variable "
+                          "pointers capability"));
+  }
+}
+
+TEST_P(ValidatePointerComparisons, BadOperandType) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability VariablePointersStorageBuffer
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer StorageBuffer %int
+%var = OpVariable %ptr_int StorageBuffer
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%ld = OpLoad %int %var
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %int ";
+  } else {
+    spirv += " %bool ";
+  }
+
+  spirv += R"(%ld %ld
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Operand type must be a pointer"));
+}
+
+TEST_P(ValidatePointerComparisons, BadStorageClassWorkgroup) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability VariablePointersStorageBuffer
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer Workgroup %int
+%var = OpVariable %ptr_int Workgroup
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %int ";
+  } else {
+    spirv += " %bool ";
+  }
+
+  spirv += R"(%var %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Workgroup storage class pointer requires "
+                        "VariablePointers capability to be specified"));
+}
+
+TEST_P(ValidatePointerComparisons, BadStorageClass) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability VariablePointersStorageBuffer
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer Private %int
+%var = OpVariable %ptr_int Private
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %int ";
+  } else {
+    spirv += " %bool ";
+  }
+
+  spirv += R"(%var %var
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("Invalid pointer storage class"));
+}
+
+TEST_P(ValidatePointerComparisons, BadDiffOperandTypes) {
+  const std::string operation = GetParam();
+
+  std::string spirv = R"(
+OpCapability Shader
+OpCapability Linkage
+OpCapability VariablePointersStorageBuffer
+OpMemoryModel Logical GLSL450
+%void = OpTypeVoid
+%bool = OpTypeBool
+%int = OpTypeInt 32 0
+%ptr_int = OpTypePointer Private %int
+%var = OpVariable %ptr_int Private
+%func_ty = OpTypeFunction %void
+%func = OpFunction %void None %func_ty
+%1 = OpLabel
+%ld = OpLoad %int %var
+%equal = )" + operation;
+
+  if (operation == "OpPtrDiff") {
+    spirv += " %int ";
+  } else {
+    spirv += " %bool ";
+  }
+
+  spirv += R"(%var %ld
+OpReturn
+OpFunctionEnd
+)";
+
+  CompileSuccessfully(spirv, SPV_ENV_UNIVERSAL_1_4);
+  EXPECT_EQ(SPV_ERROR_INVALID_ID, ValidateInstructions(SPV_ENV_UNIVERSAL_1_4));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("The types of Operand 1 and Operand 2 must match"));
+}
+
+INSTANTIATE_TEST_SUITE_P(PointerComparisons, ValidatePointerComparisons,
+                         Values("OpPtrEqual", "OpPtrNotEqual", "OpPtrDiff"));
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools
diff --git a/test/val/val_version_test.cpp b/test/val/val_version_test.cpp
index 0b194b9..70eb9b1 100644
--- a/test/val/val_version_test.cpp
+++ b/test/val/val_version_test.cpp
@@ -86,6 +86,9 @@
     case SPV_ENV_VULKAN_1_1:
     case SPV_ENV_WEBGPU_0:
       return "1.3";
+    case SPV_ENV_UNIVERSAL_1_4:
+    case SPV_ENV_VULKAN_1_1_SPIRV_1_4:
+      return "1.4";
     default:
       return "0";
   }
@@ -179,6 +182,7 @@
     std::make_tuple(SPV_ENV_VULKAN_1_0, SPV_ENV_OPENGL_4_2,    vulkan_spirv, true),
     std::make_tuple(SPV_ENV_VULKAN_1_0, SPV_ENV_OPENGL_4_3,    vulkan_spirv, true),
     std::make_tuple(SPV_ENV_VULKAN_1_0, SPV_ENV_OPENGL_4_5,    vulkan_spirv, true),
+    std::make_tuple(SPV_ENV_VULKAN_1_0, SPV_ENV_VULKAN_1_1_SPIRV_1_4, vulkan_spirv, true),
 
     std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_UNIVERSAL_1_0, vulkan_spirv, false),
     std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_UNIVERSAL_1_1, vulkan_spirv, false),
@@ -190,7 +194,8 @@
     std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_OPENGL_4_1,    vulkan_spirv, false),
     std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_OPENGL_4_2,    vulkan_spirv, false),
     std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_OPENGL_4_3,    vulkan_spirv, false),
-    std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_OPENGL_4_5,    vulkan_spirv, false)
+    std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_OPENGL_4_5,    vulkan_spirv, false),
+    std::make_tuple(SPV_ENV_VULKAN_1_1, SPV_ENV_VULKAN_1_1_SPIRV_1_4, vulkan_spirv, true)
   )
 );
 
diff --git a/tools/as/as.cpp b/tools/as/as.cpp
index 287ba51..1568579 100644
--- a/tools/as/as.cpp
+++ b/tools/as/as.cpp
@@ -41,14 +41,14 @@
                   Numeric IDs in the binary will have the same values as in the
                   source. Non-numeric IDs are allocated by filling in the gaps,
                   starting with 1 and going up.
-  --target-env {vulkan1.0|vulkan1.1|spv1.0|spv1.1|spv1.2|spv1.3}
+  --target-env {vulkan1.0|vulkan1.1|spv1.0|spv1.1|spv1.2|spv1.3|spv1.4}
                   Use Vulkan 1.0, Vulkan 1.1, SPIR-V 1.0, SPIR-V 1.1,
-                  SPIR-V 1.2, or SPIR-V 1.3
+                  SPIR-V 1.2, SPIR-V 1.3, or SPIR-V 1.4
 )",
       argv0, argv0);
 }
 
-static const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
+static const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_4;
 
 int main(int argc, char** argv) {
   const char* inFile = nullptr;
diff --git a/tools/cfg/cfg.cpp b/tools/cfg/cfg.cpp
index 9e2c448..411ef88 100644
--- a/tools/cfg/cfg.cpp
+++ b/tools/cfg/cfg.cpp
@@ -44,7 +44,7 @@
       argv0, argv0);
 }
 
-static const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_2;
+static const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_4;
 
 int main(int argc, char** argv) {
   const char* inFile = nullptr;
diff --git a/tools/dis/dis.cpp b/tools/dis/dis.cpp
index 6a2e269..2a6f411 100644
--- a/tools/dis/dis.cpp
+++ b/tools/dis/dis.cpp
@@ -60,7 +60,7 @@
       argv0, argv0);
 }
 
-static const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
+static const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_4;
 
 int main(int argc, char** argv) {
   const char* inFile = nullptr;
diff --git a/tools/link/linker.cpp b/tools/link/linker.cpp
index fb44a37..3508b13 100644
--- a/tools/link/linker.cpp
+++ b/tools/link/linker.cpp
@@ -39,8 +39,9 @@
   --allow-partial-linkage Allow partial linkage by accepting imported symbols to be unresolved.
   --verify-ids            Verify that IDs in the resulting modules are truly unique.
   --version               Display linker version information
-  --target-env            {vulkan1.0|spv1.0|spv1.1|spv1.2|opencl2.1|opencl2.2}
-                          Use Vulkan1.0/SPIR-V1.0/SPIR-V1.1/SPIR-V1.2/OpenCL-2.1/OpenCL2.2 validation rules.
+  --target-env            {vulkan1.0|vulkan1.1|spv1.0|spv1.1|spv1.2|spv1.3|spv1.4|opencl2.1|opencl2.2}
+                          Use Vulkan 1.0, Vulkan 1.1, SPIR-V 1.0, SPIR-V 1.1, SPIR-V 1.2, SPIR-V 1.3,
+                          SPIR-V1.4, OpenCL 2.1, OpenCL 2.2 validation rules.
 )",
       argv0, argv0);
 }
diff --git a/tools/opt/opt.cpp b/tools/opt/opt.cpp
index 1566ac7..3e57980 100644
--- a/tools/opt/opt.cpp
+++ b/tools/opt/opt.cpp
@@ -59,7 +59,7 @@
   return ss.str();
 }
 
-const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
+const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_4;
 
 std::string GetLegalizationPasses() {
   spvtools::Optimizer optimizer(kDefaultEnvironment);
diff --git a/tools/reduce/reduce.cpp b/tools/reduce/reduce.cpp
index 5c78cbf..55d0983 100644
--- a/tools/reduce/reduce.cpp
+++ b/tools/reduce/reduce.cpp
@@ -216,7 +216,7 @@
   DumpShader(binary, filename);
 }
 
-const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_3;
+const auto kDefaultEnvironment = SPV_ENV_UNIVERSAL_1_4;
 
 int main(int argc, const char** argv) {
   const char* in_file = nullptr;
diff --git a/tools/val/val.cpp b/tools/val/val.cpp
index 8b1d048..5e9b025 100644
--- a/tools/val/val.cpp
+++ b/tools/val/val.cpp
@@ -61,16 +61,18 @@
                                    different type with compatible layout and
                                    members.
   --version                        Display validator version information.
-  --target-env                     {vulkan1.0|vulkan1.1|opencl2.2|spv1.0|spv1.1|spv1.2|spv1.3|webgpu0}
-                                   Use Vulkan 1.0, Vulkan 1.1, OpenCL 2.2, SPIR-V 1.0,
-                                   SPIR-V 1.1, SPIR-V 1.2, SPIR-V 1.3 or WIP WebGPU validation rules.
+  --target-env                     {vulkan1.0|vulkan1.1|vulkan1.1spv1.4|opencl2.2|spv1.0|spv1.1|
+                                    spv1.2|spv1.3|spv1.4|webgpu0}
+                                   Use Vulkan 1.0, Vulkan 1.1, Vulkan 1.1 with SPIR-V 1.4,
+                                   OpenCL 2.2, SPIR-V 1.0, SPIR-V 1.1, SPIR-V 1.2, SPIR-V 1.3,
+                                   SPIR-V 1.4, or WIP WebGPU validation rules.
 )",
       argv0, argv0);
 }
 
 int main(int argc, char** argv) {
   const char* inFile = nullptr;
-  spv_target_env target_env = SPV_ENV_UNIVERSAL_1_3;
+  spv_target_env target_env = SPV_ENV_UNIVERSAL_1_4;
   spvtools::ValidatorOptions options;
   bool continue_processing = true;
   int return_code = 0;
@@ -102,15 +104,19 @@
         }
       } else if (0 == strcmp(cur_arg, "--version")) {
         printf("%s\n", spvSoftwareVersionDetailsString());
-        printf("Targets:\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n",
-               spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_0),
-               spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_1),
-               spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_2),
-               spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_3),
-               spvTargetEnvDescription(SPV_ENV_OPENCL_2_2),
-               spvTargetEnvDescription(SPV_ENV_VULKAN_1_0),
-               spvTargetEnvDescription(SPV_ENV_VULKAN_1_1),
-               spvTargetEnvDescription(SPV_ENV_WEBGPU_0));
+        printf(
+            "Targets:\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n  %s\n  "
+            "%s\n",
+            spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_0),
+            spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_1),
+            spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_2),
+            spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_3),
+            spvTargetEnvDescription(SPV_ENV_UNIVERSAL_1_4),
+            spvTargetEnvDescription(SPV_ENV_OPENCL_2_2),
+            spvTargetEnvDescription(SPV_ENV_VULKAN_1_0),
+            spvTargetEnvDescription(SPV_ENV_VULKAN_1_1),
+            spvTargetEnvDescription(SPV_ENV_VULKAN_1_1_SPIRV_1_4),
+            spvTargetEnvDescription(SPV_ENV_WEBGPU_0));
         continue_processing = false;
         return_code = 0;
       } else if (0 == strcmp(cur_arg, "--help") || 0 == strcmp(cur_arg, "-h")) {
diff --git a/utils/generate_grammar_tables.py b/utils/generate_grammar_tables.py
index aabdad5..2001401 100755
--- a/utils/generate_grammar_tables.py
+++ b/utils/generate_grammar_tables.py
@@ -405,11 +405,12 @@
             min_version=self.version)
 
 
-def generate_enum_operand_kind_entry(entry):
+def generate_enum_operand_kind_entry(entry, extension_map):
     """Returns the C initializer for the given operand enum entry.
 
     Arguments:
       - entry: a dict containing information about an enum entry
+      - extension_map: a dict mapping enum value to list of extensions
 
     Returns:
       a string containing the C initializer for spv_operand_desc_t
@@ -417,7 +418,10 @@
     enumerant = entry.get('enumerant')
     value = entry.get('value')
     caps = entry.get('capabilities', [])
-    exts = entry.get('extensions', [])
+    if value in extension_map:
+      exts = extension_map[value]
+    else:
+      exts = []
     params = entry.get('parameters', [])
     params = [p.get('kind') for p in params]
     params = zip(params, [''] * len(params))
@@ -430,22 +434,41 @@
         enumerant, value, caps, exts, params, version))
 
 
-def generate_enum_operand_kind(enum):
-    """Returns the C definition for the given operand kind."""
+def generate_enum_operand_kind(enum, synthetic_exts_list):
+    """Returns the C definition for the given operand kind.
+    Also appends to |synthetic_exts_list| a list of extension
+    lists used."""
     kind = enum.get('kind')
     assert kind is not None
 
-    # Sort all enumerants first according to their values and then
-    # their names so that the symbols with the same values are
-    # grouped together.
+    # Sort all enumerants according to their values, but otherwise
+    # preserve their order so the first name listed in the grammar
+    # as the preferred name for disassembly.
     if enum.get('category') == 'ValueEnum':
-        functor = lambda k: (k['value'], k['enumerant'])
+        functor = lambda k: (k['value'])
     else:
-        functor = lambda k: (int(k['value'], 16), k['enumerant'])
+        functor = lambda k: (int(k['value'], 16))
     entries = sorted(enum.get('enumerants', []), key=functor)
 
+    # SubgroupEqMask and SubgroupEqMaskKHR are the same number with
+    # same semantics, but one has no extension list while the other
+    # does.  Both should have the extension list.
+    # So create a mapping from enum value to the union of the extensions
+    # across all those grammar entries.  Preserve order.
+    extension_map = { }
+    for e in entries:
+      value = e.get('value')
+      extension_map[value] = []
+    for e in entries:
+      value = e.get('value')
+      exts = e.get('extensions', [])
+      for ext in exts:
+        if ext not in extension_map[value]:
+          extension_map[value].append(ext)
+    synthetic_exts_list.extend(extension_map.values())
+
     name = '{}_{}Entries'.format(PYGEN_VARIABLE_PREFIX, kind)
-    entries = ['  {}'.format(generate_enum_operand_kind_entry(e))
+    entries = ['  {}'.format(generate_enum_operand_kind_entry(e, extension_map))
                for e in entries]
 
     template = ['static const spv_operand_desc_t {name}[] = {{',
@@ -470,9 +493,9 @@
     exts = [entry.get('extensions', [])
             for enum in enums
             for entry in enum.get('enumerants', [])]
+    enums = [generate_enum_operand_kind(e, exts) for e in enums]
     exts_arrays = generate_extension_arrays(exts)
 
-    enums = [generate_enum_operand_kind(e) for e in enums]
     # We have three operand kinds that requires their optional counterpart to
     # exist in the operand info table.
     three_optional_enums = ['ImageOperands', 'AccessQualifier', 'MemoryAccess']
@@ -608,6 +631,12 @@
     return '\n\n'.join(tables)
 
 
+def precondition_operand_kinds(operand_kinds):
+    """For operand kinds that have the same number, make sure they all have
+    the same extension list"""
+    return operand_kinds
+
+
 def main():
     import argparse
     parser = argparse.ArgumentParser(description='Generate SPIR-V info tables')
@@ -701,6 +730,7 @@
                 operand_kinds.extend(core_grammar['operand_kinds'])
                 operand_kinds.extend(debuginfo_grammar['operand_kinds'])
                 extensions = get_extension_list(instructions, operand_kinds)
+                operand_kinds = precondition_operand_kinds(operand_kinds)
         if args.core_insts_output is not None:
             make_path_to_file(args.core_insts_output)
             make_path_to_file(args.operand_kinds_output)