Use a descriptor for PipelineLayout (#206)

Adds support for structures inside descriptors.
diff --git a/src/backend/CommandBufferStateTracker.cpp b/src/backend/CommandBufferStateTracker.cpp
index 81971f9..7503b05 100644
--- a/src/backend/CommandBufferStateTracker.cpp
+++ b/src/backend/CommandBufferStateTracker.cpp
@@ -395,7 +395,8 @@
         for (size_t i = 0; i < mBindgroups.size(); ++i) {
             if (auto* bindgroup = mBindgroups[i]) {
                 // TODO(kainino@chromium.org): bind group compatibility
-                if (bindgroup->GetLayout() != mLastPipeline->GetLayout()->GetBindGroupLayout(i)) {
+                auto* pipelineBGL = mLastPipeline->GetLayout()->GetBindGroupLayout(i);
+                if (pipelineBGL && bindgroup->GetLayout() != pipelineBGL) {
                     return false;
                 }
             }
diff --git a/src/backend/Device.cpp b/src/backend/Device.cpp
index 8b08a40..360ae99 100644
--- a/src/backend/Device.cpp
+++ b/src/backend/Device.cpp
@@ -119,8 +119,23 @@
     InputStateBuilder* DeviceBase::CreateInputStateBuilder() {
         return new InputStateBuilder(this);
     }
-    PipelineLayoutBuilder* DeviceBase::CreatePipelineLayoutBuilder() {
-        return new PipelineLayoutBuilder(this);
+    PipelineLayoutBase* DeviceBase::CreatePipelineLayout(
+        const nxt::PipelineLayoutDescriptor* descriptor) {
+        MaybeError validation = ValidatePipelineLayoutDescriptor(this, descriptor);
+        if (validation.IsError()) {
+            // TODO(cwallez@chromium.org): Implement the WebGPU error handling mechanism.
+            delete validation.AcquireError();
+            return nullptr;
+        }
+
+        ResultOrError<PipelineLayoutBase*> maybePipelineLayout =
+            CreatePipelineLayoutImpl(descriptor);
+        if (maybePipelineLayout.IsError()) {
+            // TODO(cwallez@chromium.org): Implement the WebGPU error handling mechanism.
+            delete maybePipelineLayout.AcquireError();
+            return nullptr;
+        }
+        return maybePipelineLayout.AcquireSuccess();
     }
     QueueBase* DeviceBase::CreateQueue() {
         ResultOrError<QueueBase*> maybeQueue = CreateQueueImpl();
diff --git a/src/backend/Device.h b/src/backend/Device.h
index 93dee15..204eaf9 100644
--- a/src/backend/Device.h
+++ b/src/backend/Device.h
@@ -45,7 +45,6 @@
         virtual DepthStencilStateBase* CreateDepthStencilState(
             DepthStencilStateBuilder* builder) = 0;
         virtual InputStateBase* CreateInputState(InputStateBuilder* builder) = 0;
-        virtual PipelineLayoutBase* CreatePipelineLayout(PipelineLayoutBuilder* builder) = 0;
         virtual RenderPassDescriptorBase* CreateRenderPassDescriptor(
             RenderPassDescriptorBuilder* builder) = 0;
         virtual RenderPipelineBase* CreateRenderPipeline(RenderPipelineBuilder* builder) = 0;
@@ -83,7 +82,7 @@
         ComputePipelineBuilder* CreateComputePipelineBuilder();
         DepthStencilStateBuilder* CreateDepthStencilStateBuilder();
         InputStateBuilder* CreateInputStateBuilder();
-        PipelineLayoutBuilder* CreatePipelineLayoutBuilder();
+        PipelineLayoutBase* CreatePipelineLayout(const nxt::PipelineLayoutDescriptor* descriptor);
         QueueBase* CreateQueue();
         RenderPassDescriptorBuilder* CreateRenderPassDescriptorBuilder();
         RenderPipelineBuilder* CreateRenderPipelineBuilder();
@@ -98,6 +97,8 @@
         void Release();
 
       private:
+        virtual ResultOrError<PipelineLayoutBase*> CreatePipelineLayoutImpl(
+            const nxt::PipelineLayoutDescriptor* descriptor) = 0;
         virtual ResultOrError<QueueBase*> CreateQueueImpl() = 0;
         virtual ResultOrError<SamplerBase*> CreateSamplerImpl(
             const nxt::SamplerDescriptor* descriptor) = 0;
diff --git a/src/backend/Error.h b/src/backend/Error.h
index 9db8358..8f0200c 100644
--- a/src/backend/Error.h
+++ b/src/backend/Error.h
@@ -36,7 +36,15 @@
     //
     // Returning an error is done via:
     //   NXT_RETURN_ERROR("My error message");
-#define NXT_RETURN_ERROR(EXPR) return MakeError(EXPR, __FILE__, __func__, __LINE__)
+#define NXT_RETURN_ERROR(MESSAGE) return MakeError(MESSAGE, __FILE__, __func__, __LINE__)
+#define NXT_TRY_ASSERT(EXPR, MESSAGE)  \
+    {                                  \
+        if (!(EXPR)) {                 \
+            NXT_RETURN_ERROR(MESSAGE); \
+        }                              \
+    }                                  \
+    for (;;)                           \
+    break
 
 #define NXT_CONCAT1(x, y) x##y
 #define NXT_CONCAT2(x, y) NXT_CONCAT1(x, y)
diff --git a/src/backend/Pipeline.cpp b/src/backend/Pipeline.cpp
index 67275e3..13c320e 100644
--- a/src/backend/Pipeline.cpp
+++ b/src/backend/Pipeline.cpp
@@ -27,10 +27,10 @@
     PipelineBase::PipelineBase(PipelineBuilder* builder)
         : mStageMask(builder->mStageMask), mLayout(std::move(builder->mLayout)) {
         if (!mLayout) {
-            mLayout = builder->GetParentBuilder()
-                          ->GetDevice()
-                          ->CreatePipelineLayoutBuilder()
-                          ->GetResult();
+            nxt::PipelineLayoutDescriptor descriptor;
+            descriptor.numBindGroupLayouts = 0;
+            descriptor.bindGroupLayouts = nullptr;
+            mLayout = builder->GetParentBuilder()->GetDevice()->CreatePipelineLayout(&descriptor);
             // Remove the external ref objects are created with
             mLayout->Release();
         }
diff --git a/src/backend/PipelineLayout.cpp b/src/backend/PipelineLayout.cpp
index d8040e2..d25c2ac 100644
--- a/src/backend/PipelineLayout.cpp
+++ b/src/backend/PipelineLayout.cpp
@@ -20,10 +20,37 @@
 
 namespace backend {
 
+    MaybeError ValidatePipelineLayoutDescriptor(DeviceBase*,
+                                                const nxt::PipelineLayoutDescriptor* descriptor) {
+        NXT_TRY_ASSERT(descriptor->nextInChain == nullptr, "nextInChain must be nullptr");
+        NXT_TRY_ASSERT(descriptor->numBindGroupLayouts <= kMaxBindGroups,
+                       "too many bind group layouts");
+        for (uint32_t i = 0; i < descriptor->numBindGroupLayouts; ++i) {
+            NXT_TRY_ASSERT(descriptor->bindGroupLayouts[i].Get() != nullptr,
+                           "bind group layouts may not be null");
+        }
+        return {};
+    }
+
     // PipelineLayoutBase
 
-    PipelineLayoutBase::PipelineLayoutBase(PipelineLayoutBuilder* builder)
-        : mBindGroupLayouts(std::move(builder->mBindGroupLayouts)), mMask(builder->mMask) {
+    PipelineLayoutBase::PipelineLayoutBase(DeviceBase* device,
+                                           const nxt::PipelineLayoutDescriptor* descriptor) {
+        ASSERT(descriptor->numBindGroupLayouts <= kMaxBindGroups);
+        for (uint32_t group = 0; group < descriptor->numBindGroupLayouts; ++group) {
+            mBindGroupLayouts[group] =
+                reinterpret_cast<BindGroupLayoutBase*>(descriptor->bindGroupLayouts[group].Get());
+            mMask.set(group);
+        }
+        // TODO(kainino@chromium.org): It shouldn't be necessary to construct default bind
+        // group layouts here. Remove these and fix things so that they are not needed.
+        for (uint32_t group = descriptor->numBindGroupLayouts; group < kMaxBindGroups; ++group) {
+            auto builder = device->CreateBindGroupLayoutBuilder();
+            mBindGroupLayouts[group] = builder->GetResult();
+            // Remove the external ref objects are created with
+            mBindGroupLayouts[group]->Release();
+            builder->Release();
+        }
     }
 
     const BindGroupLayoutBase* PipelineLayoutBase::GetBindGroupLayout(size_t group) const {
@@ -49,40 +76,4 @@
         return kMaxBindGroups + 1;
     }
 
-    // PipelineLayoutBuilder
-
-    PipelineLayoutBuilder::PipelineLayoutBuilder(DeviceBase* device) : Builder(device) {
-    }
-
-    PipelineLayoutBase* PipelineLayoutBuilder::GetResultImpl() {
-        // TODO(cwallez@chromium.org): this is a hack, have the null bind group layout somewhere in
-        // the device once we have a cache of BGL
-        for (size_t group = 0; group < kMaxBindGroups; ++group) {
-            if (!mBindGroupLayouts[group]) {
-                auto builder = mDevice->CreateBindGroupLayoutBuilder();
-                mBindGroupLayouts[group] = builder->GetResult();
-                // Remove the external ref objects are created with
-                mBindGroupLayouts[group]->Release();
-                builder->Release();
-            }
-        }
-
-        return mDevice->CreatePipelineLayout(this);
-    }
-
-    void PipelineLayoutBuilder::SetBindGroupLayout(uint32_t groupIndex,
-                                                   BindGroupLayoutBase* layout) {
-        if (groupIndex >= kMaxBindGroups) {
-            HandleError("groupIndex is over the maximum allowed");
-            return;
-        }
-        if (mMask[groupIndex]) {
-            HandleError("Bind group layout already specified");
-            return;
-        }
-
-        mBindGroupLayouts[groupIndex] = layout;
-        mMask.set(groupIndex);
-    }
-
 }  // namespace backend
diff --git a/src/backend/PipelineLayout.h b/src/backend/PipelineLayout.h
index 19062e9..262dafb 100644
--- a/src/backend/PipelineLayout.h
+++ b/src/backend/PipelineLayout.h
@@ -15,7 +15,7 @@
 #ifndef BACKEND_PIPELINELAYOUT_H_
 #define BACKEND_PIPELINELAYOUT_H_
 
-#include "backend/Builder.h"
+#include "backend/Error.h"
 #include "backend/Forward.h"
 #include "backend/RefCounted.h"
 #include "common/Constants.h"
@@ -27,11 +27,14 @@
 
 namespace backend {
 
+    MaybeError ValidatePipelineLayoutDescriptor(DeviceBase*,
+                                                const nxt::PipelineLayoutDescriptor* descriptor);
+
     using BindGroupLayoutArray = std::array<Ref<BindGroupLayoutBase>, kMaxBindGroups>;
 
     class PipelineLayoutBase : public RefCounted {
       public:
-        PipelineLayoutBase(PipelineLayoutBuilder* builder);
+        PipelineLayoutBase(DeviceBase* device, const nxt::PipelineLayoutDescriptor* descriptor);
 
         const BindGroupLayoutBase* GetBindGroupLayout(size_t group) const;
         const std::bitset<kMaxBindGroups> GetBindGroupsLayoutMask() const;
@@ -49,22 +52,6 @@
         std::bitset<kMaxBindGroups> mMask;
     };
 
-    class PipelineLayoutBuilder : public Builder<PipelineLayoutBase> {
-      public:
-        PipelineLayoutBuilder(DeviceBase* device);
-
-        // NXT API
-        void SetBindGroupLayout(uint32_t groupIndex, BindGroupLayoutBase* layout);
-
-      private:
-        friend class PipelineLayoutBase;
-
-        PipelineLayoutBase* GetResultImpl() override;
-
-        BindGroupLayoutArray mBindGroupLayouts;
-        std::bitset<kMaxBindGroups> mMask;
-    };
-
 }  // namespace backend
 
 #endif  // BACKEND_PIPELINELAYOUT_H_
diff --git a/src/backend/Sampler.cpp b/src/backend/Sampler.cpp
index 7b00a5c..6798241 100644
--- a/src/backend/Sampler.cpp
+++ b/src/backend/Sampler.cpp
@@ -20,16 +20,13 @@
 namespace backend {
 
     MaybeError ValidateSamplerDescriptor(DeviceBase*, const nxt::SamplerDescriptor* descriptor) {
+        NXT_TRY_ASSERT(descriptor->nextInChain == nullptr, "nextInChain must be nullptr");
         NXT_TRY(ValidateFilterMode(descriptor->minFilter));
         NXT_TRY(ValidateFilterMode(descriptor->magFilter));
         NXT_TRY(ValidateFilterMode(descriptor->mipmapFilter));
         NXT_TRY(ValidateAddressMode(descriptor->addressModeU));
         NXT_TRY(ValidateAddressMode(descriptor->addressModeV));
         NXT_TRY(ValidateAddressMode(descriptor->addressModeW));
-
-        if (descriptor->nextInChain != nullptr) {
-            NXT_RETURN_ERROR("nextInChain must be nullptr");
-        }
         return {};
     }
 
diff --git a/src/backend/d3d12/DeviceD3D12.cpp b/src/backend/d3d12/DeviceD3D12.cpp
index 8346213..7575c39 100644
--- a/src/backend/d3d12/DeviceD3D12.cpp
+++ b/src/backend/d3d12/DeviceD3D12.cpp
@@ -285,8 +285,9 @@
     InputStateBase* Device::CreateInputState(InputStateBuilder* builder) {
         return new InputState(this, builder);
     }
-    PipelineLayoutBase* Device::CreatePipelineLayout(PipelineLayoutBuilder* builder) {
-        return new PipelineLayout(this, builder);
+    ResultOrError<PipelineLayoutBase*> Device::CreatePipelineLayoutImpl(
+        const nxt::PipelineLayoutDescriptor* descriptor) {
+        return new PipelineLayout(this, descriptor);
     }
     ResultOrError<QueueBase*> Device::CreateQueueImpl() {
         return new Queue(this);
diff --git a/src/backend/d3d12/DeviceD3D12.h b/src/backend/d3d12/DeviceD3D12.h
index 6a22536..126f463 100644
--- a/src/backend/d3d12/DeviceD3D12.h
+++ b/src/backend/d3d12/DeviceD3D12.h
@@ -47,7 +47,6 @@
         ComputePipelineBase* CreateComputePipeline(ComputePipelineBuilder* builder) override;
         DepthStencilStateBase* CreateDepthStencilState(DepthStencilStateBuilder* builder) override;
         InputStateBase* CreateInputState(InputStateBuilder* builder) override;
-        PipelineLayoutBase* CreatePipelineLayout(PipelineLayoutBuilder* builder) override;
         RenderPassDescriptorBase* CreateRenderPassDescriptor(
             RenderPassDescriptorBuilder* builder) override;
         RenderPipelineBase* CreateRenderPipeline(RenderPipelineBuilder* builder) override;
@@ -79,6 +78,8 @@
         void ExecuteCommandLists(std::initializer_list<ID3D12CommandList*> commandLists);
 
       private:
+        ResultOrError<PipelineLayoutBase*> CreatePipelineLayoutImpl(
+            const nxt::PipelineLayoutDescriptor* descriptor) override;
         ResultOrError<QueueBase*> CreateQueueImpl() override;
         ResultOrError<SamplerBase*> CreateSamplerImpl(
             const nxt::SamplerDescriptor* descriptor) override;
diff --git a/src/backend/d3d12/PipelineLayoutD3D12.cpp b/src/backend/d3d12/PipelineLayoutD3D12.cpp
index c4e1d65..2b6674f 100644
--- a/src/backend/d3d12/PipelineLayoutD3D12.cpp
+++ b/src/backend/d3d12/PipelineLayoutD3D12.cpp
@@ -22,8 +22,8 @@
 
 namespace backend { namespace d3d12 {
 
-    PipelineLayout::PipelineLayout(Device* device, PipelineLayoutBuilder* builder)
-        : PipelineLayoutBase(builder), mDevice(device) {
+    PipelineLayout::PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor)
+        : PipelineLayoutBase(device, descriptor) {
         D3D12_ROOT_PARAMETER rootParameters[kMaxBindGroups * 2];
 
         // A root parameter is one of these types
diff --git a/src/backend/d3d12/PipelineLayoutD3D12.h b/src/backend/d3d12/PipelineLayoutD3D12.h
index a7af1f7..141456d 100644
--- a/src/backend/d3d12/PipelineLayoutD3D12.h
+++ b/src/backend/d3d12/PipelineLayoutD3D12.h
@@ -25,7 +25,7 @@
 
     class PipelineLayout : public PipelineLayoutBase {
       public:
-        PipelineLayout(Device* device, PipelineLayoutBuilder* builder);
+        PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor);
 
         uint32_t GetCbvUavSrvRootParameterIndex(uint32_t group) const;
         uint32_t GetSamplerRootParameterIndex(uint32_t group) const;
@@ -33,8 +33,6 @@
         ComPtr<ID3D12RootSignature> GetRootSignature();
 
       private:
-        Device* mDevice;
-
         std::array<uint32_t, kMaxBindGroups> mCbvUavSrvRootParameterInfo;
         std::array<uint32_t, kMaxBindGroups> mSamplerRootParameterInfo;
 
diff --git a/src/backend/metal/DeviceMTL.h b/src/backend/metal/DeviceMTL.h
index d4e83a9..456b2ca 100644
--- a/src/backend/metal/DeviceMTL.h
+++ b/src/backend/metal/DeviceMTL.h
@@ -44,7 +44,6 @@
         ComputePipelineBase* CreateComputePipeline(ComputePipelineBuilder* builder) override;
         DepthStencilStateBase* CreateDepthStencilState(DepthStencilStateBuilder* builder) override;
         InputStateBase* CreateInputState(InputStateBuilder* builder) override;
-        PipelineLayoutBase* CreatePipelineLayout(PipelineLayoutBuilder* builder) override;
         RenderPassDescriptorBase* CreateRenderPassDescriptor(
             RenderPassDescriptorBuilder* builder) override;
         RenderPipelineBase* CreateRenderPipeline(RenderPipelineBuilder* builder) override;
@@ -65,6 +64,8 @@
         ResourceUploader* GetResourceUploader() const;
 
       private:
+        ResultOrError<PipelineLayoutBase*> CreatePipelineLayoutImpl(
+            const nxt::PipelineLayoutDescriptor* descriptor) override;
         ResultOrError<QueueBase*> CreateQueueImpl() override;
         ResultOrError<SamplerBase*> CreateSamplerImpl(
             const nxt::SamplerDescriptor* descriptor) override;
diff --git a/src/backend/metal/DeviceMTL.mm b/src/backend/metal/DeviceMTL.mm
index 4d61245..368615c 100644
--- a/src/backend/metal/DeviceMTL.mm
+++ b/src/backend/metal/DeviceMTL.mm
@@ -108,8 +108,9 @@
     InputStateBase* Device::CreateInputState(InputStateBuilder* builder) {
         return new InputState(builder);
     }
-    PipelineLayoutBase* Device::CreatePipelineLayout(PipelineLayoutBuilder* builder) {
-        return new PipelineLayout(builder);
+    ResultOrError<PipelineLayoutBase*> Device::CreatePipelineLayoutImpl(
+        const nxt::PipelineLayoutDescriptor* descriptor) {
+        return new PipelineLayout(this, descriptor);
     }
     RenderPassDescriptorBase* Device::CreateRenderPassDescriptor(
         RenderPassDescriptorBuilder* builder) {
diff --git a/src/backend/metal/PipelineLayoutMTL.h b/src/backend/metal/PipelineLayoutMTL.h
index 16f42f9..ebea453 100644
--- a/src/backend/metal/PipelineLayoutMTL.h
+++ b/src/backend/metal/PipelineLayoutMTL.h
@@ -27,9 +27,11 @@
 
 namespace backend { namespace metal {
 
+    class Device;
+
     class PipelineLayout : public PipelineLayoutBase {
       public:
-        PipelineLayout(PipelineLayoutBuilder* builder);
+        PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor);
 
         using BindingIndexInfo =
             std::array<std::array<uint32_t, kMaxBindingsPerGroup>, kMaxBindGroups>;
diff --git a/src/backend/metal/PipelineLayoutMTL.mm b/src/backend/metal/PipelineLayoutMTL.mm
index f0bedbc..0a85d4f 100644
--- a/src/backend/metal/PipelineLayoutMTL.mm
+++ b/src/backend/metal/PipelineLayoutMTL.mm
@@ -15,10 +15,12 @@
 #include "backend/metal/PipelineLayoutMTL.h"
 
 #include "backend/BindGroupLayout.h"
+#include "backend/metal/DeviceMTL.h"
 
 namespace backend { namespace metal {
 
-    PipelineLayout::PipelineLayout(PipelineLayoutBuilder* builder) : PipelineLayoutBase(builder) {
+    PipelineLayout::PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor)
+        : PipelineLayoutBase(device, descriptor) {
         // Each stage has its own numbering namespace in CompilerMSL.
         for (auto stage : IterateStages(kAllStages)) {
             // Buffer number 0 is reserved for push constants
diff --git a/src/backend/null/NullBackend.cpp b/src/backend/null/NullBackend.cpp
index 26da79a..b879da4 100644
--- a/src/backend/null/NullBackend.cpp
+++ b/src/backend/null/NullBackend.cpp
@@ -63,8 +63,9 @@
     InputStateBase* Device::CreateInputState(InputStateBuilder* builder) {
         return new InputState(builder);
     }
-    PipelineLayoutBase* Device::CreatePipelineLayout(PipelineLayoutBuilder* builder) {
-        return new PipelineLayout(builder);
+    ResultOrError<PipelineLayoutBase*> Device::CreatePipelineLayoutImpl(
+        const nxt::PipelineLayoutDescriptor* descriptor) {
+        return new PipelineLayout(this, descriptor);
     }
     ResultOrError<QueueBase*> Device::CreateQueueImpl() {
         return new Queue(this);
diff --git a/src/backend/null/NullBackend.h b/src/backend/null/NullBackend.h
index 9af570b..6f2330f 100644
--- a/src/backend/null/NullBackend.h
+++ b/src/backend/null/NullBackend.h
@@ -104,7 +104,6 @@
         ComputePipelineBase* CreateComputePipeline(ComputePipelineBuilder* builder) override;
         DepthStencilStateBase* CreateDepthStencilState(DepthStencilStateBuilder* builder) override;
         InputStateBase* CreateInputState(InputStateBuilder* builder) override;
-        PipelineLayoutBase* CreatePipelineLayout(PipelineLayoutBuilder* builder) override;
         RenderPassDescriptorBase* CreateRenderPassDescriptor(
             RenderPassDescriptorBuilder* builder) override;
         RenderPipelineBase* CreateRenderPipeline(RenderPipelineBuilder* builder) override;
@@ -119,6 +118,8 @@
         std::vector<std::unique_ptr<PendingOperation>> AcquirePendingOperations();
 
       private:
+        ResultOrError<PipelineLayoutBase*> CreatePipelineLayoutImpl(
+            const nxt::PipelineLayoutDescriptor* descriptor) override;
         ResultOrError<QueueBase*> CreateQueueImpl() override;
         ResultOrError<SamplerBase*> CreateSamplerImpl(
             const nxt::SamplerDescriptor* descriptor) override;
diff --git a/src/backend/opengl/DeviceGL.cpp b/src/backend/opengl/DeviceGL.cpp
index 1eb66dd..008f8ec 100644
--- a/src/backend/opengl/DeviceGL.cpp
+++ b/src/backend/opengl/DeviceGL.cpp
@@ -77,8 +77,9 @@
     InputStateBase* Device::CreateInputState(InputStateBuilder* builder) {
         return new InputState(builder);
     }
-    PipelineLayoutBase* Device::CreatePipelineLayout(PipelineLayoutBuilder* builder) {
-        return new PipelineLayout(builder);
+    ResultOrError<PipelineLayoutBase*> Device::CreatePipelineLayoutImpl(
+        const nxt::PipelineLayoutDescriptor* descriptor) {
+        return new PipelineLayout(this, descriptor);
     }
     ResultOrError<QueueBase*> Device::CreateQueueImpl() {
         return new Queue(this);
diff --git a/src/backend/opengl/DeviceGL.h b/src/backend/opengl/DeviceGL.h
index fed98b8..9789267 100644
--- a/src/backend/opengl/DeviceGL.h
+++ b/src/backend/opengl/DeviceGL.h
@@ -35,7 +35,6 @@
         ComputePipelineBase* CreateComputePipeline(ComputePipelineBuilder* builder) override;
         DepthStencilStateBase* CreateDepthStencilState(DepthStencilStateBuilder* builder) override;
         InputStateBase* CreateInputState(InputStateBuilder* builder) override;
-        PipelineLayoutBase* CreatePipelineLayout(PipelineLayoutBuilder* builder) override;
         RenderPassDescriptorBase* CreateRenderPassDescriptor(
             RenderPassDescriptorBuilder* builder) override;
         RenderPipelineBase* CreateRenderPipeline(RenderPipelineBuilder* builder) override;
@@ -47,6 +46,8 @@
         void TickImpl() override;
 
       private:
+        ResultOrError<PipelineLayoutBase*> CreatePipelineLayoutImpl(
+            const nxt::PipelineLayoutDescriptor* descriptor) override;
         ResultOrError<QueueBase*> CreateQueueImpl() override;
         ResultOrError<SamplerBase*> CreateSamplerImpl(
             const nxt::SamplerDescriptor* descriptor) override;
diff --git a/src/backend/opengl/PipelineLayoutGL.cpp b/src/backend/opengl/PipelineLayoutGL.cpp
index 444246d..44a08d8 100644
--- a/src/backend/opengl/PipelineLayoutGL.cpp
+++ b/src/backend/opengl/PipelineLayoutGL.cpp
@@ -15,10 +15,12 @@
 #include "backend/opengl/PipelineLayoutGL.h"
 
 #include "backend/BindGroupLayout.h"
+#include "backend/opengl/DeviceGL.h"
 
 namespace backend { namespace opengl {
 
-    PipelineLayout::PipelineLayout(PipelineLayoutBuilder* builder) : PipelineLayoutBase(builder) {
+    PipelineLayout::PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor)
+        : PipelineLayoutBase(device, descriptor) {
         GLuint uboIndex = 0;
         GLuint samplerIndex = 0;
         GLuint sampledTextureIndex = 0;
diff --git a/src/backend/opengl/PipelineLayoutGL.h b/src/backend/opengl/PipelineLayoutGL.h
index 3c60787..c084590 100644
--- a/src/backend/opengl/PipelineLayoutGL.h
+++ b/src/backend/opengl/PipelineLayoutGL.h
@@ -25,7 +25,7 @@
 
     class PipelineLayout : public PipelineLayoutBase {
       public:
-        PipelineLayout(PipelineLayoutBuilder* builder);
+        PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor);
 
         using BindingIndexInfo =
             std::array<std::array<GLuint, kMaxBindingsPerGroup>, kMaxBindGroups>;
diff --git a/src/backend/opengl/TextureGL.h b/src/backend/opengl/TextureGL.h
index a6583e8..50756f4 100644
--- a/src/backend/opengl/TextureGL.h
+++ b/src/backend/opengl/TextureGL.h
@@ -21,8 +21,6 @@
 
 namespace backend { namespace opengl {
 
-    class Device;
-
     struct TextureFormatInfo {
         GLenum internalFormat;
         GLenum format;
diff --git a/src/backend/vulkan/DeviceVk.cpp b/src/backend/vulkan/DeviceVk.cpp
index 632c29b..b31bf1b 100644
--- a/src/backend/vulkan/DeviceVk.cpp
+++ b/src/backend/vulkan/DeviceVk.cpp
@@ -241,8 +241,9 @@
     InputStateBase* Device::CreateInputState(InputStateBuilder* builder) {
         return new InputState(builder);
     }
-    PipelineLayoutBase* Device::CreatePipelineLayout(PipelineLayoutBuilder* builder) {
-        return new PipelineLayout(builder);
+    ResultOrError<PipelineLayoutBase*> Device::CreatePipelineLayoutImpl(
+        const nxt::PipelineLayoutDescriptor* descriptor) {
+        return new PipelineLayout(this, descriptor);
     }
     ResultOrError<QueueBase*> Device::CreateQueueImpl() {
         return new Queue(this);
diff --git a/src/backend/vulkan/DeviceVk.h b/src/backend/vulkan/DeviceVk.h
index 14cbcf3..4b79cff 100644
--- a/src/backend/vulkan/DeviceVk.h
+++ b/src/backend/vulkan/DeviceVk.h
@@ -72,7 +72,6 @@
         ComputePipelineBase* CreateComputePipeline(ComputePipelineBuilder* builder) override;
         DepthStencilStateBase* CreateDepthStencilState(DepthStencilStateBuilder* builder) override;
         InputStateBase* CreateInputState(InputStateBuilder* builder) override;
-        PipelineLayoutBase* CreatePipelineLayout(PipelineLayoutBuilder* builder) override;
         RenderPassDescriptorBase* CreateRenderPassDescriptor(
             RenderPassDescriptorBuilder* builder) override;
         RenderPipelineBase* CreateRenderPipeline(RenderPipelineBuilder* builder) override;
@@ -84,6 +83,8 @@
         void TickImpl() override;
 
       private:
+        ResultOrError<PipelineLayoutBase*> CreatePipelineLayoutImpl(
+            const nxt::PipelineLayoutDescriptor* descriptor) override;
         ResultOrError<QueueBase*> CreateQueueImpl() override;
         ResultOrError<SamplerBase*> CreateSamplerImpl(
             const nxt::SamplerDescriptor* descriptor) override;
diff --git a/src/backend/vulkan/PipelineLayoutVk.cpp b/src/backend/vulkan/PipelineLayoutVk.cpp
index 21fb38f..8ef1091 100644
--- a/src/backend/vulkan/PipelineLayoutVk.cpp
+++ b/src/backend/vulkan/PipelineLayoutVk.cpp
@@ -22,8 +22,8 @@
 
 namespace backend { namespace vulkan {
 
-    PipelineLayout::PipelineLayout(PipelineLayoutBuilder* builder)
-        : PipelineLayoutBase(builder), mDevice(ToBackend(builder->GetDevice())) {
+    PipelineLayout::PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor)
+        : PipelineLayoutBase(device, descriptor), mDevice(device) {
         // Compute the array of VkDescriptorSetLayouts that will be chained in the create info.
         // TODO(cwallez@chromium.org) Vulkan doesn't allow holes in this array, should we expose
         // this constraints at the NXT level?
diff --git a/src/backend/vulkan/PipelineLayoutVk.h b/src/backend/vulkan/PipelineLayoutVk.h
index e26d314..a6ab0c4 100644
--- a/src/backend/vulkan/PipelineLayoutVk.h
+++ b/src/backend/vulkan/PipelineLayoutVk.h
@@ -25,7 +25,7 @@
 
     class PipelineLayout : public PipelineLayoutBase {
       public:
-        PipelineLayout(PipelineLayoutBuilder* builder);
+        PipelineLayout(Device* device, const nxt::PipelineLayoutDescriptor* descriptor);
         ~PipelineLayout();
 
         VkPipelineLayout GetHandle() const;
diff --git a/src/tests/end2end/BlendStateTests.cpp b/src/tests/end2end/BlendStateTests.cpp
index 1a7be71..c2b26b4 100644
--- a/src/tests/end2end/BlendStateTests.cpp
+++ b/src/tests/end2end/BlendStateTests.cpp
@@ -41,9 +41,7 @@
                 .SetBindingsType(nxt::ShaderStageBit::Fragment, nxt::BindingType::UniformBuffer, 0, 1)
                 .GetResult();
 
-            pipelineLayout = device.CreatePipelineLayoutBuilder()
-                .SetBindGroupLayout(0, bindGroupLayout)
-                .GetResult();
+            pipelineLayout = utils::MakeBasicPipelineLayout(device, &bindGroupLayout);
 
             renderPass = utils::CreateBasicRenderPass(device, kRTSize, kRTSize);
         }
diff --git a/src/tests/end2end/DepthStencilStateTests.cpp b/src/tests/end2end/DepthStencilStateTests.cpp
index f236aed..bb3a300 100644
--- a/src/tests/end2end/DepthStencilStateTests.cpp
+++ b/src/tests/end2end/DepthStencilStateTests.cpp
@@ -82,9 +82,7 @@
                 .SetBindingsType(nxt::ShaderStageBit::Vertex | nxt::ShaderStageBit::Fragment, nxt::BindingType::UniformBuffer, 0, 1)
                 .GetResult();
 
-            pipelineLayout = device.CreatePipelineLayoutBuilder()
-                .SetBindGroupLayout(0, bindGroupLayout)
-                .GetResult();
+            pipelineLayout = utils::MakeBasicPipelineLayout(device, &bindGroupLayout);
         }
 
         struct TestSpec {
diff --git a/src/tests/end2end/PushConstantTests.cpp b/src/tests/end2end/PushConstantTests.cpp
index 1ecd417..6a3d072 100644
--- a/src/tests/end2end/PushConstantTests.cpp
+++ b/src/tests/end2end/PushConstantTests.cpp
@@ -49,9 +49,7 @@
                 .SetBindingsType(kAllStages, nxt::BindingType::StorageBuffer, 0, extraBuffer ? 2 : 1)
                 .GetResult();
 
-            nxt::PipelineLayout pl = device.CreatePipelineLayoutBuilder()
-                .SetBindGroupLayout(0, bgl)
-                .GetResult();
+            nxt::PipelineLayout pl = utils::MakeBasicPipelineLayout(device, &bgl);
 
             nxt::BufferView views[2] = {
                 buf1.CreateBufferViewBuilder().SetExtent(0, 4).GetResult(),
@@ -155,7 +153,7 @@
         }
 
         nxt::PipelineLayout MakeEmptyLayout() {
-            return device.CreatePipelineLayoutBuilder().GetResult();
+            return utils::MakeBasicPipelineLayout(device, nullptr);
         }
 
         // The render pipeline adds one to the red channel for successful vertex push constant test
diff --git a/src/tests/end2end/RenderPassLoadOpTests.cpp b/src/tests/end2end/RenderPassLoadOpTests.cpp
index 59c9ae5..9d2983e 100644
--- a/src/tests/end2end/RenderPassLoadOpTests.cpp
+++ b/src/tests/end2end/RenderPassLoadOpTests.cpp
@@ -28,8 +28,7 @@
                 vsModule = utils::CreateShaderModule(*device, nxt::ShaderStage::Vertex, vsSource);
                 fsModule = utils::CreateShaderModule(*device, nxt::ShaderStage::Fragment, fsSource);
 
-                pipelineLayout = device->CreatePipelineLayoutBuilder()
-                    .GetResult();
+                pipelineLayout = utils::MakeBasicPipelineLayout(*device, nullptr);
             }
 
         void Draw(nxt::CommandBufferBuilder* builder) {
diff --git a/src/tests/end2end/SamplerTests.cpp b/src/tests/end2end/SamplerTests.cpp
index a35dd05..366a653 100644
--- a/src/tests/end2end/SamplerTests.cpp
+++ b/src/tests/end2end/SamplerTests.cpp
@@ -48,9 +48,7 @@
             .SetBindingsType(nxt::ShaderStageBit::Fragment, nxt::BindingType::SampledTexture, 1, 1)
             .GetResult();
 
-        auto pipelineLayout = device.CreatePipelineLayoutBuilder()
-            .SetBindGroupLayout(0, mBindGroupLayout)
-            .GetResult();
+        auto pipelineLayout = utils::MakeBasicPipelineLayout(device, &mBindGroupLayout);
 
         auto vsModule = utils::CreateShaderModule(device, nxt::ShaderStage::Vertex, R"(
             #version 450
diff --git a/src/tests/unittests/WireTests.cpp b/src/tests/unittests/WireTests.cpp
index 8c0381a..5676330 100644
--- a/src/tests/unittests/WireTests.cpp
+++ b/src/tests/unittests/WireTests.cpp
@@ -408,6 +408,34 @@
     FlushClient();
 }
 
+// Test that the wire is able to send structures that contain objects
+TEST_F(WireTests, StructureOfObjectArrayArgument) {
+    nxtBindGroupLayoutBuilder bglBuilder = nxtDeviceCreateBindGroupLayoutBuilder(device);
+    nxtBindGroupLayout bgl = nxtBindGroupLayoutBuilderGetResult(bglBuilder);
+
+    nxtBindGroupLayoutBuilder apiBglBuilder = api.GetNewBindGroupLayoutBuilder();
+    EXPECT_CALL(api, DeviceCreateBindGroupLayoutBuilder(apiDevice))
+          .WillOnce(Return(apiBglBuilder));
+    nxtBindGroupLayout apiBgl = api.GetNewBindGroupLayout();
+    EXPECT_CALL(api, BindGroupLayoutBuilderGetResult(apiBglBuilder))
+        .WillOnce(Return(apiBgl));
+
+    nxtPipelineLayoutDescriptor descriptor;
+    descriptor.nextInChain = nullptr;
+    descriptor.numBindGroupLayouts = 1;
+    descriptor.bindGroupLayouts = &bgl;
+
+    nxtDeviceCreatePipelineLayout(device, &descriptor);
+    EXPECT_CALL(api, DeviceCreatePipelineLayout(apiDevice, MatchesLambda([apiBgl](const nxtPipelineLayoutDescriptor* desc) -> bool {
+        return desc->nextInChain == nullptr &&
+            desc->numBindGroupLayouts == 1 &&
+            desc->bindGroupLayouts[0] == apiBgl;
+    })))
+        .WillOnce(Return(nullptr));
+
+    FlushClient();
+}
+
 // Test that the server doesn't forward calls to error objects or with error objects
 // Also test that when GetResult is called on an error builder, the error callback is fired
 TEST_F(WireTests, CallsSkippedAfterBuilderError) {
diff --git a/src/tests/unittests/validation/RenderPipelineValidationTests.cpp b/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
index 5c14e09..8a4ec41 100644
--- a/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
+++ b/src/tests/unittests/validation/RenderPipelineValidationTests.cpp
@@ -24,7 +24,7 @@
 
             renderpass = CreateSimpleRenderPass();
 
-            pipelineLayout = device.CreatePipelineLayoutBuilder().GetResult();
+            nxt::PipelineLayout pl = utils::MakeBasicPipelineLayout(device, nullptr);
 
             inputState = device.CreateInputStateBuilder().GetResult();
 
diff --git a/src/utils/NXTHelpers.cpp b/src/utils/NXTHelpers.cpp
index e2de02b..6581b9b 100644
--- a/src/utils/NXTHelpers.cpp
+++ b/src/utils/NXTHelpers.cpp
@@ -149,5 +149,17 @@
 
         return desc;
     }
+    nxt::PipelineLayout MakeBasicPipelineLayout(const nxt::Device& device,
+                                                const nxt::BindGroupLayout* bindGroupLayout) {
+        nxt::PipelineLayoutDescriptor descriptor;
+        if (bindGroupLayout) {
+            descriptor.numBindGroupLayouts = 1;
+            descriptor.bindGroupLayouts = bindGroupLayout;
+        } else {
+            descriptor.numBindGroupLayouts = 0;
+            descriptor.bindGroupLayouts = nullptr;
+        }
+        return device.CreatePipelineLayout(&descriptor);
+    }
 
 }  // namespace utils
diff --git a/src/utils/NXTHelpers.h b/src/utils/NXTHelpers.h
index 67959c0..c86cc83 100644
--- a/src/utils/NXTHelpers.h
+++ b/src/utils/NXTHelpers.h
@@ -49,5 +49,7 @@
                                           uint32_t height);
 
     nxt::SamplerDescriptor GetDefaultSamplerDescriptor();
+    nxt::PipelineLayout MakeBasicPipelineLayout(const nxt::Device& device,
+                                                const nxt::BindGroupLayout* bindGroupLayout);
 
 }  // namespace utils