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();
+}