Support chained extension structs on the wire

This CL also adds a couple of dummy extensions in dawn.json so that
the serialization/deserialization in the wire can be tested.

Bug: dawn:369
Change-Id: I5ec3853c286f45d9b04e8bf9d04ebd9176dc917b
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/18520
Commit-Queue: Austin Eng <enga@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
diff --git a/BUILD.gn b/BUILD.gn
index 542680d..7d54f0b 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -894,6 +894,7 @@
     "src/tests/unittests/wire/WireBasicTests.cpp",
     "src/tests/unittests/wire/WireBufferMappingTests.cpp",
     "src/tests/unittests/wire/WireErrorCallbackTests.cpp",
+    "src/tests/unittests/wire/WireExtensionTests.cpp",
     "src/tests/unittests/wire/WireFenceTests.cpp",
     "src/tests/unittests/wire/WireInjectTextureTests.cpp",
     "src/tests/unittests/wire/WireMemoryTransferServiceTests.cpp",
diff --git a/dawn.json b/dawn.json
index bfb03aa..7aff7b1 100644
--- a/dawn.json
+++ b/dawn.json
@@ -1237,6 +1237,13 @@
             {"name": "alpha to coverage enabled", "type": "bool", "default": "false"}
         ]
     },
+    "render pipeline descriptor dummy extension": {
+        "category": "structure",
+        "chained": true,
+        "members": [
+            {"name": "dummy stage", "type": "programmable stage descriptor"}
+        ]
+    },
     "sampler": {
         "category": "object"
     },
@@ -1256,6 +1263,13 @@
             {"name": "compare", "type": "compare function", "default": "never"}
         ]
     },
+    "sampler descriptor dummy anisotropic filtering": {
+        "category": "structure",
+        "chained": true,
+        "members": [
+            {"name": "max anisotropy", "type": "float"}
+        ]
+    },
     "shader module": {
         "category": "object"
     },
@@ -1376,11 +1390,13 @@
         "category": "enum",
         "javascript": false,
         "values": [
-            {"value": 0, "name": "invalid"},
+            {"value": 0, "name": "invalid", "valid": false},
             {"value": 1, "name": "surface descriptor from metal layer"},
             {"value": 2, "name": "surface descriptor from windows HWND"},
             {"value": 3, "name": "surface descriptor from xlib"},
-            {"value": 4, "name": "surface descriptor from HTML canvas id"}
+            {"value": 4, "name": "surface descriptor from HTML canvas id"},
+            {"value": 5, "name": "sampler descriptor dummy anisotropic filtering"},
+            {"value": 6, "name": "render pipeline descriptor dummy extension"}
         ]
     },
     "texture": {
diff --git a/generator/dawn_json_generator.py b/generator/dawn_json_generator.py
index 6ff4280..37ad05d 100644
--- a/generator/dawn_json_generator.py
+++ b/generator/dawn_json_generator.py
@@ -25,11 +25,15 @@
 class Name:
     def __init__(self, name, native=False):
         self.native = native
+        self.name = name
         if native:
             self.chunks = [name]
         else:
             self.chunks = name.split(' ')
 
+    def get(self):
+        return self.name
+
     def CamelChunk(self, chunk):
         return chunk[0].upper() + chunk[1:]
 
@@ -145,18 +149,23 @@
     def __init__(self, name):
         self.name = Name(name)
         self.members = []
-        self.has_dawn_object = False
+        self.may_have_dawn_object = False
 
     def update_metadata(self):
-        def has_dawn_object(member):
+        def may_have_dawn_object(member):
             if isinstance(member.type, ObjectType):
                 return True
             elif isinstance(member.type, StructureType):
-                return member.type.has_dawn_object
+                return member.type.may_have_dawn_object
             else:
                 return False
 
-        self.has_dawn_object = any(has_dawn_object(member) for member in self.members)
+        self.may_have_dawn_object = any(may_have_dawn_object(member) for member in self.members)
+
+        # set may_have_dawn_object to true if the type is chained or extensible. Chained structs
+        # may contain a Dawn object.
+        if isinstance(self, StructureType):
+            self.may_have_dawn_object = self.may_have_dawn_object or self.chained or self.extensible
 
 class StructureType(Record, Type):
     def __init__(self, name, json_data):
diff --git a/generator/templates/dawn_native/wgpu_structs.h b/generator/templates/dawn_native/wgpu_structs.h
index 5614336..887de9e 100644
--- a/generator/templates/dawn_native/wgpu_structs.h
+++ b/generator/templates/dawn_native/wgpu_structs.h
@@ -50,7 +50,13 @@
                 ChainedStruct const * nextInChain = nullptr;
             {% endif %}
             {% for member in type.members %}
-                {{as_annotated_frontendType(member)}} {{render_cpp_default_value(member)}};
+                {% set member_declaration = as_annotated_frontendType(member) + render_cpp_default_value(member) %}
+                {% if type.chained and loop.first %}
+                    //* Align the first member to ChainedStruct to match the C struct layout.
+                    alignas(ChainedStruct) {{member_declaration}};
+                {% else %}
+                    {{member_declaration}};
+                {% endif %}
             {% endfor %}
         };
 
diff --git a/generator/templates/dawn_wire/WireCmd.cpp b/generator/templates/dawn_wire/WireCmd.cpp
index c001e46..5f6a666 100644
--- a/generator/templates/dawn_wire/WireCmd.cpp
+++ b/generator/templates/dawn_wire/WireCmd.cpp
@@ -56,7 +56,7 @@
         {%- set Optional = "Optional" if member.optional else "" -%}
         {{out}} = provider.Get{{Optional}}Id({{in}});
     {% elif member.type.category == "structure"%}
-        {%- set Provider = ", provider" if member.type.has_dawn_object else "" -%}
+        {%- set Provider = ", provider" if member.type.may_have_dawn_object else "" -%}
         {% if member.annotation == "const*const*" %}
             {{as_cType(member.type.name)}}Serialize(*{{in}}, &{{out}}, buffer{{Provider}});
         {% else %}
@@ -74,7 +74,7 @@
         DESERIALIZE_TRY(resolver.Get{{Optional}}FromId({{in}}, &{{out}}));
     {%- elif member.type.category == "structure" -%}
         DESERIALIZE_TRY({{as_cType(member.type.name)}}Deserialize(&{{out}}, &{{in}}, buffer, size, allocator
-            {%- if member.type.has_dawn_object -%}
+            {%- if member.type.may_have_dawn_object -%}
                 , resolver
             {%- endif -%}
         ));
@@ -83,6 +83,15 @@
     {%- endif -%}
 {% endmacro %}
 
+namespace {
+
+    struct WGPUChainedStructTransfer {
+        WGPUSType sType;
+        bool hasNext;
+    };
+
+}  // anonymous namespace
+
 //* The main [de]serialization macro
 //* Methods are very similar to structures that have one member corresponding to each arguments.
 //* This macro takes advantage of the similarity to output [de]serialization code for a record
@@ -95,9 +104,15 @@
     //* are embedded directly in the structure. Other members are assumed to be in the
     //* memory directly following the structure in the buffer.
     struct {{Return}}{{name}}Transfer {
+        static_assert({{[is_cmd, record.extensible, record.chained].count(True)}} <= 1,
+                      "Record must be at most one of is_cmd, extensible, and chained.");
         {% if is_cmd %}
             //* Start the transfer structure with the command ID, so that casting to WireCmd gives the ID.
             {{Return}}WireCmd commandId;
+        {% elif record.extensible %}
+            bool hasNextInChain;
+        {% elif record.chained %}
+            WGPUChainedStructTransfer chain;
         {% endif %}
 
         //* Value types are directly in the command, objects being replaced with their IDs.
@@ -115,12 +130,23 @@
         {% endfor %}
     };
 
+    {% if record.chained %}
+        static_assert(offsetof({{Return}}{{name}}Transfer, chain) == 0, "");
+    {% endif %}
+
     //* Returns the required transfer size for `record` in addition to the transfer structure.
     DAWN_DECLARE_UNUSED size_t {{Return}}{{name}}GetExtraRequiredSize(const {{Return}}{{name}}{{Cmd}}& record) {
         DAWN_UNUSED(record);
 
         size_t result = 0;
 
+        //* Gather how much space will be needed for the extension chain.
+        {% if record.extensible %}
+            if (record.nextInChain != nullptr) {
+                result += GetChainedStructExtraRequiredSize(record.nextInChain);
+            }
+        {% endif %}
+
         //* Special handling of const char* that have their length embedded directly in the command
         {% for member in members if member.length == "strlen" %}
             {% set memberName = as_varName(member.name) %}
@@ -170,7 +196,7 @@
     //* and `provider` to serialize objects.
     DAWN_DECLARE_UNUSED void {{Return}}{{name}}Serialize(const {{Return}}{{name}}{{Cmd}}& record, {{Return}}{{name}}Transfer* transfer,
                            char** buffer
-        {%- if record.has_dawn_object -%}
+        {%- if record.may_have_dawn_object -%}
             , const ObjectIdProvider& provider
         {%- endif -%}
     ) {
@@ -187,6 +213,21 @@
             {{serialize_member(member, "record." + memberName, "transfer->" + memberName)}}
         {% endfor %}
 
+        {% if record.extensible %}
+            if (record.nextInChain != nullptr) {
+                transfer->hasNextInChain = true;
+                SerializeChainedStruct(record.nextInChain, buffer, provider);
+            } else {
+                transfer->hasNextInChain = false;
+            }
+        {% endif %}
+
+        {% if record.chained %}
+            //* Should be set by the root descriptor's call to SerializeChainedStruct.
+            ASSERT(transfer->chain.sType == {{as_cEnum(types["s type"].name, record.name)}});
+            ASSERT(transfer->chain.hasNext == (record.chain.next != nullptr));
+        {% endif %}
+
         //* Special handling of const char* that have their length embedded directly in the command
         {% for member in members if member.length == "strlen" %}
             {% set memberName = as_varName(member.name) %}
@@ -231,7 +272,7 @@
     //* Ids to actual objects.
     DAWN_DECLARE_UNUSED DeserializeResult {{Return}}{{name}}Deserialize({{Return}}{{name}}{{Cmd}}* record, const volatile {{Return}}{{name}}Transfer* transfer,
                                           const volatile char** buffer, size_t* size, DeserializeAllocator* allocator
-        {%- if record.has_dawn_object -%}
+        {%- if record.may_have_dawn_object -%}
             , const ObjectIdResolver& resolver
         {%- endif -%}
     ) {
@@ -243,10 +284,6 @@
             ASSERT(transfer->commandId == {{Return}}WireCmd::{{name}});
         {% endif %}
 
-        {% if record.extensible %}
-            record->nextInChain = nullptr;
-        {% endif %}
-
         {% if record.derived_method %}
             record->selfId = transfer->self;
         {% endif %}
@@ -257,6 +294,21 @@
             {{deserialize_member(member, "transfer->" + memberName, "record->" + memberName)}}
         {% endfor %}
 
+        {% if record.extensible %}
+            record->nextInChain = nullptr;
+            if (transfer->hasNextInChain) {
+                DESERIALIZE_TRY(DeserializeChainedStruct(&record->nextInChain, buffer, size, allocator, resolver));
+            }
+        {% endif %}
+
+        {% if record.chained %}
+            //* Should be set by the root descriptor's call to DeserializeChainedStruct.
+            //* Don't check |record->chain.next| matches because it is not set until the
+            //* next iteration inside DeserializeChainedStruct.
+            ASSERT(record->chain.sType == {{as_cEnum(types["s type"].name, record.name)}});
+            ASSERT(record->chain.next == nullptr);
+        {% endif %}
+
         //* Special handling of const char* that have their length embedded directly in the command
         {% for member in members if member.length == "strlen" %}
             {% set memberName = as_varName(member.name) %}
@@ -328,7 +380,7 @@
     }
 
     void {{Cmd}}::Serialize(char* buffer
-        {%- if command.has_dawn_object -%}
+        {%- if command.may_have_dawn_object -%}
             , const ObjectIdProvider& objectIdProvider
         {%- endif -%}
     ) const {
@@ -336,14 +388,14 @@
         buffer += sizeof({{Name}}Transfer);
 
         {{Name}}Serialize(*this, transfer, &buffer
-            {%- if command.has_dawn_object -%}
+            {%- if command.may_have_dawn_object -%}
                 , objectIdProvider
             {%- endif -%}
         );
     }
 
     DeserializeResult {{Cmd}}::Deserialize(const volatile char** buffer, size_t* size, DeserializeAllocator* allocator
-        {%- if command.has_dawn_object -%}
+        {%- if command.may_have_dawn_object -%}
             , const ObjectIdResolver& resolver
         {%- endif -%}
     ) {
@@ -351,7 +403,7 @@
         DESERIALIZE_TRY(GetPtrFromBuffer(buffer, size, 1, &transfer));
 
         return {{Name}}Deserialize(this, transfer, buffer, size, allocator
-            {%- if command.has_dawn_object -%}
+            {%- if command.may_have_dawn_object -%}
                 , resolver
             {%- endif -%}
         );
@@ -424,6 +476,16 @@
             return DeserializeResult::Success;
         }
 
+        size_t GetChainedStructExtraRequiredSize(const WGPUChainedStruct* chainedStruct);
+        void SerializeChainedStruct(WGPUChainedStruct const* chainedStruct,
+                                    char** buffer,
+                                    const ObjectIdProvider& provider);
+        DeserializeResult DeserializeChainedStruct(const WGPUChainedStruct** outChainNext,
+                                                   const volatile char** buffer,
+                                                   size_t* size,
+                                                   DeserializeAllocator* allocator,
+                                                   const ObjectIdResolver& resolver);
+
         //* Output structure [de]serialization first because it is used by commands.
         {% for type in by_category["structure"] %}
             {% set name = as_cType(type.name) %}
@@ -433,6 +495,116 @@
             {% endif %}
         {% endfor %}
 
+        size_t GetChainedStructExtraRequiredSize(const WGPUChainedStruct* chainedStruct) {
+            ASSERT(chainedStruct != nullptr);
+            size_t result = 0;
+            while (chainedStruct != nullptr) {
+                switch (chainedStruct->sType) {
+                    {% for sType in types["s type"].values if sType.valid and sType.name.CamelCase() not in client_side_structures %}
+                        case {{as_cEnum(types["s type"].name, sType.name)}}: {
+                            const auto& typedStruct = *reinterpret_cast<{{as_cType(sType.name)}} const *>(chainedStruct);
+                            result += sizeof({{as_cType(sType.name)}}Transfer);
+                            result += {{as_cType(sType.name)}}GetExtraRequiredSize(typedStruct);
+                            chainedStruct = typedStruct.chain.next;
+                            break;
+                        }
+                    {% endfor %}
+                    default:
+                        // Invalid enum. Reserve space just for the transfer header (sType and hasNext).
+                        // Stop iterating because this is an error.
+                        // TODO(crbug.com/dawn/369): Unknown sTypes are silently discarded.
+                        ASSERT(chainedStruct->sType == WGPUSType_Invalid);
+                        result += sizeof(WGPUChainedStructTransfer);
+                        return result;
+                }
+            }
+            return result;
+        }
+
+        void SerializeChainedStruct(WGPUChainedStruct const* chainedStruct,
+                                    char** buffer,
+                                    const ObjectIdProvider& provider) {
+            ASSERT(chainedStruct != nullptr);
+            ASSERT(buffer != nullptr);
+            do {
+                switch (chainedStruct->sType) {
+                    {% for sType in types["s type"].values if sType.valid and sType.name.CamelCase() not in client_side_structures %}
+                        {% set CType = as_cType(sType.name) %}
+                        case {{as_cEnum(types["s type"].name, sType.name)}}: {
+
+                            auto* transfer = reinterpret_cast<{{CType}}Transfer*>(*buffer);
+                            transfer->chain.sType = chainedStruct->sType;
+                            transfer->chain.hasNext = chainedStruct->next != nullptr;
+
+                            *buffer += sizeof({{CType}}Transfer);
+                            {{CType}}Serialize(*reinterpret_cast<{{CType}} const*>(chainedStruct), transfer, buffer
+                                {%- if types[sType.name.get()].may_have_dawn_object -%}
+                                , provider
+                                {%- endif -%}
+                            );
+
+                            chainedStruct = chainedStruct->next;
+                        } break;
+                    {% endfor %}
+                    default: {
+                        // Invalid enum. Serialize just the transfer header with Invalid as the sType.
+                        // TODO(crbug.com/dawn/369): Unknown sTypes are silently discarded.
+                        ASSERT(chainedStruct->sType == WGPUSType_Invalid);
+                        WGPUChainedStructTransfer* transfer = reinterpret_cast<WGPUChainedStructTransfer*>(*buffer);
+                        transfer->sType = WGPUSType_Invalid;
+                        transfer->hasNext = false;
+
+                        *buffer += sizeof(WGPUChainedStructTransfer);
+                        return;
+                    }
+                }
+            } while (chainedStruct != nullptr);
+        }
+
+        DeserializeResult DeserializeChainedStruct(const WGPUChainedStruct** outChainNext,
+                                                   const volatile char** buffer,
+                                                   size_t* size,
+                                                   DeserializeAllocator* allocator,
+                                                   const ObjectIdResolver& resolver) {
+            bool hasNext;
+            do {
+                if (*size < sizeof(WGPUChainedStructTransfer)) {
+                    return DeserializeResult::FatalError;
+                }
+                WGPUSType sType =
+                    reinterpret_cast<const volatile WGPUChainedStructTransfer*>(*buffer)->sType;
+                switch (sType) {
+                    {% for sType in types["s type"].values if sType.valid and sType.name.CamelCase() not in client_side_structures %}
+                        {% set CType = as_cType(sType.name) %}
+                        case {{as_cEnum(types["s type"].name, sType.name)}}: {
+                            const volatile {{CType}}Transfer* transfer = nullptr;
+                            DESERIALIZE_TRY(GetPtrFromBuffer(buffer, size, 1, &transfer));
+
+                            {{CType}}* outStruct = nullptr;
+                            DESERIALIZE_TRY(GetSpace(allocator, sizeof({{CType}}), &outStruct));
+                            outStruct->chain.sType = sType;
+                            outStruct->chain.next = nullptr;
+
+                            *outChainNext = &outStruct->chain;
+                            outChainNext = &outStruct->chain.next;
+
+                            DESERIALIZE_TRY({{CType}}Deserialize(outStruct, transfer, buffer, size, allocator
+                                {%- if types[sType.name.get()].may_have_dawn_object -%}
+                                    , resolver
+                                {%- endif -%}
+                            ));
+
+                            hasNext = transfer->chain.hasNext;
+                        } break;
+                    {% endfor %}
+                    default:
+                        return DeserializeResult::FatalError;
+                }
+            } while (hasNext);
+
+            return DeserializeResult::Success;
+        }
+
         //* Output [de]serialization helpers for commands
         {% for command in cmd_records["command"] %}
             {% set name = command.name.CamelCase() %}
diff --git a/generator/templates/dawn_wire/WireCmd.h b/generator/templates/dawn_wire/WireCmd.h
index 0b79acf..11e2b78 100644
--- a/generator/templates/dawn_wire/WireCmd.h
+++ b/generator/templates/dawn_wire/WireCmd.h
@@ -100,7 +100,7 @@
         //* Serialize the structure and everything it points to into serializeBuffer which must be
         //* big enough to contain all the data (as queried from GetRequiredSize).
         void Serialize(char* serializeBuffer
-            {%- if command.has_dawn_object -%}
+            {%- if command.may_have_dawn_object -%}
                 , const ObjectIdProvider& objectIdProvider
             {%- endif -%}
         ) const;
@@ -113,7 +113,7 @@
         //*  - Success if everything went well (yay!)
         //*  - FatalError is something bad happened (buffer too small for example)
         DeserializeResult Deserialize(const volatile char** buffer, size_t* size, DeserializeAllocator* allocator
-            {%- if command.has_dawn_object -%}
+            {%- if command.may_have_dawn_object -%}
                 , const ObjectIdResolver& resolver
             {%- endif -%}
         );
diff --git a/generator/templates/dawn_wire/server/ServerHandlers.cpp b/generator/templates/dawn_wire/server/ServerHandlers.cpp
index 07f3dfa..d480bde 100644
--- a/generator/templates/dawn_wire/server/ServerHandlers.cpp
+++ b/generator/templates/dawn_wire/server/ServerHandlers.cpp
@@ -27,7 +27,7 @@
         bool Server::Handle{{Suffix}}(const volatile char** commands, size_t* size) {
             {{Suffix}}Cmd cmd;
             DeserializeResult deserializeResult = cmd.Deserialize(commands, size, &mAllocator
-                {%- if command.has_dawn_object -%}
+                {%- if command.may_have_dawn_object -%}
                     , *this
                 {%- endif -%}
             );
diff --git a/generator/templates/webgpu_cpp.h b/generator/templates/webgpu_cpp.h
index 6bfcdb7..3dde1e8 100644
--- a/generator/templates/webgpu_cpp.h
+++ b/generator/templates/webgpu_cpp.h
@@ -204,7 +204,13 @@
                 ChainedStruct const * nextInChain = nullptr;
             {% endif %}
             {% for member in type.members %}
-                {{as_annotated_cppType(member)}}{{render_cpp_default_value(member)}};
+                {% set member_declaration = as_annotated_cppType(member) + render_cpp_default_value(member) %}
+                {% if type.chained and loop.first %}
+                    //* Align the first member to ChainedStruct to match the C struct layout.
+                    alignas(ChainedStruct) {{member_declaration}};
+                {% else %}
+                    {{member_declaration}};
+                {% endif %}
             {% endfor %}
         };
 
diff --git a/src/tests/unittests/wire/WireExtensionTests.cpp b/src/tests/unittests/wire/WireExtensionTests.cpp
new file mode 100644
index 0000000..78f9de7
--- /dev/null
+++ b/src/tests/unittests/wire/WireExtensionTests.cpp
@@ -0,0 +1,208 @@
+// Copyright 2020 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "tests/unittests/wire/WireTest.h"
+
+using namespace testing;
+using namespace dawn_wire;
+
+class WireExtensionTests : public WireTest {
+  public:
+    WireExtensionTests() {
+    }
+    ~WireExtensionTests() override = default;
+};
+
+// Serialize/Deserializes a chained struct correctly.
+TEST_F(WireExtensionTests, ChainedStruct) {
+    WGPUSamplerDescriptorDummyAnisotropicFiltering clientExt = {};
+    clientExt.chain.sType = WGPUSType_SamplerDescriptorDummyAnisotropicFiltering;
+    clientExt.chain.next = nullptr;
+    clientExt.maxAnisotropy = 3.14;
+
+    WGPUSamplerDescriptor clientDesc = {};
+    clientDesc.nextInChain = &clientExt.chain;
+    clientDesc.label = "sampler with anisotropic filtering";
+
+    wgpuDeviceCreateSampler(device, &clientDesc);
+    EXPECT_CALL(api, DeviceCreateSampler(apiDevice, NotNull()))
+        .WillOnce(Invoke([&](Unused, const WGPUSamplerDescriptor* serverDesc) -> WGPUSampler {
+            EXPECT_STREQ(serverDesc->label, clientDesc.label);
+
+            const auto* ext = reinterpret_cast<const WGPUSamplerDescriptorDummyAnisotropicFiltering*>(
+                serverDesc->nextInChain);
+            EXPECT_EQ(ext->chain.sType, clientExt.chain.sType);
+            EXPECT_EQ(ext->maxAnisotropy, clientExt.maxAnisotropy);
+
+            EXPECT_EQ(ext->chain.next, nullptr);
+
+            return api.GetNewSampler();
+        }));
+    FlushClient();
+}
+
+// Serialize/Deserializes multiple chained structs correctly.
+TEST_F(WireExtensionTests, MutlipleChainedStructs) {
+    WGPUSamplerDescriptorDummyAnisotropicFiltering clientExt2 = {};
+    clientExt2.chain.sType = WGPUSType_SamplerDescriptorDummyAnisotropicFiltering;
+    clientExt2.chain.next = nullptr;
+    clientExt2.maxAnisotropy = 2.71828;
+
+    WGPUSamplerDescriptorDummyAnisotropicFiltering clientExt1 = {};
+    clientExt1.chain.sType = WGPUSType_SamplerDescriptorDummyAnisotropicFiltering;
+    clientExt1.chain.next = &clientExt2.chain;
+    clientExt1.maxAnisotropy = 3.14;
+
+    WGPUSamplerDescriptor clientDesc = {};
+    clientDesc.nextInChain = &clientExt1.chain;
+    clientDesc.label = "sampler with anisotropic filtering";
+
+    wgpuDeviceCreateSampler(device, &clientDesc);
+    EXPECT_CALL(api, DeviceCreateSampler(apiDevice, NotNull()))
+        .WillOnce(Invoke([&](Unused, const WGPUSamplerDescriptor* serverDesc) -> WGPUSampler {
+            EXPECT_STREQ(serverDesc->label, clientDesc.label);
+
+            const auto* ext1 = reinterpret_cast<const WGPUSamplerDescriptorDummyAnisotropicFiltering*>(
+                serverDesc->nextInChain);
+            EXPECT_EQ(ext1->chain.sType, clientExt1.chain.sType);
+            EXPECT_EQ(ext1->maxAnisotropy, clientExt1.maxAnisotropy);
+
+            const auto* ext2 = reinterpret_cast<const WGPUSamplerDescriptorDummyAnisotropicFiltering*>(
+                ext1->chain.next);
+            EXPECT_EQ(ext2->chain.sType, clientExt2.chain.sType);
+            EXPECT_EQ(ext2->maxAnisotropy, clientExt2.maxAnisotropy);
+
+            EXPECT_EQ(ext2->chain.next, nullptr);
+
+            return api.GetNewSampler();
+        }));
+    FlushClient();
+
+    // Swap the order of the chained structs.
+    clientDesc.nextInChain = &clientExt2.chain;
+    clientExt2.chain.next = &clientExt1.chain;
+    clientExt1.chain.next = nullptr;
+
+    wgpuDeviceCreateSampler(device, &clientDesc);
+    EXPECT_CALL(api, DeviceCreateSampler(apiDevice, NotNull()))
+        .WillOnce(Invoke([&](Unused, const WGPUSamplerDescriptor* serverDesc) -> WGPUSampler {
+            EXPECT_STREQ(serverDesc->label, clientDesc.label);
+
+            const auto* ext2 = reinterpret_cast<const WGPUSamplerDescriptorDummyAnisotropicFiltering*>(
+                serverDesc->nextInChain);
+            EXPECT_EQ(ext2->chain.sType, clientExt2.chain.sType);
+            EXPECT_EQ(ext2->maxAnisotropy, clientExt2.maxAnisotropy);
+
+            const auto* ext1 = reinterpret_cast<const WGPUSamplerDescriptorDummyAnisotropicFiltering*>(
+                ext2->chain.next);
+            EXPECT_EQ(ext1->chain.sType, clientExt1.chain.sType);
+            EXPECT_EQ(ext1->maxAnisotropy, clientExt1.maxAnisotropy);
+
+            EXPECT_EQ(ext1->chain.next, nullptr);
+
+            return api.GetNewSampler();
+        }));
+    FlushClient();
+}
+
+// Test that a chained struct with Invalid sType is an error.
+TEST_F(WireExtensionTests, InvalidSType) {
+    WGPUSamplerDescriptorDummyAnisotropicFiltering clientExt = {};
+    clientExt.chain.sType = WGPUSType_Invalid;
+    clientExt.chain.next = nullptr;
+
+    WGPUSamplerDescriptor clientDesc = {};
+    clientDesc.nextInChain = &clientExt.chain;
+    clientDesc.label = "sampler with anisotropic filtering";
+
+    wgpuDeviceCreateSampler(device, &clientDesc);
+    FlushClient(false);
+}
+
+// Test that if both an invalid and valid stype are passed on the chain, it is an error.
+TEST_F(WireExtensionTests, ValidAndInvalidSTypeInChain) {
+    WGPUSamplerDescriptorDummyAnisotropicFiltering clientExt2 = {};
+    clientExt2.chain.sType = WGPUSType_Invalid;
+    clientExt2.chain.next = nullptr;
+    clientExt2.maxAnisotropy = 2.71828;
+
+    WGPUSamplerDescriptorDummyAnisotropicFiltering clientExt1 = {};
+    clientExt1.chain.sType = WGPUSType_SamplerDescriptorDummyAnisotropicFiltering;
+    clientExt1.chain.next = &clientExt2.chain;
+    clientExt1.maxAnisotropy = 3.14;
+
+    WGPUSamplerDescriptor clientDesc = {};
+    clientDesc.nextInChain = &clientExt1.chain;
+    clientDesc.label = "sampler with anisotropic filtering";
+
+    wgpuDeviceCreateSampler(device, &clientDesc);
+    FlushClient(false);
+
+    // Swap the order of the chained structs.
+    clientDesc.nextInChain = &clientExt2.chain;
+    clientExt2.chain.next = &clientExt1.chain;
+    clientExt1.chain.next = nullptr;
+
+    wgpuDeviceCreateSampler(device, &clientDesc);
+    FlushClient(false);
+}
+
+// Test that (de)?serializing a chained struct with subdescriptors works.
+TEST_F(WireExtensionTests, ChainedStructWithSubdescriptor) {
+    WGPUShaderModuleDescriptor shaderModuleDesc = {};
+
+    WGPUShaderModule apiShaderModule1 = api.GetNewShaderModule();
+    WGPUShaderModule shaderModule1 = wgpuDeviceCreateShaderModule(device, &shaderModuleDesc);
+    EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShaderModule1));
+    FlushClient();
+
+    WGPUShaderModule apiShaderModule2 = api.GetNewShaderModule();
+    WGPUShaderModule shaderModule2 = wgpuDeviceCreateShaderModule(device, &shaderModuleDesc);
+    EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShaderModule2));
+    FlushClient();
+
+    WGPUProgrammableStageDescriptor extraStageDesc = {};
+    extraStageDesc.module = shaderModule1;
+    extraStageDesc.entryPoint = "my other module";
+
+    WGPURenderPipelineDescriptorDummyExtension clientExt = {};
+    clientExt.chain.sType = WGPUSType_RenderPipelineDescriptorDummyExtension;
+    clientExt.chain.next = nullptr;
+    clientExt.dummyStage = extraStageDesc;
+
+    WGPURenderPipelineDescriptor renderPipelineDesc = {};
+    renderPipelineDesc.nextInChain = &clientExt.chain;
+    renderPipelineDesc.vertexStage.module = shaderModule2;
+    renderPipelineDesc.vertexStage.entryPoint = "my vertex module";
+
+    wgpuDeviceCreateRenderPipeline(device, &renderPipelineDesc);
+    EXPECT_CALL(api, DeviceCreateRenderPipeline(apiDevice, NotNull()))
+        .WillOnce(Invoke([&](Unused,
+                             const WGPURenderPipelineDescriptor* serverDesc) -> WGPURenderPipeline {
+            EXPECT_EQ(serverDesc->vertexStage.module, apiShaderModule2);
+            EXPECT_STREQ(serverDesc->vertexStage.entryPoint,
+                         renderPipelineDesc.vertexStage.entryPoint);
+
+            const auto* ext = reinterpret_cast<const WGPURenderPipelineDescriptorDummyExtension*>(
+                serverDesc->nextInChain);
+            EXPECT_EQ(ext->chain.sType, clientExt.chain.sType);
+            EXPECT_EQ(ext->dummyStage.module, apiShaderModule1);
+            EXPECT_STREQ(ext->dummyStage.entryPoint, extraStageDesc.entryPoint);
+
+            EXPECT_EQ(ext->chain.next, nullptr);
+
+            return api.GetNewRenderPipeline();
+        }));
+    FlushClient();
+}