Dawn: Make shader module reflection serializable

This CL make shader module reflection data, i.e. EntryPointMetadataTable
serializable, so we can cache it into on-disk blob cache. With this CL
it is possible to create a shader module with reflection data but
without calling Tint at all.
This CL also make macro DAWN_SERIALIZABLE to generate default equality
comparison for the serializable struct, and also add default equality
comparison operator in necessary structs to achieve that.

Bug: 402772740, 42240459
Change-Id: Icf7b2d3d2e1fa427f2bd9e1db4f6410ff842c08c
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/241494
Reviewed-by: Geoff Lang <geofflang@chromium.org>
Commit-Queue: Zhaoming Jiang <zhaoming.jiang@microsoft.com>
diff --git a/src/dawn/native/BindGroupLayoutInternal.cpp b/src/dawn/native/BindGroupLayoutInternal.cpp
index 22b5f88..0c1b299 100644
--- a/src/dawn/native/BindGroupLayoutInternal.cpp
+++ b/src/dawn/native/BindGroupLayoutInternal.cpp
@@ -379,12 +379,11 @@
     return {
         .binding = binding,
         .visibility = visibility,
-        .bindingLayout =
-            TextureBindingInfo{
-                .sampleType = wgpu::TextureSampleType::Float,
-                .viewDimension = wgpu::TextureViewDimension::e2D,
-                .multisampled = false,
-            },
+        .bindingLayout = TextureBindingInfo{{
+            .sampleType = wgpu::TextureSampleType::Float,
+            .viewDimension = wgpu::TextureViewDimension::e2D,
+            .multisampled = false,
+        }},
     };
 }
 
@@ -393,12 +392,11 @@
     return {
         .binding = binding,
         .visibility = visibility,
-        .bindingLayout =
-            BufferBindingInfo{
-                .type = wgpu::BufferBindingType::Uniform,
-                .minBindingSize = 0,
-                .hasDynamicOffset = false,
-            },
+        .bindingLayout = BufferBindingInfo{{
+            .type = wgpu::BufferBindingType::Uniform,
+            .minBindingSize = 0,
+            .hasDynamicOffset = false,
+        }},
     };
 }
 
@@ -414,7 +412,7 @@
     } else if (binding->texture.sampleType != wgpu::TextureSampleType::BindingNotUsed) {
         auto textureBindingInfo = TextureBindingInfo::From(binding->texture);
         if (binding->texture.viewDimension == kInternalInputAttachmentDim) {
-            bindingInfo.bindingLayout = InputAttachmentBindingInfo{textureBindingInfo.sampleType};
+            bindingInfo.bindingLayout = InputAttachmentBindingInfo{{textureBindingInfo.sampleType}};
         } else {
             bindingInfo.bindingLayout = textureBindingInfo;
         }
diff --git a/src/dawn/native/BindingInfo.cpp b/src/dawn/native/BindingInfo.cpp
index 099d9f7..ee52c714 100644
--- a/src/dawn/native/BindingInfo.cpp
+++ b/src/dawn/native/BindingInfo.cpp
@@ -302,11 +302,11 @@
 // static
 BufferBindingInfo BufferBindingInfo::From(const BufferBindingLayout& layout) {
     BufferBindingLayout defaultedLayout = layout.WithTrivialFrontendDefaults();
-    return {
+    return {{
         .type = defaultedLayout.type,
         .minBindingSize = defaultedLayout.minBindingSize,
         .hasDynamicOffset = defaultedLayout.hasDynamicOffset,
-    };
+    }};
 }
 
 // TextureBindingInfo
@@ -314,11 +314,11 @@
 // static
 TextureBindingInfo TextureBindingInfo::From(const TextureBindingLayout& layout) {
     TextureBindingLayout defaultedLayout = layout.WithTrivialFrontendDefaults();
-    return {
+    return {{
         .sampleType = defaultedLayout.sampleType,
         .viewDimension = defaultedLayout.viewDimension,
         .multisampled = defaultedLayout.multisampled,
-    };
+    }};
 }
 
 // StorageTextureBindingInfo
@@ -327,11 +327,11 @@
 StorageTextureBindingInfo StorageTextureBindingInfo::From(
     const StorageTextureBindingLayout& layout) {
     StorageTextureBindingLayout defaultedLayout = layout.WithTrivialFrontendDefaults();
-    return {
+    return {{
         .format = defaultedLayout.format,
         .viewDimension = defaultedLayout.viewDimension,
         .access = defaultedLayout.access,
-    };
+    }};
 }
 
 // SamplerBindingInfo
@@ -339,9 +339,9 @@
 // static
 SamplerBindingInfo SamplerBindingInfo::From(const SamplerBindingLayout& layout) {
     SamplerBindingLayout defaultedLayout = layout.WithTrivialFrontendDefaults();
-    return {
+    return {{
         .type = defaultedLayout.type,
-    };
+    }};
 }
 
 // SamplerBindingInfo
diff --git a/src/dawn/native/BindingInfo.h b/src/dawn/native/BindingInfo.h
index 292c256..c171f22 100644
--- a/src/dawn/native/BindingInfo.h
+++ b/src/dawn/native/BindingInfo.h
@@ -39,6 +39,7 @@
 #include "dawn/native/Format.h"
 #include "dawn/native/IntegerTypes.h"
 #include "dawn/native/PerStage.h"
+#include "dawn/native/Serializable.h"
 
 #include "dawn/native/dawn_platform.h"
 
@@ -69,51 +70,57 @@
 };
 
 // A mirror of wgpu::BufferBindingLayout for use inside dawn::native.
-struct BufferBindingInfo {
+#define BUFFER_BINDING_INFO_MEMBER(X)                           \
+    X(wgpu::BufferBindingType, type)                            \
+    X(uint64_t, minBindingSize)                                 \
+    /* hasDynamicOffset is always false in shader reflection */ \
+    X(bool, hasDynamicOffset)
+DAWN_SERIALIZABLE(struct, BufferBindingInfo, BUFFER_BINDING_INFO_MEMBER) {
     static BufferBindingInfo From(const BufferBindingLayout& layout);
-
-    wgpu::BufferBindingType type;
-    uint64_t minBindingSize;
-
-    // Always false in shader reflection.
-    bool hasDynamicOffset = false;
-
-    bool operator==(const BufferBindingInfo& other) const = default;
 };
+#undef BUFFER_BINDING_INFO_MEMBER
 
 // A mirror of wgpu::TextureBindingLayout for use inside dawn::native.
-struct TextureBindingInfo {
+#define TEXTURE_BINDING_INFO_MEMBER(X)                                                       \
+    /* For shader reflection UnfilterableFloat is never used and the sample type is Float */ \
+    /* for any texture_Nd<f32>.                                                           */ \
+    X(wgpu::TextureSampleType, sampleType)                                                   \
+    X(wgpu::TextureViewDimension, viewDimension)                                             \
+    X(bool, multisampled)
+DAWN_SERIALIZABLE(struct, TextureBindingInfo, TEXTURE_BINDING_INFO_MEMBER) {
     static TextureBindingInfo From(const TextureBindingLayout& layout);
-
-    // For shader reflection UnfilterableFloat is never used and the sample type is Float for any
-    // texture_Nd<f32>.
-    wgpu::TextureSampleType sampleType;
-    wgpu::TextureViewDimension viewDimension;
-    bool multisampled;
-
-    bool operator==(const TextureBindingInfo& other) const = default;
 };
+#undef TEXTURE_BINDING_INFO_MEMBER
 
 // A mirror of wgpu::StorageTextureBindingLayout for use inside dawn::native.
-struct StorageTextureBindingInfo {
+#define STORAGE_TEXTURE_BINDING_INFO_MEMBER(X)   \
+    X(wgpu::TextureFormat, format)               \
+    X(wgpu::TextureViewDimension, viewDimension) \
+    X(wgpu::StorageTextureAccess, access)
+DAWN_SERIALIZABLE(struct, StorageTextureBindingInfo, STORAGE_TEXTURE_BINDING_INFO_MEMBER) {
     static StorageTextureBindingInfo From(const StorageTextureBindingLayout& layout);
-
-    wgpu::TextureFormat format;
-    wgpu::TextureViewDimension viewDimension;
-    wgpu::StorageTextureAccess access;
-
-    bool operator==(const StorageTextureBindingInfo& other) const = default;
 };
+#undef STORAGE_TEXTURE_BINDING_INFO_MEMBER
 
 // A mirror of wgpu::SamplerBindingLayout for use inside dawn::native.
-struct SamplerBindingInfo {
+#define SAMPLER_BINDING_INFO_MEMBER(X)                                               \
+    /* For shader reflection NonFiltering is never used and Filtering is used for */ \
+    /* any `sampler`.                                                             */ \
+    X(wgpu::SamplerBindingType, type)
+DAWN_SERIALIZABLE(struct, SamplerBindingInfo, SAMPLER_BINDING_INFO_MEMBER) {
     static SamplerBindingInfo From(const SamplerBindingLayout& layout);
-
-    // For shader reflection NonFiltering is never used and Filtering is used for any `sampler`.
-    wgpu::SamplerBindingType type;
-
-    bool operator==(const SamplerBindingInfo& other) const = default;
 };
+#undef SAMPLER_BINDING_INFO_MEMBER
+
+// A mirror of wgpu::ExternalTextureBindingLayout for use inside dawn::native.
+#define EXTERNAL_TEXTURE_BINDING_INFO_MEMBER(X)  // ExternalTextureBindingInfo has no member
+DAWN_SERIALIZABLE(struct, ExternalTextureBindingInfo, EXTERNAL_TEXTURE_BINDING_INFO_MEMBER){};
+#undef EXTERNAL_TEXTURE_BINDING_INFO_MEMBER
+
+// Internal to vulkan only.
+#define INPUT_ATTACHMENT_BINDING_INFO_MEMBER(X) X(wgpu::TextureSampleType, sampleType)
+DAWN_SERIALIZABLE(struct, InputAttachmentBindingInfo, INPUT_ATTACHMENT_BINDING_INFO_MEMBER){};
+#undef INPUT_ATTACHMENT_BINDING_INFO_MEMBER
 
 // A mirror of wgpu::StaticSamplerBindingLayout for use inside dawn::native.
 struct StaticSamplerBindingInfo {
@@ -130,18 +137,6 @@
     bool operator==(const StaticSamplerBindingInfo& other) const = default;
 };
 
-// A mirror of wgpu::ExternalTextureBindingLayout for use inside dawn::native.
-struct ExternalTextureBindingInfo {
-    bool operator==(const ExternalTextureBindingInfo& other) const = default;
-};
-
-// Internal to vulkan only.
-struct InputAttachmentBindingInfo {
-    wgpu::TextureSampleType sampleType;
-
-    bool operator==(const InputAttachmentBindingInfo& other) const = default;
-};
-
 struct BindingInfo {
     BindingNumber binding;
     wgpu::ShaderStage visibility;
@@ -165,12 +160,11 @@
 BindingInfoType GetBindingInfoType(const BindingInfo& bindingInfo);
 
 // Match tint::BindingPoint, can convert to/from tint::BindingPoint using ToTint and FromTint.
-struct BindingSlot {
-    BindGroupIndex group;
-    BindingNumber binding;
-
-    constexpr bool operator==(const BindingSlot& rhs) const = default;
-};
+#define BINDING_SLOT_MEMBER(X) \
+    X(BindGroupIndex, group)   \
+    X(BindingNumber, binding)
+DAWN_SERIALIZABLE(struct, BindingSlot, BINDING_SLOT_MEMBER){};
+#undef BINDING_SLOT_MEMBER
 
 struct PerStageBindingCounts {
     uint32_t sampledTextureCount;
diff --git a/src/dawn/native/Blob.cpp b/src/dawn/native/Blob.cpp
index d68b1aa..7fb1fcc 100644
--- a/src/dawn/native/Blob.cpp
+++ b/src/dawn/native/Blob.cpp
@@ -97,6 +97,13 @@
     return mSize;
 }
 
+bool Blob::operator==(const Blob& other) const {
+    if (other.Size() != Size()) {
+        return false;
+    }
+    return 0 == memcmp(Data(), other.Data(), Size());
+}
+
 template <>
 void stream::Stream<Blob>::Write(stream::Sink* s, const Blob& b) {
     size_t size = b.Size();
diff --git a/src/dawn/native/Blob.h b/src/dawn/native/Blob.h
index 30f8901..52cf536 100644
--- a/src/dawn/native/Blob.h
+++ b/src/dawn/native/Blob.h
@@ -61,6 +61,8 @@
     uint8_t* Data();
     size_t Size() const;
 
+    bool operator==(const Blob& other) const;
+
   private:
     // The constructor should be responsible to take ownership of |data| and releases ownership by
     // calling |deleter|. The deleter function is called at ~Blob() and during std::move.
diff --git a/src/dawn/native/CacheKey.h b/src/dawn/native/CacheKey.h
index a65ea9a..b5e32db 100644
--- a/src/dawn/native/CacheKey.h
+++ b/src/dawn/native/CacheKey.h
@@ -54,6 +54,10 @@
         // stream::StreamIn<T>.
         friend constexpr void StreamIn(stream::Sink*, const UnsafeUnkeyedValue&) {}
 
+        // Enabling DAWN_SERIALIZABLE classes with UnsafeUnkeyedValue member to use default equality
+        // operator. Equality comparison always returns true for the same type UnsafeUnkeyedValues.
+        bool operator==(const UnsafeUnkeyedValue<T>& other) const { return true; }
+
       private:
         T mValue;
     };
diff --git a/src/dawn/native/Limits.h b/src/dawn/native/Limits.h
index 825c40e..094d47a 100644
--- a/src/dawn/native/Limits.h
+++ b/src/dawn/native/Limits.h
@@ -85,6 +85,7 @@
 struct LimitsForCompilationRequest {
     static LimitsForCompilationRequest Create(const Limits& limits);
     DAWN_VISITABLE_MEMBERS(LIMITS_FOR_COMPILATION_REQUEST_MEMBERS)
+    bool operator==(const LimitsForCompilationRequest& other) const = default;
 };
 
 // Enforce restriction for limit values, including:
diff --git a/src/dawn/native/Serializable.h b/src/dawn/native/Serializable.h
index ea4b79d..72dafb2 100644
--- a/src/dawn/native/Serializable.h
+++ b/src/dawn/native/Serializable.h
@@ -67,8 +67,8 @@
 
 // Helper macro to define a struct or class along with VisitAll methods to call
 // a functor on all members. Derives from Visitable which provides
-// implementations of StreamIn/StreamOut/FromBlob/ToBlob.
-// Example usage:
+// implementations of StreamIn/StreamOut/FromBlob/ToBlob, and provides a default equality
+// comparison. Example usage:
 //   #define MEMBERS(X) \
 //       X(int, a)              \
 //       X(float, b)            \
@@ -78,10 +78,11 @@
 //      void SomeAdditionalMethod();
 //   };
 //   #undef MEMBERS
-#define DAWN_SERIALIZABLE(qualifier, Name, MEMBERS) \
-    struct Name##__Contents {                       \
-        DAWN_VISITABLE_MEMBERS(MEMBERS)             \
-    };                                              \
+#define DAWN_SERIALIZABLE(qualifier, Name, MEMBERS)                     \
+    struct Name##__Contents {                                           \
+        DAWN_VISITABLE_MEMBERS(MEMBERS)                                 \
+        bool operator==(const Name##__Contents& other) const = default; \
+    };                                                                  \
     qualifier Name : Name##__Contents, public ::dawn::native::Serializable<Name>
 
 #endif  // SRC_DAWN_NATIVE_SERIALIZABLE_H_
diff --git a/src/dawn/native/ShaderModule.cpp b/src/dawn/native/ShaderModule.cpp
index 9f96028..34ef542 100644
--- a/src/dawn/native/ShaderModule.cpp
+++ b/src/dawn/native/ShaderModule.cpp
@@ -320,6 +320,10 @@
     DAWN_UNREACHABLE();
 }
 
+EntryPointMetadata::OverrideId FromTintOverrideId(tint::OverrideId id) {
+    return EntryPointMetadata::OverrideId{{id.value}};
+}
+
 EntryPointMetadata::Override::Type FromTintOverrideType(tint::inspector::Override::Type type) {
     switch (type) {
         case tint::inspector::Override::Type::kBool:
@@ -677,8 +681,8 @@
     if (!entryPoint.overrides.empty()) {
         for (auto& c : entryPoint.overrides) {
             auto id = name2Id.at(c.name);
-            EntryPointMetadata::Override override = {id, FromTintOverrideType(c.type),
-                                                     c.is_initialized};
+            EntryPointMetadata::Override override = {
+                {FromTintOverrideId(id), FromTintOverrideType(c.type), c.is_initialized}};
 
             std::string identifier = c.is_id_specified ? std::to_string(override.id.value) : c.name;
             metadata->overrides[identifier] = override;
@@ -705,8 +709,9 @@
         }
 
         auto id = name2Id.at(o.name);
-        EntryPointMetadata::Override override = {id, FromTintOverrideType(o.type), o.is_initialized,
-                                                 /* isUsed */ false};
+        EntryPointMetadata::Override override = {{FromTintOverrideId(id),
+                                                  FromTintOverrideType(o.type), o.is_initialized,
+                                                  /* isUsed */ false}};
         metadata->overrides[identifier] = override;
     }
 
diff --git a/src/dawn/native/ShaderModule.h b/src/dawn/native/ShaderModule.h
index 868d212..8b49bd3 100644
--- a/src/dawn/native/ShaderModule.h
+++ b/src/dawn/native/ShaderModule.h
@@ -56,6 +56,7 @@
 #include "dawn/native/Limits.h"
 #include "dawn/native/ObjectBase.h"
 #include "dawn/native/PerStage.h"
+#include "dawn/native/Serializable.h"
 #include "dawn/native/dawn_platform.h"
 #include "tint/tint.h"
 
@@ -166,25 +167,159 @@
                                                         const PipelineLayoutBase* layout);
 
 // Shader metadata for a binding, very similar to information contained in a pipeline layout.
-struct ShaderBindingInfo {
-    BindingNumber binding;
-    BindingIndex arraySize;
-
-    // The variable name of the binding resource.
-    std::string name;
-
-    std::variant<BufferBindingInfo,
-                 SamplerBindingInfo,
-                 TextureBindingInfo,
-                 StorageTextureBindingInfo,
-                 ExternalTextureBindingInfo,
-                 InputAttachmentBindingInfo>
-        bindingInfo;
-};
+using ShaderBindingInfoVariant = std::variant<BufferBindingInfo,
+                                              SamplerBindingInfo,
+                                              TextureBindingInfo,
+                                              StorageTextureBindingInfo,
+                                              ExternalTextureBindingInfo,
+                                              InputAttachmentBindingInfo>;
+#define SHADER_BINDING_INFO_MEMBER(X)              \
+    X(BindingNumber, binding)                      \
+    X(BindingIndex, arraySize)                     \
+    /*The variable name of the binding resource.*/ \
+    X(std::string, name)                           \
+    X(ShaderBindingInfoVariant, bindingInfo)
+DAWN_SERIALIZABLE(struct, ShaderBindingInfo, SHADER_BINDING_INFO_MEMBER){};
+#undef SHADER_BINDING_INFO_MEMBER
 
 using BindingGroupInfoMap = absl::flat_hash_map<BindingNumber, ShaderBindingInfo>;
 using BindingInfoArray = ityp::array<BindGroupIndex, BindingGroupInfoMap, kMaxBindGroups>;
 
+// Define types for the shader reflection data structures in detail namespaces to prevent messing
+// up dawn::native namespace. These types can be exposed within EntryPointMetadata if needed.
+namespace detail {
+#define SAMPLER_TEXTURE_PAIR_MEMBER(X) \
+    X(BindingSlot, sampler)            \
+    X(BindingSlot, texture)
+DAWN_SERIALIZABLE(struct, SamplerTexturePair, SAMPLER_TEXTURE_PAIR_MEMBER){};
+#undef SAMPLER_TEXTURE_PAIR_MEMBER
+
+/// Match tint::inspector::Inspector::LevelSampleInfo
+enum class TextureQueryType : uint8_t { TextureNumLevels, TextureNumSamples };
+
+#define TEXTURE_METADATE_QUERY_MEMBER(X) \
+    X(TextureQueryType, type)            \
+    X(uint32_t, group)                   \
+    X(uint32_t, binding)
+DAWN_SERIALIZABLE(struct, TextureMetadataQuery, TEXTURE_METADATE_QUERY_MEMBER) {
+    using TextureQueryType = TextureQueryType;
+};
+#undef TEXTURE_METADATE_QUERY_MEMBER
+
+// Structure to record the basic types (float, int and uint) of the fragment shader framebuffer
+// input/outputs (inputs being "framebuffer fetch").
+#define FRAGMENT_RENDER_ATTACHMENT_INFO_MEMBER(X) \
+    X(TextureComponentType, baseType)             \
+    X(uint8_t, componentCount)                    \
+    X(uint8_t, blendSrc)
+DAWN_SERIALIZABLE(struct, FragmentRenderAttachmentInfo, FRAGMENT_RENDER_ATTACHMENT_INFO_MEMBER){};
+#undef FRAGMENT_RENDER_ATTACHMENT_INFO_MEMBER
+
+#define INTER_STAGE_VARIABLE_INFO_MEMBER(X) \
+    X(std::string, name)                    \
+    X(InterStageComponentType, baseType)    \
+    X(uint32_t, componentCount)             \
+    X(InterpolationType, interpolationType) \
+    X(InterpolationSampling, interpolationSampling)
+DAWN_SERIALIZABLE(struct, InterStageVariableInfo, INTER_STAGE_VARIABLE_INFO_MEMBER){};
+#undef INTER_STAGE_VARIABLE_INFO_MEMBER
+
+// Match tint::OverrideId
+#define OVERRIDE_ID_MEMBER(X) X(uint16_t, value)
+DAWN_SERIALIZABLE(struct, OverrideId, OVERRIDE_ID_MEMBER){};
+#undef OVERRIDE_ID_MEMBER
+
+enum class OverrideType { Boolean, Float32, Uint32, Int32, Float16 };
+
+#define OVERRIDE_MEMBER(X) \
+    X(OverrideId, id)      \
+    X(OverrideType, type)  \
+    X(bool, isInitialized) \
+    X(bool, isUsed)
+DAWN_SERIALIZABLE(struct, Override, OVERRIDE_MEMBER) {
+    using Type = OverrideType;
+};
+#undef OVERRIDE_MEMBER
+
+using OverridesMap = absl::flat_hash_map<std::string, Override>;
+}  // namespace detail
+
+// Contains all the reflection data for a valid (ShaderModule, entryPoint, stage). This structure is
+// serializable and doesn't depend on the shader program, thus it can outlive the shader program and
+// get cached on disk. They are stored in the ShaderModuleBase so pointers to EntryPointMetadata are
+// safe to store as long as you also keep a Ref to the ShaderModuleBase.
+#define ENTRY_POINT_METADATA_MEMBER(X)                                                            \
+    /* It is valid for a shader to contain entry points that go over limits. To keep this      */ \
+    /* structure with packed arrays and bitsets, we still validate against limits when doing   */ \
+    /* reflection, but store the errors in this vector, for later use if the application tries */ \
+    /* to use the entry point.                                                                 */ \
+    X(std::vector<std::string>, infringedLimitErrors)                                             \
+    /* bindings[G][B] is the reflection data for the binding defined with @group(G) @binding(B)*/ \
+    X(BindingInfoArray, bindings)                                                                 \
+    /* Contains the reflection information of all sampler and non-sampler texture (storage     */ \
+    /* texture not included) usage in the entry point. For non-sampler usage,                  */ \
+    /* nonSamplerBindingPoint is used for sampler slot.                                        */ \
+    X(std::vector<detail::SamplerTexturePair>, samplerAndNonSamplerTexturePairs)                  \
+    X(std::vector<detail::TextureMetadataQuery>, textureQueries)                                  \
+    /* The set of vertex attributes this entryPoint uses.*/                                       \
+    X(PerVertexAttribute<VertexFormatBaseType>, vertexInputBaseTypes)                             \
+    X(VertexAttributeMask, usedVertexInputs)                                                      \
+    /* An array to record the basic types of the fragment shader framebuffer input/outputs.*/     \
+    X(PerColorAttachment<detail::FragmentRenderAttachmentInfo>, fragmentOutputVariables)          \
+    X(ColorAttachmentMask, fragmentOutputMask)                                                    \
+    X(PerColorAttachment<detail::FragmentRenderAttachmentInfo>, fragmentInputVariables)           \
+    X(ColorAttachmentMask, fragmentInputMask)                                                     \
+    /* Now that we only support vertex and fragment stages, there can't be both inter-stage    */ \
+    /* inputs and outputs in one shader stage.                                                 */ \
+    X(std::vector<bool>, usedInterStageVariables)                                                 \
+    X(std::vector<detail::InterStageVariableInfo>, interStageVariables)                           \
+    X(uint32_t, totalInterStageShaderVariables)                                                   \
+    /* The shader stage for this entry point.*/                                                   \
+    X(SingleShaderStage, stage)                                                                   \
+    /* Map identifier to override variable. */                                                    \
+    /* Identifier is unique: either the variable name or the numeric ID if specified */           \
+    X(detail::OverridesMap, overrides)                                                            \
+    /* Override variables that are not initialized in shaders. They need value initialization  */ \
+    /* from pipeline stage or it is a validation error                                         */ \
+    X(absl::flat_hash_set<std::string>, uninitializedOverrides)                                   \
+    /* Store constants with shader initialized values as well.                                 */ \
+    /* This is used by metal backend to set values with default initializers that are not      */ \
+    /* overridden.                                                                             */ \
+    X(absl::flat_hash_set<std::string>, initializedOverrides)                                     \
+    /* Reflection information about potential `pixel_local` variable use. */                      \
+    X(bool, usesPixelLocal)                                                                       \
+    X(size_t, pixelLocalBlockSize)                                                                \
+    X(std::vector<PixelLocalMemberType>, pixelLocalMembers)                                       \
+    X(bool, usesFragDepth)                                                                        \
+    X(bool, usesInstanceIndex)                                                                    \
+    X(bool, usesNumWorkgroups)                                                                    \
+    X(bool, usesSampleMaskOutput)                                                                 \
+    X(bool, usesSampleIndex)                                                                      \
+    X(bool, usesVertexIndex)                                                                      \
+    X(bool, usesTextureLoadWithDepthTexture)                                                      \
+    X(bool, usesDepthTextureWithNonComparisonSampler)                                             \
+    X(bool, usesSubgroupMatrix)                                                                   \
+    /* Immediate Data block byte size */                                                          \
+    X(uint32_t, immediateDataRangeByteSize)                                                       \
+    /* Number of texture+sampler combinations, computed as 1 for every texture+sampler         */ \
+    /* combination + 1 for every texture used without a sampler that wasn't previously counted.*/ \
+    /* Note: this is only set in compatibility mode.                                           */ \
+    X(uint32_t, numTextureSamplerCombinations)
+DAWN_SERIALIZABLE(struct, EntryPointMetadata, ENTRY_POINT_METADATA_MEMBER) {
+    using SamplerTexturePair = detail::SamplerTexturePair;
+    // TODO(crbug.com/409438000): Remove the hack of sampler placeholders for non-sampler texture.
+    static constexpr const BindingSlot nonSamplerBindingPoint{
+        {BindGroupIndex{std::numeric_limits<uint32_t>::max()},
+         BindingNumber{std::numeric_limits<uint32_t>::max()}}};
+
+    using TextureMetadataQuery = detail::TextureMetadataQuery;
+    using FragmentRenderAttachmentInfo = detail::FragmentRenderAttachmentInfo;
+    using InterStageVariableInfo = detail::InterStageVariableInfo;
+    using OverrideId = detail::OverrideId;
+    using Override = detail::Override;
+};
+#undef ENTRY_POINT_METADATA_MEMBER
+
 // The WebGPU override variables only support these scalar types
 union OverrideScalar {
     // Use int32_t for boolean to initialize the full 32bit
@@ -194,140 +329,6 @@
     uint32_t u32;
 };
 
-// Contains all the reflection data for a valid (ShaderModule, entryPoint, stage). They are
-// stored in the ShaderModuleBase and destroyed only when the shader program is destroyed so
-// pointers to EntryPointMetadata are safe to store as long as you also keep a Ref to the
-// ShaderModuleBase.
-struct EntryPointMetadata {
-    // It is valid for a shader to contain entry points that go over limits. To keep this
-    // structure with packed arrays and bitsets, we still validate against limits when
-    // doing reflection, but store the errors in this vector, for later use if the application
-    // tries to use the entry point.
-    std::vector<std::string> infringedLimitErrors;
-
-    // bindings[G][B] is the reflection data for the binding defined with @group(G) @binding(B)
-    BindingInfoArray bindings;
-
-    struct SamplerTexturePair {
-        BindingSlot sampler;
-        BindingSlot texture;
-    };
-    // TODO(crbug.com/409438000): Remove the hack of sampler placeholders for non-sampler texture.
-    static constexpr const BindingSlot nonSamplerBindingPoint = {
-        BindGroupIndex(std::numeric_limits<uint32_t>::max()),
-        BindingNumber(std::numeric_limits<uint32_t>::max())};
-    // Contains the reflection information of all sampler and non-sampler texture (storage texture
-    // not included) usage in the entry point. For non-sampler usage, nonSamplerBindingPoint is used
-    // for sampler slot.
-    std::vector<SamplerTexturePair> samplerAndNonSamplerTexturePairs;
-
-    /// Match tint::inspector::Inspector::LevelSampleInfo
-    struct TextureMetadataQuery {
-        /// The information needed to be supplied.
-        enum class TextureQueryType : uint8_t {
-            /// Texture Num Levels
-            TextureNumLevels,
-            /// Texture Num Samples
-            TextureNumSamples,
-        };
-        /// The type of function
-        TextureQueryType type = TextureQueryType::TextureNumLevels;
-        /// The group number
-        uint32_t group = 0;
-        /// The binding number
-        uint32_t binding = 0;
-    };
-    std::vector<TextureMetadataQuery> textureQueries;
-
-    // The set of vertex attributes this entryPoint uses.
-    PerVertexAttribute<VertexFormatBaseType> vertexInputBaseTypes;
-    VertexAttributeMask usedVertexInputs;
-
-    // An array to record the basic types (float, int and uint) of the fragment shader framebuffer
-    // input/outputs (inputs being "framebuffer fetch").
-    struct FragmentRenderAttachmentInfo {
-        TextureComponentType baseType;
-        uint8_t componentCount;
-        uint8_t blendSrc;
-    };
-    PerColorAttachment<FragmentRenderAttachmentInfo> fragmentOutputVariables;
-    ColorAttachmentMask fragmentOutputMask;
-
-    PerColorAttachment<FragmentRenderAttachmentInfo> fragmentInputVariables;
-    ColorAttachmentMask fragmentInputMask;
-
-    struct InterStageVariableInfo {
-        std::string name;
-        InterStageComponentType baseType;
-        uint32_t componentCount;
-        InterpolationType interpolationType;
-        InterpolationSampling interpolationSampling;
-    };
-    // Now that we only support vertex and fragment stages, there can't be both inter-stage
-    // inputs and outputs in one shader stage.
-    std::vector<bool> usedInterStageVariables;
-    std::vector<InterStageVariableInfo> interStageVariables;
-    uint32_t totalInterStageShaderVariables;
-
-    // The shader stage for this entry point.
-    SingleShaderStage stage;
-
-    struct Override {
-        tint::OverrideId id;
-
-        // Match tint::inspector::Override::Type
-        // Bool is defined as a macro on linux X11 and cannot compile
-        enum class Type { Boolean, Float32, Uint32, Int32, Float16 } type;
-
-        // If the constant doesn't not have an initializer in the shader
-        // Then it is required for the pipeline stage to have a constant record to initialize a
-        // value
-        bool isInitialized;
-
-        // Set to true if the override is used in the entry point
-        bool isUsed = true;
-    };
-
-    using OverridesMap = absl::flat_hash_map<std::string, Override>;
-
-    // Map identifier to override variable
-    // Identifier is unique: either the variable name or the numeric ID if specified
-    OverridesMap overrides;
-
-    // Override variables that are not initialized in shaders
-    // They need value initialization from pipeline stage or it is a validation error
-    absl::flat_hash_set<std::string> uninitializedOverrides;
-
-    // Store constants with shader initialized values as well
-    // This is used by metal backend to set values with default initializers that are not
-    // overridden
-    absl::flat_hash_set<std::string> initializedOverrides;
-
-    // Reflection information about potential `pixel_local` variable use.
-    bool usesPixelLocal = false;
-    size_t pixelLocalBlockSize = 0;
-    std::vector<PixelLocalMemberType> pixelLocalMembers;
-
-    bool usesFragDepth = false;
-    bool usesInstanceIndex = false;
-    bool usesNumWorkgroups = false;
-    bool usesSampleMaskOutput = false;
-    bool usesSampleIndex = false;
-    bool usesVertexIndex = false;
-    bool usesTextureLoadWithDepthTexture = false;
-    bool usesDepthTextureWithNonComparisonSampler = false;
-    bool usesSubgroupMatrix = false;
-
-    // Immediate Data block byte size
-    uint32_t immediateDataRangeByteSize = 0;
-
-    // Number of texture+sampler combinations, computed as
-    // 1 for every texture+sampler combination + 1 for every texture used
-    // without a sampler that wasn't previously counted.
-    // Note: this is only set in compatibility mode.
-    uint32_t numTextureSamplerCombinations = 0;
-};
-
 class ShaderModuleBase : public RefCountedWithExternalCount<ApiObjectBase>,
                          public CachedObject,
                          public ContentLessObjectCacheable<ShaderModuleBase> {
diff --git a/src/dawn/native/TintUtils.cpp b/src/dawn/native/TintUtils.cpp
index c2b8d3d..2ab9fe1 100644
--- a/src/dawn/native/TintUtils.cpp
+++ b/src/dawn/native/TintUtils.cpp
@@ -180,7 +180,7 @@
     for (const auto& [key, value] : constants) {
         const auto& o = metadata.overrides.at(key);
 
-        map.insert({o.id, value});
+        map.insert({{o.id.value}, value});
     }
     return map;
 }
diff --git a/src/dawn/native/TintUtils.h b/src/dawn/native/TintUtils.h
index 5c3b1dc..d8f83e1 100644
--- a/src/dawn/native/TintUtils.h
+++ b/src/dawn/native/TintUtils.h
@@ -76,7 +76,7 @@
 }
 
 constexpr BindingSlot FromTint(const tint::BindingPoint& tintBindingPoint) {
-    return {BindGroupIndex(tintBindingPoint.group), BindingNumber(tintBindingPoint.binding)};
+    return {{BindGroupIndex(tintBindingPoint.group), BindingNumber(tintBindingPoint.binding)}};
 }
 
 }  // namespace dawn::native
diff --git a/src/dawn/native/stream/Stream.cpp b/src/dawn/native/stream/Stream.cpp
index 1682b63..b3a5cb0 100644
--- a/src/dawn/native/stream/Stream.cpp
+++ b/src/dawn/native/stream/Stream.cpp
@@ -33,6 +33,12 @@
 
 namespace dawn::native::stream {
 
+constexpr void StreamIn(Sink* s) {}
+
+MaybeError StreamOut(Source* s) {
+    return {};
+}
+
 template <>
 void Stream<std::string>::Write(Sink* s, const std::string& t) {
     StreamIn(s, t.length());
diff --git a/src/dawn/native/stream/Stream.h b/src/dawn/native/stream/Stream.h
index d501925..3ffd37d 100644
--- a/src/dawn/native/stream/Stream.h
+++ b/src/dawn/native/stream/Stream.h
@@ -104,6 +104,14 @@
     return StreamOut(s, vs...);
 }
 
+// Helper to call StreamIn on an empty parameter pack, e.g. for a DAWN_SERIALIZABLE struct with no
+// member. Do nothing.
+constexpr void StreamIn(Sink* s);
+
+// Helper to call StreamOut on an empty parameter pack, e.g. for a DAWN_SERIALIZABLE struct with no
+// member. Do nothing and return success.
+MaybeError StreamOut(Source* s);
+
 // Stream specialization for fundamental types.
 template <typename T>
 class Stream<T, std::enable_if_t<std::is_fundamental_v<T>>> {
diff --git a/src/tint/lang/hlsl/writer/common/options.h b/src/tint/lang/hlsl/writer/common/options.h
index 12b04df..f080142 100644
--- a/src/tint/lang/hlsl/writer/common/options.h
+++ b/src/tint/lang/hlsl/writer/common/options.h
@@ -121,6 +121,8 @@
     /// The binding points that will be ignored by the rebustness transform.
     std::vector<BindingPoint> ignored_by_robustness_transform;
 
+    bool operator==(const Bindings& other) const = default;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(Bindings,
                  uniform,
@@ -146,6 +148,8 @@
     /// into the uniform buffer where the length of the buffer is stored.
     std::unordered_map<BindingPoint, uint32_t> bindpoint_to_size_index;
 
+    bool operator==(const ArrayLengthFromUniformOptions& other) const = default;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(ArrayLengthFromUniformOptions, ubo_binding, bindpoint_to_size_index);
 };
@@ -178,6 +182,8 @@
     /// The bind group index of all pixel local storage attachments
     uint32_t group_index = 0;
 
+    bool operator==(const PixelLocalOptions& other) const = default;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(PixelLocalOptions, attachments, group_index);
 };
@@ -260,6 +266,8 @@
     /// Pixel local configuration
     PixelLocalOptions pixel_local;
 
+    bool operator==(const Options& other) const = default;
+
     /// Reflect the fields of this class so that it can be used by tint::ForeachField()
     TINT_REFLECT(Options,
                  remapped_entry_point_name,