[dawn][metal] Add texture component swizzle feature support

Bug: 414312052
Change-Id: I4fe036c99f0eb965f53ff21562ac9daccc934fd3
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/249434
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Zhaoming Jiang <zhaoming.jiang@microsoft.com>
Commit-Queue: Fr <beaufort.francois@gmail.com>
diff --git a/src/dawn/native/Texture.cpp b/src/dawn/native/Texture.cpp
index 248fe5b..148ba87 100644
--- a/src/dawn/native/Texture.cpp
+++ b/src/dawn/native/Texture.cpp
@@ -1577,6 +1577,14 @@
                             texture->GetSampleCount(),
                             texture->GetNumMipLevels(),
                             texture->GetArrayLayers())) {
+    if (auto* swizzleDesc = descriptor.Get<TextureComponentSwizzleDescriptor>()) {
+        auto swizzle = swizzleDesc->swizzle.WithTrivialFrontendDefaults();
+        mSwizzleRed = swizzle.r;
+        mSwizzleGreen = swizzle.g;
+        mSwizzleBlue = swizzle.b;
+        mSwizzleAlpha = swizzle.a;
+    }
+
     GetObjectTrackingList()->Track(this);
 
     // Emit a warning if invalid usages were removed for this view.
@@ -1691,6 +1699,32 @@
     return mInternalUsage;
 }
 
+wgpu::ComponentSwizzle TextureViewBase::GetSwizzleRed() const {
+    return mSwizzleRed;
+}
+
+wgpu::ComponentSwizzle TextureViewBase::GetSwizzleGreen() const {
+    return mSwizzleGreen;
+}
+
+wgpu::ComponentSwizzle TextureViewBase::GetSwizzleBlue() const {
+    return mSwizzleBlue;
+}
+
+wgpu::ComponentSwizzle TextureViewBase::GetSwizzleAlpha() const {
+    return mSwizzleAlpha;
+}
+
+bool TextureViewBase::UsesNonDefaultSwizzle() const {
+    // TODO(414312052): Refine this condition. A view might not be strictly necessary
+    // in case of the given swizzle works identically to default with the original
+    // format, e.g. a R8Unorm texture with swizzle.r set to R and swizzle.g set to One.
+    // This current check provides a correct, though potentially overly broad,
+    // first approximation.
+    return mSwizzleRed != wgpu::ComponentSwizzle::R || mSwizzleGreen != wgpu::ComponentSwizzle::G ||
+           mSwizzleBlue != wgpu::ComponentSwizzle::B || mSwizzleAlpha != wgpu::ComponentSwizzle::A;
+}
+
 bool TextureViewBase::IsYCbCr() const {
     return false;
 }
diff --git a/src/dawn/native/Texture.h b/src/dawn/native/Texture.h
index 96d2903..a4f8c90 100644
--- a/src/dawn/native/Texture.h
+++ b/src/dawn/native/Texture.h
@@ -315,6 +315,12 @@
     wgpu::TextureUsage GetUsage() const;
     wgpu::TextureUsage GetInternalUsage() const;
 
+    wgpu::ComponentSwizzle GetSwizzleRed() const;
+    wgpu::ComponentSwizzle GetSwizzleGreen() const;
+    wgpu::ComponentSwizzle GetSwizzleBlue() const;
+    wgpu::ComponentSwizzle GetSwizzleAlpha() const;
+    bool UsesNonDefaultSwizzle() const;
+
     virtual bool IsYCbCr() const;
     // Valid to call only if `IsYCbCr()` is true.
     virtual YCbCrVkDescriptor GetYCbCrVkDescriptor() const;
@@ -334,6 +340,10 @@
     SubresourceRange mRange;
     const wgpu::TextureUsage mUsage = wgpu::TextureUsage::None;
     const wgpu::TextureUsage mInternalUsage = wgpu::TextureUsage::None;
+    wgpu::ComponentSwizzle mSwizzleRed = wgpu::ComponentSwizzle::R;
+    wgpu::ComponentSwizzle mSwizzleGreen = wgpu::ComponentSwizzle::G;
+    wgpu::ComponentSwizzle mSwizzleBlue = wgpu::ComponentSwizzle::B;
+    wgpu::ComponentSwizzle mSwizzleAlpha = wgpu::ComponentSwizzle::A;
 };
 
 }  // namespace dawn::native
diff --git a/src/dawn/native/metal/PhysicalDeviceMTL.mm b/src/dawn/native/metal/PhysicalDeviceMTL.mm
index 73c9fef..aed4563 100644
--- a/src/dawn/native/metal/PhysicalDeviceMTL.mm
+++ b/src/dawn/native/metal/PhysicalDeviceMTL.mm
@@ -717,6 +717,10 @@
         EnableFeature(Feature::BufferMapExtendedUsages);
     }
 #endif
+
+    if ([*mDevice supportsFamily:MTLGPUFamilyMac2]) {
+        EnableFeature(Feature::TextureComponentSwizzle);
+    }
 }
 
 void PhysicalDevice::InitializeVendorArchitectureImpl() {
diff --git a/src/dawn/native/metal/TextureMTL.mm b/src/dawn/native/metal/TextureMTL.mm
index 8a14036..591ee01 100644
--- a/src/dawn/native/metal/TextureMTL.mm
+++ b/src/dawn/native/metal/TextureMTL.mm
@@ -101,6 +101,26 @@
     }
 }
 
+MTLTextureSwizzle MetalTextureSwizzle(wgpu::ComponentSwizzle swizzle) {
+    switch (swizzle) {
+        case wgpu::ComponentSwizzle::Zero:
+            return MTLTextureSwizzleZero;
+        case wgpu::ComponentSwizzle::One:
+            return MTLTextureSwizzleOne;
+        case wgpu::ComponentSwizzle::R:
+            return MTLTextureSwizzleRed;
+        case wgpu::ComponentSwizzle::G:
+            return MTLTextureSwizzleGreen;
+        case wgpu::ComponentSwizzle::B:
+            return MTLTextureSwizzleBlue;
+        case wgpu::ComponentSwizzle::A:
+            return MTLTextureSwizzleAlpha;
+
+        case wgpu::ComponentSwizzle::Undefined:
+            DAWN_UNREACHABLE();
+    }
+}
+
 bool RequiresCreatingNewTextureView(
     const TextureBase* texture,
     wgpu::TextureUsage internalViewUsage,
@@ -156,6 +176,16 @@
             break;
     }
 
+    // TODO(414312052): Use TextureViewBase::UsesNonDefaultSwizzle() instead of
+    // textureViewDescriptor.
+    if (auto* swizzleDesc = textureViewDescriptor.Get<TextureComponentSwizzleDescriptor>()) {
+        auto swizzle = swizzleDesc->swizzle.WithTrivialFrontendDefaults();
+        if (swizzle.r != wgpu::ComponentSwizzle::R || swizzle.g != wgpu::ComponentSwizzle::G ||
+            swizzle.b != wgpu::ComponentSwizzle::B || swizzle.a != wgpu::ComponentSwizzle::A) {
+            return true;
+        }
+    }
+
     return false;
 }
 
@@ -808,10 +838,26 @@
         auto mipLevelRange = NSMakeRange(descriptor->baseMipLevel, descriptor->mipLevelCount);
         auto arrayLayerRange = NSMakeRange(descriptor->baseArrayLayer, descriptor->arrayLayerCount);
 
-        mMtlTextureView = AcquireNSPRef([mtlTexture newTextureViewWithPixelFormat:viewFormat
-                                                                      textureType:textureViewType
-                                                                           levels:mipLevelRange
-                                                                           slices:arrayLayerRange]);
+        if (UsesNonDefaultSwizzle()) {
+            MTLTextureSwizzleChannels swizzle;
+            swizzle.red = MetalTextureSwizzle(GetSwizzleRed());
+            swizzle.green = MetalTextureSwizzle(GetSwizzleGreen());
+            swizzle.blue = MetalTextureSwizzle(GetSwizzleBlue());
+            swizzle.alpha = MetalTextureSwizzle(GetSwizzleAlpha());
+            mMtlTextureView =
+                AcquireNSPRef([mtlTexture newTextureViewWithPixelFormat:viewFormat
+                                                            textureType:textureViewType
+                                                                 levels:mipLevelRange
+                                                                 slices:arrayLayerRange
+                                                                swizzle:swizzle]);
+        } else {
+            mMtlTextureView =
+                AcquireNSPRef([mtlTexture newTextureViewWithPixelFormat:viewFormat
+                                                            textureType:textureViewType
+                                                                 levels:mipLevelRange
+                                                                 slices:arrayLayerRange]);
+        }
+
         if (mMtlTextureView == nil) {
             return DAWN_INTERNAL_ERROR("Failed to create MTLTexture view.");
         }
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index f580f26..85a4642 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -688,6 +688,7 @@
     "end2end/SubresourceRenderAttachmentTests.cpp",
     "end2end/SupportedFeatureArch.cpp",
     "end2end/Texture3DTests.cpp",
+    "end2end/TextureComponentSwizzleTests.cpp",
     "end2end/TextureCorruptionTests.cpp",
     "end2end/TextureFormatTests.cpp",
     "end2end/TextureFormatsTier1Tests.cpp",
diff --git a/src/dawn/tests/end2end/TextureComponentSwizzleTests.cpp b/src/dawn/tests/end2end/TextureComponentSwizzleTests.cpp
new file mode 100644
index 0000000..2a7272e
--- /dev/null
+++ b/src/dawn/tests/end2end/TextureComponentSwizzleTests.cpp
@@ -0,0 +1,236 @@
+// Copyright 2025 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 <array>
+#include <vector>
+
+#include "dawn/tests/DawnTest.h"
+#include "dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "dawn/utils/TextureUtils.h"
+#include "dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+// Define all component swizzle options to test.
+constexpr std::array<wgpu::ComponentSwizzle, 6> kComponentSwizzles = {
+    wgpu::ComponentSwizzle::Zero, wgpu::ComponentSwizzle::One, wgpu::ComponentSwizzle::R,
+    wgpu::ComponentSwizzle::G,    wgpu::ComponentSwizzle::B,   wgpu::ComponentSwizzle::A};
+
+class TextureComponentSwizzleTest : public DawnTest {
+  protected:
+    void SetUp() override {
+        DawnTest::SetUp();
+        DAWN_TEST_UNSUPPORTED_IF(!device.HasFeature(wgpu::FeatureName::TextureComponentSwizzle));
+    }
+
+    std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
+        std::vector<wgpu::FeatureName> requiredFeatures = {};
+        if (SupportsFeatures({wgpu::FeatureName::TextureComponentSwizzle})) {
+            requiredFeatures.push_back(wgpu::FeatureName::TextureComponentSwizzle);
+        }
+        return requiredFeatures;
+    }
+
+    struct TestParams {
+        wgpu::TextureFormat format;
+        // Input data for the source texture. Max 4 bytes for convenience.
+        std::array<uint8_t, 4> inputData;
+        // The expected values (R, G, B, A) that `textureLoad` would produce
+        // for this format *before* any swizzling. This accounts for default values
+        // for missing channels (e.g., G=0, B=0, A=255 for R8Unorm).
+        std::array<uint8_t, 4> baseLoadValues;
+    };
+
+    // Calculates the expected value after swizzling and normalization.
+    uint8_t GetExpectedValue(wgpu::ComponentSwizzle swizzle,
+                             const std::array<uint8_t, 4>& rgbaData) {
+        switch (swizzle) {
+            case wgpu::ComponentSwizzle::Zero:
+                return 0;
+            case wgpu::ComponentSwizzle::One:
+                return 255;
+            case wgpu::ComponentSwizzle::R:
+                return rgbaData[0];
+            case wgpu::ComponentSwizzle::G:
+                return rgbaData[1];
+            case wgpu::ComponentSwizzle::B:
+                return rgbaData[2];
+            case wgpu::ComponentSwizzle::A:
+                return rgbaData[3];
+            case wgpu::ComponentSwizzle::Undefined:
+            default:
+                DAWN_UNREACHABLE();
+                return 0;
+        }
+    }
+
+    void RunTest(TestParams params) {
+        wgpu::ShaderModule module = utils::CreateShaderModule(device, R"(
+            @group(0) @binding(0) var texture : texture_2d<f32>;
+
+            @vertex fn vs_main(@builtin(vertex_index) vertexIndex : u32) -> @builtin(position) vec4f {
+                var pos = array<vec2f, 3>(
+                    vec2f(-1.0, -1.0),
+                    vec2f(-1.0, 3.0),
+                    vec2f(3.0, -1.0)
+                );
+                return vec4f(pos[vertexIndex], 0.0, 1.0);
+            }
+
+            @fragment fn fs_main() -> @location(0) vec4f {
+                // textureLoad samples at an integer coordinate and mip level 0.
+                return textureLoad(texture, vec2i(0, 0), 0);
+            })");
+
+        utils::ComboRenderPipelineDescriptor pipelineDesc;
+        pipelineDesc.vertex.module = module;
+        pipelineDesc.cFragment.module = module;
+        wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
+
+        wgpu::TextureDescriptor outputTextureDesc = {};
+        outputTextureDesc.usage =
+            wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+        outputTextureDesc.size = {1, 1, 1};
+        outputTextureDesc.format = wgpu::TextureFormat::RGBA8Unorm;
+        wgpu::Texture outputTexture = device.CreateTexture(&outputTextureDesc);
+
+        auto RunSwizzleTest = [this, params, pipeline, outputTexture](
+                                  wgpu::ComponentSwizzle swizzleRed,
+                                  wgpu::ComponentSwizzle swizzleGreen,
+                                  wgpu::ComponentSwizzle swizzleBlue,
+                                  wgpu::ComponentSwizzle swizzleAlpha) {
+            // Create source texture.
+            wgpu::TextureDescriptor textureDesc = {};
+            textureDesc.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::TextureBinding;
+            textureDesc.size = {1, 1, 1};
+            textureDesc.format = params.format;
+            wgpu::Texture texture = device.CreateTexture(&textureDesc);
+
+            wgpu::TexelCopyTextureInfo dest = {.texture = texture};
+            wgpu::TexelCopyBufferLayout dataLayout = {.bytesPerRow = 256, .rowsPerImage = 1};
+            wgpu::Extent3D writeSize = {1, 1, 1};
+            const uint32_t bytesPerTexel = utils::GetTexelBlockSizeInBytes(params.format);
+            device.GetQueue().WriteTexture(&dest, params.inputData.data(), bytesPerTexel,
+                                           &dataLayout, &writeSize);
+
+            // Create the TextureView for the source texture with the specified swizzle.
+            wgpu::TextureViewDescriptor viewDesc = {};
+            wgpu::TextureComponentSwizzleDescriptor swizzleDesc = {};
+            swizzleDesc.swizzle.r = swizzleRed;
+            swizzleDesc.swizzle.g = swizzleGreen;
+            swizzleDesc.swizzle.b = swizzleBlue;
+            swizzleDesc.swizzle.a = swizzleAlpha;
+            viewDesc.nextInChain = &swizzleDesc;
+            wgpu::TextureView textureView = texture.CreateView(&viewDesc);
+
+            // Set up bind group using the swizzled texture view.
+            wgpu::BindGroup bindGroup =
+                utils::MakeBindGroup(device, pipeline.GetBindGroupLayout(0), {{0, textureView}});
+
+            // Issue render commands.
+            wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+            wgpu::RenderPassDescriptor renderPassDesc = {};
+            wgpu::RenderPassColorAttachment colorAttachment = {};
+            colorAttachment.view = outputTexture.CreateView();
+            colorAttachment.loadOp = wgpu::LoadOp::Clear;
+            colorAttachment.storeOp = wgpu::StoreOp::Store;
+            colorAttachment.clearValue = {0.0f, 0.0f, 0.0f, 0.0f};
+            renderPassDesc.colorAttachmentCount = 1;
+            renderPassDesc.colorAttachments = &colorAttachment;
+
+            wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPassDesc);
+            pass.SetPipeline(pipeline);
+            pass.SetBindGroup(0, bindGroup);
+            pass.Draw(3);
+            pass.End();
+
+            // Submit commands to the queue.
+            wgpu::CommandBuffer commands = encoder.Finish();
+            queue.Submit(1, &commands);
+
+            utils::RGBA8 expectedColor =
+                utils::RGBA8(GetExpectedValue(swizzleRed, params.baseLoadValues),
+                             GetExpectedValue(swizzleGreen, params.baseLoadValues),
+                             GetExpectedValue(swizzleBlue, params.baseLoadValues),
+                             GetExpectedValue(swizzleAlpha, params.baseLoadValues));
+            EXPECT_PIXEL_RGBA8_EQ(expectedColor, outputTexture, 0, 0);
+        };
+
+        for (auto swizzleRed : kComponentSwizzles) {
+            RunSwizzleTest(swizzleRed, wgpu::ComponentSwizzle::G, wgpu::ComponentSwizzle::B,
+                           wgpu::ComponentSwizzle::A);
+        }
+
+        for (auto swizzleGreen : kComponentSwizzles) {
+            RunSwizzleTest(wgpu::ComponentSwizzle::R, swizzleGreen, wgpu::ComponentSwizzle::B,
+                           wgpu::ComponentSwizzle::A);
+        }
+
+        for (auto swizzleBlue : kComponentSwizzles) {
+            RunSwizzleTest(wgpu::ComponentSwizzle::R, wgpu::ComponentSwizzle::G, swizzleBlue,
+                           wgpu::ComponentSwizzle::A);
+        }
+
+        for (auto swizzleAlpha : kComponentSwizzles) {
+            RunSwizzleTest(wgpu::ComponentSwizzle::R, wgpu::ComponentSwizzle::G,
+                           wgpu::ComponentSwizzle::B, swizzleAlpha);
+        }
+    }
+};
+
+// Test that texture component swizzle works as expected when the 'texture-component-swizzle'
+// feature is enabled. These tests verify each component's swizzle mapping independently
+// by using a render shader to read from a swizzled texture and render to a texture.
+// Those covers formats with all channels, and formats where some channels are implicitly
+// generated (0.0 or 1.0) by `textureLoad` due to the base format missing components.
+TEST_P(TextureComponentSwizzleTest, AllChannelsPresent) {
+    TestParams params;
+    params.format = wgpu::TextureFormat::RGBA8Unorm;
+    params.inputData = {255, 128, 64, 0};
+    params.baseLoadValues = {255, 128, 64, 0};
+    RunTest(params);
+}
+TEST_P(TextureComponentSwizzleTest, OnlyRedChannelPresent) {
+    TestParams params;
+    params.format = wgpu::TextureFormat::R8Unorm;
+    params.inputData = {128, 0, 0, 0};
+    params.baseLoadValues = {128, 0, 0, 255};  // G, B default to 0, A defaults to 255.
+    RunTest(params);
+}
+TEST_P(TextureComponentSwizzleTest, OnlyRedAndGreenChannelPresent) {
+    TestParams params;
+    params.format = wgpu::TextureFormat::RG8Unorm;
+    params.inputData = {128, 64, 0, 0};
+    params.baseLoadValues = {128, 64, 0, 255};  // B defaults to 0, A defaults to 255.
+    RunTest(params);
+}
+
+DAWN_INSTANTIATE_TEST(TextureComponentSwizzleTest, MetalBackend());
+
+}  // anonymous namespace
+}  // namespace dawn