[dawn][native] Add BGLEntryArraySize and validation in BGL

This adds the new chained struct that will be used to specify the
arraySize of BGL. It is gated both behind UnsafeAPI and a new killswitch
toggle. Validation is added for this structure in the BGL as well as
tests.

Bug: 393558555
Change-Id: I2c73d31f297d75889738e18fdff15ec448901860
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/236414
Reviewed-by: Loko Kung <lokokung@google.com>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Brandon Jones <bajones@chromium.org>
diff --git a/src/dawn/dawn.json b/src/dawn/dawn.json
index 6aedbf9..5ce3cc2 100644
--- a/src/dawn/dawn.json
+++ b/src/dawn/dawn.json
@@ -495,6 +495,15 @@
         ]
     },
 
+    "bind group layout entry array size": {
+        "category": "structure",
+        "chained": "in",
+        "chain roots": ["bind group layout entry"],
+        "members": [
+            {"name": "array size", "type": "uint32_t", "default": 0}
+        ]
+    },
+
     "bind group layout entry": {
         "category": "structure",
         "extensible": "in",
@@ -3773,6 +3782,7 @@
             {"value": 10, "name": "surface color management", "tags": ["upstream", "emscripten"]},
             {"value": 11, "name": "request adapter WebXR options", "tags": ["upstream", "emscripten"]},
             {"value": 12, "name": "adapter properties subgroups"},
+            {"value": 13, "name": "bind group layout entry array size"},
 
             {"value": 0, "name": "texture binding view dimension descriptor", "tags": ["compat"]},
 
diff --git a/src/dawn/native/BindGroupLayoutInternal.cpp b/src/dawn/native/BindGroupLayoutInternal.cpp
index abcb102..04960e4 100644
--- a/src/dawn/native/BindGroupLayoutInternal.cpp
+++ b/src/dawn/native/BindGroupLayoutInternal.cpp
@@ -37,6 +37,7 @@
 
 #include "dawn/common/Enumerator.h"
 #include "dawn/common/MatchVariant.h"
+#include "dawn/common/Range.h"
 #include "dawn/native/ChainUtils.h"
 #include "dawn/native/Device.h"
 #include "dawn/native/Error.h"
@@ -94,6 +95,11 @@
                                         bool allowInternalBinding) {
     DAWN_TRY(ValidateShaderStage(entry->visibility));
 
+    uint32_t arraySize = 1;
+    if (auto* arraySizeInfo = entry.Get<BindGroupLayoutEntryArraySize>()) {
+        arraySize = arraySizeInfo->arraySize;
+    }
+
     int bindingMemberCount = 0;
 
     if (entry->buffer.type != wgpu::BufferBindingType::BindingNotUsed) {
@@ -117,11 +123,21 @@
                 "(note that read-only storage buffer bindings are allowed).",
                 entry->visibility, wgpu::ShaderStage::Vertex);
         }
+
+        // TODO(393558555): Support arraySize != 1 for non-dynamic buffers.
+        DAWN_INVALID_IF(arraySize != 1,
+                        "arraySize (%u) != 1 for a buffer binding is not implemented yet.",
+                        arraySize);
     }
 
     if (entry->sampler.type != wgpu::SamplerBindingType::BindingNotUsed) {
         bindingMemberCount++;
         DAWN_TRY(ValidateSamplerBindingType(entry->sampler.type));
+
+        // TODO(393558555): Support arraySize != 1 for samplers.
+        DAWN_INVALID_IF(arraySize != 1,
+                        "arraySize (%u) != 1 for a sampler binding is not implemented yet.",
+                        arraySize);
     }
 
     if (entry->texture.sampleType != wgpu::TextureSampleType::BindingNotUsed) {
@@ -193,6 +209,11 @@
             default:
                 DAWN_UNREACHABLE();
         }
+
+        // TODO(393558555): Support arraySize != 1 for storage textures.
+        DAWN_INVALID_IF(arraySize != 1,
+                        "arraySize (%u) != 1 for a storage texture binding is not implemented yet.",
+                        arraySize);
     }
 
     if (auto* staticSamplerBindingLayout = entry.Get<StaticSamplerBindingLayout>()) {
@@ -203,6 +224,10 @@
                         wgpu::FeatureName::StaticSamplers);
 
         DAWN_TRY(device->ValidateObject(staticSamplerBindingLayout->sampler));
+        DAWN_INVALID_IF(
+            arraySize != 1,
+            "BindGroupLayoutEntry arraySize (%u) is greater than 1 for a static sampler entry.",
+            arraySize);
 
         if (staticSamplerBindingLayout->sampledTextureBinding == WGPU_LIMIT_U32_UNDEFINED) {
             DAWN_INVALID_IF(staticSamplerBindingLayout->sampler->IsYCbCr(),
@@ -212,6 +237,10 @@
 
     if (entry.Get<ExternalTextureBindingLayout>()) {
         bindingMemberCount++;
+        DAWN_INVALID_IF(
+            arraySize != 1,
+            "BindGroupLayoutEntry arraySize (%u) is greater than 1 for an external texture entry.",
+            arraySize);
     }
 
     DAWN_INVALID_IF(bindingMemberCount == 0,
@@ -222,6 +251,13 @@
                     "BindGroupLayoutEntry had more than one of buffer, sampler, texture, "
                     "storageTexture, or externalTexture set");
 
+    if (auto* arraySizeInfo = entry.Get<BindGroupLayoutEntryArraySize>()) {
+        DAWN_INVALID_IF(arraySizeInfo->arraySize != 1 &&
+                            entry->texture.sampleType == wgpu::TextureSampleType::BindingNotUsed,
+                        "Entry that is not a sampled texture has an arraySize (%u) that is not 1.",
+                        arraySizeInfo->arraySize);
+    }
+
     return {};
 }
 
@@ -378,21 +414,48 @@
     for (uint32_t i = 0; i < descriptor->entryCount; ++i) {
         UnpackedPtr<BindGroupLayoutEntry> entry;
         DAWN_TRY_ASSIGN(entry, ValidateAndUnpack(&descriptor->entries[i]));
-        BindingNumber bindingNumber = BindingNumber(entry->binding);
 
-        DAWN_INVALID_IF(bindingNumber >= kMaxBindingsPerBindGroupTyped,
-                        "Binding number (%u) exceeds the maxBindingsPerBindGroup limit (%u).",
-                        uint32_t(bindingNumber), kMaxBindingsPerBindGroup);
-        DAWN_INVALID_IF(bindingMap.count(bindingNumber) != 0,
-                        "On entries[%u]: binding index (%u) was specified by a previous entry.", i,
-                        entry->binding);
+        BindingNumber bindingNumber = BindingNumber(entry->binding);
+        DAWN_INVALID_IF(
+            bindingNumber >= kMaxBindingsPerBindGroupTyped,
+            "On entries[%u]: binding number (%u) exceeds the maxBindingsPerBindGroup limit (%u).",
+            i, uint32_t(bindingNumber), kMaxBindingsPerBindGroup);
+
+        BindingNumber arraySize{1};
+        if (auto* arraySizeInfo = entry.Get<BindGroupLayoutEntryArraySize>()) {
+            arraySize = BindingNumber(arraySizeInfo->arraySize);
+
+            DAWN_INVALID_IF(
+                device->IsToggleEnabled(Toggle::DisableBindGroupLayoutEntryArraySize),
+                "On entries[%u]: chaining of BindGroupLayoutEntryArraySize is disabled.", i);
+            DAWN_INVALID_IF(!device->IsToggleEnabled(Toggle::AllowUnsafeAPIs),
+                            "On entries[%u]: chaining of BindGroupLayoutEntryArraySize is unsafe "
+                            "while it is being implemented.",
+                            i);
+
+            DAWN_INVALID_IF(arraySize == BindingNumber(0), "On entries[%u]: arraySize is 0.", i);
+            DAWN_INVALID_IF(arraySize > kMaxBindingsPerBindGroupTyped - bindingNumber,
+                            "On entries[%u]: binding (%u) + arraySize (%u) is %u which is larger "
+                            "than maxBindingsPerBindGroup (%u).",
+                            i, arraySize, bindingNumber,
+                            uint32_t(arraySize) + uint32_t(bindingNumber),
+                            kMaxBindingsPerBindGroupTyped);
+        }
+
+        // Check that the same binding is not set twice. bindingNumber + arraySize cannot overflow
+        // as they are both smaller than kMaxBindingsPerBindGroupTyped.
+        static_assert(kMaxBindingsPerBindGroup < std::numeric_limits<uint32_t>::max() / 2);
+        for (BindingNumber usedBinding : Range(bindingNumber, bindingNumber + arraySize)) {
+            DAWN_INVALID_IF(bindingMap.count(usedBinding) != 0,
+                            "On entries[%u]: binding index (%u) was specified by a previous entry.",
+                            i, entry->binding);
+            bindingMap.insert({usedBinding, i});
+        }
 
         DAWN_TRY_CONTEXT(ValidateBindGroupLayoutEntry(device, entry, allowInternalBinding),
                          "validating entries[%u]", i);
 
         IncrementBindingCounts(&bindingCounts, entry);
-
-        bindingMap.insert({bindingNumber, i});
     }
 
     // Perform a second validation pass for static samplers. This is done after initial validation
diff --git a/src/dawn/native/BindingInfo.cpp b/src/dawn/native/BindingInfo.cpp
index ca1c442..3f04c74 100644
--- a/src/dawn/native/BindingInfo.cpp
+++ b/src/dawn/native/BindingInfo.cpp
@@ -54,22 +54,27 @@
 
 void IncrementBindingCounts(BindingCounts* bindingCounts,
                             const UnpackedPtr<BindGroupLayoutEntry>& entry) {
-    bindingCounts->totalCount += 1;
+    uint32_t arraySize = 1;
+    if (const auto* arraySizeInfo = entry.Get<BindGroupLayoutEntryArraySize>()) {
+        arraySize = arraySizeInfo->arraySize;
+    }
+
+    bindingCounts->totalCount += arraySize;
 
     uint32_t PerStageBindingCounts::*perStageBindingCountMember = nullptr;
 
     if (entry->buffer.type != wgpu::BufferBindingType::BindingNotUsed) {
-        ++bindingCounts->bufferCount;
+        bindingCounts->bufferCount += arraySize;
         const BufferBindingLayout& buffer = entry->buffer;
 
         if (buffer.minBindingSize == 0) {
-            ++bindingCounts->unverifiedBufferCount;
+            bindingCounts->unverifiedBufferCount += arraySize;
         }
 
         switch (buffer.type) {
             case wgpu::BufferBindingType::Uniform:
                 if (buffer.hasDynamicOffset) {
-                    ++bindingCounts->dynamicUniformBufferCount;
+                    bindingCounts->dynamicUniformBufferCount += arraySize;
                 }
                 perStageBindingCountMember = &PerStageBindingCounts::uniformBufferCount;
                 break;
@@ -79,7 +84,7 @@
             case kInternalReadOnlyStorageBufferBinding:
             case wgpu::BufferBindingType::ReadOnlyStorage:
                 if (buffer.hasDynamicOffset) {
-                    ++bindingCounts->dynamicStorageBufferCount;
+                    bindingCounts->dynamicStorageBufferCount += arraySize;
                 }
                 perStageBindingCountMember = &PerStageBindingCounts::storageBufferCount;
                 break;
@@ -110,7 +115,7 @@
 
     DAWN_ASSERT(perStageBindingCountMember != nullptr);
     for (SingleShaderStage stage : IterateStages(entry->visibility)) {
-        ++(bindingCounts->perStage[stage].*perStageBindingCountMember);
+        (bindingCounts->perStage[stage].*perStageBindingCountMember) += arraySize;
     }
 }
 
diff --git a/src/dawn/native/Toggles.cpp b/src/dawn/native/Toggles.cpp
index 715b2d5..d85eb16 100644
--- a/src/dawn/native/Toggles.cpp
+++ b/src/dawn/native/Toggles.cpp
@@ -153,6 +153,10 @@
      {"disable_sample_variables",
       "Disables gl_SampleMask and related functionality which is unsupported on some platforms.",
       "https://crbug.com/dawn/673", ToggleStage::Device}},
+    {Toggle::DisableBindGroupLayoutEntryArraySize,
+     {"disable_bind_group_layout_entry_array_size",
+      "Disable uses of wgpu::BindGroupLayoutEntryArraySize.",
+      "https://issues.chromium.org/393558555", ToggleStage::Device}},
     {Toggle::UseD3D12SmallShaderVisibleHeapForTesting,
      {"use_d3d12_small_shader_visible_heap",
       "Enable use of a small D3D12 shader visible heap, instead of using a large one by default. "
diff --git a/src/dawn/native/Toggles.h b/src/dawn/native/Toggles.h
index a7781b0..0d86dce 100644
--- a/src/dawn/native/Toggles.h
+++ b/src/dawn/native/Toggles.h
@@ -61,6 +61,7 @@
     DisableBaseInstance,
     DisableIndexedDrawBuffers,
     DisableSampleVariables,
+    DisableBindGroupLayoutEntryArraySize,
     UseD3D12SmallShaderVisibleHeapForTesting,
     UseDXC,
     DisableRobustness,
diff --git a/src/dawn/tests/unittests/validation/BindGroupValidationTests.cpp b/src/dawn/tests/unittests/validation/BindGroupValidationTests.cpp
index ed8340b..75a4e76 100644
--- a/src/dawn/tests/unittests/validation/BindGroupValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/BindGroupValidationTests.cpp
@@ -25,6 +25,7 @@
 // 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 <limits>
 #include <tuple>
 #include <utility>
 #include <vector>
@@ -1797,6 +1798,386 @@
     ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
 }
 
+// Control case for a valid use of wgpu::BindGroupLayoutEntryArraySize
+TEST_F(BindGroupLayoutValidationTest, ArraySizeSuccess) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+    arraySize.arraySize = 1;
+
+    wgpu::BindGroupLayoutEntry entry;
+    entry.binding = 0;
+    entry.visibility = wgpu::ShaderStage::Fragment;
+    entry.texture.sampleType = wgpu::TextureSampleType::Float;
+    entry.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutDescriptor desc;
+    desc.entryCount = 1;
+    desc.entries = &entry;
+    device.CreateBindGroupLayout(&desc);
+}
+
+class BindGroupLayoutArraySizeDisabledValidationTest : public BindGroupValidationTest {
+  protected:
+    std::vector<const char*> GetEnabledToggles() override {
+        return {"disable_bind_group_layout_entry_array_size"};
+    }
+};
+
+// Check that using a wgpu::BindGroupLayoutEntryArraySize is disallowed by that toggle.
+TEST_F(BindGroupLayoutArraySizeDisabledValidationTest, ArraySizeDisabled) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+    arraySize.arraySize = 1;
+
+    wgpu::BindGroupLayoutEntry entry;
+    entry.binding = 0;
+    entry.visibility = wgpu::ShaderStage::Fragment;
+    entry.texture.sampleType = wgpu::TextureSampleType::Float;
+    entry.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutDescriptor desc;
+    desc.entryCount = 1;
+    desc.entries = &entry;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
+// Check that using arraySize != 1 is only allowed for sampled textures.
+TEST_F(BindGroupLayoutValidationTest, ArraySizeAllowedBindingTypes) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize1;
+    arraySize1.arraySize = 1;
+
+    wgpu::BindGroupLayoutEntryArraySize arraySize2;
+    arraySize2.arraySize = 2;
+
+    // Sampled texture
+    {
+        wgpu::BindGroupLayoutEntry entry;
+        entry.binding = 0;
+        entry.visibility = wgpu::ShaderStage::Fragment;
+        entry.texture.sampleType = wgpu::TextureSampleType::Float;
+
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 1;
+        desc.entries = &entry;
+
+        // Success case, arraySize 1
+        entry.nextInChain = &arraySize1;
+        device.CreateBindGroupLayout(&desc);
+        // Second success case, arraySize 2
+        entry.nextInChain = &arraySize2;
+        device.CreateBindGroupLayout(&desc);
+    }
+
+    // Uniform buffer
+    {
+        wgpu::BindGroupLayoutEntry entry;
+        entry.binding = 0;
+        entry.visibility = wgpu::ShaderStage::Fragment;
+        entry.buffer.type = wgpu::BufferBindingType::Uniform;
+
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 1;
+        desc.entries = &entry;
+
+        // Success case, arraySize 1
+        entry.nextInChain = &arraySize1;
+        device.CreateBindGroupLayout(&desc);
+        // Error case, arraySize 2
+        entry.nextInChain = &arraySize2;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+    }
+
+    // Storage buffer
+    {
+        wgpu::BindGroupLayoutEntry entry;
+        entry.binding = 0;
+        entry.visibility = wgpu::ShaderStage::Fragment;
+        entry.buffer.type = wgpu::BufferBindingType::Uniform;
+
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 1;
+        desc.entries = &entry;
+
+        // Success case, arraySize 1
+        entry.nextInChain = &arraySize1;
+        device.CreateBindGroupLayout(&desc);
+        // Error case, arraySize 2
+        entry.nextInChain = &arraySize2;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+    }
+
+    // Storage texture
+    {
+        wgpu::BindGroupLayoutEntry entry;
+        entry.binding = 0;
+        entry.visibility = wgpu::ShaderStage::Fragment;
+        entry.storageTexture.format = wgpu::TextureFormat::R32Uint;
+        entry.storageTexture.access = wgpu::StorageTextureAccess::ReadOnly;
+
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 1;
+        desc.entries = &entry;
+
+        // Success case, arraySize 1
+        entry.nextInChain = &arraySize1;
+        device.CreateBindGroupLayout(&desc);
+        // Error case, arraySize 2
+        entry.nextInChain = &arraySize2;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+    }
+
+    // Sampler
+    {
+        wgpu::BindGroupLayoutEntry entry;
+        entry.binding = 0;
+        entry.visibility = wgpu::ShaderStage::Fragment;
+        entry.sampler.type = wgpu::SamplerBindingType::Filtering;
+
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 1;
+        desc.entries = &entry;
+
+        // Success case, arraySize 1
+        entry.nextInChain = &arraySize1;
+        device.CreateBindGroupLayout(&desc);
+        // Error case, arraySize 2
+        entry.nextInChain = &arraySize2;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+    }
+}
+
+// Check that arraySize = 0 is not allowed.
+TEST_F(BindGroupLayoutValidationTest, ArraySizeZero) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+
+    wgpu::BindGroupLayoutEntry entry;
+    entry.binding = 0;
+    entry.visibility = wgpu::ShaderStage::Fragment;
+    entry.texture.sampleType = wgpu::TextureSampleType::Float;
+    entry.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutDescriptor desc;
+    desc.entryCount = 1;
+    desc.entries = &entry;
+
+    // Success case, arraySize is 1
+    arraySize.arraySize = 1;
+    device.CreateBindGroupLayout(&desc);
+
+    // Error case, arraySize is 0
+    arraySize.arraySize = 0;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
+// Check that binding + arraySize must fit in maxBindingsPerBindGroup
+TEST_F(BindGroupLayoutValidationTest, ArrayEndPastMaxBindingsPerBindGroup) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+
+    wgpu::BindGroupLayoutEntry entry;
+    entry.binding = kMaxBindingsPerBindGroup - 1;
+    entry.visibility = wgpu::ShaderStage::Fragment;
+    entry.texture.sampleType = wgpu::TextureSampleType::Float;
+    entry.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutDescriptor desc;
+    desc.entryCount = 1;
+    desc.entries = &entry;
+
+    // Success case, arraySize is 1 so the last used binding is maxBindingsPerBindGroup
+    arraySize.arraySize = 1;
+    device.CreateBindGroupLayout(&desc);
+
+    // Error case, arraySize is 2 so we go past maxBindingsPerBindGroup
+    arraySize.arraySize = 2;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
+// Check that arraySize + binding overflowing is caught by some validation.
+TEST_F(BindGroupLayoutValidationTest, ArraySizePlusBindingOverflow) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+    arraySize.arraySize = std::numeric_limits<uint32_t>::max();
+
+    wgpu::BindGroupLayoutEntry entry;
+    entry.binding = 3;
+    entry.visibility = wgpu::ShaderStage::Fragment;
+    entry.texture.sampleType = wgpu::TextureSampleType::Float;
+    entry.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutDescriptor desc;
+    desc.entryCount = 1;
+    desc.entries = &entry;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
+// Check that arraySize is taken into account when looking for duplicate bindings.
+TEST_F(BindGroupLayoutValidationTest, ArraySizeDuplicateBindings) {
+    const uint32_t kPlaceholderBinding = 42;  // will be replaced before each check.
+
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+    arraySize.arraySize = 3;
+
+    // Check single entry, then an array that overlaps it.
+    {
+        wgpu::BindGroupLayoutEntry entries[2] = {
+            {
+                .binding = kPlaceholderBinding,
+                .visibility = wgpu::ShaderStage::Fragment,
+                .texture = {.sampleType = wgpu::TextureSampleType::Float},
+            },
+            {
+                .nextInChain = &arraySize,
+                .binding = 1,
+                .visibility = wgpu::ShaderStage::Fragment,
+                .texture = {.sampleType = wgpu::TextureSampleType::Float},
+            },
+        };
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 2;
+        desc.entries = entries;
+
+        // Success cases, 0 and 4 are not in [1, 4)
+        entries[0].binding = 0;
+        device.CreateBindGroupLayout(&desc);
+        entries[0].binding = 4;
+        device.CreateBindGroupLayout(&desc);
+
+        // Error cases, 1, 2, 3 are in [1, 4)
+        entries[0].binding = 1;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[0].binding = 2;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[0].binding = 3;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+    }
+
+    // Check an array, then a single entry that goes in it.
+    {
+        wgpu::BindGroupLayoutEntry entries[2] = {
+            {
+                .nextInChain = &arraySize,
+                .binding = 1,
+                .visibility = wgpu::ShaderStage::Fragment,
+                .texture = {.sampleType = wgpu::TextureSampleType::Float},
+            },
+            {
+                .binding = kPlaceholderBinding,
+                .visibility = wgpu::ShaderStage::Fragment,
+                .texture = {.sampleType = wgpu::TextureSampleType::Float},
+            },
+        };
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 2;
+        desc.entries = entries;
+
+        // Success cases, 0 and 4 are not in [1, 4)
+        entries[1].binding = 0;
+        device.CreateBindGroupLayout(&desc);
+        entries[1].binding = 4;
+        device.CreateBindGroupLayout(&desc);
+
+        // Error cases, 1, 2, 3 are in [1, 4)
+        entries[1].binding = 1;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[1].binding = 2;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[1].binding = 3;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+    }
+
+    // Check two arrays that overlap
+    {
+        wgpu::BindGroupLayoutEntry entries[2] = {
+            {
+                .nextInChain = &arraySize,
+                .binding = kPlaceholderBinding,
+                .visibility = wgpu::ShaderStage::Fragment,
+                .texture = {.sampleType = wgpu::TextureSampleType::Float},
+            },
+            {
+                .nextInChain = &arraySize,
+                .binding = 3,
+                .visibility = wgpu::ShaderStage::Fragment,
+                .texture = {.sampleType = wgpu::TextureSampleType::Float},
+            },
+        };
+        wgpu::BindGroupLayoutDescriptor desc;
+        desc.entryCount = 2;
+        desc.entries = entries;
+
+        // Success cases, 0 and 6 make the arrays not overlap
+        entries[0].binding = 0;
+        device.CreateBindGroupLayout(&desc);
+        entries[0].binding = 6;
+        device.CreateBindGroupLayout(&desc);
+
+        // Error cases, 1, 2, 3, 4, 5 make the arrays overlap
+        entries[0].binding = 1;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[0].binding = 2;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[0].binding = 3;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[0].binding = 4;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+        entries[0].binding = 5;
+        ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+    }
+}
+
+// Check that arraySize counts towards the binding limits.
+TEST_F(BindGroupLayoutValidationTest, ArraySizeCountsTowardsLimit) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+
+    wgpu::BindGroupLayoutEntry entries[2] = {
+        {
+            .nextInChain = &arraySize,
+            .binding = 1,
+            .visibility = wgpu::ShaderStage::Fragment,
+            .texture = {.sampleType = wgpu::TextureSampleType::Float},
+        },
+        {
+            .binding = 0,
+            .visibility = wgpu::ShaderStage::Fragment,
+            .texture = {.sampleType = wgpu::TextureSampleType::Float},
+        },
+    };
+    wgpu::BindGroupLayoutDescriptor desc;
+    desc.entryCount = 2;
+    desc.entries = entries;
+
+    wgpu::Limits limits = GetSupportedLimits();
+
+    // Success case: we are just at the limit with the arraySize.
+    arraySize.arraySize = limits.maxSampledTexturesPerShaderStage - 1;
+    device.CreateBindGroupLayout(&desc);
+
+    // Error case: we are just above the limit with the arraySize.
+    arraySize.arraySize = limits.maxSampledTexturesPerShaderStage;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
+// Test that only arraySize = 1 is allowed for external textures
+TEST_F(BindGroupLayoutValidationTest, ExternalTextureWithArraySize) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+
+    wgpu::ExternalTextureBindingLayout externalTextureLayout = {};
+    externalTextureLayout.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutEntry binding = {};
+    binding.binding = 0;
+    binding.nextInChain = &externalTextureLayout;
+
+    wgpu::BindGroupLayoutDescriptor desc = {};
+    desc.entryCount = 1;
+    desc.entries = &binding;
+
+    // Success case, arraySize is 1.
+    arraySize.arraySize = 1;
+    device.CreateBindGroupLayout(&desc);
+
+    // Error case, arraySize is not 1.
+    arraySize.arraySize = 2;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
 class BindGroupLayoutWithStaticSamplersValidationTest : public BindGroupLayoutValidationTest {
     std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
         return {wgpu::FeatureName::StaticSamplers};
@@ -2006,6 +2387,31 @@
         utils::MakeBindGroup(device, layout, {{0, device.CreateSampler(&samplerDesc)}}));
 }
 
+// Test that only arraySize = 1 is allowed for static samplers
+TEST_F(BindGroupLayoutWithStaticSamplersValidationTest, StaticSamplerWithArraySize) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+
+    wgpu::StaticSamplerBindingLayout staticSamplerBinding = {};
+    staticSamplerBinding.sampler = device.CreateSampler();
+    staticSamplerBinding.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutEntry binding = {};
+    binding.binding = 0;
+    binding.nextInChain = &staticSamplerBinding;
+
+    wgpu::BindGroupLayoutDescriptor desc = {};
+    desc.entryCount = 1;
+    desc.entries = &binding;
+
+    // Success case, arraySize is 1.
+    arraySize.arraySize = 1;
+    device.CreateBindGroupLayout(&desc);
+
+    // Error case, arraySize is not 1.
+    arraySize.arraySize = 2;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
 constexpr uint32_t kBindingSize = 8;
 
 class SetBindGroupValidationTest : public ValidationTest {
diff --git a/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp b/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp
index de7ee23..b42ad63 100644
--- a/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/UnsafeAPIValidationTests.cpp
@@ -64,6 +64,23 @@
     )"));
 }
 
+// Check that using a wgpu::BindGroupLayoutEntryArraySize is an unsafe API.
+TEST_F(UnsafeAPIValidationTest, BindGroupLayoutEntryArraySize) {
+    wgpu::BindGroupLayoutEntryArraySize arraySize;
+    arraySize.arraySize = 1;
+
+    wgpu::BindGroupLayoutEntry entry;
+    entry.binding = 0;
+    entry.visibility = wgpu::ShaderStage::Fragment;
+    entry.texture.sampleType = wgpu::TextureSampleType::Float;
+    entry.nextInChain = &arraySize;
+
+    wgpu::BindGroupLayoutDescriptor desc;
+    desc.entryCount = 1;
+    desc.entries = &entry;
+    ASSERT_DEVICE_ERROR(device.CreateBindGroupLayout(&desc));
+}
+
 class TimestampQueryUnsafeAPIValidationTest : public ValidationTest {
   protected:
     std::vector<const char*> GetDisabledToggles() override { return {"allow_unsafe_apis"}; }
diff --git a/tools/android/BUILD.gn b/tools/android/BUILD.gn
index 4ac723a..3d3969e 100644
--- a/tools/android/BUILD.gn
+++ b/tools/android/BUILD.gn
@@ -54,6 +54,7 @@
     "java/android/dawn/BindGroupLayout.kt",
     "java/android/dawn/BindGroupLayoutDescriptor.kt",
     "java/android/dawn/BindGroupLayoutEntry.kt",
+    "java/android/dawn/BindGroupLayoutEntryArraySize.kt",
     "java/android/dawn/BlendComponent.kt",
     "java/android/dawn/BlendFactor.kt",
     "java/android/dawn/BlendOperation.kt",