[compat] Move sample_{index,mask} validation to Tint

Add a `validation_mode` enum to the WGSL reader options, and pass it
down to the WGSL validator. Set the validation mode to `kCompat` from
Dawn if compatibility mode is enabled on the device.

Remove the pipeline-time validation code from Dawn and update the unit
tests accordingly.

Fixed: 340200030
Fixed: 343033152
Change-Id: I56fe738c971b354c8cb4b14aedf13cd838e37f16
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/190782
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Commit-Queue: James Price <jrprice@google.com>
diff --git a/src/dawn/native/RenderPipeline.cpp b/src/dawn/native/RenderPipeline.cpp
index 79e3c84..89ad6ac 100644
--- a/src/dawn/native/RenderPipeline.cpp
+++ b/src/dawn/native/RenderPipeline.cpp
@@ -723,16 +723,6 @@
     }
 
     if (device->IsCompatibilityMode()) {
-        DAWN_INVALID_IF(
-            fragmentMetadata.usesSampleMaskOutput,
-            "sample_mask is not supported in compatibility mode in the fragment stage (%s, %s)",
-            descriptor->module, &entryPoint);
-
-        DAWN_INVALID_IF(
-            fragmentMetadata.usesSampleIndex,
-            "sample_index is not supported in compatibility mode in the fragment stage (%s, %s)",
-            descriptor->module, &entryPoint);
-
         // Check that all the color target states match.
         ColorAttachmentIndex firstColorTargetIndex{};
         const ColorTargetState* firstColorTargetState = nullptr;
diff --git a/src/dawn/native/ShaderModule.cpp b/src/dawn/native/ShaderModule.cpp
index 6ba5f1f..719b7b2 100644
--- a/src/dawn/native/ShaderModule.cpp
+++ b/src/dawn/native/ShaderModule.cpp
@@ -348,9 +348,11 @@
 
 ResultOrError<tint::Program> ParseWGSL(const tint::Source::File* file,
                                        const tint::wgsl::AllowedFeatures& allowedFeatures,
+                                       const tint::wgsl::ValidationMode mode,
                                        const std::vector<tint::wgsl::Extension>& internalExtensions,
                                        OwnedCompilationMessages* outMessages) {
     tint::wgsl::reader::Options options;
+    options.mode = mode;
     options.allowed_features = allowedFeatures;
     options.allowed_features.extensions.insert(internalExtensions.begin(),
                                                internalExtensions.end());
@@ -774,7 +776,6 @@
             totalInterStageShaderComponents += 1;
         }
         metadata->usesSampleMaskOutput = entryPoint.output_sample_mask_used;
-        metadata->usesSampleIndex = entryPoint.sample_index_used;
         if (entryPoint.sample_index_used) {
             totalInterStageShaderComponents += 1;
         }
@@ -1153,8 +1154,10 @@
     }
 
     tint::Program program;
+    auto validationMode = device->IsCompatibilityMode() ? tint::wgsl::ValidationMode::kCompat
+                                                        : tint::wgsl::ValidationMode::kFull;
     DAWN_TRY_ASSIGN(program, ParseWGSL(tintFile.get(), device->GetWGSLAllowedFeatures(),
-                                       internalExtensions, outMessages));
+                                       validationMode, internalExtensions, outMessages));
 
     parseResult->tintProgram = AcquireRef(new TintProgram(std::move(program), std::move(tintFile)));
 
diff --git a/src/dawn/native/ShaderModule.h b/src/dawn/native/ShaderModule.h
index 18ffe10..e6a8913 100644
--- a/src/dawn/native/ShaderModule.h
+++ b/src/dawn/native/ShaderModule.h
@@ -275,7 +275,6 @@
     bool usesInstanceIndex = false;
     bool usesNumWorkgroups = false;
     bool usesSampleMaskOutput = false;
-    bool usesSampleIndex = false;
     bool usesVertexIndex = false;
 };
 
diff --git a/src/dawn/tests/end2end/MaxLimitTests.cpp b/src/dawn/tests/end2end/MaxLimitTests.cpp
index 352b55b..6bbbef8 100644
--- a/src/dawn/tests/end2end/MaxLimitTests.cpp
+++ b/src/dawn/tests/end2end/MaxLimitTests.cpp
@@ -876,8 +876,9 @@
     };
 
     void DoTest(const MaxInterStageLimitTestsSpec& spec) {
-        // Compat mode does not support sample index.
-        DAWN_TEST_UNSUPPORTED_IF(IsCompatibilityMode() && spec.hasSampleIndex);
+        // Compat mode does not support sample index or sample mask.
+        DAWN_TEST_UNSUPPORTED_IF(IsCompatibilityMode() &&
+                                 (spec.hasSampleIndex || spec.hasSampleMask));
 
         wgpu::RenderPipeline pipeline = CreateRenderPipeline(spec);
         EXPECT_NE(nullptr, pipeline.Get());
diff --git a/src/dawn/tests/unittests/validation/CompatValidationTests.cpp b/src/dawn/tests/unittests/validation/CompatValidationTests.cpp
index af71bfc..3313b26 100644
--- a/src/dawn/tests/unittests/validation/CompatValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/CompatValidationTests.cpp
@@ -213,100 +213,24 @@
     ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&testDescriptor));
 }
 
-TEST_F(CompatValidationTest, CanNotUseFragmentShaderWithSampleMask) {
-    wgpu::ShaderModule moduleSampleMaskOutput = utils::CreateShaderModule(device, R"(
-        @vertex fn vs() -> @builtin(position) vec4f {
-            return vec4f(1);
-        }
+TEST_F(CompatValidationTest, CanNotUseSampleMask) {
+    auto wgsl = R"(
         struct Output {
             @builtin(sample_mask) mask_out: u32,
             @location(0) color : vec4f,
         }
-        @fragment fn fsWithoutSampleMaskUsage() -> @location(0) vec4f {
-            return vec4f(1.0, 1.0, 1.0, 1.0);
-        }
-        @fragment fn fsWithSampleMaskUsage() -> Output {
-            var o: Output;
-            // We need to make sure this sample_mask isn't optimized out even its value equals "no op".
-            o.mask_out = 0xFFFFFFFFu;
-            o.color = vec4f(1.0, 1.0, 1.0, 1.0);
-            return o;
-        }
-    )");
-
-    // Check we can use a fragment shader that doesn't use sample_mask from
-    // the same module as one that does.
-    {
-        utils::ComboRenderPipelineDescriptor descriptor;
-        descriptor.vertex.module = moduleSampleMaskOutput;
-        descriptor.cFragment.module = moduleSampleMaskOutput;
-        descriptor.cFragment.entryPoint = "fsWithoutSampleMaskUsage";
-        descriptor.multisample.count = 4;
-        descriptor.multisample.alphaToCoverageEnabled = false;
-
-        device.CreateRenderPipeline(&descriptor);
-    }
-
-    // Check we can not use a fragment shader that uses sample_mask.
-    {
-        utils::ComboRenderPipelineDescriptor descriptor;
-        descriptor.vertex.module = moduleSampleMaskOutput;
-        descriptor.cFragment.module = moduleSampleMaskOutput;
-        descriptor.cFragment.entryPoint = "fsWithSampleMaskUsage";
-        descriptor.multisample.count = 4;
-        descriptor.multisample.alphaToCoverageEnabled = false;
-
-        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor),
-                            testing::HasSubstr("sample_mask"));
-    }
+    )";
+    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, wgsl),  //
+                        testing::HasSubstr("sample_mask"));
 }
 
-TEST_F(CompatValidationTest, CanNotUseFragmentShaderWithSampleIndex) {
-    wgpu::ShaderModule moduleSampleMaskOutput = utils::CreateShaderModule(device, R"(
-        @vertex fn vs() -> @builtin(position) vec4f {
-            return vec4f(1);
+TEST_F(CompatValidationTest, CanNotUseSampleIndex) {
+    auto wgsl = R"(
+        @fragment fn fsWithSampleIndexUsage(@builtin(sample_index) sNdx: u32) {
         }
-        struct Output {
-            @location(0) color : vec4f,
-        }
-        @fragment fn fsWithoutSampleIndexUsage() -> @location(0) vec4f {
-            return vec4f(1.0, 1.0, 1.0, 1.0);
-        }
-        @fragment fn fsWithSampleIndexUsage(@builtin(sample_index) sNdx: u32) -> Output {
-            var o: Output;
-            _ = sNdx;
-            o.color = vec4f(1.0, 1.0, 1.0, 1.0);
-            return o;
-        }
-    )");
-
-    // Check we can use a fragment shader that doesn't use sample_index from
-    // the same module as one that does.
-    {
-        utils::ComboRenderPipelineDescriptor descriptor;
-        descriptor.vertex.module = moduleSampleMaskOutput;
-        descriptor.vertex.entryPoint = "vs";
-        descriptor.cFragment.module = moduleSampleMaskOutput;
-        descriptor.cFragment.entryPoint = "fsWithoutSampleIndexUsage";
-        descriptor.multisample.count = 4;
-        descriptor.multisample.alphaToCoverageEnabled = false;
-
-        device.CreateRenderPipeline(&descriptor);
-    }
-
-    // Check we can not use a fragment shader that uses sample_index.
-    {
-        utils::ComboRenderPipelineDescriptor descriptor;
-        descriptor.vertex.module = moduleSampleMaskOutput;
-        descriptor.vertex.entryPoint = "vs";
-        descriptor.cFragment.module = moduleSampleMaskOutput;
-        descriptor.cFragment.entryPoint = "fsWithSampleIndexUsage";
-        descriptor.multisample.count = 4;
-        descriptor.multisample.alphaToCoverageEnabled = false;
-
-        ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor),
-                            testing::HasSubstr("sample_index"));
-    }
+    )";
+    ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, wgsl),
+                        testing::HasSubstr("sample_index"));
 }
 
 TEST_F(CompatValidationTest, CanNotUseShaderWithUnsupportedInterpolateTypeOrSampling) {
diff --git a/src/tint/lang/wgsl/common/BUILD.bazel b/src/tint/lang/wgsl/common/BUILD.bazel
index 8fee191..9612768 100644
--- a/src/tint/lang/wgsl/common/BUILD.bazel
+++ b/src/tint/lang/wgsl/common/BUILD.bazel
@@ -43,6 +43,7 @@
   ],
   hdrs = [
     "allowed_features.h",
+    "validation_mode.h",
   ],
   deps = [
     "//src/tint/lang/wgsl",
diff --git a/src/tint/lang/wgsl/common/BUILD.cmake b/src/tint/lang/wgsl/common/BUILD.cmake
index 370d725..da8e8ac 100644
--- a/src/tint/lang/wgsl/common/BUILD.cmake
+++ b/src/tint/lang/wgsl/common/BUILD.cmake
@@ -41,6 +41,7 @@
 tint_add_target(tint_lang_wgsl_common lib
   lang/wgsl/common/allowed_features.h
   lang/wgsl/common/common.cc
+  lang/wgsl/common/validation_mode.h
 )
 
 tint_target_add_dependencies(tint_lang_wgsl_common lib
diff --git a/src/tint/lang/wgsl/common/BUILD.gn b/src/tint/lang/wgsl/common/BUILD.gn
index 7ae73aa4..74a1576 100644
--- a/src/tint/lang/wgsl/common/BUILD.gn
+++ b/src/tint/lang/wgsl/common/BUILD.gn
@@ -46,6 +46,7 @@
   sources = [
     "allowed_features.h",
     "common.cc",
+    "validation_mode.h",
   ]
   deps = [
     "${tint_src_dir}/lang/wgsl",
diff --git a/src/tint/lang/wgsl/common/validation_mode.h b/src/tint/lang/wgsl/common/validation_mode.h
new file mode 100644
index 0000000..b08b6ae
--- /dev/null
+++ b/src/tint/lang/wgsl/common/validation_mode.h
@@ -0,0 +1,45 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_TINT_LANG_WGSL_COMMON_VALIDATION_MODE_H_
+#define SRC_TINT_LANG_WGSL_COMMON_VALIDATION_MODE_H_
+
+#include <cstdint>
+
+namespace tint::wgsl {
+
+/// The validation mode to use for WGSL validation.
+enum class ValidationMode : uint8_t {
+    // Validate against the full WebGPU standard.
+    kFull,
+    // Validate against WebGPU's "compatibility mode".
+    kCompat,
+};
+
+}  // namespace tint::wgsl
+
+#endif  // SRC_TINT_LANG_WGSL_COMMON_VALIDATION_MODE_H_
diff --git a/src/tint/lang/wgsl/reader/options.h b/src/tint/lang/wgsl/reader/options.h
index bf2208f..04399cf 100644
--- a/src/tint/lang/wgsl/reader/options.h
+++ b/src/tint/lang/wgsl/reader/options.h
@@ -29,6 +29,7 @@
 #define SRC_TINT_LANG_WGSL_READER_OPTIONS_H_
 
 #include "src/tint/lang/wgsl/common/allowed_features.h"
+#include "src/tint/lang/wgsl/common/validation_mode.h"
 #include "src/tint/utils/reflection/reflection.h"
 
 namespace tint::wgsl::reader {
@@ -38,8 +39,11 @@
     /// The extensions and language features that are allowed to be used.
     AllowedFeatures allowed_features{};
 
+    /// The validation mode to use.
+    ValidationMode mode = ValidationMode::kFull;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField().
-    TINT_REFLECT(Options, allowed_features);
+    TINT_REFLECT(Options, allowed_features, mode);
 };
 
 }  // namespace tint::wgsl::reader
diff --git a/src/tint/lang/wgsl/reader/reader.cc b/src/tint/lang/wgsl/reader/reader.cc
index 86bacef..a51fa32 100644
--- a/src/tint/lang/wgsl/reader/reader.cc
+++ b/src/tint/lang/wgsl/reader/reader.cc
@@ -46,7 +46,7 @@
     }
     Parser parser(file);
     parser.Parse();
-    return resolver::Resolve(parser.builder(), options.allowed_features);
+    return resolver::Resolve(parser.builder(), options.allowed_features, options.mode);
 }
 
 Result<core::ir::Module> WgslToIR(const Source::File* file, const Options& options) {
diff --git a/src/tint/lang/wgsl/resolver/BUILD.bazel b/src/tint/lang/wgsl/resolver/BUILD.bazel
index e5ea130..2d3103f 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.bazel
+++ b/src/tint/lang/wgsl/resolver/BUILD.bazel
@@ -108,6 +108,7 @@
     "builtins_validation_test.cc",
     "call_test.cc",
     "call_validation_test.cc",
+    "compatibility_mode_test.cc",
     "compound_assignment_validation_test.cc",
     "compound_statement_test.cc",
     "const_assert_test.cc",
diff --git a/src/tint/lang/wgsl/resolver/BUILD.cmake b/src/tint/lang/wgsl/resolver/BUILD.cmake
index 326af91..ec81eb9 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.cmake
+++ b/src/tint/lang/wgsl/resolver/BUILD.cmake
@@ -106,6 +106,7 @@
   lang/wgsl/resolver/builtins_validation_test.cc
   lang/wgsl/resolver/call_test.cc
   lang/wgsl/resolver/call_validation_test.cc
+  lang/wgsl/resolver/compatibility_mode_test.cc
   lang/wgsl/resolver/compound_assignment_validation_test.cc
   lang/wgsl/resolver/compound_statement_test.cc
   lang/wgsl/resolver/const_assert_test.cc
diff --git a/src/tint/lang/wgsl/resolver/BUILD.gn b/src/tint/lang/wgsl/resolver/BUILD.gn
index 6fb89d0..4062da5 100644
--- a/src/tint/lang/wgsl/resolver/BUILD.gn
+++ b/src/tint/lang/wgsl/resolver/BUILD.gn
@@ -108,6 +108,7 @@
       "builtins_validation_test.cc",
       "call_test.cc",
       "call_validation_test.cc",
+      "compatibility_mode_test.cc",
       "compound_assignment_validation_test.cc",
       "compound_statement_test.cc",
       "const_assert_test.cc",
diff --git a/src/tint/lang/wgsl/resolver/compatibility_mode_test.cc b/src/tint/lang/wgsl/resolver/compatibility_mode_test.cc
new file mode 100644
index 0000000..49b96a6
--- /dev/null
+++ b/src/tint/lang/wgsl/resolver/compatibility_mode_test.cc
@@ -0,0 +1,183 @@
+// Copyright 2024 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "src/tint/lang/wgsl/common/validation_mode.h"
+#include "src/tint/lang/wgsl/resolver/resolver.h"
+#include "src/tint/lang/wgsl/resolver/resolver_helper_test.h"
+
+#include "gmock/gmock.h"
+
+namespace tint::resolver {
+namespace {
+
+using namespace tint::core::fluent_types;     // NOLINT
+using namespace tint::core::number_suffixes;  // NOLINT
+
+class ResolverCompatibilityModeTest : public ResolverTest {
+  protected:
+    ResolverCompatibilityModeTest() {
+        resolver_ = std::make_unique<Resolver>(this, wgsl::AllowedFeatures::Everything(),
+                                               wgsl::ValidationMode::kCompat);
+    }
+};
+
+TEST_F(ResolverCompatibilityModeTest, SampleMask_Parameter) {
+    // @fragment
+    // fn main(@builtin(sample_mask) mask : u32) {
+    // }
+
+    Func("main",
+         Vector{Param("mask", ty.i32(),
+                      Vector{
+                          create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_mask")),
+                      })},
+         ty.void_(), Empty,
+         Vector{
+             Stage(ast::PipelineStage::kFragment),
+         },
+         Vector{
+             Builtin(core::BuiltinValue::kPosition),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of '@builtin(sample_mask)' is not allowed in compatibility mode)");
+}
+
+TEST_F(ResolverCompatibilityModeTest, SampleMask_ReturnValue) {
+    // @fragment
+    // fn main() -> @builtin(sample_mask) u32 {
+    //   return 0;
+    // }
+
+    Func("main", Empty, ty.u32(),
+         Vector{
+             Return(0_u),
+         },
+         Vector{
+             Stage(ast::PipelineStage::kFragment),
+         },
+         Vector{
+             create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_mask")),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of '@builtin(sample_mask)' is not allowed in compatibility mode)");
+}
+
+TEST_F(ResolverCompatibilityModeTest, SampleMask_StructMember) {
+    // struct S {
+    //   @builtin(sample_mask) mask : u32,
+    // }
+
+    Structure(
+        "S",
+        Vector{
+            Member("mask", ty.u32(),
+                   Vector{
+                       create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_mask")),
+                   }),
+        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of '@builtin(sample_mask)' is not allowed in compatibility mode)");
+}
+
+TEST_F(ResolverCompatibilityModeTest, SampleIndex_Parameter) {
+    // @fragment
+    // fn main(@builtin(sample_index) mask : u32) {
+    // }
+
+    Func("main",
+         Vector{Param("mask", ty.i32(),
+                      Vector{
+                          create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_index")),
+                      })},
+         ty.void_(), Empty,
+         Vector{
+             Stage(ast::PipelineStage::kFragment),
+         },
+         Vector{
+             Builtin(core::BuiltinValue::kPosition),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of '@builtin(sample_index)' is not allowed in compatibility mode)");
+}
+
+TEST_F(ResolverCompatibilityModeTest, SampleIndex_ReturnValue) {
+    // @fragment
+    // fn main() -> @builtin(sample_index) u32 {
+    //   return 0;
+    // }
+
+    Func("main", Empty, ty.u32(),
+         Vector{
+             Return(0_u),
+         },
+         Vector{
+             Stage(ast::PipelineStage::kFragment),
+         },
+         Vector{
+             create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_index")),
+         });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of '@builtin(sample_index)' is not allowed in compatibility mode)");
+}
+
+TEST_F(ResolverCompatibilityModeTest, SampleIndex_StructMember) {
+    // struct S {
+    //   @builtin(sample_index) mask : u32,
+    // }
+
+    Structure(
+        "S",
+        Vector{
+            Member("mask", ty.u32(),
+                   Vector{
+                       create<ast::BuiltinAttribute>({}, Expr(Source{{12, 34}}, "sample_index")),
+                   }),
+        });
+
+    EXPECT_FALSE(r()->Resolve());
+    EXPECT_EQ(
+        r()->error(),
+        R"(12:34 error: use of '@builtin(sample_index)' is not allowed in compatibility mode)");
+}
+
+}  // namespace
+}  // namespace tint::resolver
diff --git a/src/tint/lang/wgsl/resolver/resolve.cc b/src/tint/lang/wgsl/resolver/resolve.cc
index 3765013..d9db3e8 100644
--- a/src/tint/lang/wgsl/resolver/resolve.cc
+++ b/src/tint/lang/wgsl/resolver/resolve.cc
@@ -33,8 +33,10 @@
 
 namespace tint::resolver {
 
-Program Resolve(ProgramBuilder& builder, const wgsl::AllowedFeatures& allowed_features) {
-    Resolver resolver(&builder, std::move(allowed_features));
+Program Resolve(ProgramBuilder& builder,
+                const wgsl::AllowedFeatures& allowed_features,
+                wgsl::ValidationMode mode) {
+    Resolver resolver(&builder, std::move(allowed_features), mode);
     resolver.Resolve();
     return Program(std::move(builder));
 }
diff --git a/src/tint/lang/wgsl/resolver/resolve.h b/src/tint/lang/wgsl/resolver/resolve.h
index 3e73d80..7c958e1 100644
--- a/src/tint/lang/wgsl/resolver/resolve.h
+++ b/src/tint/lang/wgsl/resolver/resolve.h
@@ -29,6 +29,7 @@
 #define SRC_TINT_LANG_WGSL_RESOLVER_RESOLVE_H_
 
 #include "src/tint/lang/wgsl/common/allowed_features.h"
+#include "src/tint/lang/wgsl/common/validation_mode.h"
 
 namespace tint {
 class Program;
@@ -39,10 +40,11 @@
 
 /// Performs semantic analysis and validation on the program builder @p builder
 /// @param allowed_features the extensions and features that are allowed to be used
+/// @param mode the validation mode to uses
 /// @returns the resolved Program. Program.Diagnostics() may contain validation errors.
-Program Resolve(
-    ProgramBuilder& builder,
-    const wgsl::AllowedFeatures& allowed_features = wgsl::AllowedFeatures::Everything());
+Program Resolve(ProgramBuilder& builder,
+                const wgsl::AllowedFeatures& allowed_features = wgsl::AllowedFeatures::Everything(),
+                wgsl::ValidationMode mode = wgsl::ValidationMode::kFull);
 
 }  // namespace tint::resolver
 
diff --git a/src/tint/lang/wgsl/resolver/resolver.cc b/src/tint/lang/wgsl/resolver/resolver.cc
index 8215747..7951c47 100644
--- a/src/tint/lang/wgsl/resolver/resolver.cc
+++ b/src/tint/lang/wgsl/resolver/resolver.cc
@@ -132,7 +132,9 @@
 
 }  // namespace
 
-Resolver::Resolver(ProgramBuilder* builder, const wgsl::AllowedFeatures& allowed_features)
+Resolver::Resolver(ProgramBuilder* builder,
+                   const wgsl::AllowedFeatures& allowed_features,
+                   wgsl::ValidationMode mode)
     : b(*builder),
       diagnostics_(builder->Diagnostics()),
       const_eval_(builder->constants, diagnostics_),
@@ -142,6 +144,7 @@
                  sem_,
                  enabled_extensions_,
                  allowed_features_,
+                 mode,
                  atomic_composite_info_,
                  valid_type_storage_layouts_),
       allowed_features_(allowed_features) {}
diff --git a/src/tint/lang/wgsl/resolver/resolver.h b/src/tint/lang/wgsl/resolver/resolver.h
index 80394c2..e57b6ad 100644
--- a/src/tint/lang/wgsl/resolver/resolver.h
+++ b/src/tint/lang/wgsl/resolver/resolver.h
@@ -42,6 +42,7 @@
 #include "src/tint/lang/core/intrinsic/table.h"
 #include "src/tint/lang/core/type/input_attachment.h"
 #include "src/tint/lang/wgsl/common/allowed_features.h"
+#include "src/tint/lang/wgsl/common/validation_mode.h"
 #include "src/tint/lang/wgsl/intrinsic/dialect.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/dependency_graph.h"
@@ -100,7 +101,10 @@
     /// Constructor
     /// @param builder the program builder
     /// @param allowed_features the extensions and features that are allowed to be used
-    explicit Resolver(ProgramBuilder* builder, const wgsl::AllowedFeatures& allowed_features);
+    /// @param mode the validation mode to use
+    Resolver(ProgramBuilder* builder,
+             const wgsl::AllowedFeatures& allowed_features,
+             wgsl::ValidationMode mode = wgsl::ValidationMode::kFull);
 
     /// Destructor
     ~Resolver();
diff --git a/src/tint/lang/wgsl/resolver/resolver_helper_test.h b/src/tint/lang/wgsl/resolver/resolver_helper_test.h
index 96326a5..19f239c 100644
--- a/src/tint/lang/wgsl/resolver/resolver_helper_test.h
+++ b/src/tint/lang/wgsl/resolver/resolver_helper_test.h
@@ -132,7 +132,7 @@
     /// declared in WGSL.
     std::string FriendlyName(const core::type::Type* type) { return type->FriendlyName(); }
 
-  private:
+  protected:
     std::unique_ptr<Resolver> resolver_;
 };
 
diff --git a/src/tint/lang/wgsl/resolver/validator.cc b/src/tint/lang/wgsl/resolver/validator.cc
index 75abdac..2f9951d 100644
--- a/src/tint/lang/wgsl/resolver/validator.cc
+++ b/src/tint/lang/wgsl/resolver/validator.cc
@@ -162,6 +162,7 @@
     SemHelper& sem,
     const wgsl::Extensions& enabled_extensions,
     const wgsl::AllowedFeatures& allowed_features,
+    const wgsl::ValidationMode mode,
     const Hashmap<const core::type::Type*, const Source*, 8>& atomic_composite_info,
     Hashset<TypeAndAddressSpace, 8>& valid_type_storage_layouts)
     : symbols_(builder->Symbols()),
@@ -169,6 +170,7 @@
       sem_(sem),
       enabled_extensions_(enabled_extensions),
       allowed_features_(allowed_features),
+      mode_(mode),
       atomic_composite_info_(atomic_composite_info),
       valid_type_storage_layouts_(valid_type_storage_layouts) {
     // Set default severities for filterable diagnostic rules.
@@ -1026,6 +1028,12 @@
             }
             break;
         case core::BuiltinValue::kSampleMask:
+            if (mode_ == wgsl::ValidationMode::kCompat) {
+                AddError(attr->builtin->source) << "use of " << style::Attribute("@builtin")
+                                                << style::Code("(", style::Enum(builtin), ")")
+                                                << " is not allowed in compatibility mode";
+                return false;
+            }
             if (stage != ast::PipelineStage::kNone && !(stage == ast::PipelineStage::kFragment)) {
                 is_stage_mismatch = true;
             }
@@ -1035,6 +1043,12 @@
             }
             break;
         case core::BuiltinValue::kSampleIndex:
+            if (mode_ == wgsl::ValidationMode::kCompat) {
+                AddError(attr->builtin->source) << "use of " << style::Attribute("@builtin")
+                                                << style::Code("(", style::Enum(builtin), ")")
+                                                << " is not allowed in compatibility mode";
+                return false;
+            }
             if (stage != ast::PipelineStage::kNone &&
                 !(stage == ast::PipelineStage::kFragment && is_input)) {
                 is_stage_mismatch = true;
diff --git a/src/tint/lang/wgsl/resolver/validator.h b/src/tint/lang/wgsl/resolver/validator.h
index eff9ce6..6d624a6 100644
--- a/src/tint/lang/wgsl/resolver/validator.h
+++ b/src/tint/lang/wgsl/resolver/validator.h
@@ -37,6 +37,7 @@
 #include "src/tint/lang/wgsl/ast/input_attachment_index_attribute.h"
 #include "src/tint/lang/wgsl/ast/pipeline_stage.h"
 #include "src/tint/lang/wgsl/common/allowed_features.h"
+#include "src/tint/lang/wgsl/common/validation_mode.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
 #include "src/tint/lang/wgsl/resolver/sem_helper.h"
 #include "src/tint/utils/containers/hashmap.h"
@@ -118,12 +119,14 @@
     /// @param helper the SEM helper to validate with
     /// @param enabled_extensions all the extensions declared in current module
     /// @param allowed_features the allowed extensions and features
+    /// @param mode the validation mode to use
     /// @param atomic_composite_info atomic composite info of the module
     /// @param valid_type_storage_layouts a set of validated type layouts by address space
     Validator(ProgramBuilder* builder,
               SemHelper& helper,
               const wgsl::Extensions& enabled_extensions,
               const wgsl::AllowedFeatures& allowed_features,
+              wgsl::ValidationMode mode,
               const Hashmap<const core::type::Type*, const Source*, 8>& atomic_composite_info,
               Hashset<TypeAndAddressSpace, 8>& valid_type_storage_layouts);
     ~Validator();
@@ -626,6 +629,7 @@
     DiagnosticFilterStack diagnostic_filters_;
     const wgsl::Extensions& enabled_extensions_;
     const wgsl::AllowedFeatures& allowed_features_;
+    const wgsl::ValidationMode mode_;
     const Hashmap<const core::type::Type*, const Source*, 8>& atomic_composite_info_;
     Hashset<TypeAndAddressSpace, 8>& valid_type_storage_layouts_;
 };