[spirv-reader][ir] Ignore `cull_distance`, skip unused builtins

This CL adds the ability to skip the `cull_distance` annotated structure
members when converting from SPIR-V. These get added as a standard part
of the `gl_PerVertex` structure created when emitting GLSL as SPIR-V.

This CL updates the ShaderIO routines to skip emitting unused members
of builtin `kPointSize` or `kClipDistances` as they also get emitted
even if unused. Any member which doesn't not have a location, builtin or
or colour attribute attached are skipped.

Bug: 42250952
Change-Id: Id6cd9cd9be45102ead9988ff53bf2932022fae31
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/220414
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: dan sinclair <dsinclair@chromium.org>
diff --git a/src/tint/lang/core/ir/validator.cc b/src/tint/lang/core/ir/validator.cc
index b5ab776..5d1a57a 100644
--- a/src/tint/lang/core/ir/validator.cc
+++ b/src/tint/lang/core/ir/validator.cc
@@ -348,6 +348,17 @@
     /* type_error */ "__point_size must be a f32",
 };
 
+constexpr BuiltinChecker kCullDistanceChecker{
+    /* name */ "__cull_distance",
+    /* stages */ EnumSet<Function::PipelineStage>(Function::PipelineStage::kVertex),
+    /* direction */ BuiltinChecker::IODirection::kOutput,
+    /* type_check */
+    [](const core::type::Type* ty) -> bool {
+        return ty->Is<core::type::Array>() && ty->DeepestElement()->Is<core::type::F32>();
+    },
+    /* type_error */ "__cull_distance must be an array of f32",
+};
+
 constexpr BuiltinChecker kFragDepthChecker{
     /* name */ "frag_depth",
     /* stages */ EnumSet<Function::PipelineStage>(Function::PipelineStage::kFragment),
@@ -465,6 +476,8 @@
     switch (builtin) {
         case BuiltinValue::kPointSize:
             return kPointSizeChecker;
+        case BuiltinValue::kCullDistance:
+            return kCullDistanceChecker;
         case BuiltinValue::kFragDepth:
             return kFragDepthChecker;
         case BuiltinValue::kFrontFacing:
diff --git a/src/tint/lang/spirv/reader/lower/shader_io.cc b/src/tint/lang/spirv/reader/lower/shader_io.cc
index e1f375c..ddff8a4 100644
--- a/src/tint/lang/spirv/reader/lower/shader_io.cc
+++ b/src/tint/lang/spirv/reader/lower/shader_io.cc
@@ -198,6 +198,10 @@
             if (auto* str = var_type->As<core::type::Struct>()) {
                 // Add an output for each member of the struct.
                 for (auto* member : str->Members()) {
+                    if (ShouldSkipMemberEmission(var, member)) {
+                        continue;
+                    }
+
                     // Use the base variable attributes if not specified directly on the member.
                     auto member_attributes = member->Attributes();
                     if (auto base_loc = var_attributes.location) {
@@ -253,6 +257,72 @@
         }
     }
 
+    /// Returns true if the struct member should be skipped on emission
+    /// @param var the var which references the structure
+    /// @param member the member to check
+    /// @returns true if the member should be skipped.
+    bool ShouldSkipMemberEmission(core::ir::Var* var, const core::type::StructMember* member) {
+        auto var_attributes = var->Attributes();
+        auto member_attributes = member->Attributes();
+
+        // If neither the var, nor the member has attributes, then skip
+        if (!var_attributes.builtin.has_value() && !var_attributes.color.has_value() &&
+            !var_attributes.location.has_value()) {
+            if (!member_attributes.builtin.has_value() && !member_attributes.color.has_value() &&
+                !member_attributes.location.has_value()) {
+                return true;
+            }
+        }
+
+        // The `gl_PerVertex` structure always gets emitted by glslang, but it may only be used by
+        // the `gl_Position` variable. The structure will also contain the `gl_PointSize`,
+        // `gl_ClipDistance` and `gl_CullDistance`.
+
+        if (member_attributes.builtin == core::BuiltinValue::kPointSize) {
+            // TODO(dsinclair): Validate that all accesses of this member are then used only to
+            // assign the value of 1.0.
+            return true;
+        }
+        if (member_attributes.builtin == core::BuiltinValue::kCullDistance) {
+            TINT_ASSERT(!IsIndexAccessed(var->Result(0), member->Index()));
+            return true;
+        }
+        if (member_attributes.builtin == core::BuiltinValue::kClipDistances) {
+            return !IsIndexAccessed(var->Result(0), member->Index());
+        }
+        return false;
+    }
+
+    /// Returns true if the `idx` member of `val` is accessed. The `val` must be of type
+    /// `Structure` which contains the given member index.
+    /// @param val the value to check
+    /// @param idx the index to look for
+    /// @returns true if `idx` is accessed.
+    bool IsIndexAccessed(core::ir::Value* val, uint32_t idx) {
+        for (auto& usage : val->UsagesUnsorted()) {
+            // Only care about access chains
+            auto* chain = usage->instruction->As<core::ir::Access>();
+            if (!chain) {
+                continue;
+            }
+            if (chain->Indices().IsEmpty()) {
+                continue;
+            }
+
+            // A member access has to be a constant index
+            auto* cnst = chain->Indices()[0]->As<core::ir::Constant>();
+            if (!cnst) {
+                continue;
+            }
+
+            uint32_t v = cnst->Value()->ValueAs<uint32_t>();
+            if (v == idx) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /// Replace a use of an input pointer value.
     /// @param var the originating input variable
     /// @param value the input pointer value
diff --git a/src/tint/lang/spirv/reader/lower/shader_io_test.cc b/src/tint/lang/spirv/reader/lower/shader_io_test.cc
index a19b8e2..9b06a0b 100644
--- a/src/tint/lang/spirv/reader/lower/shader_io_test.cc
+++ b/src/tint/lang/spirv/reader/lower/shader_io_test.cc
@@ -2144,5 +2144,266 @@
     EXPECT_EQ(expect, str());
 }
 
+TEST_F(SpirvReader_ShaderIOTest, PointSize) {
+    auto* builtin_str =
+        ty.Struct(mod.symbols.New("Builtins"), Vector{
+                                                   core::type::Manager::StructMemberDesc{
+                                                       mod.symbols.New("position"),
+                                                       ty.vec4<f32>(),
+                                                       BuiltinAttrs(core::BuiltinValue::kPosition),
+                                                   },
+                                                   core::type::Manager::StructMemberDesc{
+                                                       mod.symbols.New("point_size"),
+                                                       ty.f32(),
+                                                       BuiltinAttrs(core::BuiltinValue::kPointSize),
+                                                   },
+                                               });
+    auto* builtins = b.Var("builtins", ty.ptr(core::AddressSpace::kOut, builtin_str));
+    mod.root_block->Append(builtins);
+
+    auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+    b.Append(ep->Block(), [&] {  //
+        auto* ptr = ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>());
+        b.Store(b.Access(ptr, builtins, 0_u), b.Splat<vec4<f32>>(1_f));
+        b.Store(b.Access(ty.ptr(core::AddressSpace::kOut, ty.f32()), builtins, 1_u), 1_f);
+        b.Return(ep);
+    });
+
+    auto* src = R"(
+Builtins = struct @align(16) {
+  position:vec4<f32> @offset(0), @builtin(position)
+  point_size:f32 @offset(16), @builtin(__point_size)
+}
+
+$B1: {  # root
+  %builtins:ptr<__out, Builtins, read_write> = var
+}
+
+%foo = @vertex func():void {
+  $B2: {
+    %3:ptr<__out, vec4<f32>, read_write> = access %builtins, 0u
+    store %3, vec4<f32>(1.0f)
+    %4:ptr<__out, f32, read_write> = access %builtins, 1u
+    store %4, 1.0f
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Builtins = struct @align(16) {
+  position:vec4<f32> @offset(0)
+  point_size:f32 @offset(16)
+}
+
+$B1: {  # root
+  %builtins:ptr<private, Builtins, read_write> = var
+}
+
+%foo_inner = func():void {
+  $B2: {
+    %3:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+    store %3, vec4<f32>(1.0f)
+    %4:ptr<private, f32, read_write> = access %builtins, 1u
+    store %4, 1.0f
+    ret
+  }
+}
+%foo = @vertex func():vec4<f32> [@position] {
+  $B3: {
+    %6:void = call %foo_inner
+    %7:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+    %8:vec4<f32> = load %7
+    ret %8
+  }
+}
+)";
+
+    Run(ShaderIO);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Outputs_Struct_UnusedOutputs) {
+    auto* builtin_str = ty.Struct(mod.symbols.New("Builtins"),
+                                  Vector{
+                                      core::type::Manager::StructMemberDesc{
+                                          mod.symbols.New("position"),
+                                          ty.vec4<f32>(),
+                                          BuiltinAttrs(core::BuiltinValue::kPosition),
+                                      },
+                                      core::type::Manager::StructMemberDesc{
+                                          mod.symbols.New("clip_distance"),
+                                          ty.array<f32, 3>(),
+                                          BuiltinAttrs(core::BuiltinValue::kClipDistances),
+                                      },
+                                      core::type::Manager::StructMemberDesc{
+                                          mod.symbols.New("point_size"),
+                                          ty.f32(),
+                                          BuiltinAttrs(core::BuiltinValue::kPointSize),
+                                      },
+                                      core::type::Manager::StructMemberDesc{
+                                          mod.symbols.New("cull_distance"),
+                                          ty.array<f32, 3>(),
+                                          BuiltinAttrs(core::BuiltinValue::kCullDistance),
+                                      },
+                                  });
+    auto* builtins = b.Var("builtins", ty.ptr(core::AddressSpace::kOut, builtin_str));
+    mod.root_block->Append(builtins);
+
+    auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+    b.Append(ep->Block(), [&] {  //
+        auto* ptr = ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>());
+        b.Store(b.Access(ptr, builtins, 0_u), b.Splat<vec4<f32>>(1_f));
+        b.Return(ep);
+    });
+
+    auto* src = R"(
+Builtins = struct @align(16) {
+  position:vec4<f32> @offset(0), @builtin(position)
+  clip_distance:array<f32, 3> @offset(16), @builtin(clip_distances)
+  point_size:f32 @offset(28), @builtin(__point_size)
+  cull_distance:array<f32, 3> @offset(32), @builtin(__cull_distance)
+}
+
+$B1: {  # root
+  %builtins:ptr<__out, Builtins, read_write> = var
+}
+
+%foo = @vertex func():void {
+  $B2: {
+    %3:ptr<__out, vec4<f32>, read_write> = access %builtins, 0u
+    store %3, vec4<f32>(1.0f)
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Builtins = struct @align(16) {
+  position:vec4<f32> @offset(0)
+  clip_distance:array<f32, 3> @offset(16)
+  point_size:f32 @offset(28)
+  cull_distance:array<f32, 3> @offset(32)
+}
+
+$B1: {  # root
+  %builtins:ptr<private, Builtins, read_write> = var
+}
+
+%foo_inner = func():void {
+  $B2: {
+    %3:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+    store %3, vec4<f32>(1.0f)
+    ret
+  }
+}
+%foo = @vertex func():vec4<f32> [@position] {
+  $B3: {
+    %5:void = call %foo_inner
+    %6:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+    %7:vec4<f32> = load %6
+    ret %7
+  }
+}
+)";
+
+    Run(ShaderIO);
+
+    EXPECT_EQ(expect, str());
+}
+
+TEST_F(SpirvReader_ShaderIOTest, Outputs_Struct_MultipledUsed) {
+    auto* builtin_str = ty.Struct(mod.symbols.New("Builtins"),
+                                  Vector{
+                                      core::type::Manager::StructMemberDesc{
+                                          mod.symbols.New("position"),
+                                          ty.vec4<f32>(),
+                                          BuiltinAttrs(core::BuiltinValue::kPosition),
+                                      },
+                                      core::type::Manager::StructMemberDesc{
+                                          mod.symbols.New("clip_distance"),
+                                          ty.array<f32, 3>(),
+                                          BuiltinAttrs(core::BuiltinValue::kClipDistances),
+                                      },
+                                  });
+    auto* builtins = b.Var("builtins", ty.ptr(core::AddressSpace::kOut, builtin_str));
+    mod.root_block->Append(builtins);
+
+    auto* ep = b.Function("foo", ty.void_(), core::ir::Function::PipelineStage::kVertex);
+    b.Append(ep->Block(), [&] {  //
+        auto* ptr1 = ty.ptr(core::AddressSpace::kOut, ty.vec4<f32>());
+        b.Store(b.Access(ptr1, builtins, 0_u), b.Splat<vec4<f32>>(1_f));
+
+        auto* ptr2 = ty.ptr(core::AddressSpace::kOut, ty.array<f32, 3>());
+        b.Store(b.Access(ptr2, builtins, 1_u), b.Splat<array<f32, 3>>(1_f));
+        b.Return(ep);
+    });
+
+    auto* src = R"(
+Builtins = struct @align(16) {
+  position:vec4<f32> @offset(0), @builtin(position)
+  clip_distance:array<f32, 3> @offset(16), @builtin(clip_distances)
+}
+
+$B1: {  # root
+  %builtins:ptr<__out, Builtins, read_write> = var
+}
+
+%foo = @vertex func():void {
+  $B2: {
+    %3:ptr<__out, vec4<f32>, read_write> = access %builtins, 0u
+    store %3, vec4<f32>(1.0f)
+    %4:ptr<__out, array<f32, 3>, read_write> = access %builtins, 1u
+    store %4, array<f32, 3>(1.0f)
+    ret
+  }
+}
+)";
+    EXPECT_EQ(src, str());
+
+    auto* expect = R"(
+Builtins = struct @align(16) {
+  position:vec4<f32> @offset(0)
+  clip_distance:array<f32, 3> @offset(16)
+}
+
+tint_symbol = struct @align(16) {
+  position:vec4<f32> @offset(0), @builtin(position)
+  clip_distance:array<f32, 3> @offset(16), @builtin(clip_distances)
+}
+
+$B1: {  # root
+  %builtins:ptr<private, Builtins, read_write> = var
+}
+
+%foo_inner = func():void {
+  $B2: {
+    %3:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+    store %3, vec4<f32>(1.0f)
+    %4:ptr<private, array<f32, 3>, read_write> = access %builtins, 1u
+    store %4, array<f32, 3>(1.0f)
+    ret
+  }
+}
+%foo = @vertex func():tint_symbol {
+  $B3: {
+    %6:void = call %foo_inner
+    %7:ptr<private, vec4<f32>, read_write> = access %builtins, 0u
+    %8:vec4<f32> = load %7
+    %9:ptr<private, array<f32, 3>, read_write> = access %builtins, 1u
+    %10:array<f32, 3> = load %9
+    %11:tint_symbol = construct %8, %10
+    ret %11
+  }
+}
+)";
+
+    Run(ShaderIO);
+
+    EXPECT_EQ(expect, str());
+}
 }  // namespace
 }  // namespace tint::spirv::reader::lower
diff --git a/src/tint/lang/spirv/reader/parser/parser.cc b/src/tint/lang/spirv/reader/parser/parser.cc
index 271973b..c43fe01 100644
--- a/src/tint/lang/spirv/reader/parser/parser.cc
+++ b/src/tint/lang/spirv/reader/parser/parser.cc
@@ -155,6 +155,8 @@
                 return core::BuiltinValue::kWorkgroupId;
             case spv::BuiltIn::ClipDistance:
                 return core::BuiltinValue::kClipDistances;
+            case spv::BuiltIn::CullDistance:
+                return core::BuiltinValue::kCullDistance;
             default:
                 TINT_UNIMPLEMENTED() << "unhandled SPIR-V BuiltIn: " << static_cast<uint32_t>(b);
         }