Add basic SPIR-V 1.4 support to the SPIR-V writer

Fixed: 422422592

* Adds command line option to specify SPIR-V binary version
* Adds a binary version to the spir-v writer options
  * passed on to the binary writer for the module header
* When SPIR-V 1.4 is enabled, OpEntryPoint lists all global variables

Change-Id: I13ca8f247d42b53e24f0b3921b3d9c09dc362d77
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/245514
Reviewed-by: James Price <jrprice@google.com>
Commit-Queue: Alan Baker <alanbaker@google.com>
Auto-Submit: Alan Baker <alanbaker@google.com>
diff --git a/src/tint/cmd/tint/main.cc b/src/tint/cmd/tint/main.cc
index 5b368a5..8778487 100644
--- a/src/tint/cmd/tint/main.cc
+++ b/src/tint/cmd/tint/main.cc
@@ -168,6 +168,7 @@
 
 #if TINT_BUILD_SPV_WRITER
     bool use_storage_input_output_16 = true;
+    tint::spirv::writer::SpvVersion spirv_version = tint::spirv::writer::SpvVersion::kSpv13;
 #endif  // TINT_BULD_SPV_WRITER
 
 #if TINT_BUILD_MSL_WRITER
@@ -410,6 +411,16 @@
         options.Add<BoolOption>("use-storage-input-output-16",
                                 "Use the StorageInputOutput16 SPIR-V capability", Default{true});
     TINT_DEFER(opts->use_storage_input_output_16 = *use_storage_input_output_16.value);
+
+    tint::Vector<EnumName<tint::spirv::writer::SpvVersion>, 2> version_enum_names{
+        EnumName(tint::spirv::writer::SpvVersion::kSpv13, "1.3"),
+        EnumName(tint::spirv::writer::SpvVersion::kSpv14, "1.4"),
+    };
+    auto& spirv_version = options.Add<EnumOption<tint::spirv::writer::SpvVersion>>(
+        "spirv-version", R"(Specify the SPIR-V binary version.
+Valid values are 1.3 and 1.4)",
+        version_enum_names, Default{tint::spirv::writer::SpvVersion::kSpv13});
+    TINT_DEFER(opts->spirv_version = *spirv_version.value);
 #endif  // TINT_BUILD_SPV_WRITER
 
 #if TINT_BUILD_GLSL_WRITER
@@ -890,6 +901,7 @@
     gen_options.disable_robustness = !options.enable_robustness;
     gen_options.disable_workgroup_init = options.disable_workgroup_init;
     gen_options.use_storage_input_output_16 = options.use_storage_input_output_16;
+    gen_options.spirv_version = options.spirv_version;
 
     auto entry_point = inspector.GetEntryPoint(options.ep_name);
 
diff --git a/src/tint/lang/spirv/writer/common/binary_writer.cc b/src/tint/lang/spirv/writer/common/binary_writer.cc
index bf78190..ed6275f 100644
--- a/src/tint/lang/spirv/writer/common/binary_writer.cc
+++ b/src/tint/lang/spirv/writer/common/binary_writer.cc
@@ -52,9 +52,9 @@
     ProcessInstruction(inst);
 }
 
-void BinaryWriter::WriteHeader(uint32_t bound, uint32_t version) {
+void BinaryWriter::WriteHeader(uint32_t bound, uint32_t version, uint32_t spirv_version) {
     out_.push_back(spv::MagicNumber);
-    out_.push_back(0x00010300);  // Version 1.3
+    out_.push_back(spirv_version);
     out_.push_back(kGeneratorId | version);
     out_.push_back(bound);
     out_.push_back(0);
diff --git a/src/tint/lang/spirv/writer/common/binary_writer.h b/src/tint/lang/spirv/writer/common/binary_writer.h
index 0463810..4909235 100644
--- a/src/tint/lang/spirv/writer/common/binary_writer.h
+++ b/src/tint/lang/spirv/writer/common/binary_writer.h
@@ -44,7 +44,8 @@
     /// Writes the SPIR-V header.
     /// @param bound the bound to output
     /// @param version the generator version number
-    void WriteHeader(uint32_t bound, uint32_t version = 0);
+    /// @param spirv_version the SPIR-V binary version (default SPIR-V 1.3).
+    void WriteHeader(uint32_t bound, uint32_t version = 0, uint32_t spirv_version = 0x10300u);
 
     /// Writes the given module data into a binary. Note, this does not emit the SPIR-V header. You
     /// **must** call WriteHeader() before WriteModule() if you want the SPIR-V to be emitted.
diff --git a/src/tint/lang/spirv/writer/common/binary_writer_test.cc b/src/tint/lang/spirv/writer/common/binary_writer_test.cc
index 6ee91a3..c2254aa 100644
--- a/src/tint/lang/spirv/writer/common/binary_writer_test.cc
+++ b/src/tint/lang/spirv/writer/common/binary_writer_test.cc
@@ -46,6 +46,19 @@
     EXPECT_EQ(res[4], 0u);           // Reserved
 }
 
+TEST_F(SpirvWriterBinaryWriterTest, Preamble_Spirv1p4) {
+    BinaryWriter bw;
+    bw.WriteHeader(5, 0, 0x10400);
+
+    auto res = bw.Result();
+    ASSERT_EQ(res.size(), 5u);
+    EXPECT_EQ(res[0], spv::MagicNumber);
+    EXPECT_EQ(res[1], 0x00010400u);  // SPIR-V 1.4
+    EXPECT_EQ(res[2], 23u << 16);    // Generator ID
+    EXPECT_EQ(res[3], 5u);           // ID Bound
+    EXPECT_EQ(res[4], 0u);           // Reserved
+}
+
 TEST_F(SpirvWriterBinaryWriterTest, Float) {
     Module m;
 
diff --git a/src/tint/lang/spirv/writer/common/options.h b/src/tint/lang/spirv/writer/common/options.h
index d1372b4..37741e5 100644
--- a/src/tint/lang/spirv/writer/common/options.h
+++ b/src/tint/lang/spirv/writer/common/options.h
@@ -129,6 +129,12 @@
                  input_attachment);
 };
 
+/// Supported SPIR-V binary versions.
+enum class SpvVersion : uint32_t {
+    kSpv13 = 0x10300u,  // SPIR-V 1.3
+    kSpv14 = 0x10400u,  // SPIR-V 1.4
+};
+
 /// Configuration options used for generating SPIR-V.
 struct Options {
     struct RangeOffsets {
@@ -210,6 +216,15 @@
     /// Offsets of the minDepth and maxDepth push constants.
     std::optional<RangeOffsets> depth_range_offsets = std::nullopt;
 
+    /// SPIR-V binary version.
+    SpvVersion spirv_version = SpvVersion::kSpv13;
+
+    /// Returns true if the binary version is less than major.minor.
+    bool SpirvVersionLess(uint32_t major, uint32_t minor) const {
+        return static_cast<uint32_t>(spirv_version) <
+               (((major & 0xffff) << 16) | ((minor & 0xffff) << 8));
+    }
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(Options,
                  remapped_entry_point_name,
@@ -232,9 +247,17 @@
                  use_vulkan_memory_model,
                  scalarize_clamp_builtin,
                  dva_transform_handle,
-                 depth_range_offsets);
+                 depth_range_offsets,
+                 spirv_version);
 };
 
 }  // namespace tint::spirv::writer
 
+namespace tint {
+
+/// Reflect enum information for SPIR-V version.
+TINT_REFLECT_ENUM_RANGE(spirv::writer::SpvVersion, kSpv13, kSpv14);
+
+}  // namespace tint
+
 #endif  // SRC_TINT_LANG_SPIRV_WRITER_COMMON_OPTIONS_H_
diff --git a/src/tint/lang/spirv/writer/common/options_test.cc b/src/tint/lang/spirv/writer/common/options_test.cc
index 70c136a..9f4e3b7 100644
--- a/src/tint/lang/spirv/writer/common/options_test.cc
+++ b/src/tint/lang/spirv/writer/common/options_test.cc
@@ -39,5 +39,31 @@
     TINT_ASSERT_ALL_FIELDS_REFLECTED(Options);
 }
 
+using SpirvWriterOptionsTest = ::testing::Test;
+
+TEST_F(SpirvWriterOptionsTest, SpirvVersionLess) {
+    Options options;
+
+    // Default is SPIR-V 1.3
+    EXPECT_FALSE(options.SpirvVersionLess(1, 0));
+    EXPECT_FALSE(options.SpirvVersionLess(1, 1));
+    EXPECT_FALSE(options.SpirvVersionLess(1, 2));
+    EXPECT_FALSE(options.SpirvVersionLess(1, 3));
+    EXPECT_TRUE(options.SpirvVersionLess(1, 4));
+    EXPECT_TRUE(options.SpirvVersionLess(1, 5));
+    EXPECT_TRUE(options.SpirvVersionLess(1, 6));
+    EXPECT_TRUE(options.SpirvVersionLess(1, 7));
+
+    options.spirv_version = SpvVersion::kSpv14;
+    EXPECT_FALSE(options.SpirvVersionLess(1, 0));
+    EXPECT_FALSE(options.SpirvVersionLess(1, 1));
+    EXPECT_FALSE(options.SpirvVersionLess(1, 2));
+    EXPECT_FALSE(options.SpirvVersionLess(1, 3));
+    EXPECT_FALSE(options.SpirvVersionLess(1, 4));
+    EXPECT_TRUE(options.SpirvVersionLess(1, 5));
+    EXPECT_TRUE(options.SpirvVersionLess(1, 6));
+    EXPECT_TRUE(options.SpirvVersionLess(1, 7));
+}
+
 }  // namespace
 }  // namespace tint::spirv::writer
diff --git a/src/tint/lang/spirv/writer/printer/printer.cc b/src/tint/lang/spirv/writer/printer/printer.cc
index 8680fd38..602f165 100644
--- a/src/tint/lang/spirv/writer/printer/printer.cc
+++ b/src/tint/lang/spirv/writer/printer/printer.cc
@@ -219,7 +219,8 @@
 
         // Serialize the module into binary SPIR-V.
         BinaryWriter writer;
-        writer.WriteHeader(module_.IdBound(), kWriterVersion);
+        writer.WriteHeader(module_.IdBound(), kWriterVersion,
+                           static_cast<uint32_t>(options_.spirv_version));
         writer.WriteModule(module_);
 
         output_.spirv = std::move(writer.Result());
@@ -862,27 +863,35 @@
             }
 
             auto* ptr = var->Result()->Type()->As<core::type::Pointer>();
-            if (!(ptr->AddressSpace() == core::AddressSpace::kIn ||
-                  ptr->AddressSpace() == core::AddressSpace::kOut)) {
-                continue;
-            }
+            if (options_.SpirvVersionLess(1, 4)) {
+                // In SPIR-V 1.3 or earlier, OpEntryPoint should list only statically used
+                // input/output variables.
+                if (!(ptr->AddressSpace() == core::AddressSpace::kIn ||
+                      ptr->AddressSpace() == core::AddressSpace::kOut)) {
+                    continue;
+                }
 
-            // Determine if this IO variable is used by the entry point.
-            bool used = false;
-            for (const auto& use : var->Result()->UsagesUnsorted()) {
-                auto* block = use->instruction->Block();
-                while (block->Parent()) {
-                    block = block->Parent()->Block();
+                // Determine if this IO variable is used by the entry point.
+                bool used = false;
+                for (const auto& use : var->Result()->UsagesUnsorted()) {
+                    auto* block = use->instruction->Block();
+                    while (block->Parent()) {
+                        block = block->Parent()->Block();
+                    }
+                    if (block == func->Block()) {
+                        used = true;
+                        break;
+                    }
                 }
-                if (block == func->Block()) {
-                    used = true;
-                    break;
+                if (!used) {
+                    continue;
                 }
+                operands.push_back(Value(var));
+            } else {
+                // In SPIR-V 1.4 or later, OpEntryPoint must list all global variables statically
+                // used by the entry point. It may be a superset of the used variables though.
+                operands.push_back(Value(var));
             }
-            if (!used) {
-                continue;
-            }
-            operands.push_back(Value(var));
 
             // Add the `DepthReplacing` execution mode if `frag_depth` is used.
             if (var->Attributes().builtin == core::BuiltinValue::kFragDepth) {
diff --git a/src/tint/lang/spirv/writer/writer_test.cc b/src/tint/lang/spirv/writer/writer_test.cc
index da107c7..43fa833 100644
--- a/src/tint/lang/spirv/writer/writer_test.cc
+++ b/src/tint/lang/spirv/writer/writer_test.cc
@@ -154,6 +154,88 @@
     EXPECT_INST("OpEntryPoint GLCompute %main \"main\"");
 }
 
+TEST_F(SpirvWriterTest, EntryPoint_FunctionVar_Spirv1p3) {
+    auto* func = b.ComputeFunction("main");
+    b.Append(func->Block(), [&] {  //
+        b.Var("x", 0_u);
+        b.Return(func);
+    });
+
+    Options options;
+    options.remapped_entry_point_name = "";
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST("OpEntryPoint GLCompute %main \"main\"\n");
+}
+
+TEST_F(SpirvWriterTest, EntryPoint_FunctionVar_Spirv1p4) {
+    auto* func = b.ComputeFunction("main");
+    b.Append(func->Block(), [&] {  //
+        b.Var("x", 0_u);
+        b.Return(func);
+    });
+
+    Options options;
+    options.remapped_entry_point_name = "";
+    options.spirv_version = SpvVersion::kSpv14;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST("OpEntryPoint GLCompute %main \"main\"\n");
+}
+
+TEST_F(SpirvWriterTest, EntryPoint_StorageVar_Spirv1p3) {
+    auto* v = b.Var("v", core::AddressSpace::kStorage, ty.u32(), core::Access::kReadWrite);
+    mod.root_block->Append(v);
+    v->SetBindingPoint(0, 0);
+    auto* func = b.ComputeFunction("main");
+    b.Append(func->Block(), [&] {  //
+        b.Load(v);
+        b.Return(func);
+    });
+
+    Options options;
+    options.remapped_entry_point_name = "";
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST("OpEntryPoint GLCompute %main \"main\"\n");
+}
+
+TEST_F(SpirvWriterTest, EntryPoint_StorageVar_Spirv1p4) {
+    auto* v = b.Var("v", core::AddressSpace::kStorage, ty.u32(), core::Access::kReadWrite);
+    mod.root_block->Append(v);
+    v->SetBindingPoint(0, 0);
+    auto* func = b.ComputeFunction("main");
+    b.Append(func->Block(), [&] {  //
+        b.Load(v);
+        b.Return(func);
+    });
+
+    Options options;
+    options.remapped_entry_point_name = "";
+    options.spirv_version = SpvVersion::kSpv14;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST("OpEntryPoint GLCompute %main \"main\" %1");
+}
+
+TEST_F(SpirvWriterTest, EntryPoint_StorageVar_CalledFunction_Spirv1p4) {
+    auto* v = b.Var("v", core::AddressSpace::kStorage, ty.u32(), core::Access::kReadWrite);
+    mod.root_block->Append(v);
+    v->SetBindingPoint(0, 0);
+    auto* foo = b.Function("foo", ty.void_());
+    b.Append(foo->Block(), [&] {  //
+        b.Load(v);
+        b.Return(foo);
+    });
+    auto* func = b.ComputeFunction("main");
+    b.Append(func->Block(), [&] {  //
+        b.Call(foo);
+        b.Return(func);
+    });
+
+    Options options;
+    options.remapped_entry_point_name = "";
+    options.spirv_version = SpvVersion::kSpv14;
+    ASSERT_TRUE(Generate(options)) << Error() << output_;
+    EXPECT_INST("OpEntryPoint GLCompute %main \"main\" %1");
+}
+
 TEST_F(SpirvWriterTest, StripAllNames) {
     auto* str =
         ty.Struct(mod.symbols.New("MyStruct"), {