diff --git a/generator/templates/dawn/wire/WireCmd.cpp b/generator/templates/dawn/wire/WireCmd.cpp index 26c0291d3e..10a7d36e31 100644 --- a/generator/templates/dawn/wire/WireCmd.cpp +++ b/generator/templates/dawn/wire/WireCmd.cpp @@ -165,7 +165,7 @@ if (has_{{memberName}}) {% endif %} { - result += std::strlen(record.{{memberName}}); + result += Align(std::strlen(record.{{memberName}}), kWireBufferAlignment); } {% endfor %} @@ -178,7 +178,9 @@ {% if member.annotation != "value" %} {{ assert(member.annotation != "const*const*") }} auto memberLength = {{member_length(member, "record.")}}; - result += memberLength * {{member_transfer_sizeof(member)}}; + auto size = WireAlignSizeofN<{{member_transfer_type(member)}}>(memberLength); + ASSERT(size); + result += *size; //* Structures might contain more pointers so we need to add their extra size as well. {% if member.type.category == "structure" %} for (decltype(memberLength) i = 0; i < memberLength; ++i) { @@ -431,7 +433,7 @@ {% set Cmd = Name + "Cmd" %} size_t {{Cmd}}::GetRequiredSize() const { - size_t size = sizeof({{Name}}Transfer) + {{Name}}GetExtraRequiredSize(*this); + size_t size = WireAlignSizeof<{{Name}}Transfer>() + {{Name}}GetExtraRequiredSize(*this); return size; } @@ -509,7 +511,7 @@ ) %} 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 += WireAlignSizeof<{{as_cType(sType.name)}}Transfer>(); result += {{as_cType(sType.name)}}GetExtraRequiredSize(typedStruct); chainedStruct = typedStruct.chain.next; break; @@ -519,7 +521,7 @@ case WGPUSType_Invalid: default: // Invalid enum. Reserve space just for the transfer header (sType and hasNext). - result += sizeof(WGPUChainedStructTransfer); + result += WireAlignSizeof(); chainedStruct = chainedStruct->next; break; } @@ -600,7 +602,7 @@ WIRE_TRY(deserializeBuffer->Read(&transfer)); {{CType}}* outStruct; - WIRE_TRY(GetSpace(allocator, sizeof({{CType}}), &outStruct)); + WIRE_TRY(GetSpace(allocator, 1u, &outStruct)); outStruct->chain.sType = sType; outStruct->chain.next = nullptr; @@ -629,7 +631,7 @@ WIRE_TRY(deserializeBuffer->Read(&transfer)); {{ChainedStruct}}* outStruct; - WIRE_TRY(GetSpace(allocator, sizeof({{ChainedStruct}}), &outStruct)); + WIRE_TRY(GetSpace(allocator, 1u, &outStruct)); outStruct->sType = WGPUSType_Invalid; outStruct->next = nullptr; @@ -654,13 +656,23 @@ namespace dawn::wire { // Always writes to |out| on success. template WireResult GetSpace(DeserializeAllocator* allocator, N count, T** out) { - constexpr size_t kMaxCountWithoutOverflows = std::numeric_limits::max() / sizeof(T); - if (count > kMaxCountWithoutOverflows) { + // Because we use this function extensively when `count` == 1, we can optimize the + // size computations a bit more for those cases via constexpr version of the + // alignment computation. + constexpr size_t kSizeofT = WireAlignSizeof(); + size_t size = 0; + if (count == 1) { + size = kSizeofT; + } else { + auto sizeN = WireAlignSizeofN(count); + // A size of 0 indicates an overflow, so return an error. + if (!sizeN) { return WireResult::FatalError; + } + size = *sizeN; } - size_t totalSize = sizeof(T) * count; - *out = static_cast(allocator->GetSpace(totalSize)); + *out = static_cast(allocator->GetSpace(size)); if (*out == nullptr) { return WireResult::FatalError; } diff --git a/src/dawn/common/Constants.h b/src/dawn/common/Constants.h index bf1f132b6c..027b6c0659 100644 --- a/src/dawn/common/Constants.h +++ b/src/dawn/common/Constants.h @@ -15,6 +15,7 @@ #ifndef SRC_DAWN_COMMON_CONSTANTS_H_ #define SRC_DAWN_COMMON_CONSTANTS_H_ +#include #include static constexpr uint32_t kMaxBindGroups = 4u; @@ -65,4 +66,7 @@ static constexpr uint8_t kSampledTexturesPerExternalTexture = 4u; static constexpr uint8_t kSamplersPerExternalTexture = 1u; static constexpr uint8_t kUniformsPerExternalTexture = 1u; +// Wire buffer alignments. +static constexpr size_t kWireBufferAlignment = 8u; + #endif // SRC_DAWN_COMMON_CONSTANTS_H_ diff --git a/src/dawn/common/Math.h b/src/dawn/common/Math.h index 9984c4b0b0..c8b518fb89 100644 --- a/src/dawn/common/Math.h +++ b/src/dawn/common/Math.h @@ -20,6 +20,7 @@ #include #include +#include #include #include "dawn/common/Assert.h" @@ -61,6 +62,26 @@ T Align(T value, size_t alignment) { return (value + (alignmentT - 1)) & ~(alignmentT - 1); } +template +constexpr size_t AlignSizeof() { + static_assert(Alignment != 0 && (Alignment & (Alignment - 1)) == 0, + "Alignment must be a valid power of 2."); + static_assert(sizeof(T) <= std::numeric_limits::max() - (Alignment - 1)); + return (sizeof(T) + (Alignment - 1)) & ~(Alignment - 1); +} + +// Returns an aligned size for an n-sized array of T elements. If the size would overflow, returns +// nullopt instead. +template +std::optional AlignSizeofN(uint64_t n) { + constexpr uint64_t kMaxCountWithoutOverflows = + (std::numeric_limits::max() - Alignment + 1) / sizeof(T); + if (n > kMaxCountWithoutOverflows) { + return std::nullopt; + } + return Align(sizeof(T) * n, Alignment); +} + template DAWN_FORCE_INLINE T* AlignPtr(T* ptr, size_t alignment) { ASSERT(IsPowerOfTwo(alignment)); diff --git a/src/dawn/tests/unittests/MathTests.cpp b/src/dawn/tests/unittests/MathTests.cpp index d88e8587bf..a38fb675dd 100644 --- a/src/dawn/tests/unittests/MathTests.cpp +++ b/src/dawn/tests/unittests/MathTests.cpp @@ -13,6 +13,7 @@ // limitations under the License. #include +#include #include #include "dawn/EnumClassBitmasks.h" @@ -180,6 +181,41 @@ TEST(Math, Align) { ASSERT_EQ(Align(static_cast(0xFFFFFFFFFFFFFFFF), 1), 0xFFFFFFFFFFFFFFFFull); } +TEST(Math, AlignSizeof) { + // Basic types should align to self if alignment is a divisor. + ASSERT_EQ((AlignSizeof()), 1u); + + ASSERT_EQ((AlignSizeof()), 2u); + ASSERT_EQ((AlignSizeof()), 2u); + + ASSERT_EQ((AlignSizeof()), 4u); + ASSERT_EQ((AlignSizeof()), 4u); + ASSERT_EQ((AlignSizeof()), 4u); + + ASSERT_EQ((AlignSizeof()), 8u); + ASSERT_EQ((AlignSizeof()), 8u); + ASSERT_EQ((AlignSizeof()), 8u); + ASSERT_EQ((AlignSizeof()), 8u); + + // Everything in range (align, 2*align] aligns to 2*align. + ASSERT_EQ((AlignSizeof()), 8u); + ASSERT_EQ((AlignSizeof()), 8u); + ASSERT_EQ((AlignSizeof()), 8u); + ASSERT_EQ((AlignSizeof()), 8u); +} + +TEST(Math, AlignSizeofN) { + // Everything in range (align, 2*align] aligns to 2*align. + ASSERT_EQ(*(AlignSizeofN(5)), 8u); + ASSERT_EQ(*(AlignSizeofN(6)), 8u); + ASSERT_EQ(*(AlignSizeofN(7)), 8u); + ASSERT_EQ(*(AlignSizeofN(8)), 8u); + + // Extremes should return nullopt. + ASSERT_EQ((AlignSizeofN(std::numeric_limits::max())), std::nullopt); + ASSERT_EQ((AlignSizeofN(std::numeric_limits::max())), std::nullopt); +} + // Tests for IsPtrAligned TEST(Math, IsPtrAligned) { constexpr size_t kTestAlignment = 8; diff --git a/src/dawn/wire/BufferConsumer.h b/src/dawn/wire/BufferConsumer.h index 1ae8451109..12c40361cb 100644 --- a/src/dawn/wire/BufferConsumer.h +++ b/src/dawn/wire/BufferConsumer.h @@ -17,10 +17,22 @@ #include +#include "dawn/common/Constants.h" +#include "dawn/common/Math.h" #include "dawn/wire/WireResult.h" namespace dawn::wire { +// Wire specific alignment helpers. +template +constexpr size_t WireAlignSizeof() { + return AlignSizeof(); +} +template +std::optional WireAlignSizeofN(size_t n) { + return AlignSizeofN(n); +} + // BufferConsumer is a utility class that allows reading bytes from a buffer // while simultaneously decrementing the amount of remaining space by exactly // the amount read. It helps prevent bugs where incrementing a pointer and diff --git a/src/dawn/wire/BufferConsumer_impl.h b/src/dawn/wire/BufferConsumer_impl.h index 6b5d0a1323..52b6720219 100644 --- a/src/dawn/wire/BufferConsumer_impl.h +++ b/src/dawn/wire/BufferConsumer_impl.h @@ -15,11 +15,11 @@ #ifndef SRC_DAWN_WIRE_BUFFERCONSUMER_IMPL_H_ #define SRC_DAWN_WIRE_BUFFERCONSUMER_IMPL_H_ -#include "dawn/wire/BufferConsumer.h" - #include #include +#include "dawn/wire/BufferConsumer.h" + namespace dawn::wire { template @@ -36,13 +36,14 @@ WireResult BufferConsumer::Peek(T** data) { template template WireResult BufferConsumer::Next(T** data) { - if (sizeof(T) > mSize) { + constexpr size_t kSize = WireAlignSizeof(); + if (kSize > mSize) { return WireResult::FatalError; } *data = reinterpret_cast(mBuffer); - mBuffer += sizeof(T); - mSize -= sizeof(T); + mBuffer += kSize; + mSize -= kSize; return WireResult::Success; } @@ -51,20 +52,15 @@ template WireResult BufferConsumer::NextN(N count, T** data) { static_assert(std::is_unsigned::value, "|count| argument of NextN must be unsigned."); - constexpr size_t kMaxCountWithoutOverflows = std::numeric_limits::max() / sizeof(T); - if (count > kMaxCountWithoutOverflows) { - return WireResult::FatalError; - } - - // Cannot overflow because |count| is not greater than |kMaxCountWithoutOverflows|. - size_t totalSize = sizeof(T) * count; - if (totalSize > mSize) { + // If size is zero then it indicates an overflow. + auto size = WireAlignSizeofN(count); + if (size && *size > mSize) { return WireResult::FatalError; } *data = reinterpret_cast(mBuffer); - mBuffer += totalSize; - mSize -= totalSize; + mBuffer += *size; + mSize -= *size; return WireResult::Success; } diff --git a/src/dawn/wire/ChunkedCommandSerializer.h b/src/dawn/wire/ChunkedCommandSerializer.h index 7ac72e5ddb..8ad64ff94f 100644 --- a/src/dawn/wire/ChunkedCommandSerializer.h +++ b/src/dawn/wire/ChunkedCommandSerializer.h @@ -17,73 +17,97 @@ #include #include +#include #include #include #include "dawn/common/Alloc.h" #include "dawn/common/Compiler.h" +#include "dawn/common/Constants.h" +#include "dawn/common/Math.h" #include "dawn/wire/Wire.h" #include "dawn/wire/WireCmd_autogen.h" namespace dawn::wire { +// Simple command extension struct used when a command needs to serialize additional information +// that is not baked directly into the command already. +struct CommandExtension { + size_t size; + std::function serialize; +}; + +namespace detail { + +inline WireResult SerializeCommandExtension(SerializeBuffer* serializeBuffer) { + return WireResult::Success; +} + +template +WireResult SerializeCommandExtension(SerializeBuffer* serializeBuffer, + Extension&& e, + Extensions&&... es) { + char* buffer; + WIRE_TRY(serializeBuffer->NextN(e.size, &buffer)); + e.serialize(buffer); + + WIRE_TRY(SerializeCommandExtension(serializeBuffer, std::forward(es)...)); + return WireResult::Success; +} + +} // namespace detail + class ChunkedCommandSerializer { public: explicit ChunkedCommandSerializer(CommandSerializer* serializer); template void SerializeCommand(const Cmd& cmd) { - SerializeCommand(cmd, 0, [](SerializeBuffer*) { return WireResult::Success; }); + SerializeCommandImpl( + cmd, [](const Cmd& cmd, size_t requiredSize, SerializeBuffer* serializeBuffer) { + return cmd.Serialize(requiredSize, serializeBuffer); + }); } - template - void SerializeCommand(const Cmd& cmd, - size_t extraSize, - ExtraSizeSerializeFn&& SerializeExtraSize) { + template + void SerializeCommand(const Cmd& cmd, CommandExtension&& e, Extensions&&... es) { SerializeCommandImpl( cmd, [](const Cmd& cmd, size_t requiredSize, SerializeBuffer* serializeBuffer) { return cmd.Serialize(requiredSize, serializeBuffer); }, - extraSize, std::forward(SerializeExtraSize)); + std::forward(e), std::forward(es)...); } - template - void SerializeCommand(const Cmd& cmd, const ObjectIdProvider& objectIdProvider) { - SerializeCommand(cmd, objectIdProvider, 0, - [](SerializeBuffer*) { return WireResult::Success; }); - } - - template + template void SerializeCommand(const Cmd& cmd, const ObjectIdProvider& objectIdProvider, - size_t extraSize, - ExtraSizeSerializeFn&& SerializeExtraSize) { + Extensions&&... extensions) { SerializeCommandImpl( cmd, [&objectIdProvider](const Cmd& cmd, size_t requiredSize, SerializeBuffer* serializeBuffer) { return cmd.Serialize(requiredSize, serializeBuffer, objectIdProvider); }, - extraSize, std::forward(SerializeExtraSize)); + std::forward(extensions)...); } private: - template + template void SerializeCommandImpl(const Cmd& cmd, SerializeCmdFn&& SerializeCmd, - size_t extraSize, - ExtraSizeSerializeFn&& SerializeExtraSize) { + Extensions&&... extensions) { size_t commandSize = cmd.GetRequiredSize(); - size_t requiredSize = commandSize + extraSize; + size_t requiredSize = (Align(extensions.size, kWireBufferAlignment) + ... + commandSize); if (requiredSize <= mMaxAllocationSize) { char* allocatedBuffer = static_cast(mSerializer->GetCmdSpace(requiredSize)); if (allocatedBuffer != nullptr) { SerializeBuffer serializeBuffer(allocatedBuffer, requiredSize); - WireResult r1 = SerializeCmd(cmd, requiredSize, &serializeBuffer); - WireResult r2 = SerializeExtraSize(&serializeBuffer); - if (DAWN_UNLIKELY(r1 != WireResult::Success || r2 != WireResult::Success)) { + WireResult rCmd = SerializeCmd(cmd, requiredSize, &serializeBuffer); + WireResult rExts = + detail::SerializeCommandExtension(&serializeBuffer, extensions...); + if (DAWN_UNLIKELY(rCmd != WireResult::Success || rExts != WireResult::Success)) { mSerializer->OnSerializeError(); } } @@ -95,9 +119,9 @@ class ChunkedCommandSerializer { return; } SerializeBuffer serializeBuffer(cmdSpace.get(), requiredSize); - WireResult r1 = SerializeCmd(cmd, requiredSize, &serializeBuffer); - WireResult r2 = SerializeExtraSize(&serializeBuffer); - if (DAWN_UNLIKELY(r1 != WireResult::Success || r2 != WireResult::Success)) { + WireResult rCmd = SerializeCmd(cmd, requiredSize, &serializeBuffer); + WireResult rExts = detail::SerializeCommandExtension(&serializeBuffer, extensions...); + if (DAWN_UNLIKELY(rCmd != WireResult::Success || rExts != WireResult::Success)) { mSerializer->OnSerializeError(); return; } diff --git a/src/dawn/wire/client/Buffer.cpp b/src/dawn/wire/client/Buffer.cpp index 32da663827..4315452775 100644 --- a/src/dawn/wire/client/Buffer.cpp +++ b/src/dawn/wire/client/Buffer.cpp @@ -47,6 +47,8 @@ WGPUBuffer Buffer::Create(Device* device, const WGPUBufferDescriptor* descriptor cmd.writeHandleCreateInfoLength = 0; cmd.writeHandleCreateInfo = nullptr; + size_t readHandleCreateInfoLength = 0; + size_t writeHandleCreateInfoLength = 0; if (mappable) { if ((descriptor->usage & WGPUBufferUsage_MapRead) != 0) { // Create the read handle on buffer creation. @@ -56,7 +58,8 @@ WGPUBuffer Buffer::Create(Device* device, const WGPUBufferDescriptor* descriptor device->InjectError(WGPUErrorType_OutOfMemory, "Failed to create buffer mapping"); return CreateError(device, descriptor); } - cmd.readHandleCreateInfoLength = readHandle->SerializeCreateSize(); + readHandleCreateInfoLength = readHandle->SerializeCreateSize(); + cmd.readHandleCreateInfoLength = readHandleCreateInfoLength; } if ((descriptor->usage & WGPUBufferUsage_MapWrite) != 0 || descriptor->mappedAtCreation) { @@ -67,7 +70,8 @@ WGPUBuffer Buffer::Create(Device* device, const WGPUBufferDescriptor* descriptor device->InjectError(WGPUErrorType_OutOfMemory, "Failed to create buffer mapping"); return CreateError(device, descriptor); } - cmd.writeHandleCreateInfoLength = writeHandle->SerializeCreateSize(); + writeHandleCreateInfoLength = writeHandle->SerializeCreateSize(); + cmd.writeHandleCreateInfoLength = writeHandleCreateInfoLength; } } @@ -95,27 +99,28 @@ WGPUBuffer Buffer::Create(Device* device, const WGPUBufferDescriptor* descriptor cmd.result = buffer->GetWireHandle(); + // clang-format off + // Turning off clang format here because for some reason it does not format the + // CommandExtensions consistently, making it harder to read. wireClient->SerializeCommand( - cmd, cmd.readHandleCreateInfoLength + cmd.writeHandleCreateInfoLength, - [&](SerializeBuffer* serializeBuffer) { - if (readHandle != nullptr) { - char* readHandleBuffer; - WIRE_TRY(serializeBuffer->NextN(cmd.readHandleCreateInfoLength, &readHandleBuffer)); - // Serialize the ReadHandle into the space after the command. - readHandle->SerializeCreate(readHandleBuffer); - buffer->mReadHandle = std::move(readHandle); - } - if (writeHandle != nullptr) { - char* writeHandleBuffer; - WIRE_TRY( - serializeBuffer->NextN(cmd.writeHandleCreateInfoLength, &writeHandleBuffer)); - // Serialize the WriteHandle into the space after the command. - writeHandle->SerializeCreate(writeHandleBuffer); - buffer->mWriteHandle = std::move(writeHandle); - } - - return WireResult::Success; - }); + cmd, + CommandExtension{readHandleCreateInfoLength, + [&](char* readHandleBuffer) { + if (readHandle != nullptr) { + // Serialize the ReadHandle into the space after the command. + readHandle->SerializeCreate(readHandleBuffer); + buffer->mReadHandle = std::move(readHandle); + } + }}, + CommandExtension{writeHandleCreateInfoLength, + [&](char* writeHandleBuffer) { + if (writeHandle != nullptr) { + // Serialize the WriteHandle into the space after the command. + writeHandle->SerializeCreate(writeHandleBuffer); + buffer->mWriteHandle = std::move(writeHandle); + } + }}); + // clang-format on return ToAPI(buffer); } @@ -310,16 +315,12 @@ void Buffer::Unmap() { cmd.size = mMapSize; client->SerializeCommand( - cmd, writeDataUpdateInfoLength, [&](SerializeBuffer* serializeBuffer) { - char* writeHandleBuffer; - WIRE_TRY(serializeBuffer->NextN(writeDataUpdateInfoLength, &writeHandleBuffer)); - - // Serialize flush metadata into the space after the command. - // This closes the handle for writing. - mWriteHandle->SerializeDataUpdate(writeHandleBuffer, cmd.offset, cmd.size); - - return WireResult::Success; - }); + cmd, CommandExtension{writeDataUpdateInfoLength, [&](char* writeHandleBuffer) { + // Serialize flush metadata into the space after the command. + // This closes the handle for writing. + mWriteHandle->SerializeDataUpdate(writeHandleBuffer, + cmd.offset, cmd.size); + }}); // If mDestructWriteHandleOnUnmap is true, that means the write handle is merely // for mappedAtCreation usage. It is destroyed on unmap after flush to server diff --git a/src/dawn/wire/client/Client.h b/src/dawn/wire/client/Client.h index 8648522bd8..f16af64e17 100644 --- a/src/dawn/wire/client/Client.h +++ b/src/dawn/wire/client/Client.h @@ -85,11 +85,9 @@ class Client : public ClientBase { mSerializer.SerializeCommand(cmd, *this); } - template - void SerializeCommand(const Cmd& cmd, - size_t extraSize, - ExtraSizeSerializeFn&& SerializeExtraSize) { - mSerializer.SerializeCommand(cmd, *this, extraSize, SerializeExtraSize); + template + void SerializeCommand(const Cmd& cmd, Extensions&&... es) { + mSerializer.SerializeCommand(cmd, *this, std::forward(es)...); } void Disconnect(); diff --git a/src/dawn/wire/server/Server.h b/src/dawn/wire/server/Server.h index 281275685c..2dfaba7245 100644 --- a/src/dawn/wire/server/Server.h +++ b/src/dawn/wire/server/Server.h @@ -184,11 +184,9 @@ class Server : public ServerBase { mSerializer.SerializeCommand(cmd); } - template - void SerializeCommand(const Cmd& cmd, - size_t extraSize, - ExtraSizeSerializeFn&& SerializeExtraSize) { - mSerializer.SerializeCommand(cmd, extraSize, SerializeExtraSize); + template + void SerializeCommand(const Cmd& cmd, Extensions&&... es) { + mSerializer.SerializeCommand(cmd, std::forward(es)...); } void SetForwardingDeviceCallbacks(ObjectData* deviceObject); diff --git a/src/dawn/wire/server/ServerBuffer.cpp b/src/dawn/wire/server/ServerBuffer.cpp index f07bdfad77..e5208fdc79 100644 --- a/src/dawn/wire/server/ServerBuffer.cpp +++ b/src/dawn/wire/server/ServerBuffer.cpp @@ -237,12 +237,14 @@ void Server::OnBufferMapAsyncCallback(MapUserdata* data, WGPUBufferMapAsyncStatu cmd.readDataUpdateInfo = nullptr; const void* readData = nullptr; + size_t readDataUpdateInfoLength = 0; if (isSuccess) { if (isRead) { // Get the serialization size of the message to initialize ReadHandle data. readData = mProcs.bufferGetConstMappedRange(data->bufferObj, data->offset, data->size); - cmd.readDataUpdateInfoLength = + readDataUpdateInfoLength = bufferData->readHandle->SizeOfSerializeDataUpdate(data->offset, data->size); + cmd.readDataUpdateInfoLength = readDataUpdateInfoLength; } else { ASSERT(data->mode & WGPUMapMode_Write); // The in-flight map request returned successfully. @@ -259,16 +261,15 @@ void Server::OnBufferMapAsyncCallback(MapUserdata* data, WGPUBufferMapAsyncStatu } } - SerializeCommand(cmd, cmd.readDataUpdateInfoLength, [&](SerializeBuffer* serializeBuffer) { - if (isSuccess && isRead) { - char* readHandleBuffer; - WIRE_TRY(serializeBuffer->NextN(cmd.readDataUpdateInfoLength, &readHandleBuffer)); - // The in-flight map request returned successfully. - bufferData->readHandle->SerializeDataUpdate(readData, data->offset, data->size, - readHandleBuffer); - } - return WireResult::Success; - }); + SerializeCommand(cmd, CommandExtension{readDataUpdateInfoLength, [&](char* readHandleBuffer) { + if (isSuccess && isRead) { + // The in-flight map request returned + // successfully. + bufferData->readHandle->SerializeDataUpdate( + readData, data->offset, data->size, + readHandleBuffer); + } + }}); } } // namespace dawn::wire::server