spirv-reader: support remaining interpolation decorations

NoPerspective interpolation maps to 'linear'

Centroid maps to 'centroid'
Sample maps to 'sample'
Otherwise, allow 'center' to be defaulted.

Fixed: tint:935
Change-Id: I0b040da0c57d2a363f9dc9474c1ac889e0fe2278
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/56840
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: James Price <jrprice@google.com>
Auto-Submit: David Neto <dneto@google.com>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index ca610e4..0998926 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -949,6 +949,7 @@
     tip_type = ref_type->type;
   }
 
+  // Recursively flatten matrices, arrays, and structures.
   if (auto* matrix_type = tip_type->As<Matrix>()) {
     index_prefix.push_back(0);
     const auto num_columns = static_cast<int>(matrix_type->columns);
@@ -981,13 +982,21 @@
     for (int i = 0; i < static_cast<int>(members.size()); ++i) {
       index_prefix.back() = i;
       auto* location = parser_impl_.GetMemberLocation(*struct_type, i);
-      auto* saved_location = SetLocation(decos, location);
-      if (!EmitPipelineInput(var_name, var_type, decos, index_prefix,
+      SetLocation(decos, location);
+      ast::DecorationList member_decos(*decos);
+      if (!parser_impl_.ConvertInterpolationDecorations(
+              struct_type,
+              parser_impl_.GetMemberInterpolationDecorations(*struct_type, i),
+              &member_decos)) {
+        return false;
+      }
+      if (!EmitPipelineInput(var_name, var_type, &member_decos, index_prefix,
                              members[i], forced_param_type, params,
                              statements)) {
         return false;
       }
-      SetLocation(decos, saved_location);
+      // Copy the location as updated by nested expansion of the member.
+      SetLocation(decos, GetLocation(member_decos));
     }
     return success();
   }
@@ -999,7 +1008,7 @@
   const auto param_name = namer_.MakeDerivedName(var_name + "_param");
   // Create the parameter.
   // TODO(dneto): Note: If the parameter has non-location decorations,
-  // then those decoration AST nodes  will be reused between multiple elements
+  // then those decoration AST nodes will be reused between multiple elements
   // of a matrix, array, or structure.  Normally that's disallowed but currently
   // the SPIR-V reader will make duplicates when the entire AST is cloned
   // at the top level of the SPIR-V reader flow.  Consider rewriting this
@@ -1062,7 +1071,7 @@
   }
   for (auto*& deco : *decos) {
     if (deco->Is<ast::LocationDecoration>()) {
-      // Replace this location decoration with a new one with one higher index.
+      // Replace this location decoration with the replacement.
       // The old one doesn't leak because it's kept in the builder's AST node
       // list.
       ast::Decoration* result = deco;
@@ -1075,6 +1084,16 @@
   return nullptr;
 }
 
+ast::Decoration* FunctionEmitter::GetLocation(
+    const ast::DecorationList& decos) {
+  for (auto* const& deco : decos) {
+    if (deco->Is<ast::LocationDecoration>()) {
+      return deco;
+    }
+  }
+  return nullptr;
+}
+
 bool FunctionEmitter::EmitPipelineOutput(std::string var_name,
                                          const Type* var_type,
                                          ast::DecorationList* decos,
@@ -1083,12 +1102,12 @@
                                          const Type* forced_member_type,
                                          ast::StructMemberList* return_members,
                                          ast::ExpressionList* return_exprs) {
-  // TODO(dneto): Handle structs where the locations are annotated on members.
   tip_type = tip_type->UnwrapAlias();
   if (auto* ref_type = tip_type->As<Reference>()) {
     tip_type = ref_type->type;
   }
 
+  // Recursively flatten matrices, arrays, and structures.
   if (auto* matrix_type = tip_type->As<Matrix>()) {
     index_prefix.push_back(0);
     const auto num_columns = static_cast<int>(matrix_type->columns);
@@ -1123,13 +1142,21 @@
     for (int i = 0; i < static_cast<int>(members.size()); ++i) {
       index_prefix.back() = i;
       auto* location = parser_impl_.GetMemberLocation(*struct_type, i);
-      auto* saved_location = SetLocation(decos, location);
-      if (!EmitPipelineOutput(var_name, var_type, decos, index_prefix,
+      SetLocation(decos, location);
+      ast::DecorationList member_decos(*decos);
+      if (!parser_impl_.ConvertInterpolationDecorations(
+              struct_type,
+              parser_impl_.GetMemberInterpolationDecorations(*struct_type, i),
+              &member_decos)) {
+        return false;
+      }
+      if (!EmitPipelineOutput(var_name, var_type, &member_decos, index_prefix,
                               members[i], forced_member_type, return_members,
                               return_exprs)) {
         return false;
       }
-      SetLocation(decos, saved_location);
+      // Copy the location as updated by nested expansion of the member.
+      SetLocation(decos, GetLocation(member_decos));
     }
     return success();
   }
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index 8bfa7db..9d6a82e 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -479,10 +479,16 @@
   /// Assumes the list contains at most one Location decoration.
   /// @param decos the decoration list to modify
   /// @param replacement the location decoration to place into the list
-  /// @returns the location decoration that was replaced, if one was replaced.
+  /// @returns the location decoration that was replaced, if one was replaced,
+  /// or null otherwise.
   ast::Decoration* SetLocation(ast::DecorationList* decos,
                                ast::Decoration* replacement);
 
+  /// Returns the Location dcoration, if it exists.
+  /// @param decos the list of decorations to search
+  /// @returns the Location decoration, or nullptr if it doesn't exist
+  ast::Decoration* GetLocation(const ast::DecorationList& decos);
+
   /// Create an ast::BlockStatement representing the body of the function.
   /// This creates the statement stack, which is non-empty for the lifetime
   /// of the function.
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index 608ba3f..25a67bf 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -233,6 +233,24 @@
   return false;
 }
 
+// @param a SPIR-V decoration
+// @return true when the given decoration is an interpolation decoration.
+bool IsInterpolationDecoration(const Decoration& deco) {
+  if (deco.size() < 1) {
+    return false;
+  }
+  switch (deco[0]) {
+    case SpvDecorationFlat:
+    case SpvDecorationNoPerspective:
+    case SpvDecorationCentroid:
+    case SpvDecorationSample:
+      return true;
+    default:
+      break;
+  }
+  return false;
+}
+
 }  // namespace
 
 TypedExpression::TypedExpression() = default;
@@ -1104,6 +1122,9 @@
           break;
         case SpvDecorationLocation:
         case SpvDecorationFlat:
+        case SpvDecorationNoPerspective:
+        case SpvDecorationCentroid:
+        case SpvDecorationSample:
           // IO decorations are handled when emitting the entry point.
           break;
         default: {
@@ -1571,6 +1592,7 @@
                                                const Type** store_type,
                                                ast::DecorationList* decorations,
                                                bool transfer_pipeline_io) {
+  DecorationList interpolation_decorations;
   for (auto& deco : GetDecorationsFor(id)) {
     if (deco.empty()) {
       return Fail() << "malformed decoration on ID " << id << ": it is empty";
@@ -1643,16 +1665,8 @@
             create<ast::LocationDecoration>(Source{}, deco[1]));
       }
     }
-    if (deco[0] == SpvDecorationFlat) {
-      if (transfer_pipeline_io) {
-        // In WGSL, integral types are always flat, and so the decoration
-        // is never specified.
-        if (!(*store_type)->IsIntegerScalarOrVector()) {
-          decorations->emplace_back(create<ast::InterpolateDecoration>(
-              Source{}, ast::InterpolationType::kFlat,
-              ast::InterpolationSampling::kNone));
-        }
-      }
+    if (transfer_pipeline_io && IsInterpolationDecoration(deco)) {
+      interpolation_decorations.push_back(deco);
     }
     if (deco[0] == SpvDecorationDescriptorSet) {
       if (deco.size() == 1) {
@@ -1671,6 +1685,98 @@
           create<ast::BindingDecoration>(Source{}, deco[1]));
     }
   }
+
+  if (transfer_pipeline_io) {
+    if (!ConvertInterpolationDecorations(*store_type, interpolation_decorations,
+                                         decorations)) {
+      return false;
+    }
+  }
+
+  return success();
+}
+
+DecorationList ParserImpl::GetMemberInterpolationDecorations(
+    const Struct& struct_type,
+    int member_index) {
+  // Yes, I could have used std::copy_if or std::copy_if.
+  DecorationList result;
+  for (const auto& deco : GetDecorationsForMember(
+           struct_id_for_symbol_[struct_type.name], member_index)) {
+    if (IsInterpolationDecoration(deco)) {
+      result.emplace_back(deco);
+    }
+  }
+  return result;
+}
+
+bool ParserImpl::ConvertInterpolationDecorations(
+    const Type* store_type,
+    const DecorationList& decorations,
+    ast::DecorationList* ast_decos) {
+  bool has_interpolate_no_perspective = false;
+  bool has_interpolate_sampling_centroid = false;
+  bool has_interpolate_sampling_sample = false;
+
+  for (const auto& deco : decorations) {
+    if (deco[0] == SpvDecorationFlat) {
+      // In WGSL, integral types are always flat, and so the decoration
+      // is never specified.
+      if (!store_type->IsIntegerScalarOrVector()) {
+        ast_decos->emplace_back(create<ast::InterpolateDecoration>(
+            Source{}, ast::InterpolationType::kFlat,
+            ast::InterpolationSampling::kNone));
+        // Only one interpolate attribute is allowed.
+        return true;
+      }
+    }
+    if (deco[0] == SpvDecorationNoPerspective) {
+      if (store_type->IsIntegerScalarOrVector()) {
+        // This doesn't capture the array or struct case.
+        return Fail() << "NoPerspective is invalid on integral IO";
+      }
+      has_interpolate_no_perspective = true;
+    }
+    if (deco[0] == SpvDecorationCentroid) {
+      if (store_type->IsIntegerScalarOrVector()) {
+        // This doesn't capture the array or struct case.
+        return Fail()
+               << "Centroid interpolation sampling is invalid on integral IO";
+      }
+      has_interpolate_sampling_centroid = true;
+    }
+    if (deco[0] == SpvDecorationSample) {
+      if (store_type->IsIntegerScalarOrVector()) {
+        // This doesn't capture the array or struct case.
+        return Fail()
+               << "Sample interpolation sampling is invalid on integral IO";
+      }
+      has_interpolate_sampling_sample = true;
+    }
+  }
+
+  // Apply non-integral interpolation.
+  if (has_interpolate_no_perspective || has_interpolate_sampling_centroid ||
+      has_interpolate_sampling_sample) {
+    const ast::InterpolationType type =
+        has_interpolate_no_perspective ? ast::InterpolationType::kLinear
+                                       : ast::InterpolationType::kPerspective;
+    const ast::InterpolationSampling sampling =
+        has_interpolate_sampling_centroid
+            ? ast::InterpolationSampling::kCentroid
+            : (has_interpolate_sampling_sample
+                   ? ast::InterpolationSampling::kSample
+                   : ast::InterpolationSampling::
+                         kNone /* Center is the default */);
+    if (type == ast::InterpolationType::kPerspective &&
+        sampling == ast::InterpolationSampling::kNone) {
+      // This is the default. Don't add a decoration.
+    } else {
+      ast_decos->emplace_back(
+          create<ast::InterpolateDecoration>(type, sampling));
+    }
+  }
+
   return success();
 }
 
diff --git a/src/reader/spirv/parser_impl.h b/src/reader/spirv/parser_impl.h
index b041605..b93a993 100644
--- a/src/reader/spirv/parser_impl.h
+++ b/src/reader/spirv/parser_impl.h
@@ -252,6 +252,15 @@
                                      ast::DecorationList* ast_decos,
                                      bool transfer_pipeline_io);
 
+  /// Converts SPIR-V interpolation decorations into AST decorations.
+  /// @param store_type the store type for the variable or member
+  /// @param decorations the SPIR-V interpolation decorations
+  /// @param ast_decos the decoration list to populate.
+  /// @returns false if conversion fails
+  bool ConvertInterpolationDecorations(const Type* store_type,
+                                       const DecorationList& decorations,
+                                       ast::DecorationList* ast_decos);
+
   /// Converts a SPIR-V struct member decoration. If the decoration is
   /// recognized but deliberately dropped, then returns nullptr without a
   /// diagnostic. On failure, emits a diagnostic and returns nullptr.
@@ -386,6 +395,13 @@
   ast::Decoration* GetMemberLocation(const Struct& struct_type,
                                      int member_index);
 
+  /// Returns the SPIR-V interpolation decorations, if any, on a struct member.
+  /// @param struct_type the parser's structure type.
+  /// @param member_index the member index
+  /// @returns a list of SPIR-V decorations.
+  DecorationList GetMemberInterpolationDecorations(const Struct& struct_type,
+                                                   int member_index);
+
   /// 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 941129c..54c8468 100644
--- a/src/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/reader/spirv/parser_impl_module_var_test.cc
@@ -55,6 +55,7 @@
 std::string CommonCapabilities() {
   return R"(
     OpCapability Shader
+    OpCapability SampleRateShading
     OpMemoryModel Logical Simple
 )";
 }
@@ -7760,6 +7761,633 @@
   EXPECT_EQ(got, expected) << got;
 }
 
+TEST_F(SpvModuleScopeVarParserTest,
+       EntryPointWrapping_Interpolation_Floating_Fragment_In) {
+  // Flat decorations are dropped for integral
+  const auto assembly = CommonCapabilities() + R"(
+     OpEntryPoint Fragment %main "main" %1 %2 %3 %4 %5 %6
+     OpExecutionMode %main OriginUpperLeft
+     OpDecorate %1 Location 1
+     OpDecorate %2 Location 2
+     OpDecorate %3 Location 3
+     OpDecorate %4 Location 4
+     OpDecorate %5 Location 5
+     OpDecorate %6 Location 6
+
+     ; %1 perspective center
+
+     OpDecorate %2 Centroid ; perspective centroid
+
+     OpDecorate %3 Sample ; perspective sample
+
+     OpDecorate %4 NoPerspective; linear center
+
+     OpDecorate %5 NoPerspective ; linear centroid
+     OpDecorate %5 Centroid
+
+     OpDecorate %6 NoPerspective ; linear sample
+     OpDecorate %6 Sample
+
+)" + CommonTypes() +
+                        R"(
+     %ptr_in_float = OpTypePointer Input %float
+     %1 = OpVariable %ptr_in_float Input
+     %2 = OpVariable %ptr_in_float Input
+     %3 = OpVariable %ptr_in_float Input
+     %4 = OpVariable %ptr_in_float Input
+     %5 = OpVariable %ptr_in_float Input
+     %6 = OpVariable %ptr_in_float Input
+
+     %main = OpFunction %void None %voidfn
+     %entry = OpLabel
+     OpReturn
+     OpFunctionEnd
+  )";
+  auto p = parser(test::Assemble(assembly));
+
+  ASSERT_TRUE(p->BuildAndParseInternalModule());
+  EXPECT_TRUE(p->error().empty());
+  const auto got = p->program().to_str();
+  const std::string expected =
+      R"(Module{
+  Variable{
+    x_1
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_2
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_3
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_4
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_5
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_6
+    private
+    undefined
+    __f32
+  }
+  Function main_1 -> __void
+  ()
+  {
+    Return{}
+  }
+  Function main -> __void
+  StageDecoration{fragment}
+  (
+    VariableConst{
+      Decorations{
+        LocationDecoration{1}
+      }
+      x_1_param
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{2}
+        InterpolateDecoration{perspective centroid}
+      }
+      x_2_param
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{3}
+        InterpolateDecoration{perspective sample}
+      }
+      x_3_param
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{4}
+        InterpolateDecoration{linear none}
+      }
+      x_4_param
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{5}
+        InterpolateDecoration{linear centroid}
+      }
+      x_5_param
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{6}
+        InterpolateDecoration{linear sample}
+      }
+      x_6_param
+      none
+      undefined
+      __f32
+    }
+  )
+  {
+    Assignment{
+      Identifier[not set]{x_1}
+      Identifier[not set]{x_1_param}
+    }
+    Assignment{
+      Identifier[not set]{x_2}
+      Identifier[not set]{x_2_param}
+    }
+    Assignment{
+      Identifier[not set]{x_3}
+      Identifier[not set]{x_3_param}
+    }
+    Assignment{
+      Identifier[not set]{x_4}
+      Identifier[not set]{x_4_param}
+    }
+    Assignment{
+      Identifier[not set]{x_5}
+      Identifier[not set]{x_5_param}
+    }
+    Assignment{
+      Identifier[not set]{x_6}
+      Identifier[not set]{x_6_param}
+    }
+    Call[not set]{
+      Identifier[not set]{main_1}
+      (
+      )
+    }
+  }
+}
+)";
+  EXPECT_EQ(got, expected) << got;
+}
+
+TEST_F(SpvModuleScopeVarParserTest,
+       EntryPointWrapping_Flatten_Interpolation_Floating_Fragment_In) {
+  const auto assembly = CommonCapabilities() + R"(
+     OpEntryPoint Fragment %main "main" %1
+     OpExecutionMode %main OriginUpperLeft
+     OpDecorate %1 Location 1
+
+     ; member 0 perspective center
+
+     OpMemberDecorate %10 1 Centroid ; perspective centroid
+
+     OpMemberDecorate %10 2 Sample ; perspective sample
+
+     OpMemberDecorate %10 3 NoPerspective; linear center
+
+     OpMemberDecorate %10 4 NoPerspective ; linear centroid
+     OpMemberDecorate %10 4 Centroid
+
+     OpMemberDecorate %10 5 NoPerspective ; linear sample
+     OpMemberDecorate %10 5 Sample
+
+)" + CommonTypes() +
+                        R"(
+
+     %10 = OpTypeStruct %float %float %float %float %float %float
+     %ptr_in_strct = OpTypePointer Input %10
+     %1 = OpVariable %ptr_in_strct Input
+
+     %main = OpFunction %void None %voidfn
+     %entry = OpLabel
+     OpReturn
+     OpFunctionEnd
+  )";
+  auto p = parser(test::Assemble(assembly));
+
+  ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
+  EXPECT_TRUE(p->error().empty());
+  const auto got = p->program().to_str();
+  const std::string expected =
+      R"(Module{
+  Struct S {
+    StructMember{field0: __f32}
+    StructMember{field1: __f32}
+    StructMember{field2: __f32}
+    StructMember{field3: __f32}
+    StructMember{field4: __f32}
+    StructMember{field5: __f32}
+  }
+  Variable{
+    x_1
+    private
+    undefined
+    __type_name_S
+  }
+  Function main_1 -> __void
+  ()
+  {
+    Return{}
+  }
+  Function main -> __void
+  StageDecoration{fragment}
+  (
+    VariableConst{
+      Decorations{
+        LocationDecoration{1}
+      }
+      x_1_param
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{2}
+        InterpolateDecoration{perspective centroid}
+      }
+      x_1_param_1
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{3}
+        InterpolateDecoration{perspective sample}
+      }
+      x_1_param_2
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{4}
+        InterpolateDecoration{linear none}
+      }
+      x_1_param_3
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{5}
+        InterpolateDecoration{linear centroid}
+      }
+      x_1_param_4
+      none
+      undefined
+      __f32
+    }
+    VariableConst{
+      Decorations{
+        LocationDecoration{6}
+        InterpolateDecoration{linear sample}
+      }
+      x_1_param_5
+      none
+      undefined
+      __f32
+    }
+  )
+  {
+    Assignment{
+      MemberAccessor[not set]{
+        Identifier[not set]{x_1}
+        Identifier[not set]{field0}
+      }
+      Identifier[not set]{x_1_param}
+    }
+    Assignment{
+      MemberAccessor[not set]{
+        Identifier[not set]{x_1}
+        Identifier[not set]{field1}
+      }
+      Identifier[not set]{x_1_param_1}
+    }
+    Assignment{
+      MemberAccessor[not set]{
+        Identifier[not set]{x_1}
+        Identifier[not set]{field2}
+      }
+      Identifier[not set]{x_1_param_2}
+    }
+    Assignment{
+      MemberAccessor[not set]{
+        Identifier[not set]{x_1}
+        Identifier[not set]{field3}
+      }
+      Identifier[not set]{x_1_param_3}
+    }
+    Assignment{
+      MemberAccessor[not set]{
+        Identifier[not set]{x_1}
+        Identifier[not set]{field4}
+      }
+      Identifier[not set]{x_1_param_4}
+    }
+    Assignment{
+      MemberAccessor[not set]{
+        Identifier[not set]{x_1}
+        Identifier[not set]{field5}
+      }
+      Identifier[not set]{x_1_param_5}
+    }
+    Call[not set]{
+      Identifier[not set]{main_1}
+      (
+      )
+    }
+  }
+}
+)";
+  EXPECT_EQ(got, expected) << got;
+}
+
+TEST_F(SpvModuleScopeVarParserTest,
+       EntryPointWrapping_Interpolation_Floating_Fragment_Out) {
+  // Flat decorations are dropped for integral
+  const auto assembly = CommonCapabilities() + R"(
+     OpEntryPoint Fragment %main "main" %1 %2 %3 %4 %5 %6
+     OpExecutionMode %main OriginUpperLeft
+     OpDecorate %1 Location 1
+     OpDecorate %2 Location 2
+     OpDecorate %3 Location 3
+     OpDecorate %4 Location 4
+     OpDecorate %5 Location 5
+     OpDecorate %6 Location 6
+
+     ; %1 perspective center
+
+     OpDecorate %2 Centroid ; perspective centroid
+
+     OpDecorate %3 Sample ; perspective sample
+
+     OpDecorate %4 NoPerspective; linear center
+
+     OpDecorate %5 NoPerspective ; linear centroid
+     OpDecorate %5 Centroid
+
+     OpDecorate %6 NoPerspective ; linear sample
+     OpDecorate %6 Sample
+
+)" + CommonTypes() +
+                        R"(
+     %ptr_out_float = OpTypePointer Output %float
+     %1 = OpVariable %ptr_out_float Output
+     %2 = OpVariable %ptr_out_float Output
+     %3 = OpVariable %ptr_out_float Output
+     %4 = OpVariable %ptr_out_float Output
+     %5 = OpVariable %ptr_out_float Output
+     %6 = OpVariable %ptr_out_float Output
+
+     %main = OpFunction %void None %voidfn
+     %entry = OpLabel
+     OpReturn
+     OpFunctionEnd
+  )";
+  auto p = parser(test::Assemble(assembly));
+
+  ASSERT_TRUE(p->BuildAndParseInternalModule());
+  EXPECT_TRUE(p->error().empty());
+  const auto got = p->program().to_str();
+  const std::string expected =
+      R"(Module{
+  Struct main_out {
+    StructMember{[[ LocationDecoration{1}
+ ]] x_1_1: __f32}
+    StructMember{[[ LocationDecoration{2}
+ InterpolateDecoration{perspective centroid}
+ ]] x_2_1: __f32}
+    StructMember{[[ LocationDecoration{3}
+ InterpolateDecoration{perspective sample}
+ ]] x_3_1: __f32}
+    StructMember{[[ LocationDecoration{4}
+ InterpolateDecoration{linear none}
+ ]] x_4_1: __f32}
+    StructMember{[[ LocationDecoration{5}
+ InterpolateDecoration{linear centroid}
+ ]] x_5_1: __f32}
+    StructMember{[[ LocationDecoration{6}
+ InterpolateDecoration{linear sample}
+ ]] x_6_1: __f32}
+  }
+  Variable{
+    x_1
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_2
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_3
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_4
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_5
+    private
+    undefined
+    __f32
+  }
+  Variable{
+    x_6
+    private
+    undefined
+    __f32
+  }
+  Function main_1 -> __void
+  ()
+  {
+    Return{}
+  }
+  Function main -> __type_name_main_out
+  StageDecoration{fragment}
+  ()
+  {
+    Call[not set]{
+      Identifier[not set]{main_1}
+      (
+      )
+    }
+    Return{
+      {
+        TypeConstructor[not set]{
+          __type_name_main_out
+          Identifier[not set]{x_1}
+          Identifier[not set]{x_2}
+          Identifier[not set]{x_3}
+          Identifier[not set]{x_4}
+          Identifier[not set]{x_5}
+          Identifier[not set]{x_6}
+        }
+      }
+    }
+  }
+}
+)";
+  EXPECT_EQ(got, expected) << got;
+}
+
+TEST_F(SpvModuleScopeVarParserTest,
+       EntryPointWrapping_Flatten_Interpolation_Floating_Fragment_Out) {
+  const auto assembly = CommonCapabilities() + R"(
+     OpEntryPoint Fragment %main "main" %1
+     OpExecutionMode %main OriginUpperLeft
+
+     OpDecorate %1 Location 1
+
+     ; member 0 perspective center
+
+     OpMemberDecorate %10 1 Centroid ; perspective centroid
+
+     OpMemberDecorate %10 2 Sample ; perspective sample
+
+     OpMemberDecorate %10 3 NoPerspective; linear center
+
+     OpMemberDecorate %10 4 NoPerspective ; linear centroid
+     OpMemberDecorate %10 4 Centroid
+
+     OpMemberDecorate %10 5 NoPerspective ; linear sample
+     OpMemberDecorate %10 5 Sample
+
+)" + CommonTypes() +
+                        R"(
+
+     %10 = OpTypeStruct %float %float %float %float %float %float
+     %ptr_in_strct = OpTypePointer Output %10
+     %1 = OpVariable %ptr_in_strct Output
+
+     %main = OpFunction %void None %voidfn
+     %entry = OpLabel
+     OpReturn
+     OpFunctionEnd
+  )";
+  auto p = parser(test::Assemble(assembly));
+
+  ASSERT_TRUE(p->BuildAndParseInternalModule());
+  EXPECT_TRUE(p->error().empty());
+  const auto got = p->program().to_str();
+  const std::string expected =
+      R"(Module{
+  Struct S {
+    StructMember{field0: __f32}
+    StructMember{field1: __f32}
+    StructMember{field2: __f32}
+    StructMember{field3: __f32}
+    StructMember{field4: __f32}
+    StructMember{field5: __f32}
+  }
+  Struct main_out {
+    StructMember{[[ LocationDecoration{1}
+ ]] x_1_1: __f32}
+    StructMember{[[ LocationDecoration{2}
+ InterpolateDecoration{perspective centroid}
+ ]] x_1_2: __f32}
+    StructMember{[[ LocationDecoration{3}
+ InterpolateDecoration{perspective sample}
+ ]] x_1_3: __f32}
+    StructMember{[[ LocationDecoration{4}
+ InterpolateDecoration{linear none}
+ ]] x_1_4: __f32}
+    StructMember{[[ LocationDecoration{5}
+ InterpolateDecoration{linear centroid}
+ ]] x_1_5: __f32}
+    StructMember{[[ LocationDecoration{6}
+ InterpolateDecoration{linear sample}
+ ]] x_1_6: __f32}
+  }
+  Variable{
+    x_1
+    private
+    undefined
+    __type_name_S
+  }
+  Function main_1 -> __void
+  ()
+  {
+    Return{}
+  }
+  Function main -> __type_name_main_out
+  StageDecoration{fragment}
+  ()
+  {
+    Call[not set]{
+      Identifier[not set]{main_1}
+      (
+      )
+    }
+    Return{
+      {
+        TypeConstructor[not set]{
+          __type_name_main_out
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{field0}
+          }
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{field1}
+          }
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{field2}
+          }
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{field3}
+          }
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{field4}
+          }
+          MemberAccessor[not set]{
+            Identifier[not set]{x_1}
+            Identifier[not set]{field5}
+          }
+        }
+      }
+    }
+  }
+}
+)";
+  EXPECT_EQ(got, expected) << got;
+}
+
 }  // namespace
 }  // namespace spirv
 }  // namespace reader