[inspector] Report if sample_mask is returned by the entry point

BUG=tint:424

Change-Id: Ic378173ac4e6697b57102db611cdc0b29b25c72a
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/56302
Auto-Submit: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/inspector/entry_point.h b/src/inspector/entry_point.h
index b3570bb..6fc4de6 100644
--- a/src/inspector/entry_point.h
+++ b/src/inspector/entry_point.h
@@ -69,17 +69,19 @@
   /// The entry point stage
   ast::PipelineStage stage = ast::PipelineStage::kNone;
   /// The workgroup x size
-  uint32_t workgroup_size_x;
+  uint32_t workgroup_size_x = 0;
   /// The workgroup y size
-  uint32_t workgroup_size_y;
+  uint32_t workgroup_size_y = 0;
   /// The workgroup z size
-  uint32_t workgroup_size_z;
+  uint32_t workgroup_size_z = 0;
   /// List of the input variable accessed via this entry point.
   std::vector<StageVariable> input_variables;
   /// List of the output variable accessed via this entry point.
   std::vector<StageVariable> output_variables;
   /// List of the pipeline overridable constants accessed via this entry point.
   std::vector<OverridableConstant> overridable_constants;
+  /// Does the entry point use the sample_mask builtin
+  bool sample_mask_used = false;
 
   /// @returns the size of the workgroup in {x,y,z} format
   std::tuple<uint32_t, uint32_t, uint32_t> workgroup_size() {
diff --git a/src/inspector/inspector.cc b/src/inspector/inspector.cc
index c99e6b8..6afa77d 100644
--- a/src/inspector/inspector.cc
+++ b/src/inspector/inspector.cc
@@ -100,6 +100,9 @@
                                   entry_point.output_variables);
     }
 
+    entry_point.sample_mask_used = ContainsSampleMaskBuiltin(
+        sem->ReturnType(), func->return_type_decorations());
+
     for (auto* var : sem->ReferencedModuleVariables()) {
       auto* decl = var->Declaration();
 
@@ -535,6 +538,31 @@
   variables.push_back(stage_variable);
 }
 
+bool Inspector::ContainsSampleMaskBuiltin(
+    sem::Type* type,
+    const ast::DecorationList& decorations) const {
+  auto* unwrapped_type = type->UnwrapRef();
+
+  if (auto* struct_ty = unwrapped_type->As<sem::Struct>()) {
+    // Recurse into members.
+    for (auto* member : struct_ty->Members()) {
+      if (ContainsSampleMaskBuiltin(member->Type(),
+                                    member->Declaration()->decorations())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // Base case: check for [[builtin(sample_mask)]]
+  auto* builtin = ast::GetDecoration<ast::BuiltinDecoration>(decorations);
+  if (!builtin || builtin->value() != ast::Builtin::kSampleMask) {
+    return false;
+  }
+
+  return true;
+}
+
 std::vector<ResourceBinding> Inspector::GetStorageBufferResourceBindingsImpl(
     const std::string& entry_point,
     bool read_only) {
diff --git a/src/inspector/inspector.h b/src/inspector/inspector.h
index 954907f..aba355c 100644
--- a/src/inspector/inspector.h
+++ b/src/inspector/inspector.h
@@ -151,6 +151,12 @@
                                    const ast::DecorationList& decorations,
                                    std::vector<StageVariable>& variables) const;
 
+  /// Recursively determine if the type contains [[builtin(sample_mask)]]
+  /// If `type` is a struct, recurse into members to check for the decoration.
+  /// Otherwise, check `decorations` for the decoration.
+  bool ContainsSampleMaskBuiltin(sem::Type* type,
+                                 const ast::DecorationList& decorations) const;
+
   /// @param entry_point name of the entry point to get information about.
   /// @param read_only if true get only read-only bindings, if false get
   ///                  write-only bindings.
diff --git a/src/inspector/inspector_test.cc b/src/inspector/inspector_test.cc
index 8d0d4a8..4b8af26 100644
--- a/src/inspector/inspector_test.cc
+++ b/src/inspector/inspector_test.cc
@@ -1559,6 +1559,50 @@
   EXPECT_EQ(0u, result[0].overridable_constants.size());
 }
 
+TEST_F(InspectorGetEntryPointTest, SampleMaskNotReferenced) {
+  MakeEmptyBodyFunction("ep_func", {Stage(ast::PipelineStage::kFragment)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_FALSE(result[0].sample_mask_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, SampleMaskSimpleReferenced) {
+  auto* in_var =
+      Param("in_var", ty.u32(), {Builtin(ast::Builtin::kSampleMask)});
+  Func("ep_func", {in_var}, ty.u32(), {Return("in_var")},
+       {Stage(ast::PipelineStage::kFragment)},
+       {Builtin(ast::Builtin::kSampleMask)});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].sample_mask_used);
+}
+
+TEST_F(InspectorGetEntryPointTest, SampleMaskStructReferenced) {
+  ast::StructMemberList members;
+  members.push_back(Member("inner_sample_mask", ty.u32(),
+                           {Builtin(ast::Builtin::kSampleMask)}));
+  Structure("out_struct", members, {});
+
+  Func("ep_func", {}, ty.type_name("out_struct"),
+       {Decl(Var("out_var", ty.type_name("out_struct"))), Return("out_var")},
+       {Stage(ast::PipelineStage::kFragment)}, {});
+
+  Inspector& inspector = Build();
+
+  auto result = inspector.GetEntryPoints();
+
+  ASSERT_EQ(1u, result.size());
+  EXPECT_TRUE(result[0].sample_mask_used);
+}
+
 // TODO(rharrison): Reenable once GetRemappedNameForEntryPoint isn't a pass
 // through
 TEST_F(InspectorGetRemappedNameForEntryPointTest, DISABLED_NoFunctions) {