Add maxAnisotropy to GPUSamplerDescriptor

Adds some maxAnisotropy implementation.
Adds an end2end test, drawing a slanted plane with a texture of which each mipmap has a different color, with different maxAnisotropy values.
You can get an idea of what it does at https://jsfiddle.net/t64kpu81/85/
Needs further CTS.

Bug: dawn:568
Change-Id: I89ac56d8cf0fbb655358bf6effa016ddc1f8426f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/35143
Commit-Queue: Kai Ninomiya <kainino@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
diff --git a/dawn.json b/dawn.json
index a485484..f11d123 100644
--- a/dawn.json
+++ b/dawn.json
@@ -1530,7 +1530,8 @@
             {"name": "mipmap filter", "type": "filter mode", "default": "nearest"},
             {"name": "lod min clamp", "type": "float", "default": "0.0f"},
             {"name": "lod max clamp", "type": "float", "default": "1000.0f"},
-            {"name": "compare", "type": "compare function", "default": "undefined"}
+            {"name": "compare", "type": "compare function", "default": "undefined"},
+            {"name": "max anisotropy", "type": "uint16_t", "default": "1"}
         ]
     },
     "sampler descriptor dummy anisotropic filtering": {
@@ -1925,15 +1926,18 @@
     "void const *": {
         "category": "native"
     },
-    "uint32_t": {
-        "category": "native"
-    },
     "int32_t": {
         "category": "native"
     },
     "size_t": {
         "category": "native"
     },
+    "uint16_t": {
+        "category": "native"
+    },
+    "uint32_t": {
+        "category": "native"
+    },
     "uint64_t": {
         "category": "native"
     },
diff --git a/src/dawn_native/Sampler.cpp b/src/dawn_native/Sampler.cpp
index 11b398a..3a75cb1 100644
--- a/src/dawn_native/Sampler.cpp
+++ b/src/dawn_native/Sampler.cpp
@@ -20,6 +20,12 @@
 
 #include <cmath>
 
+namespace {
+    uint16_t GetClampedMaxAnisotropy(uint16_t value) {
+        return value >= 1u ? value : 1u;
+    }
+}  // anonymous namespace
+
 namespace dawn_native {
 
     MaybeError ValidateSamplerDescriptor(DeviceBase*, const SamplerDescriptor* descriptor) {
@@ -40,6 +46,16 @@
                 "Min lod clamp value cannot greater than max lod clamp value");
         }
 
+        if (descriptor->maxAnisotropy > 1) {
+            if (descriptor->minFilter != wgpu::FilterMode::Linear ||
+                descriptor->magFilter != wgpu::FilterMode::Linear ||
+                descriptor->mipmapFilter != wgpu::FilterMode::Linear) {
+                return DAWN_VALIDATION_ERROR(
+                    "min, mag, and mipmap filter should be linear when using anisotropic "
+                    "filtering");
+            }
+        }
+
         DAWN_TRY(ValidateFilterMode(descriptor->minFilter));
         DAWN_TRY(ValidateFilterMode(descriptor->magFilter));
         DAWN_TRY(ValidateFilterMode(descriptor->mipmapFilter));
@@ -62,7 +78,8 @@
           mMipmapFilter(descriptor->mipmapFilter),
           mLodMinClamp(descriptor->lodMinClamp),
           mLodMaxClamp(descriptor->lodMaxClamp),
-          mCompareFunction(descriptor->compare) {
+          mCompareFunction(descriptor->compare),
+          mMaxAnisotropy(GetClampedMaxAnisotropy(descriptor->maxAnisotropy)) {
     }
 
     SamplerBase::SamplerBase(DeviceBase* device, ObjectBase::ErrorTag tag)
@@ -87,7 +104,8 @@
     size_t SamplerBase::ComputeContentHash() {
         ObjectContentHasher recorder;
         recorder.Record(mAddressModeU, mAddressModeV, mAddressModeW, mMagFilter, mMinFilter,
-                        mMipmapFilter, mLodMinClamp, mLodMaxClamp, mCompareFunction);
+                        mMipmapFilter, mLodMinClamp, mLodMaxClamp, mCompareFunction,
+                        mMaxAnisotropy);
         return recorder.GetContentHash();
     }
 
@@ -105,7 +123,7 @@
                a->mAddressModeW == b->mAddressModeW && a->mMagFilter == b->mMagFilter &&
                a->mMinFilter == b->mMinFilter && a->mMipmapFilter == b->mMipmapFilter &&
                a->mLodMinClamp == b->mLodMinClamp && a->mLodMaxClamp == b->mLodMaxClamp &&
-               a->mCompareFunction == b->mCompareFunction;
+               a->mCompareFunction == b->mCompareFunction && a->mMaxAnisotropy == b->mMaxAnisotropy;
     }
 
 }  // namespace dawn_native
diff --git a/src/dawn_native/Sampler.h b/src/dawn_native/Sampler.h
index 62ef4d5..2fd938c 100644
--- a/src/dawn_native/Sampler.h
+++ b/src/dawn_native/Sampler.h
@@ -42,6 +42,10 @@
             bool operator()(const SamplerBase* a, const SamplerBase* b) const;
         };
 
+        uint16_t GetMaxAnisotropy() const {
+            return mMaxAnisotropy;
+        }
+
       private:
         SamplerBase(DeviceBase* device, ObjectBase::ErrorTag tag);
 
@@ -55,6 +59,7 @@
         float mLodMinClamp;
         float mLodMaxClamp;
         wgpu::CompareFunction mCompareFunction;
+        uint16_t mMaxAnisotropy;
     };
 
 }  // namespace dawn_native
diff --git a/src/dawn_native/d3d12/SamplerD3D12.cpp b/src/dawn_native/d3d12/SamplerD3D12.cpp
index e453fc4..7d63e0d 100644
--- a/src/dawn_native/d3d12/SamplerD3D12.cpp
+++ b/src/dawn_native/d3d12/SamplerD3D12.cpp
@@ -69,13 +69,21 @@
                 ? D3D12_FILTER_REDUCTION_TYPE_STANDARD
                 : D3D12_FILTER_REDUCTION_TYPE_COMPARISON;
 
-        mSamplerDesc.Filter =
-            D3D12_ENCODE_BASIC_FILTER(minFilter, magFilter, mipmapFilter, reduction);
+        // https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ns-d3d12-d3d12_sampler_desc
+        mSamplerDesc.MaxAnisotropy = std::min<uint16_t>(GetMaxAnisotropy(), 16u);
+
+        if (mSamplerDesc.MaxAnisotropy > 1) {
+            mSamplerDesc.Filter = D3D12_ENCODE_ANISOTROPIC_FILTER(reduction);
+        } else {
+            mSamplerDesc.Filter =
+                D3D12_ENCODE_BASIC_FILTER(minFilter, magFilter, mipmapFilter, reduction);
+        }
+
         mSamplerDesc.AddressU = AddressMode(descriptor->addressModeU);
         mSamplerDesc.AddressV = AddressMode(descriptor->addressModeV);
         mSamplerDesc.AddressW = AddressMode(descriptor->addressModeW);
         mSamplerDesc.MipLODBias = 0.f;
-        mSamplerDesc.MaxAnisotropy = 1;
+
         if (descriptor->compare != wgpu::CompareFunction::Undefined) {
             mSamplerDesc.ComparisonFunc = ToD3D12ComparisonFunc(descriptor->compare);
         } else {
diff --git a/src/dawn_native/metal/SamplerMTL.mm b/src/dawn_native/metal/SamplerMTL.mm
index 0559605..34a5b1f 100644
--- a/src/dawn_native/metal/SamplerMTL.mm
+++ b/src/dawn_native/metal/SamplerMTL.mm
@@ -75,6 +75,8 @@
 
         mtlDesc.lodMinClamp = descriptor->lodMinClamp;
         mtlDesc.lodMaxClamp = descriptor->lodMaxClamp;
+        // https://developer.apple.com/documentation/metal/mtlsamplerdescriptor/1516164-maxanisotropy
+        mtlDesc.maxAnisotropy = std::min<uint16_t>(GetMaxAnisotropy(), 16u);
 
         if (descriptor->compare != wgpu::CompareFunction::Undefined) {
             // Sampler compare is unsupported before A9, which we validate in
diff --git a/src/dawn_native/opengl/SamplerGL.cpp b/src/dawn_native/opengl/SamplerGL.cpp
index 6f9235d..32aa56f 100644
--- a/src/dawn_native/opengl/SamplerGL.cpp
+++ b/src/dawn_native/opengl/SamplerGL.cpp
@@ -82,7 +82,8 @@
     void Sampler::SetupGLSampler(GLuint sampler,
                                  const SamplerDescriptor* descriptor,
                                  bool forceNearest) {
-        const OpenGLFunctions& gl = ToBackend(GetDevice())->gl;
+        Device* device = ToBackend(GetDevice());
+        const OpenGLFunctions& gl = device->gl;
 
         if (forceNearest) {
             gl.SamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
@@ -105,6 +106,11 @@
             gl.SamplerParameteri(sampler, GL_TEXTURE_COMPARE_FUNC,
                                  ToOpenGLCompareFunction(descriptor->compare));
         }
+
+        if (gl.IsAtLeastGL(4, 6) ||
+            gl.IsGLExtensionSupported("GL_EXT_texture_filter_anisotropic")) {
+            gl.SamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY, GetMaxAnisotropy());
+        }
     }
 
     GLuint Sampler::GetFilteringHandle() const {
diff --git a/src/dawn_native/vulkan/DeviceVk.cpp b/src/dawn_native/vulkan/DeviceVk.cpp
index 2fda4ed..398cc57 100644
--- a/src/dawn_native/vulkan/DeviceVk.cpp
+++ b/src/dawn_native/vulkan/DeviceVk.cpp
@@ -316,6 +316,10 @@
             mComputeSubgroupSize = FindComputeSubgroupSize();
         }
 
+        if (mDeviceInfo.features.samplerAnisotropy == VK_TRUE) {
+            usedKnobs.features.samplerAnisotropy = VK_TRUE;
+        }
+
         if (IsExtensionEnabled(Extension::TextureCompressionBC)) {
             ASSERT(ToBackend(GetAdapter())->GetDeviceInfo().features.textureCompressionBC ==
                    VK_TRUE);
diff --git a/src/dawn_native/vulkan/SamplerVk.cpp b/src/dawn_native/vulkan/SamplerVk.cpp
index 9429830..033f7b7 100644
--- a/src/dawn_native/vulkan/SamplerVk.cpp
+++ b/src/dawn_native/vulkan/SamplerVk.cpp
@@ -71,8 +71,6 @@
         createInfo.addressModeV = VulkanSamplerAddressMode(descriptor->addressModeV);
         createInfo.addressModeW = VulkanSamplerAddressMode(descriptor->addressModeW);
         createInfo.mipLodBias = 0.0f;
-        createInfo.anisotropyEnable = VK_FALSE;
-        createInfo.maxAnisotropy = 1.0f;
         if (descriptor->compare != wgpu::CompareFunction::Undefined) {
             createInfo.compareOp = ToVulkanCompareOp(descriptor->compare);
             createInfo.compareEnable = VK_TRUE;
@@ -86,6 +84,18 @@
         createInfo.unnormalizedCoordinates = VK_FALSE;
 
         Device* device = ToBackend(GetDevice());
+        uint16_t maxAnisotropy = GetMaxAnisotropy();
+        if (device->GetDeviceInfo().features.samplerAnisotropy == VK_TRUE && maxAnisotropy > 1) {
+            createInfo.anisotropyEnable = VK_TRUE;
+            // https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkSamplerCreateInfo.html
+            createInfo.maxAnisotropy =
+                std::min(static_cast<float>(maxAnisotropy),
+                         device->GetDeviceInfo().properties.limits.maxSamplerAnisotropy);
+        } else {
+            createInfo.anisotropyEnable = VK_FALSE;
+            createInfo.maxAnisotropy = 1;
+        }
+
         return CheckVkSuccess(
             device->fn.CreateSampler(device->GetVkDevice(), &createInfo, nullptr, &*mHandle),
             "CreateSampler");
diff --git a/src/tests/BUILD.gn b/src/tests/BUILD.gn
index 342aa80..258d26e 100644
--- a/src/tests/BUILD.gn
+++ b/src/tests/BUILD.gn
@@ -310,6 +310,7 @@
     "end2end/RenderBundleTests.cpp",
     "end2end/RenderPassLoadOpTests.cpp",
     "end2end/RenderPassTests.cpp",
+    "end2end/SamplerFilterAnisotropicTests.cpp",
     "end2end/SamplerTests.cpp",
     "end2end/ScissorTests.cpp",
     "end2end/ShaderFloat16Tests.cpp",
diff --git a/src/tests/DawnTest.cpp b/src/tests/DawnTest.cpp
index e47b054..446ebd0 100644
--- a/src/tests/DawnTest.cpp
+++ b/src/tests/DawnTest.cpp
@@ -85,6 +85,21 @@
 
     DawnTestEnvironment* gTestEnv = nullptr;
 
+    template <typename T>
+    void printBuffer(testing::AssertionResult& result, const T* buffer, const size_t count) {
+        static constexpr unsigned int kBytes = sizeof(T);
+
+        for (size_t index = 0; index < count; ++index) {
+            auto byteView = reinterpret_cast<const uint8_t*>(buffer + index);
+            for (unsigned int b = 0; b < kBytes; ++b) {
+                char buf[4];
+                sprintf(buf, "%02X ", byteView[b]);
+                result << buf;
+            }
+        }
+        result << std::endl;
+    }
+
 }  // anonymous namespace
 
 const RGBA8 RGBA8::kZero = RGBA8(0, 0, 0, 0);
@@ -1153,6 +1168,14 @@
     return !(*this == other);
 }
 
+bool RGBA8::operator<=(const RGBA8& other) const {
+    return (r <= other.r && g <= other.g && b <= other.b && a <= other.a);
+}
+
+bool RGBA8::operator>=(const RGBA8& other) const {
+    return (r >= other.r && g >= other.g && b >= other.b && a >= other.a);
+}
+
 std::ostream& operator<<(std::ostream& stream, const RGBA8& color) {
     return stream << "RGBA8(" << static_cast<int>(color.r) << ", " << static_cast<int>(color.g)
                   << ", " << static_cast<int>(color.b) << ", " << static_cast<int>(color.a) << ")";
@@ -1191,26 +1214,12 @@
                                                   << mExpected[i] << ", actual " << actual[i]
                                                   << std::endl;
 
-                auto printBuffer = [&](const T* buffer) {
-                    static constexpr unsigned int kBytes = sizeof(T);
-
-                    for (size_t index = 0; index < mExpected.size(); ++index) {
-                        auto byteView = reinterpret_cast<const uint8_t*>(buffer + index);
-                        for (unsigned int b = 0; b < kBytes; ++b) {
-                            char buf[4];
-                            sprintf(buf, "%02X ", byteView[b]);
-                            result << buf;
-                        }
-                    }
-                    result << std::endl;
-                };
-
                 if (mExpected.size() <= 1024) {
                     result << "Expected:" << std::endl;
-                    printBuffer(mExpected.data());
+                    printBuffer(result, mExpected.data(), mExpected.size());
 
                     result << "Actual:" << std::endl;
-                    printBuffer(actual);
+                    printBuffer(result, actual, mExpected.size());
                 }
 
                 return result;
@@ -1226,4 +1235,59 @@
     template class ExpectEq<uint64_t>;
     template class ExpectEq<RGBA8>;
     template class ExpectEq<float>;
+
+    template <typename T>
+    ExpectBetweenColors<T>::ExpectBetweenColors(T value0, T value1) {
+        T l, h;
+        l.r = std::min(value0.r, value1.r);
+        l.g = std::min(value0.g, value1.g);
+        l.b = std::min(value0.b, value1.b);
+        l.a = std::min(value0.a, value1.a);
+
+        h.r = std::max(value0.r, value1.r);
+        h.g = std::max(value0.g, value1.g);
+        h.b = std::max(value0.b, value1.b);
+        h.a = std::max(value0.a, value1.a);
+
+        mLowerColorChannels.push_back(l);
+        mHigherColorChannels.push_back(h);
+
+        mValues0.push_back(value0);
+        mValues1.push_back(value1);
+    }
+
+    template <typename T>
+    testing::AssertionResult ExpectBetweenColors<T>::Check(const void* data, size_t size) {
+        DAWN_ASSERT(size == sizeof(T) * mLowerColorChannels.size());
+        DAWN_ASSERT(mHigherColorChannels.size() == mLowerColorChannels.size());
+        DAWN_ASSERT(mValues0.size() == mValues1.size());
+        DAWN_ASSERT(mValues0.size() == mLowerColorChannels.size());
+
+        const T* actual = static_cast<const T*>(data);
+
+        for (size_t i = 0; i < mLowerColorChannels.size(); ++i) {
+            if (!(actual[i] >= mLowerColorChannels[i] && actual[i] <= mHigherColorChannels[i])) {
+                testing::AssertionResult result = testing::AssertionFailure()
+                                                  << "Expected data[" << i << "] to be between "
+                                                  << mValues0[i] << " and " << mValues1[i]
+                                                  << ", actual " << actual[i] << std::endl;
+
+                if (mLowerColorChannels.size() <= 1024) {
+                    result << "Expected between:" << std::endl;
+                    printBuffer(result, mValues0.data(), mLowerColorChannels.size());
+                    result << "and" << std::endl;
+                    printBuffer(result, mValues1.data(), mLowerColorChannels.size());
+
+                    result << "Actual:" << std::endl;
+                    printBuffer(result, actual, mLowerColorChannels.size());
+                }
+
+                return result;
+            }
+        }
+
+        return testing::AssertionSuccess();
+    }
+
+    template class ExpectBetweenColors<RGBA8>;
 }  // namespace detail
diff --git a/src/tests/DawnTest.h b/src/tests/DawnTest.h
index e175132..aefafeb 100644
--- a/src/tests/DawnTest.h
+++ b/src/tests/DawnTest.h
@@ -73,6 +73,9 @@
 #define EXPECT_TEXTURE_FLOAT_EQ(expected, texture, x, y, width, height, level, slice) \
     AddTextureExpectation(__FILE__, __LINE__, expected, texture, x, y, width, height, level, slice)
 
+#define EXPECT_PIXEL_RGBA8_BETWEEN(color0, color1, texture, x, y) \
+    AddTextureBetweenColorsExpectation(__FILE__, __LINE__, color0, color1, texture, x, y)
+
 // TODO(enga): Migrate other texure expectation helpers to this common one.
 #define EXPECT_TEXTURE_EQ(...) AddTextureExpectation(__FILE__, __LINE__, __VA_ARGS__)
 
@@ -95,6 +98,8 @@
     }
     bool operator==(const RGBA8& other) const;
     bool operator!=(const RGBA8& other) const;
+    bool operator<=(const RGBA8& other) const;
+    bool operator>=(const RGBA8& other) const;
 
     uint8_t r, g, b, a;
 
@@ -170,6 +175,8 @@
 
     template <typename T>
     class ExpectEq;
+    template <typename T>
+    class ExpectBetweenColors;
 }  // namespace detail
 
 namespace dawn_wire {
@@ -331,6 +338,24 @@
                                          x, y, 1, 1, level, slice, aspect, sizeof(T), bytesPerRow);
     }
 
+    template <typename T>
+    std::ostringstream& AddTextureBetweenColorsExpectation(
+        const char* file,
+        int line,
+        const T& color0,
+        const T& color1,
+        const wgpu::Texture& texture,
+        uint32_t x,
+        uint32_t y,
+        uint32_t level = 0,
+        uint32_t slice = 0,
+        wgpu::TextureAspect aspect = wgpu::TextureAspect::All,
+        uint32_t bytesPerRow = 0) {
+        return AddTextureExpectationImpl(
+            file, line, new detail::ExpectBetweenColors<T>(color0, color1), texture, x, y, 1, 1,
+            level, slice, aspect, sizeof(T), bytesPerRow);
+    }
+
     void WaitABit();
     void FlushWire();
     void WaitForAllOperations();
@@ -518,6 +543,26 @@
     extern template class ExpectEq<uint64_t>;
     extern template class ExpectEq<RGBA8>;
     extern template class ExpectEq<float>;
+
+    template <typename T>
+    class ExpectBetweenColors : public Expectation {
+      public:
+        // Inclusive for now
+        ExpectBetweenColors(T value0, T value1);
+        testing::AssertionResult Check(const void* data, size_t size) override;
+
+      private:
+        std::vector<T> mLowerColorChannels;
+        std::vector<T> mHigherColorChannels;
+
+        // used for printing error
+        std::vector<T> mValues0;
+        std::vector<T> mValues1;
+    };
+    // A color is considered between color0 and color1 when all channel values are within range of
+    // each counterparts. It doesn't matter which value is higher or lower. Essentially color =
+    // lerp(color0, color1, t) where t is [0,1]. But I don't want to be too strict here.
+    extern template class ExpectBetweenColors<RGBA8>;
 }  // namespace detail
 
 #endif  // TESTS_DAWNTEST_H_
diff --git a/src/tests/end2end/SamplerFilterAnisotropicTests.cpp b/src/tests/end2end/SamplerFilterAnisotropicTests.cpp
new file mode 100644
index 0000000..cd96b97
--- /dev/null
+++ b/src/tests/end2end/SamplerFilterAnisotropicTests.cpp
@@ -0,0 +1,286 @@
+// Copyright 2020 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cmath>
+
+#include "tests/DawnTest.h"
+
+#include "common/Assert.h"
+#include "common/Constants.h"
+#include "utils/ComboRenderPipelineDescriptor.h"
+#include "utils/WGPUHelpers.h"
+
+constexpr static unsigned int kRTSize = 16;
+
+namespace {
+    // MipLevel colors, ordering from base level to high level
+    // each mipmap of the texture is having a different color
+    // so we can check if the sampler anisotropic filtering is fetching
+    // from the correct miplevel
+    const std::array<RGBA8, 3> colors = {RGBA8::kRed, RGBA8::kGreen, RGBA8::kBlue};
+}  // namespace
+
+class SamplerFilterAnisotropicTest : public DawnTest {
+  protected:
+    void SetUp() override {
+        DawnTest::SetUp();
+        mRenderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
+
+        wgpu::ShaderModule vsModule = utils::CreateShaderModuleFromWGSL(device, R"(
+            [[block]] struct Uniforms {
+                [[offset(0)]] matrix : mat4x4<f32>;
+            };
+
+            [[location(0)]] var<in> position : vec4<f32>;
+            [[location(1)]] var<in> uv : vec2<f32>;
+
+            [[set(0), binding(2)]] var<uniform> uniforms : Uniforms;
+
+            [[builtin(position)]] var<out> Position : vec4<f32>;
+            [[location(0)]] var<out> fragUV : vec2<f32>;
+
+            [[stage(vertex)]] fn main() -> void {
+                fragUV = uv;
+                Position = uniforms.matrix * position;
+            }
+        )");
+        wgpu::ShaderModule fsModule = utils::CreateShaderModuleFromWGSL(device, R"(
+            [[set(0), binding(0)]] var<uniform_constant> sampler0 : sampler;
+            [[set(0), binding(1)]] var<uniform_constant> texture0 : texture_2d<f32>;
+
+            [[builtin(frag_coord)]] var<in> FragCoord : vec4<f32>;
+
+            [[location(0)]] var<in> fragUV: vec2<f32>;
+
+            [[location(0)]] var<out> fragColor : vec4<f32>;
+
+            [[stage(fragment)]] fn main() -> void {
+                fragColor = textureSample(texture0, sampler0, fragUV);
+            })");
+
+        utils::ComboVertexStateDescriptor vertexState;
+        vertexState.cVertexBuffers[0].attributeCount = 2;
+        vertexState.cAttributes[0].format = wgpu::VertexFormat::Float4;
+        vertexState.cAttributes[1].shaderLocation = 1;
+        vertexState.cAttributes[1].offset = 4 * sizeof(float);
+        vertexState.cAttributes[1].format = wgpu::VertexFormat::Float2;
+        vertexState.vertexBufferCount = 1;
+        vertexState.cVertexBuffers[0].arrayStride = 6 * sizeof(float);
+
+        utils::ComboRenderPipelineDescriptor pipelineDescriptor(device);
+        pipelineDescriptor.vertexStage.module = vsModule;
+        pipelineDescriptor.cFragmentStage.module = fsModule;
+        pipelineDescriptor.vertexState = &vertexState;
+        pipelineDescriptor.cColorStates[0].format = mRenderPass.colorFormat;
+
+        mPipeline = device.CreateRenderPipeline(&pipelineDescriptor);
+        mBindGroupLayout = mPipeline.GetBindGroupLayout(0);
+
+        InitTexture();
+    }
+
+    void InitTexture() {
+        const uint32_t mipLevelCount = colors.size();
+
+        const uint32_t textureWidthLevel0 = 1 << mipLevelCount;
+        const uint32_t textureHeightLevel0 = 1 << mipLevelCount;
+
+        wgpu::TextureDescriptor descriptor;
+        descriptor.dimension = wgpu::TextureDimension::e2D;
+        descriptor.size.width = textureWidthLevel0;
+        descriptor.size.height = textureHeightLevel0;
+        descriptor.size.depth = 1;
+        descriptor.sampleCount = 1;
+        descriptor.format = wgpu::TextureFormat::RGBA8Unorm;
+        descriptor.mipLevelCount = mipLevelCount;
+        descriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::Sampled;
+        wgpu::Texture texture = device.CreateTexture(&descriptor);
+
+        const uint32_t rowPixels = kTextureBytesPerRowAlignment / sizeof(RGBA8);
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+
+        // Populate each mip level with a different color
+        for (uint32_t level = 0; level < mipLevelCount; ++level) {
+            const uint32_t texWidth = textureWidthLevel0 >> level;
+            const uint32_t texHeight = textureHeightLevel0 >> level;
+
+            const RGBA8 color = colors[level];
+
+            std::vector<RGBA8> data(rowPixels * texHeight, color);
+            wgpu::Buffer stagingBuffer = utils::CreateBufferFromData(
+                device, data.data(), data.size() * sizeof(RGBA8), wgpu::BufferUsage::CopySrc);
+            wgpu::BufferCopyView bufferCopyView =
+                utils::CreateBufferCopyView(stagingBuffer, 0, kTextureBytesPerRowAlignment);
+            wgpu::TextureCopyView textureCopyView =
+                utils::CreateTextureCopyView(texture, level, {0, 0, 0});
+            wgpu::Extent3D copySize = {texWidth, texHeight, 1};
+            encoder.CopyBufferToTexture(&bufferCopyView, &textureCopyView, &copySize);
+        }
+        wgpu::CommandBuffer copy = encoder.Finish();
+        queue.Submit(1, &copy);
+
+        mTextureView = texture.CreateView();
+    }
+
+    // void TestFilterAnisotropic(const FilterAnisotropicTestCase& testCase) {
+    void TestFilterAnisotropic(const uint16_t maxAnisotropy) {
+        wgpu::Sampler sampler;
+        {
+            wgpu::SamplerDescriptor descriptor = {};
+            descriptor.minFilter = wgpu::FilterMode::Linear;
+            descriptor.magFilter = wgpu::FilterMode::Linear;
+            descriptor.mipmapFilter = wgpu::FilterMode::Linear;
+            descriptor.maxAnisotropy = maxAnisotropy;
+            sampler = device.CreateSampler(&descriptor);
+        }
+
+        // The transform matrix gives us a slanted plane
+        // Tweaking happens at: https://jsfiddle.net/t8k7c95o/5/
+        // You can get an idea of what the test looks like at the url rendered by webgl
+        std::array<float, 16> transform = {-1.7320507764816284,
+                                           1.8322050568049563e-16,
+                                           -6.176817699518044e-17,
+                                           -6.170640314703498e-17,
+                                           -2.1211504944260596e-16,
+                                           -1.496108889579773,
+                                           0.5043753981590271,
+                                           0.5038710236549377,
+                                           0,
+                                           -43.63650894165039,
+                                           -43.232173919677734,
+                                           -43.18894577026367,
+                                           0,
+                                           21.693578720092773,
+                                           21.789791107177734,
+                                           21.86800193786621};
+        wgpu::Buffer transformBuffer = utils::CreateBufferFromData(
+            device, transform.data(), sizeof(transform), wgpu::BufferUsage::Uniform);
+
+        wgpu::BindGroup bindGroup = utils::MakeBindGroup(
+            device, mBindGroupLayout,
+            {{0, sampler}, {1, mTextureView}, {2, transformBuffer, 0, sizeof(transform)}});
+
+        // The plane is scaled on z axis in the transform matrix
+        // so uv here is also scaled
+        // vertex attribute layout:
+        // position : vec4, uv : vec2
+        const float vertexData[] = {
+            -0.5, 0.5, -0.5, 1, 0, 0,  0.5, 0.5, -0.5, 1, 1, 0, -0.5, 0.5, 0.5, 1, 0, 50,
+            -0.5, 0.5, 0.5,  1, 0, 50, 0.5, 0.5, -0.5, 1, 1, 0, 0.5,  0.5, 0.5, 1, 1, 50,
+        };
+        wgpu::Buffer vertexBuffer = utils::CreateBufferFromData(
+            device, vertexData, sizeof(vertexData), wgpu::BufferUsage::Vertex);
+
+        wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+        {
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&mRenderPass.renderPassInfo);
+            pass.SetPipeline(mPipeline);
+            pass.SetBindGroup(0, bindGroup);
+            pass.SetVertexBuffer(0, vertexBuffer);
+            pass.Draw(6);
+            pass.EndPass();
+        }
+
+        wgpu::CommandBuffer commands = encoder.Finish();
+        queue.Submit(1, &commands);
+
+        // https://jsfiddle.net/t8k7c95o/5/
+        // (x, y) -> (8, [0,15)) full readpixels result on Mac metal backend Intel GPU
+        // maxAnisotropy: 1
+        //  0 - 00 00 00
+        //  1 - 00 00 ff
+        //  2 - 00 00 ff
+        //  3 - 00 00 ff
+        //  4 - 00 00 ff
+        //  5 - 00 00 ff
+        //  6 - 00 ef 10
+        //  7 - 00 ef 10
+        //  8 - a7 58 00
+        //  9 - a7 58 00
+        // 10 - ff 00 00
+        // 11 - ff 00 00
+        // 12 - ff 00 00
+        // 13 - ff 00 00
+        // 14 - ff 00 00
+        // 15 - ff 00 00
+
+        // maxAnisotropy: 2
+        //  0 - 00 00 00
+        //  1 - 00 00 ff
+        //  2 - 00 00 ff
+        //  3 - 00 00 ff
+        //  4 - 00 f7 08
+        //  5 - 00 f7 08
+        //  6 - ed 12 00
+        //  7 - ed 12 10
+        //  8 - ff 00 00
+        //  9 - ff 00 00
+        // 10 - ff 00 00
+        // 11 - ff 00 00
+        // 12 - ff 00 00
+        // 13 - ff 00 00
+        // 14 - ff 00 00
+        // 15 - ff 00 00
+
+        // maxAnisotropy: 16
+        //  0 - 00 00 00
+        //  1 - 00 00 ff
+        //  2 - 00 ad 52
+        //  3 - 00 ad 52
+        //  4 - 81 7e 00
+        //  5 - 81 7e 00
+        //  6 - ff 00 00
+        //  7 - ff 00 00
+        //  8 - ff 00 00
+        //  9 - ff 00 00
+        // 10 - ff 00 00
+        // 11 - ff 00 00
+        // 12 - ff 00 00
+        // 13 - ff 00 00
+        // 14 - ff 00 00
+        // 15 - ff 00 00
+
+        if (maxAnisotropy >= 16) {
+            EXPECT_PIXEL_RGBA8_BETWEEN(colors[0], colors[1], mRenderPass.color, 8, 4);
+            EXPECT_PIXEL_RGBA8_EQ(colors[0], mRenderPass.color, 8, 7);
+        } else if (maxAnisotropy == 2) {
+            EXPECT_PIXEL_RGBA8_BETWEEN(colors[1], colors[2], mRenderPass.color, 8, 4);
+            EXPECT_PIXEL_RGBA8_BETWEEN(colors[0], colors[1], mRenderPass.color, 8, 7);
+        } else if (maxAnisotropy <= 1) {
+            EXPECT_PIXEL_RGBA8_EQ(colors[2], mRenderPass.color, 8, 4);
+            EXPECT_PIXEL_RGBA8_BETWEEN(colors[1], colors[2], mRenderPass.color, 8, 7);
+        }
+    }
+
+    utils::BasicRenderPass mRenderPass;
+    wgpu::BindGroupLayout mBindGroupLayout;
+    wgpu::RenderPipeline mPipeline;
+    wgpu::TextureView mTextureView;
+};
+
+TEST_P(SamplerFilterAnisotropicTest, SlantedPlaneMipmap) {
+    DAWN_SKIP_TEST_IF(IsOpenGL());
+    const uint16_t maxAnisotropyLists[] = {1, 2, 16, 128};
+    for (uint16_t t : maxAnisotropyLists) {
+        TestFilterAnisotropic(t);
+    }
+}
+
+DAWN_INSTANTIATE_TEST(SamplerFilterAnisotropicTest,
+                      D3D12Backend(),
+                      MetalBackend(),
+                      OpenGLBackend(),
+                      OpenGLESBackend(),
+                      VulkanBackend());
diff --git a/src/tests/unittests/validation/SamplerValidationTests.cpp b/src/tests/unittests/validation/SamplerValidationTests.cpp
index 96f9187..60e2564 100644
--- a/src/tests/unittests/validation/SamplerValidationTests.cpp
+++ b/src/tests/unittests/validation/SamplerValidationTests.cpp
@@ -51,4 +51,75 @@
         }
     }
 
+    TEST_F(SamplerValidationTest, InvalidFilterAnisotropic) {
+        wgpu::SamplerDescriptor kValidAnisoSamplerDesc = {};
+        kValidAnisoSamplerDesc.maxAnisotropy = 2;
+        kValidAnisoSamplerDesc.minFilter = wgpu::FilterMode::Linear;
+        kValidAnisoSamplerDesc.magFilter = wgpu::FilterMode::Linear;
+        kValidAnisoSamplerDesc.mipmapFilter = wgpu::FilterMode::Linear;
+        {
+            // when maxAnisotropy > 1, min, mag, mipmap filter should be linear
+            device.CreateSampler(&kValidAnisoSamplerDesc);
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.minFilter = wgpu::FilterMode::Nearest;
+            samplerDesc.magFilter = wgpu::FilterMode::Nearest;
+            samplerDesc.mipmapFilter = wgpu::FilterMode::Nearest;
+            ASSERT_DEVICE_ERROR(device.CreateSampler(&samplerDesc));
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.minFilter = wgpu::FilterMode::Nearest;
+            ASSERT_DEVICE_ERROR(device.CreateSampler(&samplerDesc));
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.magFilter = wgpu::FilterMode::Nearest;
+            ASSERT_DEVICE_ERROR(device.CreateSampler(&samplerDesc));
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.mipmapFilter = wgpu::FilterMode::Nearest;
+            ASSERT_DEVICE_ERROR(device.CreateSampler(&samplerDesc));
+        }
+    }
+
+    TEST_F(SamplerValidationTest, ValidFilterAnisotropic) {
+        wgpu::SamplerDescriptor kValidAnisoSamplerDesc = {};
+        kValidAnisoSamplerDesc.maxAnisotropy = 2;
+        kValidAnisoSamplerDesc.minFilter = wgpu::FilterMode::Linear;
+        kValidAnisoSamplerDesc.magFilter = wgpu::FilterMode::Linear;
+        kValidAnisoSamplerDesc.mipmapFilter = wgpu::FilterMode::Linear;
+        {
+            wgpu::SamplerDescriptor samplerDesc = {};
+            device.CreateSampler(&samplerDesc);
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.maxAnisotropy = 16;
+            device.CreateSampler(&samplerDesc);
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.maxAnisotropy = 32;
+            device.CreateSampler(&samplerDesc);
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.maxAnisotropy = 0x7FFF;
+            device.CreateSampler(&samplerDesc);
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.maxAnisotropy = 0x8000;
+            device.CreateSampler(&samplerDesc);
+        }
+        {
+            wgpu::SamplerDescriptor samplerDesc = kValidAnisoSamplerDesc;
+            samplerDesc.maxAnisotropy = 0xFFFF;
+            device.CreateSampler(&samplerDesc);
+        }
+    }
+
 }  // anonymous namespace