Wrap multiplanar iosurface in wgpuTexture

This CL supports wrapping multiplanar iosurface in wgpuTexture.
It also provides mechanism to create TextureView on each planes.

Bug:1307194

Change-Id: I5e82f47944fdea542abba097240c880628b1181f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/81482
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Shaobo Yan <shaobo.yan@intel.com>
diff --git a/include/dawn/native/MetalBackend.h b/include/dawn/native/MetalBackend.h
index e8699bd..6db34a1 100644
--- a/include/dawn/native/MetalBackend.h
+++ b/include/dawn/native/MetalBackend.h
@@ -43,6 +43,8 @@
         ExternalImageDescriptorIOSurface();
 
         IOSurfaceRef ioSurface;
+
+        // This has been deprecated.
         uint32_t plane;
     };
 
diff --git a/src/dawn/native/metal/BackendMTL.mm b/src/dawn/native/metal/BackendMTL.mm
index 920bb1d..1f75adb 100644
--- a/src/dawn/native/metal/BackendMTL.mm
+++ b/src/dawn/native/metal/BackendMTL.mm
@@ -358,6 +358,12 @@
                 mSupportedFeatures.EnableFeature(Feature::Depth32FloatStencil8);
             }
 
+            // Uses newTextureWithDescriptor::iosurface::plane which is available
+            // on ios 11.0+ and macOS 11.0+
+            if (@available(macOS 10.11, iOS 11.0, *)) {
+                mSupportedFeatures.EnableFeature(Feature::MultiPlanarFormats);
+            }
+
 #if defined(DAWN_PLATFORM_MACOS)
             // MTLPixelFormatDepth24Unorm_Stencil8 is only available on macOS 10.11+
             if ([*mDevice isDepth24Stencil8PixelFormatSupported]) {
diff --git a/src/dawn/native/metal/DeviceMTL.h b/src/dawn/native/metal/DeviceMTL.h
index d72cc3f..a6b6592 100644
--- a/src/dawn/native/metal/DeviceMTL.h
+++ b/src/dawn/native/metal/DeviceMTL.h
@@ -54,8 +54,7 @@
         MaybeError SubmitPendingCommandBuffer();
 
         Ref<Texture> CreateTextureWrappingIOSurface(const ExternalImageDescriptor* descriptor,
-                                                    IOSurfaceRef ioSurface,
-                                                    uint32_t plane);
+                                                    IOSurfaceRef ioSurface);
         void WaitForCommandsToBeScheduled();
 
         ResultOrError<std::unique_ptr<StagingBufferBase>> CreateStagingBuffer(size_t size) override;
diff --git a/src/dawn/native/metal/DeviceMTL.mm b/src/dawn/native/metal/DeviceMTL.mm
index e654c8b..e2e784e 100644
--- a/src/dawn/native/metal/DeviceMTL.mm
+++ b/src/dawn/native/metal/DeviceMTL.mm
@@ -432,21 +432,18 @@
     }
 
     Ref<Texture> Device::CreateTextureWrappingIOSurface(const ExternalImageDescriptor* descriptor,
-                                                        IOSurfaceRef ioSurface,
-                                                        uint32_t plane) {
+                                                        IOSurfaceRef ioSurface) {
         const TextureDescriptor* textureDescriptor = FromAPI(descriptor->cTextureDescriptor);
 
         if (ConsumedError(ValidateTextureDescriptor(this, textureDescriptor))) {
             return nullptr;
         }
-        if (ConsumedError(
-                ValidateIOSurfaceCanBeWrapped(this, textureDescriptor, ioSurface, plane))) {
+        if (ConsumedError(ValidateIOSurfaceCanBeWrapped(this, textureDescriptor, ioSurface))) {
             return nullptr;
         }
 
         Ref<Texture> result;
-        if (ConsumedError(Texture::CreateFromIOSurface(this, descriptor, ioSurface, plane),
-                          &result)) {
+        if (ConsumedError(Texture::CreateFromIOSurface(this, descriptor, ioSurface), &result)) {
             return nullptr;
         }
         return result;
diff --git a/src/dawn/native/metal/MetalBackend.mm b/src/dawn/native/metal/MetalBackend.mm
index c01cd40..c0214e5 100644
--- a/src/dawn/native/metal/MetalBackend.mm
+++ b/src/dawn/native/metal/MetalBackend.mm
@@ -37,8 +37,8 @@
     WGPUTexture WrapIOSurface(WGPUDevice device,
                               const ExternalImageDescriptorIOSurface* cDescriptor) {
         Device* backendDevice = ToBackend(FromAPI(device));
-        Ref<TextureBase> texture = backendDevice->CreateTextureWrappingIOSurface(
-            cDescriptor, cDescriptor->ioSurface, cDescriptor->plane);
+        Ref<TextureBase> texture =
+            backendDevice->CreateTextureWrappingIOSurface(cDescriptor, cDescriptor->ioSurface);
         return ToAPI(texture.Detach());
     }
 
diff --git a/src/dawn/native/metal/TextureMTL.h b/src/dawn/native/metal/TextureMTL.h
index 0ac8103..ba7f97b 100644
--- a/src/dawn/native/metal/TextureMTL.h
+++ b/src/dawn/native/metal/TextureMTL.h
@@ -17,6 +17,7 @@
 
 #include "dawn/native/Texture.h"
 
+#include "dawn/common/CoreFoundationRef.h"
 #include "dawn/common/NSRef.h"
 #include "dawn/native/DawnNative.h"
 
@@ -31,8 +32,7 @@
     MTLPixelFormat MetalPixelFormat(wgpu::TextureFormat format);
     MaybeError ValidateIOSurfaceCanBeWrapped(const DeviceBase* device,
                                              const TextureDescriptor* descriptor,
-                                             IOSurfaceRef ioSurface,
-                                             uint32_t plane);
+                                             IOSurfaceRef ioSurface);
 
     class Texture final : public TextureBase {
       public:
@@ -41,13 +41,13 @@
         static ResultOrError<Ref<Texture>> CreateFromIOSurface(
             Device* device,
             const ExternalImageDescriptor* descriptor,
-            IOSurfaceRef ioSurface,
-            uint32_t plane);
+            IOSurfaceRef ioSurface);
         static Ref<Texture> CreateWrapping(Device* device,
                                            const TextureDescriptor* descriptor,
                                            NSPRef<id<MTLTexture>> wrapped);
 
         id<MTLTexture> GetMTLTexture();
+        IOSurfaceRef GetIOSurface();
         NSPRef<id<MTLTexture>> CreateFormatView(wgpu::TextureFormat format);
 
         void EnsureSubresourceContentInitialized(CommandRecordingContext* commandContext,
@@ -62,8 +62,7 @@
         MaybeError InitializeAsInternalTexture(const TextureDescriptor* descriptor);
         MaybeError InitializeFromIOSurface(const ExternalImageDescriptor* descriptor,
                                            const TextureDescriptor* textureDescriptor,
-                                           IOSurfaceRef ioSurface,
-                                           uint32_t plane);
+                                           IOSurfaceRef ioSurface);
         void InitializeAsWrapping(const TextureDescriptor* descriptor,
                                   NSPRef<id<MTLTexture>> wrapped);
 
@@ -74,7 +73,9 @@
                                 TextureBase::ClearValue clearValue);
 
         NSPRef<id<MTLTexture>> mMtlTexture;
+
         MTLTextureUsage mMtlUsage;
+        CFRef<IOSurfaceRef> mIOSurface = nullptr;
     };
 
     class TextureView final : public TextureViewBase {
diff --git a/src/dawn/native/metal/TextureMTL.mm b/src/dawn/native/metal/TextureMTL.mm
index c47039f..da5755a 100644
--- a/src/dawn/native/metal/TextureMTL.mm
+++ b/src/dawn/native/metal/TextureMTL.mm
@@ -181,12 +181,25 @@
                     return wgpu::TextureFormat::RG8Unorm;
                 case kCVPixelFormatType_OneComponent8:
                     return wgpu::TextureFormat::R8Unorm;
+                case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
+                    return wgpu::TextureFormat::R8BG8Biplanar420Unorm;
                 default:
                     return DAWN_FORMAT_VALIDATION_ERROR("Unsupported IOSurface format (%x).",
                                                         format);
             }
         }
 
+        uint32_t GetIOSurfacePlane(wgpu::TextureAspect aspect) {
+            switch (aspect) {
+                case wgpu::TextureAspect::Plane0Only:
+                    return 0;
+                case wgpu::TextureAspect::Plane1Only:
+                    return 1;
+                default:
+                    UNREACHABLE();
+            }
+        }
+
 #if defined(DAWN_PLATFORM_MACOS)
         MTLStorageMode kIOSurfaceStorageMode = MTLStorageModeManaged;
 #elif defined(DAWN_PLATFORM_IOS)
@@ -392,15 +405,7 @@
 
     MaybeError ValidateIOSurfaceCanBeWrapped(const DeviceBase*,
                                              const TextureDescriptor* descriptor,
-                                             IOSurfaceRef ioSurface,
-                                             uint32_t plane) {
-        // IOSurfaceGetPlaneCount can return 0 for non-planar IOSurfaces but we will treat
-        // non-planar like it is a single plane.
-        size_t surfacePlaneCount = std::max(size_t(1), IOSurfaceGetPlaneCount(ioSurface));
-        DAWN_INVALID_IF(plane >= surfacePlaneCount,
-                        "IOSurface plane (%u) exceeds the surface's plane count (%u).", plane,
-                        surfacePlaneCount);
-
+                                             IOSurfaceRef ioSurface) {
         DAWN_INVALID_IF(descriptor->dimension != wgpu::TextureDimension::e2D,
                         "Texture dimension (%s) is not %s.", descriptor->dimension,
                         wgpu::TextureDimension::e2D);
@@ -414,8 +419,8 @@
         DAWN_INVALID_IF(descriptor->sampleCount != 1, "Sample count (%u) is not 1.",
                         descriptor->sampleCount);
 
-        uint32_t surfaceWidth = IOSurfaceGetWidthOfPlane(ioSurface, plane);
-        uint32_t surfaceHeight = IOSurfaceGetHeightOfPlane(ioSurface, plane);
+        uint32_t surfaceWidth = IOSurfaceGetWidth(ioSurface);
+        uint32_t surfaceHeight = IOSurfaceGetHeight(ioSurface);
 
         DAWN_INVALID_IF(
             descriptor->size.width != surfaceWidth || descriptor->size.height != surfaceHeight ||
@@ -497,13 +502,12 @@
     ResultOrError<Ref<Texture>> Texture::CreateFromIOSurface(
         Device* device,
         const ExternalImageDescriptor* descriptor,
-        IOSurfaceRef ioSurface,
-        uint32_t plane) {
+        IOSurfaceRef ioSurface) {
         const TextureDescriptor* textureDescriptor = FromAPI(descriptor->cTextureDescriptor);
 
         Ref<Texture> texture =
-            AcquireRef(new Texture(device, textureDescriptor, TextureState::OwnedInternal));
-        DAWN_TRY(texture->InitializeFromIOSurface(descriptor, textureDescriptor, ioSurface, plane));
+            AcquireRef(new Texture(device, textureDescriptor, TextureState::OwnedExternal));
+        DAWN_TRY(texture->InitializeFromIOSurface(descriptor, textureDescriptor, ioSurface));
         return texture;
     }
 
@@ -546,20 +550,28 @@
 
     MaybeError Texture::InitializeFromIOSurface(const ExternalImageDescriptor* descriptor,
                                                 const TextureDescriptor* textureDescriptor,
-                                                IOSurfaceRef ioSurface,
-                                                uint32_t plane) {
-        Device* device = ToBackend(GetDevice());
+                                                IOSurfaceRef ioSurface) {
+        mIOSurface = ioSurface;
 
-        NSRef<MTLTextureDescriptor> mtlDesc = CreateMetalTextureDescriptor();
-        [*mtlDesc setStorageMode:kIOSurfaceStorageMode];
+        // Uses WGPUTexture which wraps multiplanar ioSurface needs to create
+        // texture view explicitly. Wrap the ioSurface and delay to extract
+        // MTLTexture from the plane of it when creating texture view.
+        // WGPUTexture which wraps non-multplanar ioSurface needs to support
+        // ops that doesn't require creating texture view(e.g. copy). Extract
+        // MTLTexture from such ioSurface to support this.
+        if (!GetFormat().IsMultiPlanar()) {
+            Device* device = ToBackend(GetDevice());
 
-        mMtlUsage = [*mtlDesc usage];
-        mMtlTexture = AcquireNSPRef([device->GetMTLDevice() newTextureWithDescriptor:mtlDesc.Get()
-                                                                           iosurface:ioSurface
-                                                                               plane:plane]);
+            NSRef<MTLTextureDescriptor> mtlDesc = CreateMetalTextureDescriptor();
+            [*mtlDesc setStorageMode:kIOSurfaceStorageMode];
 
+            mMtlUsage = [*mtlDesc usage];
+            mMtlTexture =
+                AcquireNSPRef([device->GetMTLDevice() newTextureWithDescriptor:mtlDesc.Get()
+                                                                     iosurface:ioSurface
+                                                                         plane:0]);
+        }
         SetIsSubresourceContentInitialized(descriptor->isInitialized, GetAllSubresources());
-
         return {};
     }
 
@@ -569,12 +581,17 @@
     void Texture::DestroyImpl() {
         TextureBase::DestroyImpl();
         mMtlTexture = nullptr;
+        mIOSurface = nullptr;
     }
 
     id<MTLTexture> Texture::GetMTLTexture() {
         return mMtlTexture.Get();
     }
 
+    IOSurfaceRef Texture::GetIOSurface() {
+        return mIOSurface.Get();
+    }
+
     NSPRef<id<MTLTexture>> Texture::CreateFormatView(wgpu::TextureFormat format) {
         if (GetFormat().format == format) {
             return mMtlTexture;
@@ -821,6 +838,37 @@
             mMtlTextureView = nullptr;
         } else if (!RequiresCreatingNewTextureView(texture, descriptor)) {
             mMtlTextureView = mtlTexture;
+        } else if (texture->GetFormat().IsMultiPlanar()) {
+            NSRef<MTLTextureDescriptor> mtlDescRef = AcquireNSRef([MTLTextureDescriptor new]);
+            MTLTextureDescriptor* mtlDesc = mtlDescRef.Get();
+
+            mtlDesc.sampleCount = texture->GetSampleCount();
+            mtlDesc.usage = MetalTextureUsage(texture->GetFormat(), texture->GetInternalUsage(),
+                                              texture->GetSampleCount());
+            mtlDesc.pixelFormat = MetalPixelFormat(descriptor->format);
+            mtlDesc.mipmapLevelCount = texture->GetNumMipLevels();
+            mtlDesc.storageMode = kIOSurfaceStorageMode;
+
+            uint32_t plane = GetIOSurfacePlane(descriptor->aspect);
+            mtlDesc.width = IOSurfaceGetWidthOfPlane(texture->GetIOSurface(), plane);
+            mtlDesc.height = IOSurfaceGetHeightOfPlane(texture->GetIOSurface(), plane);
+
+            // Multiplanar texture is validated to only have single layer, single mipLevel
+            // and 2d textures (depth == 1)
+            ASSERT(texture->GetArrayLayers() == 1 &&
+                   texture->GetDimension() == wgpu::TextureDimension::e2D &&
+                   texture->GetNumMipLevels() == 1);
+            mtlDesc.arrayLength = 1;
+            mtlDesc.depth = 1;
+
+            mMtlTextureView = AcquireNSPRef([ToBackend(GetDevice())->GetMTLDevice()
+                newTextureWithDescriptor:mtlDesc
+                               iosurface:texture->GetIOSurface()
+                                   plane:plane]);
+            if (mMtlTextureView == nil) {
+                return DAWN_INTERNAL_ERROR(
+                    "Failed to create MTLTexture view for external texture.");
+            }
         } else {
             MTLPixelFormat format = MetalPixelFormat(descriptor->format);
             if (descriptor->aspect == wgpu::TextureAspect::StencilOnly) {
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index ae104a4..990b5b9 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -451,7 +451,10 @@
   }
 
   if (dawn_enable_metal) {
-    sources += [ "end2end/IOSurfaceWrappingTests.cpp" ]
+    sources += [
+      "end2end/IOSurfaceWrappingTests.cpp",
+      "end2end/VideoViewsTests_mac.cpp",
+    ]
     frameworks = [ "IOSurface.framework" ]
   }
 
@@ -468,7 +471,8 @@
     deps += [ "${dawn_root}/src/dawn/utils:glfw" ]
   }
 
-  if (dawn_enable_d3d12 || (dawn_enable_vulkan && is_chromeos)) {
+  if (dawn_enable_d3d12 || (dawn_enable_vulkan && is_chromeos) ||
+      dawn_enable_metal) {
     sources += [
       "end2end/VideoViewsTests.cpp",
       "end2end/VideoViewsTests.h",
diff --git a/src/dawn/tests/end2end/IOSurfaceWrappingTests.cpp b/src/dawn/tests/end2end/IOSurfaceWrappingTests.cpp
index 3bfc742..cfef5f1 100644
--- a/src/dawn/tests/end2end/IOSurfaceWrappingTests.cpp
+++ b/src/dawn/tests/end2end/IOSurfaceWrappingTests.cpp
@@ -96,13 +96,11 @@
       public:
         wgpu::Texture WrapIOSurface(const wgpu::TextureDescriptor* descriptor,
                                     IOSurfaceRef ioSurface,
-                                    uint32_t plane,
                                     bool isInitialized = true) {
             dawn::native::metal::ExternalImageDescriptorIOSurface externDesc;
             externDesc.cTextureDescriptor =
                 reinterpret_cast<const WGPUTextureDescriptor*>(descriptor);
             externDesc.ioSurface = ioSurface;
-            externDesc.plane = plane;
             externDesc.isInitialized = isInitialized;
             WGPUTexture texture = dawn::native::metal::WrapIOSurface(device.Get(), &externDesc);
             return wgpu::Texture::Acquire(texture);
@@ -134,7 +132,7 @@
 // Test a successful wrapping of an IOSurface in a texture
 TEST_P(IOSurfaceValidationTests, Success) {
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
-    wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get(), 0);
+    wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get());
     ASSERT_NE(texture.Get(), nullptr);
 }
 
@@ -145,16 +143,7 @@
     wgpu::ChainedStruct chainedDescriptor;
     descriptor.nextInChain = &chainedDescriptor;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
-    ASSERT_EQ(texture.Get(), nullptr);
-}
-
-// Test an error occurs if the plane is too large
-TEST_P(IOSurfaceValidationTests, PlaneTooLarge) {
-    DAWN_TEST_UNSUPPORTED_IF(UsesWire());
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 1));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -164,8 +153,7 @@
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     descriptor.dimension = wgpu::TextureDimension::e3D;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -174,8 +162,7 @@
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     descriptor.mipLevelCount = 2;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -184,8 +171,7 @@
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     descriptor.size.depthOrArrayLayers = 2;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -194,8 +180,7 @@
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     descriptor.sampleCount = 4;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -204,8 +189,7 @@
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     descriptor.size.width = 11;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -214,8 +198,7 @@
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     descriptor.size.height = 11;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -224,8 +207,7 @@
     DAWN_TEST_UNSUPPORTED_IF(UsesWire());
     descriptor.format = wgpu::TextureFormat::R8Unorm;
 
-    ASSERT_DEVICE_ERROR(wgpu::Texture texture =
-                            WrapIOSurface(&descriptor, defaultIOSurface.get(), 0));
+    ASSERT_DEVICE_ERROR(wgpu::Texture texture = WrapIOSurface(&descriptor, defaultIOSurface.get()));
     ASSERT_EQ(texture.Get(), nullptr);
 }
 
@@ -305,7 +287,7 @@
             textureDescriptor.sampleCount = 1;
             textureDescriptor.mipLevelCount = 1;
             textureDescriptor.usage = wgpu::TextureUsage::TextureBinding;
-            wgpu::Texture wrappingTexture = WrapIOSurface(&textureDescriptor, ioSurface, 0);
+            wgpu::Texture wrappingTexture = WrapIOSurface(&textureDescriptor, ioSurface);
 
             wgpu::TextureView textureView = wrappingTexture.CreateView();
 
@@ -345,7 +327,7 @@
         textureDescriptor.sampleCount = 1;
         textureDescriptor.mipLevelCount = 1;
         textureDescriptor.usage = wgpu::TextureUsage::RenderAttachment;
-        wgpu::Texture ioSurfaceTexture = WrapIOSurface(&textureDescriptor, ioSurface, 0);
+        wgpu::Texture ioSurfaceTexture = WrapIOSurface(&textureDescriptor, ioSurface);
 
         wgpu::TextureView ioSurfaceView = ioSurfaceTexture.CreateView();
 
@@ -471,7 +453,7 @@
     textureDescriptor.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
 
     // wrap ioSurface and ensure color is not visible when isInitialized set to false
-    wgpu::Texture ioSurfaceTexture = WrapIOSurface(&textureDescriptor, ioSurface.get(), 0, false);
+    wgpu::Texture ioSurfaceTexture = WrapIOSurface(&textureDescriptor, ioSurface.get(), false);
     EXPECT_PIXEL_RGBA8_EQ(RGBA8(0, 0, 0, 0), ioSurfaceTexture, 0, 0);
 }
 
diff --git a/src/dawn/tests/end2end/VideoViewsTests.cpp b/src/dawn/tests/end2end/VideoViewsTests.cpp
index c79bc5b..febfb50 100644
--- a/src/dawn/tests/end2end/VideoViewsTests.cpp
+++ b/src/dawn/tests/end2end/VideoViewsTests.cpp
@@ -117,6 +117,58 @@
     }
 }
 
+uint32_t VideoViewsTests::NumPlanes(wgpu::TextureFormat format) {
+    switch (format) {
+        case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+            return 2;
+        default:
+            UNREACHABLE();
+            return 0;
+    }
+}
+std::vector<uint8_t> VideoViewsTests::GetTestTextureDataWithPlaneIndex(size_t planeIndex,
+                                                                       size_t bytesPerRow,
+                                                                       size_t height,
+                                                                       bool isCheckerboard) {
+    std::vector<uint8_t> texelData = VideoViewsTests::GetTestTextureData(
+        wgpu::TextureFormat::R8BG8Biplanar420Unorm, isCheckerboard);
+    const uint32_t texelDataRowBytes = kYUVImageDataWidthInTexels;
+    const uint32_t texelDataHeight =
+        planeIndex == 0 ? kYUVImageDataHeightInTexels : kYUVImageDataHeightInTexels / 2;
+
+    std::vector<uint8_t> texels(bytesPerRow * height, 0);
+    uint32_t plane_first_texel_offset = 0;
+    // The size of the test video frame is 4 x 4
+    switch (planeIndex) {
+        case VideoViewsTests::kYUVLumaPlaneIndex:
+            for (uint32_t i = 0; i < texelDataHeight; ++i) {
+                if (i < texelDataHeight) {
+                    for (uint32_t j = 0; j < texelDataRowBytes; ++j) {
+                        texels[bytesPerRow * i + j] =
+                            texelData[texelDataRowBytes * i + j + plane_first_texel_offset];
+                    }
+                }
+            }
+            return texels;
+        case VideoViewsTests::kYUVChromaPlaneIndex:
+            // TexelData is 4 * 6 size, first 4 * 4 is Y plane, UV plane started
+            // at index 16.
+            plane_first_texel_offset = 16;
+            for (uint32_t i = 0; i < texelDataHeight; ++i) {
+                if (i < texelDataHeight) {
+                    for (uint32_t j = 0; j < texelDataRowBytes; ++j) {
+                        texels[bytesPerRow * i + j] =
+                            texelData[texelDataRowBytes * i + j + plane_first_texel_offset];
+                    }
+                }
+            }
+            return texels;
+        default:
+            UNREACHABLE();
+            return {};
+    }
+}
+
 // Vertex shader used to render a sampled texture into a quad.
 wgpu::ShaderModule VideoViewsTests::GetTestVertexShaderModule() const {
     return utils::CreateShaderModule(device, R"(
diff --git a/src/dawn/tests/end2end/VideoViewsTests.h b/src/dawn/tests/end2end/VideoViewsTests.h
index 6dcedb9..60d93a0 100644
--- a/src/dawn/tests/end2end/VideoViewsTests.h
+++ b/src/dawn/tests/end2end/VideoViewsTests.h
@@ -28,7 +28,8 @@
     virtual ~VideoViewsTestBackend();
 
     virtual void OnSetUp(WGPUDevice device) = 0;
-    virtual void OnTearDown() = 0;
+    virtual void OnTearDown() {
+    }
 
     class PlatformTexture {
       public:
@@ -74,6 +75,11 @@
                                                           RGBA8{90, 240, 0, 0xFF}};  // UV
 
     static std::vector<uint8_t> GetTestTextureData(wgpu::TextureFormat format, bool isCheckerboard);
+    static uint32_t NumPlanes(wgpu::TextureFormat format);
+    static std::vector<uint8_t> GetTestTextureDataWithPlaneIndex(size_t planeIndex,
+                                                                 size_t bytesPerRow,
+                                                                 size_t height,
+                                                                 bool isCheckerboard);
 
   protected:
     void SetUp() override;
diff --git a/src/dawn/tests/end2end/VideoViewsTests_mac.cpp b/src/dawn/tests/end2end/VideoViewsTests_mac.cpp
new file mode 100644
index 0000000..151c0b3
--- /dev/null
+++ b/src/dawn/tests/end2end/VideoViewsTests_mac.cpp
@@ -0,0 +1,187 @@
+// Copyright 2022 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 "VideoViewsTests.h"
+
+#include "dawn/common/Assert.h"
+#include "dawn/common/CoreFoundationRef.h"
+#include "dawn/native/MetalBackend.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <CoreVideo/CVPixelBuffer.h>
+#include <IOSurface/IOSurfaceRef.h>
+
+namespace {
+    void AddIntegerValue(CFMutableDictionaryRef dictionary, const CFStringRef key, int32_t value) {
+        CFNumberRef number(CFNumberCreate(nullptr, kCFNumberSInt32Type, &value));
+        CFDictionaryAddValue(dictionary, key, number);
+        CFRelease(number);
+    }
+
+}  // anonymous namespace
+
+class PlatformTextureIOSurface : public VideoViewsTestBackend::PlatformTexture {
+  public:
+    PlatformTextureIOSurface(wgpu::Texture&& texture, IOSurfaceRef iosurface)
+        : PlatformTexture(std::move(texture)) {
+        mIOSurface = AcquireCFRef<IOSurfaceRef>(iosurface);
+    }
+    ~PlatformTextureIOSurface() override {
+        mIOSurface = nullptr;
+    }
+
+    bool CanWrapAsWGPUTexture() override {
+        return true;
+    }
+
+  private:
+    CFRef<IOSurfaceRef> mIOSurface = nullptr;
+};
+
+class VideoViewsTestBackendIOSurface : public VideoViewsTestBackend {
+  public:
+    void OnSetUp(WGPUDevice device) override {
+        mWGPUDevice = device;
+    }
+
+  private:
+    OSType ToCVFormat(wgpu::TextureFormat format) {
+        switch (format) {
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
+            default:
+                UNREACHABLE();
+                return 0;
+        }
+    }
+
+    size_t GetSubSamplingFactorPerPlane(wgpu::TextureFormat format, size_t plane) {
+        switch (format) {
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                return plane == VideoViewsTests::kYUVLumaPlaneIndex ? 1 : 2;
+            default:
+                UNREACHABLE();
+                return 0;
+        }
+    }
+
+    size_t BytesPerElement(wgpu::TextureFormat format, size_t plane) {
+        switch (format) {
+            case wgpu::TextureFormat::R8BG8Biplanar420Unorm:
+                return plane == VideoViewsTests::kYUVLumaPlaneIndex ? 1 : 2;
+            default:
+                UNREACHABLE();
+                return 0;
+        }
+    }
+
+    std::unique_ptr<VideoViewsTestBackend::PlatformTexture> CreateVideoTextureForTest(
+        wgpu::TextureFormat format,
+        wgpu::TextureUsage usage,
+        bool isCheckerboard) override {
+        CFMutableDictionaryRef dict(CFDictionaryCreateMutable(kCFAllocatorDefault, 0,
+                                                              &kCFTypeDictionaryKeyCallBacks,
+                                                              &kCFTypeDictionaryValueCallBacks));
+        AddIntegerValue(dict, kIOSurfaceWidth, VideoViewsTests::kYUVImageDataWidthInTexels);
+        AddIntegerValue(dict, kIOSurfaceHeight, VideoViewsTests::kYUVImageDataHeightInTexels);
+        AddIntegerValue(dict, kIOSurfacePixelFormat, ToCVFormat(format));
+
+        size_t num_planes = VideoViewsTests::NumPlanes(format);
+
+        CFMutableArrayRef planes(
+            CFArrayCreateMutable(kCFAllocatorDefault, num_planes, &kCFTypeArrayCallBacks));
+        size_t total_bytes_alloc = 0;
+        for (size_t plane = 0; plane < num_planes; ++plane) {
+            const size_t factor = GetSubSamplingFactorPerPlane(format, plane);
+            const size_t plane_width = VideoViewsTests::kYUVImageDataWidthInTexels / factor;
+            const size_t plane_height = VideoViewsTests::kYUVImageDataHeightInTexels / factor;
+            const size_t plane_bytes_per_element = BytesPerElement(format, plane);
+            const size_t plane_bytes_per_row = IOSurfaceAlignProperty(
+                kIOSurfacePlaneBytesPerRow, plane_width * plane_bytes_per_element);
+            const size_t plane_bytes_alloc =
+                IOSurfaceAlignProperty(kIOSurfacePlaneSize, plane_height * plane_bytes_per_row);
+            const size_t plane_offset =
+                IOSurfaceAlignProperty(kIOSurfacePlaneOffset, total_bytes_alloc);
+
+            CFMutableDictionaryRef plane_info(
+                CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks,
+                                          &kCFTypeDictionaryValueCallBacks));
+
+            AddIntegerValue(plane_info, kIOSurfacePlaneWidth, plane_width);
+            AddIntegerValue(plane_info, kIOSurfacePlaneHeight, plane_height);
+            AddIntegerValue(plane_info, kIOSurfacePlaneBytesPerElement, plane_bytes_per_element);
+            AddIntegerValue(plane_info, kIOSurfacePlaneBytesPerRow, plane_bytes_per_row);
+            AddIntegerValue(plane_info, kIOSurfacePlaneSize, plane_bytes_alloc);
+            AddIntegerValue(plane_info, kIOSurfacePlaneOffset, plane_offset);
+            CFArrayAppendValue(planes, plane_info);
+            CFRelease(plane_info);
+            total_bytes_alloc = plane_offset + plane_bytes_alloc;
+        }
+        CFDictionaryAddValue(dict, kIOSurfacePlaneInfo, planes);
+        CFRelease(planes);
+
+        total_bytes_alloc = IOSurfaceAlignProperty(kIOSurfaceAllocSize, total_bytes_alloc);
+        AddIntegerValue(dict, kIOSurfaceAllocSize, total_bytes_alloc);
+
+        IOSurfaceRef surface = IOSurfaceCreate(dict);
+        CFRelease(dict);
+
+        IOSurfaceLock(surface, 0, nullptr);
+        for (size_t plane = 0; plane < num_planes; ++plane) {
+            std::vector<uint8_t> data = VideoViewsTests::GetTestTextureDataWithPlaneIndex(
+                plane, IOSurfaceGetBytesPerRowOfPlane(surface, plane),
+                IOSurfaceGetHeightOfPlane(surface, plane), isCheckerboard);
+            void* pointer = IOSurfaceGetBaseAddressOfPlane(surface, plane);
+            memcpy(pointer, data.data(), data.size());
+        }
+        IOSurfaceUnlock(surface, 0, nullptr);
+
+        wgpu::TextureDescriptor textureDesc;
+        textureDesc.format = format;
+        textureDesc.dimension = wgpu::TextureDimension::e2D;
+        textureDesc.usage = usage;
+        textureDesc.size = {VideoViewsTests::kYUVImageDataWidthInTexels,
+                            VideoViewsTests::kYUVImageDataHeightInTexels, 1};
+
+        wgpu::DawnTextureInternalUsageDescriptor internalDesc;
+        internalDesc.internalUsage = wgpu::TextureUsage::CopySrc;
+        textureDesc.nextInChain = &internalDesc;
+
+        dawn::native::metal::ExternalImageDescriptorIOSurface descriptor = {};
+        descriptor.cTextureDescriptor =
+            reinterpret_cast<const WGPUTextureDescriptor*>(&textureDesc);
+        descriptor.isInitialized = true;
+        descriptor.ioSurface = surface;
+
+        return std::make_unique<PlatformTextureIOSurface>(
+            wgpu::Texture::Acquire(dawn::native::metal::WrapIOSurface(mWGPUDevice, &descriptor)),
+            surface);
+    }
+
+    void DestroyVideoTextureForTest(
+        std::unique_ptr<VideoViewsTestBackend::PlatformTexture>&& platformTexture) override {
+    }
+
+    WGPUDevice mWGPUDevice = nullptr;
+};
+
+// static
+BackendTestConfig VideoViewsTestBackend::Backend() {
+    return MetalBackend();
+}
+
+// static
+std::unique_ptr<VideoViewsTestBackend> VideoViewsTestBackend::Create() {
+    return std::make_unique<VideoViewsTestBackendIOSurface>();
+}
diff --git a/src/dawn/tests/end2end/VideoViewsTests_win.cpp b/src/dawn/tests/end2end/VideoViewsTests_win.cpp
index f2f7ed5..21889d8 100644
--- a/src/dawn/tests/end2end/VideoViewsTests_win.cpp
+++ b/src/dawn/tests/end2end/VideoViewsTests_win.cpp
@@ -77,9 +77,6 @@
         mD3d11Device = std::move(d3d11Device);
     }
 
-    void OnTearDown() override {
-    }
-
   protected:
     static DXGI_FORMAT GetDXGITextureFormat(wgpu::TextureFormat format) {
         switch (format) {