Allow changing Vulkan allocator heap block size

The Vulkan ResourceMemoryAllocator defaults to a 8mb block size. That's
significantly different than VMA heap block size of 64kb used by VMA
with Ganesh/Vulkan. Graphite/Dawn/Vulkan wants to configure this
similarly so add a chainable struct wgpu::DawnDeviceAllocatorControl
that allows configuring the heap block size.

Bug: 407730048
Change-Id: Ib08f1010bf7be2014d8859631bd0848f79b49f98
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/235614
Reviewed-by: Loko Kung <lokokung@google.com>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Kyle Charbonneau <kylechar@google.com>
diff --git a/docs/dawn/features/dawn_device_allocator_control.md b/docs/dawn/features/dawn_device_allocator_control.md
new file mode 100644
index 0000000..0e764f1
--- /dev/null
+++ b/docs/dawn/features/dawn_device_allocator_control.md
@@ -0,0 +1,7 @@
+# Dawn Device Allocator Control (experimental)
+
+This feature allows `wgpu::DawnDeviceAllocatorControl` to be chained on `wgpu::DeviceDescriptor`. If
+`wgpu::DawnDeviceAllocatorControl` is chained and `wgpu::FeatureName::DawnDeviceAllocatorControl` is
+not in `requiredFeatures` then creating the device will fail with a validation error.
+
+It is available in the Vulkan backend.
diff --git a/src/dawn/dawn.json b/src/dawn/dawn.json
index b630980..fed5999 100644
--- a/src/dawn/dawn.json
+++ b/src/dawn/dawn.json
@@ -264,6 +264,15 @@
             {"name": "function userdata", "type": "void *", "default": "nullptr"}
         ]
     },
+    "dawn device allocator control": {
+      "tags": ["dawn"],
+      "category": "structure",
+      "chained": "in",
+      "chain roots": ["device descriptor"],
+      "members": [
+          {"name": "allocator heap block size", "type": "size_t", "default": 0}
+      ]
+    },
     "dawn WGSL blocklist": {
         "tags": ["dawn", "native"],
         "category": "structure",
@@ -2311,7 +2320,8 @@
             {"value": 53, "name": "dawn texel copy buffer row alignment", "tags": ["dawn"]},
             {"value": 54, "name": "flexible texture views", "tags": ["dawn"]},
             {"value": 55, "name": "chromium experimental subgroup matrix", "tags": ["dawn"]},
-            {"value": 56, "name": "shared fence EGL sync", "tags": ["dawn", "native"]}
+            {"value": 56, "name": "shared fence EGL sync", "tags": ["dawn", "native"]},
+            {"value": 57, "name": "dawn device allocator control", "tags": ["dawn"]}
         ]
     },
     "filter mode": {
@@ -3834,7 +3844,8 @@
             {"value": 62, "name": "dawn injected invalid s type", "tags": ["dawn"]},
             {"value": 63, "name": "dawn compilation message utf16", "tags": ["dawn", "emscripten"]},
             {"value": 64, "name": "dawn fake buffer OOM for testing", "tags": ["dawn"]},
-            {"value": 65, "name": "surface descriptor from windows WinUI swap chain panel", "tags": ["dawn"]}
+            {"value": 65, "name": "surface descriptor from windows WinUI swap chain panel", "tags": ["dawn"]},
+            {"value": 66, "name": "dawn device allocator control", "tags": ["dawn"]}
         ]
     },
     "texture": {
diff --git a/src/dawn/native/Adapter.cpp b/src/dawn/native/Adapter.cpp
index e4e14a3..6be0557 100644
--- a/src/dawn/native/Adapter.cpp
+++ b/src/dawn/native/Adapter.cpp
@@ -35,9 +35,11 @@
 #include <utility>
 #include <vector>
 
+#include "dawn/common/Math.h"
 #include "dawn/common/StringViewUtils.h"
 #include "dawn/native/ChainUtils.h"
 #include "dawn/native/Device.h"
+#include "dawn/native/Error.h"
 #include "dawn/native/Instance.h"
 #include "dawn/native/PhysicalDevice.h"
 #include "partition_alloc/pointers/raw_ptr.h"
@@ -345,6 +347,15 @@
         DAWN_TRY_CONTEXT(ValidateLimits(GetLimits(), requiredLimits), "validating required limits");
     }
 
+    if (auto* allocatorDesc = descriptor.Get<DawnDeviceAllocatorControl>()) {
+        DAWN_INVALID_IF(!requiredFeatureSet.count(wgpu::FeatureName::DawnDeviceAllocatorControl),
+                        "%s is not enabled.", wgpu::FeatureName::DawnDeviceAllocatorControl);
+
+        DAWN_INVALID_IF(!IsPowerOfTwo(allocatorDesc->allocatorHeapBlockSize),
+                        "allocator heap block size (%d) isn't a power of two.",
+                        allocatorDesc->allocatorHeapBlockSize);
+    }
+
     return mPhysicalDevice->CreateDevice(this, descriptor, deviceToggles, std::move(lostEvent));
 }
 
diff --git a/src/dawn/native/Features.cpp b/src/dawn/native/Features.cpp
index 403b9c8..469f201 100644
--- a/src/dawn/native/Features.cpp
+++ b/src/dawn/native/Features.cpp
@@ -404,7 +404,13 @@
       FeatureInfo::FeatureState::Stable}},
     {Feature::ChromiumExperimentalSubgroupMatrix,
      {"Support the \"enable chromium_experimental_subgroup_matrix;\" directive in WGSL.",
-      "https://github.com/gpuweb/gpuweb/issues/4195", FeatureInfo::FeatureState::Experimental}}};
+      "https://github.com/gpuweb/gpuweb/issues/4195", FeatureInfo::FeatureState::Experimental}},
+    {Feature::DawnDeviceAllocatorControl,
+     {"Supports configuring device allocator via DawnDeviceAllocatorControl",
+      "https://dawn.googlesource.com/dawn/+/refs/heads/main/docs/dawn/features/"
+      "dawn_device_allocator_control.md",
+      FeatureInfo::FeatureState::Experimental}}};
+
 }  // anonymous namespace
 
 void FeaturesSet::EnableFeature(Feature feature) {
diff --git a/src/dawn/native/vulkan/DeviceVk.cpp b/src/dawn/native/vulkan/DeviceVk.cpp
index 486f4e8..9719550 100644
--- a/src/dawn/native/vulkan/DeviceVk.cpp
+++ b/src/dawn/native/vulkan/DeviceVk.cpp
@@ -142,7 +142,11 @@
     }
 
     mRenderPassCache = std::make_unique<RenderPassCache>(this);
-    mResourceMemoryAllocator = std::make_unique<MutexProtected<ResourceMemoryAllocator>>(this);
+
+    VkDeviceSize heapBlockSize =
+        ResourceMemoryAllocator::GetHeapBlockSize(descriptor.Get<DawnDeviceAllocatorControl>());
+    mResourceMemoryAllocator =
+        std::make_unique<MutexProtected<ResourceMemoryAllocator>>(this, heapBlockSize);
 
     mExternalMemoryService = std::make_unique<external_memory::Service>(this);
 
diff --git a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
index 9a987e2..2832a54 100644
--- a/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
+++ b/src/dawn/native/vulkan/PhysicalDeviceVk.cpp
@@ -224,6 +224,7 @@
     EnableFeature(Feature::AdapterPropertiesMemoryHeaps);
     EnableFeature(Feature::StaticSamplers);
     EnableFeature(Feature::FlexibleTextureViews);
+    EnableFeature(Feature::DawnDeviceAllocatorControl);
 
     // Initialize supported extensions
     if (mDeviceInfo.features.textureCompressionBC == VK_TRUE) {
diff --git a/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.cpp b/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.cpp
index 1c4cd52..31de727 100644
--- a/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.cpp
+++ b/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.cpp
@@ -44,14 +44,13 @@
 
 namespace {
 
-// TODO(crbug.com/dawn/849): This is a hardcoded heurstic to choose when to
-// suballocate but it should ideally depend on the size of the memory heaps and other
-// factors.
-constexpr uint64_t kMaxSizeForSubAllocation = 4ull * 1024ull * 1024ull;  // 4MiB
-
-// Have each bucket of the buddy system allocate at least some resource of the maximum
-// size
-constexpr uint64_t kBuddyHeapsSize = 2 * kMaxSizeForSubAllocation;
+VkDeviceSize GetMaxSuballocationSize(VkDeviceSize heapBlockSize) {
+    // Have each bucket of the buddy system allocate at least some resource of the maximum
+    // size
+    // TODO(crbug.com/dawn/849): This is a hardcoded heuristic to choose when to suballocate but it
+    // should ideally depend on the size of the memory heaps and other factors.
+    return heapBlockSize / 2;
+}
 
 bool IsMemoryKindMappable(MemoryKind memoryKind) {
     return memoryKind & (MemoryKind::ReadMappable | MemoryKind::WriteMappable);
@@ -103,21 +102,22 @@
   public:
     SingleTypeAllocator(Device* device,
                         size_t memoryTypeIndex,
-                        VkDeviceSize memoryHeapSize,
+                        VkDeviceSize maxHeapSize,
+                        VkDeviceSize heapBlockSize,
                         ResourceMemoryAllocator* memoryAllocator)
         : mDevice(device),
           mResourceMemoryAllocator(memoryAllocator),
           mMemoryTypeIndex(memoryTypeIndex),
-          mMemoryHeapSize(memoryHeapSize),
+          mMaxHeapSize(maxHeapSize),
           mPooledMemoryAllocator(this),
           mBuddySystem(
               // Round down to a power of 2 that's <= mMemoryHeapSize. This will always
-              // be a multiple of kBuddyHeapsSize because kBuddyHeapsSize is a power of 2.
-              uint64_t(1) << Log2(mMemoryHeapSize),
+              // be a multiple of heapBlockSize because heapBlockSize is a power of 2.
+              uint64_t(1) << Log2(mMaxHeapSize),
               // Take the min in the very unlikely case the memory heap is tiny.
-              std::min(uint64_t(1) << Log2(mMemoryHeapSize), kBuddyHeapsSize),
+              std::min(uint64_t(1) << Log2(mMaxHeapSize), heapBlockSize),
               &mPooledMemoryAllocator) {
-        DAWN_ASSERT(IsPowerOfTwo(kBuddyHeapsSize));
+        DAWN_ASSERT(IsPowerOfTwo(heapBlockSize));
     }
     ~SingleTypeAllocator() override = default;
 
@@ -135,7 +135,7 @@
     // Implementation of the MemoryAllocator interface to be a client of BuddyMemoryAllocator
 
     ResultOrError<std::unique_ptr<ResourceHeapBase>> AllocateResourceHeap(uint64_t size) override {
-        if (size > mMemoryHeapSize) {
+        if (size > mMaxHeapSize) {
             return DAWN_OUT_OF_MEMORY_ERROR("Allocation size too large");
         }
 
@@ -166,20 +166,30 @@
     raw_ptr<Device> mDevice;
     raw_ptr<ResourceMemoryAllocator> mResourceMemoryAllocator;
     size_t mMemoryTypeIndex;
-    VkDeviceSize mMemoryHeapSize;
+    VkDeviceSize mMaxHeapSize;
     PooledResourceMemoryAllocator mPooledMemoryAllocator;
     BuddyMemoryAllocator mBuddySystem;
 };
 
-// Implementation of ResourceMemoryAllocator
+VkDeviceSize ResourceMemoryAllocator::GetHeapBlockSize(const DawnDeviceAllocatorControl* control) {
+    static constexpr VkDeviceSize kDefaultHeapBlockSize = 8ull * 1024ull * 1024ull;  // 8MiB
+    VkDeviceSize heapBlockSize = kDefaultHeapBlockSize;
+    if (control && control->allocatorHeapBlockSize > 0) {
+        heapBlockSize = control->allocatorHeapBlockSize;
+    }
+    DAWN_ASSERT(IsPowerOfTwo(heapBlockSize));
+    return heapBlockSize;
+}
 
-ResourceMemoryAllocator::ResourceMemoryAllocator(Device* device) : mDevice(device) {
+// Implementation of ResourceMemoryAllocator
+ResourceMemoryAllocator::ResourceMemoryAllocator(Device* device, VkDeviceSize heapBlockSize)
+    : mDevice(device), mMaxSizeForSuballocation(GetMaxSuballocationSize(heapBlockSize)) {
     const VulkanDeviceInfo& info = mDevice->GetDeviceInfo();
     mAllocatorsPerType.reserve(info.memoryTypes.size());
 
     for (size_t i = 0; i < info.memoryTypes.size(); i++) {
         mAllocatorsPerType.emplace_back(std::make_unique<SingleTypeAllocator>(
-            mDevice, i, info.memoryHeaps[info.memoryTypes[i].heapIndex].size, this));
+            mDevice, i, info.memoryHeaps[info.memoryTypes[i].heapIndex].size, heapBlockSize, this));
     }
 }
 
@@ -198,7 +208,7 @@
     // Sub-allocate non-mappable resources because at the moment the mapped pointer
     // is part of the resource and not the heap, which doesn't match the Vulkan model.
     // TODO(crbug.com/dawn/849): allow sub-allocating mappable resources, maybe.
-    if (!forceDisableSubAllocation && requirements.size < kMaxSizeForSubAllocation &&
+    if (!forceDisableSubAllocation && requirements.size < mMaxSizeForSuballocation &&
         !IsMemoryKindMappable(kind) &&
         !mDevice->IsToggleEnabled(Toggle::DisableResourceSuballocation)) {
         // When sub-allocating, Vulkan requires that we respect bufferImageGranularity. Some
diff --git a/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.h b/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.h
index ccd3f9f..d80087e 100644
--- a/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.h
+++ b/src/dawn/native/vulkan/ResourceMemoryAllocatorVk.h
@@ -61,7 +61,11 @@
 
 class ResourceMemoryAllocator {
   public:
-    explicit ResourceMemoryAllocator(Device* device);
+    // Returns heap block size as specified by `control` or the default value if not.
+    static VkDeviceSize GetHeapBlockSize(const DawnDeviceAllocatorControl* control);
+
+    // `heapBlockSize` must be a power of two.
+    ResourceMemoryAllocator(Device* device, VkDeviceSize heapBlockSize);
     ~ResourceMemoryAllocator();
 
     ResultOrError<ResourceMemoryAllocation> Allocate(const VkMemoryRequirements& requirements,
@@ -88,6 +92,7 @@
 
   private:
     raw_ptr<Device> mDevice;
+    const VkDeviceSize mMaxSizeForSuballocation;
 
     class SingleTypeAllocator;
     std::vector<std::unique_ptr<SingleTypeAllocator>> mAllocatorsPerType;
diff --git a/src/dawn/tests/unittests/native/DeviceCreationTests.cpp b/src/dawn/tests/unittests/native/DeviceCreationTests.cpp
index b103406..56bee86 100644
--- a/src/dawn/tests/unittests/native/DeviceCreationTests.cpp
+++ b/src/dawn/tests/unittests/native/DeviceCreationTests.cpp
@@ -114,6 +114,50 @@
     EXPECT_NE(device, nullptr);
 }
 
+// Test successful call to CreateDevice with allocator descriptor.
+TEST_F(DeviceCreationTest, CreateDeviceWithAllocatorSuccess) {
+    wgpu::DawnDeviceAllocatorControl allocationDesc = {};
+    allocationDesc.allocatorHeapBlockSize = 4 * 1024;
+
+    wgpu::DeviceDescriptor desc = {};
+    wgpu::FeatureName feature = wgpu::FeatureName::DawnDeviceAllocatorControl;
+    desc.requiredFeatures = &feature;
+    desc.requiredFeatureCount = 1;
+    desc.nextInChain = &allocationDesc;
+
+    wgpu::Device device = unsafeAdapter.CreateDevice(&desc);
+    EXPECT_NE(device, nullptr);
+}
+
+// Test failed call to CreateDevice with allocator descriptor. This is using an adapter that does
+// not have DawnDeviceAllocatorControl feature enabled.
+TEST_F(DeviceCreationTest, CreateDeviceWithAllocatorFailedMissingFeature) {
+    wgpu::DawnDeviceAllocatorControl allocationDesc = {};
+    allocationDesc.allocatorHeapBlockSize = 4 * 1024;
+
+    wgpu::DeviceDescriptor desc = {};
+    desc.nextInChain = &allocationDesc;
+
+    wgpu::Device device = adapter.CreateDevice(&desc);
+    EXPECT_EQ(device, nullptr);
+}
+
+// Test failed call to CreateDevice with allocator descriptor. The heap block size provided is not a
+// power of two.
+TEST_F(DeviceCreationTest, CreateDeviceWithAllocatorFailedHeapBlockSize) {
+    wgpu::DawnDeviceAllocatorControl allocationDesc = {};
+    allocationDesc.allocatorHeapBlockSize = 4 * 1024 + 1;
+
+    wgpu::DeviceDescriptor desc = {};
+    wgpu::FeatureName feature = wgpu::FeatureName::DawnDeviceAllocatorControl;
+    desc.requiredFeatures = &feature;
+    desc.requiredFeatureCount = 1;
+    desc.nextInChain = &allocationDesc;
+
+    wgpu::Device device = unsafeAdapter.CreateDevice(&desc);
+    EXPECT_EQ(device, nullptr);
+}
+
 // Test successful call to CreateDevice with toggle descriptor.
 TEST_F(DeviceCreationTest, CreateDeviceWithTogglesSuccess) {
     wgpu::DeviceDescriptor desc = {};
diff --git a/src/dawn/wire/SupportedFeatures.cpp b/src/dawn/wire/SupportedFeatures.cpp
index efc6052..3786b3e 100644
--- a/src/dawn/wire/SupportedFeatures.cpp
+++ b/src/dawn/wire/SupportedFeatures.cpp
@@ -118,6 +118,7 @@
         case WGPUFeatureName_FlexibleTextureViews:
         case WGPUFeatureName_ChromiumExperimentalSubgroupMatrix:
         case WGPUFeatureName_CoreFeaturesAndLimits:
+        case WGPUFeatureName_DawnDeviceAllocatorControl:
             return true;
     }