spirv-reader: sample_mask_in, sample_mask_out

TODO: passing pointer to them as a function parameter

Bug: tint:471
Change-Id: Ibd55bdc77a2bfb0f5712dd9bf332910999b8d0d1
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/40123
Commit-Queue: David Neto <dneto@google.com>
Auto-Submit: David Neto <dneto@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 2a55512..41ac1a4 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -747,6 +747,9 @@
       namer_(pi->namer()),
       function_(function),
       i32_(builder_.create<type::I32>()),
+      u32_(builder_.create<type::U32>()),
+      sample_mask_in_id(0u),
+      sample_mask_out_id(0u),
       ep_info_(ep_info) {
   PushNewStatementBlock(nullptr, 0, nullptr);
 }
@@ -2032,6 +2035,17 @@
       Fail() << "unhandled use of a pointer to the SampleId builtin, with ID: "
              << id;
       return {};
+    case SkipReason::kSampleMaskInBuiltinPointer:
+      Fail()
+          << "unhandled use of a pointer to the SampleMask builtin, with ID: "
+          << id;
+      return {};
+    case SkipReason::kSampleMaskOutBuiltinPointer:
+      // The result type is always u32.
+      auto name = namer_.Name(sample_mask_out_id);
+      return TypedExpression{u32_,
+                             create<ast::IdentifierExpression>(
+                                 Source{}, builder_.Symbols().Register(name))};
   }
   if (identifier_values_.count(id) || parser_impl_.IsScalarSpecConstant(id)) {
     auto name = namer_.Name(id);
@@ -2968,25 +2982,41 @@
       return true;
 
     case SpvOpStore: {
-      const auto ptr_id = inst.GetSingleWordInOperand(0);
+      auto ptr_id = inst.GetSingleWordInOperand(0);
       const auto value_id = inst.GetSingleWordInOperand(1);
 
+      auto rhs = MakeExpression(value_id);
+
       // Handle exceptional cases
-      if (GetSkipReason(ptr_id) == SkipReason::kPointSizeBuiltinPointer) {
-        if (const auto* c = constant_mgr_->FindDeclaredConstant(value_id)) {
-          // If we're writing a constant 1.0, then skip the write.  That's all
-          // that WebGPU handles.
-          auto* ct = c->type();
-          if (ct->AsFloat() && (ct->AsFloat()->width() == 32) &&
-              (c->GetFloat() == 1.0f)) {
-            // Don't store to PointSize
-            return true;
+      switch (GetSkipReason(ptr_id)) {
+        case SkipReason::kPointSizeBuiltinPointer:
+          if (const auto* c = constant_mgr_->FindDeclaredConstant(value_id)) {
+            // If we're writing a constant 1.0, then skip the write.  That's all
+            // that WebGPU handles.
+            auto* ct = c->type();
+            if (ct->AsFloat() && (ct->AsFloat()->width() == 32) &&
+                (c->GetFloat() == 1.0f)) {
+              // Don't store to PointSize
+              return true;
+            }
           }
-        }
-        return Fail() << "cannot store a value other than constant 1.0 to "
-                         "PointSize builtin: "
-                      << inst.PrettyPrint();
+          return Fail() << "cannot store a value other than constant 1.0 to "
+                           "PointSize builtin: "
+                        << inst.PrettyPrint();
+
+        case SkipReason::kSampleMaskOutBuiltinPointer:
+          ptr_id = sample_mask_out_id;
+          if (rhs.type != u32_) {
+            // WGSL requires sample_mask_out to be signed.
+            rhs = TypedExpression{
+                u32_, create<ast::TypeConstructorExpression>(
+                          Source{}, u32_, ast::ExpressionList{rhs.expr})};
+          }
+          break;
+        default:
+          break;
       }
+
       const auto ptr_type_id = def_use_mgr_->GetDef(ptr_id)->type_id();
       const auto& builtin_position_info = parser_impl_.GetBuiltInPositionInfo();
       if (ptr_type_id == builtin_position_info.pointer_type_id) {
@@ -2996,9 +3026,7 @@
       }
 
       // Handle an ordinary store as an assignment.
-      // TODO(dneto): Order of evaluation?
       auto lhs = MakeExpression(ptr_id);
-      auto rhs = MakeExpression(value_id);
       AddStatement(
           create<ast::AssignmentStatement>(Source{}, lhs.expr, rhs.expr));
       return success();
@@ -3024,6 +3052,25 @@
                         Source{}, i32_, ast::ExpressionList{id_expr})};
           return EmitConstDefinition(inst, expr);
         }
+        case SkipReason::kSampleMaskInBuiltinPointer: {
+          auto name = namer_.Name(sample_mask_in_id);
+          ast::Expression* id_expr = create<ast::IdentifierExpression>(
+              Source{}, builder_.Symbols().Register(name));
+          auto* load_result_type = parser_impl_.ConvertType(inst.type_id());
+          ast::Expression* ast_expr = nullptr;
+          if (load_result_type == i32_) {
+            ast_expr = create<ast::TypeConstructorExpression>(
+                Source{}, i32_, ast::ExpressionList{id_expr});
+          } else if (load_result_type == u32_) {
+            ast_expr = id_expr;
+          } else {
+            return Fail() << "loading the whole SampleMask input array is not "
+                             "supported: "
+                          << inst.PrettyPrint();
+          }
+          return EmitConstDefinition(
+              inst, TypedExpression{load_result_type, ast_expr});
+        }
         default:
           break;
       }
@@ -3495,9 +3542,8 @@
   TypedExpression current_expr(MakeOperand(inst, 0));
 
   auto make_index = [this, source](uint32_t literal) {
-    auto* type = create<type::U32>();
     return create<ast::ScalarConstructorExpression>(
-        source, create<ast::UintLiteral>(source, type, literal));
+        source, create<ast::UintLiteral>(source, u32_, literal));
   };
 
   const auto composite = inst.GetSingleWordInOperand(0);
@@ -3655,8 +3701,8 @@
   for (auto& special_var : parser_impl_.special_builtins()) {
     const auto id = special_var.first;
     const auto builtin = special_var.second;
-    def_info_[id] =
-        std::make_unique<DefInfo>(*(def_use_mgr_->GetDef(id)), 0, index);
+    const auto* var = def_use_mgr_->GetDef(id);
+    def_info_[id] = std::make_unique<DefInfo>(*var, 0, index);
     ++index;
     auto& def = def_info_[id];
     switch (builtin) {
@@ -3666,6 +3712,19 @@
       case SpvBuiltInSampleId:
         def->skip = SkipReason::kSampleIdBuiltinPointer;
         break;
+      case SpvBuiltInSampleMask: {
+        // Distinguish between input and output variable.
+        const auto storage_class =
+            static_cast<SpvStorageClass>(var->GetSingleWordInOperand(0));
+        if (storage_class == SpvStorageClassInput) {
+          sample_mask_in_id = id;
+          def->skip = SkipReason::kSampleMaskInBuiltinPointer;
+        } else {
+          sample_mask_out_id = id;
+          def->skip = SkipReason::kSampleMaskOutBuiltinPointer;
+        }
+        break;
+      }
       default:
         return Fail() << "unrecognized special builtin: " << int(builtin);
     }
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index 6f1ca66..d58c66f 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -44,6 +44,7 @@
 #include "src/reader/spirv/parser_impl.h"
 #include "src/type/i32_type.h"
 #include "src/type/texture_type.h"
+#include "src/type/u32_type.h"
 
 namespace tint {
 namespace reader {
@@ -227,6 +228,14 @@
   /// `kSampleIdBuiltinPointer`: the value is a pointer to the SampleId builtin
   /// variable.  Don't generate its address.
   kSampleIdBuiltinPointer,
+
+  /// `kSampleMaskInBuiltinPointer`: the value is a pointer to the SampleMaskIn
+  /// builtin input variable.  Don't generate its address.
+  kSampleMaskInBuiltinPointer,
+
+  /// `kSampleMaskOutBuiltinPointer`: the value is a pointer to the SampleMask
+  /// builtin output variable.
+  kSampleMaskOutBuiltinPointer,
 };
 
 /// Bookkeeping info for a SPIR-V ID defined in the function, or some
@@ -335,6 +344,12 @@
     case SkipReason::kSampleIdBuiltinPointer:
       o << " skip:sampleid_pointer";
       break;
+    case SkipReason::kSampleMaskInBuiltinPointer:
+      o << " skip:samplemaskin_pointer";
+      break;
+    case SkipReason::kSampleMaskOutBuiltinPointer:
+      o << " skip:samplemaskout_pointer";
+      break;
   }
   o << "}";
   return o;
@@ -1085,6 +1100,12 @@
   Namer& namer_;
   const spvtools::opt::Function& function_;
   type::I32* const i32_;  // The unique I32 type object.
+  type::U32* const u32_;  // The unique U32 type object.
+
+  // The SPIR-V ID for the SampleMask input variable.
+  uint32_t sample_mask_in_id;
+  // The SPIR-V ID for the SampleMask output variable.
+  uint32_t sample_mask_out_id;
 
   // A stack of statement lists. Each list is contained in a construct in
   // the next deeper element of stack. The 0th entry represents the statements
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index a72b474..8b8556f 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -1231,6 +1231,31 @@
   return success_;
 }
 
+// @param var_id SPIR-V id of an OpVariable, assumed to be pointer
+// to an array
+// @returns the IntConstant for the size of the array, or nullptr
+const spvtools::opt::analysis::IntConstant* ParserImpl::GetArraySize(
+    uint32_t var_id) {
+  auto* var = def_use_mgr_->GetDef(var_id);
+  if (!var || var->opcode() != SpvOpVariable) {
+    return nullptr;
+  }
+  auto* ptr_type = def_use_mgr_->GetDef(var->type_id());
+  if (!ptr_type || ptr_type->opcode() != SpvOpTypePointer) {
+    return nullptr;
+  }
+  auto* array_type = def_use_mgr_->GetDef(ptr_type->GetSingleWordInOperand(1));
+  if (!array_type || array_type->opcode() != SpvOpTypeArray) {
+    return nullptr;
+  }
+  auto* size = constant_mgr_->FindDeclaredConstant(
+      array_type->GetSingleWordInOperand(1));
+  if (!size) {
+    return nullptr;
+  }
+  return size->AsIntConstant();
+}
+
 ast::Variable* ParserImpl::MakeVariable(
     uint32_t id,
     ast::StorageClass sc,
@@ -1277,6 +1302,20 @@
             type = forced_type;
           }
           break;
+        case SpvBuiltInSampleMask: {
+          // In SPIR-V this is used for both input and output variable.
+          // The SPIR-V variable has store type of array of integer scalar,
+          // either signed or unsigned.
+          // WGSL requires the store type to be u32.
+          auto* size = GetArraySize(id);
+          if (!size || size->GetZeroExtendedValue() != 1) {
+            Fail() << "WGSL supports a sample mask of at most 32 bits. "
+                      "SampleMask must be an array of 1 element.";
+          }
+          special_builtins_[id] = spv_builtin;
+          type = builder_.create<type::U32>();
+          break;
+        }
         default:
           break;
       }
diff --git a/src/reader/spirv/parser_impl.h b/src/reader/spirv/parser_impl.h
index 3b14f77..bfe11de 100644
--- a/src/reader/spirv/parser_impl.h
+++ b/src/reader/spirv/parser_impl.h
@@ -287,6 +287,11 @@
   /// @returns true if parser is still successful.
   bool EmitFunction(const spvtools::opt::Function& f);
 
+  /// Returns the integer constant for the array size of the given variable.
+  /// @param var_id SPIR-V ID for an array variable
+  /// @returns the integer constant for its array size, or nullptr.
+  const spvtools::opt::analysis::IntConstant* GetArraySize(uint32_t var_id);
+
   /// Creates an AST Variable node for a SPIR-V ID, including any attached
   /// decorations, unless it's an ignorable builtin variable.
   /// @param id the SPIR-V result ID
diff --git a/src/reader/spirv/parser_impl_module_var_test.cc b/src/reader/spirv/parser_impl_module_var_test.cc
index 2fd4ea2..21ed781 100644
--- a/src/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/reader/spirv/parser_impl_module_var_test.cc
@@ -2403,6 +2403,571 @@
 })")) << module_str;
 }
 
+// Returns the start of a shader for testing SampleMask
+// parameterized by store type.
+std::string SampleMaskPreamble(std::string store_type) {
+  return R"(
+    OpCapability Shader
+    OpMemoryModel Logical Simple
+    OpEntryPoint Fragment %main "main" %1
+    OpExecutionMode %main OriginUpperLeft
+    OpDecorate %1 BuiltIn SampleMask
+    %void = OpTypeVoid
+    %voidfn = OpTypeFunction %void
+    %float = OpTypeFloat 32
+    %uint = OpTypeInt 32 0
+    %int = OpTypeInt 32 1
+    %int_12 = OpConstant %int 12
+    %uint_0 = OpConstant %uint 0
+    %uint_1 = OpConstant %uint 1
+    %uint_2 = OpConstant %uint 2
+    %uarr1 = OpTypeArray %uint %uint_1
+    %uarr2 = OpTypeArray %uint %uint_2
+    %iarr1 = OpTypeArray %int %uint_1
+    %iarr2 = OpTypeArray %int %uint_2
+    %iptr_in_ty = OpTypePointer Input %int
+    %uptr_in_ty = OpTypePointer Input %uint
+    %iptr_out_ty = OpTypePointer Output %int
+    %uptr_out_ty = OpTypePointer Output %uint
+    %in_ty = OpTypePointer Input )" +
+         store_type + R"(
+    %out_ty = OpTypePointer Output )" +
+         store_type + R"(
+)";
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_In_ArraySize2_Error) {
+  const std::string assembly = SampleMaskPreamble("%uarr2") + R"(
+    %1 = OpVariable %in_ty Input
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_in_ty %1 %uint_0
+    %3 = OpLoad %int %2
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(),
+              HasSubstr("WGSL supports a sample mask of at most 32 bits. "
+                        "SampleMask must be an array of 1 element"))
+      << p->error() << assembly;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_In_U32_Direct) {
+  const std::string assembly = SampleMaskPreamble("%uarr1") + R"(
+    %1 = OpVariable %in_ty Input
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_in_ty %1 %uint_0
+    %3 = OpLoad %uint %2
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_in}
+    }
+    x_1
+    in
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    VariableDeclStatement{
+      VariableConst{
+        x_3
+        none
+        __u32
+        {
+          Identifier[not set]{x_1}
+        }
+      }
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_In_U32_CopyObject) {
+  const std::string assembly = SampleMaskPreamble("%uarr1") + R"(
+    %1 = OpVariable %in_ty Input
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_in_ty %1 %uint_0
+    %3 = OpCopyObject %uptr_in_ty %2
+    %4 = OpLoad %uint %3
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_in}
+    }
+    x_1
+    in
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    VariableDeclStatement{
+      VariableConst{
+        x_4
+        none
+        __u32
+        {
+          Identifier[not set]{x_1}
+        }
+      }
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_In_U32_AccessChain) {
+  const std::string assembly = SampleMaskPreamble("%uarr1") + R"(
+    %1 = OpVariable %in_ty Input
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_in_ty %1 %uint_0
+    %3 = OpAccessChain %uptr_in_ty %2
+    %4 = OpLoad %uint %3
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_in}
+    }
+    x_1
+    in
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    VariableDeclStatement{
+      VariableConst{
+        x_4
+        none
+        __u32
+        {
+          Identifier[not set]{x_1}
+        }
+      }
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_In_I32_Direct) {
+  const std::string assembly = SampleMaskPreamble("%iarr1") + R"(
+    %1 = OpVariable %in_ty Input
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %iptr_in_ty %1 %uint_0
+    %3 = OpLoad %int %2
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_in}
+    }
+    x_1
+    in
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    VariableDeclStatement{
+      VariableConst{
+        x_3
+        none
+        __i32
+        {
+          TypeConstructor[not set]{
+            __i32
+            Identifier[not set]{x_1}
+          }
+        }
+      }
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_In_I32_CopyObject) {
+  const std::string assembly = SampleMaskPreamble("%iarr1") + R"(
+    %1 = OpVariable %in_ty Input
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %iptr_in_ty %1 %uint_0
+    %3 = OpCopyObject %iptr_in_ty %2
+    %4 = OpLoad %int %3
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_in}
+    }
+    x_1
+    in
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    VariableDeclStatement{
+      VariableConst{
+        x_4
+        none
+        __i32
+        {
+          TypeConstructor[not set]{
+            __i32
+            Identifier[not set]{x_1}
+          }
+        }
+      }
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_In_I32_AccessChain) {
+  const std::string assembly = SampleMaskPreamble("%iarr1") + R"(
+    %1 = OpVariable %in_ty Input
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %iptr_in_ty %1 %uint_0
+    %3 = OpAccessChain %iptr_in_ty %2
+    %4 = OpLoad %int %3
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_in}
+    }
+    x_1
+    in
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    VariableDeclStatement{
+      VariableConst{
+        x_4
+        none
+        __i32
+        {
+          TypeConstructor[not set]{
+            __i32
+            Identifier[not set]{x_1}
+          }
+        }
+      }
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_Out_ArraySize2_Error) {
+  const std::string assembly = SampleMaskPreamble("%uarr2") + R"(
+    %1 = OpVariable %out_ty Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_out_ty %1 %uint_0
+    OpStore %2 %uint_0
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_FALSE(p->BuildAndParseInternalModule());
+  EXPECT_THAT(p->error(),
+              HasSubstr("WGSL supports a sample mask of at most 32 bits. "
+                        "SampleMask must be an array of 1 element"))
+      << p->error() << assembly;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_Out_U32_Direct) {
+  const std::string assembly = SampleMaskPreamble("%uarr1") + R"(
+    %1 = OpVariable %out_ty Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_out_ty %1 %uint_0
+    OpStore %2 %uint_0
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_out}
+    }
+    x_1
+    out
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    Assignment{
+      Identifier[not set]{x_1}
+      ScalarConstructor[not set]{0}
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_Out_U32_CopyObject) {
+  const std::string assembly = SampleMaskPreamble("%uarr1") + R"(
+    %1 = OpVariable %out_ty Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_out_ty %1 %uint_0
+    %3 = OpCopyObject %uptr_out_ty %2
+    OpStore %2 %uint_0
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_out}
+    }
+    x_1
+    out
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    Assignment{
+      Identifier[not set]{x_1}
+      ScalarConstructor[not set]{0}
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_Out_U32_AccessChain) {
+  const std::string assembly = SampleMaskPreamble("%uarr1") + R"(
+    %1 = OpVariable %out_ty Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %uptr_out_ty %1 %uint_0
+    %3 = OpAccessChain %uptr_out_ty %2
+    OpStore %2 %uint_0
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_out}
+    }
+    x_1
+    out
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+    Assignment{
+      Identifier[not set]{x_1}
+      ScalarConstructor[not set]{0}
+    })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_Out_I32_Direct) {
+  const std::string assembly = SampleMaskPreamble("%iarr1") + R"(
+    %1 = OpVariable %out_ty Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %iptr_out_ty %1 %uint_0
+    OpStore %2 %int_12
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_out}
+    }
+    x_1
+    out
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  {
+    Assignment{
+      Identifier[not set]{x_1}
+      TypeConstructor[not set]{
+        __u32
+        ScalarConstructor[not set]{12}
+      }
+    }
+    Return{}
+  })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_Out_I32_CopyObject) {
+  const std::string assembly = SampleMaskPreamble("%iarr1") + R"(
+    %1 = OpVariable %out_ty Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %iptr_out_ty %1 %uint_0
+    %3 = OpCopyObject %iptr_out_ty %2
+    OpStore %2 %int_12
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_out}
+    }
+    x_1
+    out
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  {
+    Assignment{
+      Identifier[not set]{x_1}
+      TypeConstructor[not set]{
+        __u32
+        ScalarConstructor[not set]{12}
+      }
+    }
+    Return{}
+  })"))
+      << module_str;
+}
+
+TEST_F(SpvModuleScopeVarParserTest, SampleMask_Out_I32_AccessChain) {
+  const std::string assembly = SampleMaskPreamble("%iarr1") + R"(
+    %1 = OpVariable %out_ty Output
+
+    %main = OpFunction %void None %voidfn
+    %entry = OpLabel
+    %2 = OpAccessChain %iptr_out_ty %1 %uint_0
+    %3 = OpAccessChain %iptr_out_ty %2
+    OpStore %2 %int_12
+    OpReturn
+    OpFunctionEnd
+ )";
+  auto p = parser(test::Assemble(assembly));
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
+  EXPECT_TRUE(p->error().empty());
+  const auto module_str = p->program().to_str();
+  // Correct declaration
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  Variable{
+    Decorations{
+      BuiltinDecoration{sample_mask_out}
+    }
+    x_1
+    out
+    __u32
+  })"));
+
+  // Correct bodies
+  EXPECT_THAT(module_str, HasSubstr(R"(
+  {
+    Assignment{
+      Identifier[not set]{x_1}
+      TypeConstructor[not set]{
+        __u32
+        ScalarConstructor[not set]{12}
+      }
+    }
+    Return{}
+  })"))
+      << module_str;
+}
+
+// TODO(dneto): Test passing pointer to SampleMask as function parameter,
+// both input case and output case.
+
 }  // namespace
 }  // namespace spirv
 }  // namespace reader