Modernizes HTTP/2 frame representations in spdy_protocol.h

Introduces value-semantic, standard-layout POD structs, C++20 type
traits, concepts, compile-time size calculation and serialization,
and a 64-byte single-cache-line SpdyFrame variant based on the analysis in
go/http2-frame-representations.

This change introduces the types, but does not use them anywhere, yet.

Key changes:
- Defined standard-layout POD structs for all 14 HTTP/2 frame types
  (DataFrame, HeadersFrame, PriorityFrame, RstStreamFrame, SettingsFrame,
  PushPromiseFrame, PingFrame, GoAwayFrame, WindowUpdateFrame,
  ContinuationFrame, AltSvcFrame, PriorityUpdateFrame, AcceptChFrame,
  UnknownFrame).
- Added C++20 FrameTraits<T> and Http2Frame concept.
- Added compile-time FrameSize<T> and SerializeFrame<T> in SpdyFrameBuilder.
- Added SpdyFrame variant fitting in 64 bytes (1 cache line) and dispatch
  helpers.
- Maintained backward compatibility for all existing SpdyFrameIR
  classes and SpdyFrameVisitor without changing callers.

PiperOrigin-RevId: 981852294
diff --git a/quiche/http2/core/spdy_frame_builder.h b/quiche/http2/core/spdy_frame_builder.h
index ee879a4..5628753 100644
--- a/quiche/http2/core/spdy_frame_builder.h
+++ b/quiche/http2/core/spdy_frame_builder.h
@@ -135,6 +135,335 @@
   size_t offset_;    // Position at which the latest frame begins.
 };
 
+// Packs the exclusive dependency bit (bit 31) and the 31-bit parent stream ID
+// into a single 32-bit unsigned integer in network wire format for PRIORITY and
+// HEADERS frames.
+inline uint32_t PackStreamDependencyValues(bool exclusive,
+                                           SpdyStreamId parent_stream_id) {
+  uint32_t parent = parent_stream_id & 0x7fffffff;
+  uint32_t e_bit = exclusive ? 0x80000000 : 0;
+  return parent | e_bit;
+}
+
+inline bool SerializeDataFrame(const DataFrame& frame,
+                               SpdyFrameBuilder& builder) {
+  uint8_t flags = frame.flags;
+  if (frame.fin()) {
+    flags |= DATA_FLAG_FIN;
+  }
+  if (frame.padded()) {
+    flags |= DATA_FLAG_PADDED;
+  }
+  size_t payload_len = frame.data.size() +
+                       (frame.padded() ? (1 + frame.padding_payload_len) : 0);
+  if (!builder.BeginNewFrame(SpdyFrameType::DATA, flags, frame.stream_id,
+                             payload_len)) {
+    return false;
+  }
+  if (frame.padded()) {
+    if (!builder.WriteUInt8(frame.padding_payload_len)) {
+      return false;
+    }
+  }
+  if (!builder.WriteBytes(frame.data.data(), frame.data.size())) {
+    return false;
+  }
+  if (frame.padded() && frame.padding_payload_len > 0) {
+    std::string padding(frame.padding_payload_len, 0);
+    if (!builder.WriteBytes(padding.data(), padding.length())) {
+      return false;
+    }
+  }
+  return true;
+}
+
+inline bool SerializeHeadersFrame(const HeadersFrame& frame,
+                                  SpdyFrameBuilder& builder) {
+  uint8_t flags = frame.flags;
+  if (frame.fin()) {
+    flags |= CONTROL_FLAG_FIN;
+  }
+  if (frame.end_headers()) {
+    flags |= HEADERS_FLAG_END_HEADERS;
+  }
+  if (frame.padded()) {
+    flags |= HEADERS_FLAG_PADDED;
+  }
+  if (frame.has_priority) {
+    flags |= HEADERS_FLAG_PRIORITY;
+  }
+  size_t payload_len = frame.hpack_block.size() +
+                       (frame.padded() ? (1 + frame.padding_payload_len) : 0) +
+                       (frame.has_priority ? 5 : 0);
+  if (!builder.BeginNewFrame(SpdyFrameType::HEADERS, flags, frame.stream_id,
+                             payload_len)) {
+    return false;
+  }
+  if (frame.padded()) {
+    if (!builder.WriteUInt8(frame.padding_payload_len)) {
+      return false;
+    }
+  }
+  if (frame.has_priority) {
+    int weight = ClampHttp2Weight(frame.priority.weight);
+    if (!builder.WriteUInt32(PackStreamDependencyValues(
+            frame.priority.exclusive, frame.priority.parent_stream_id))) {
+      return false;
+    }
+    if (!builder.WriteUInt8(static_cast<uint8_t>(weight - 1))) {
+      return false;
+    }
+  }
+  if (!builder.WriteBytes(frame.hpack_block.data(), frame.hpack_block.size())) {
+    return false;
+  }
+  if (frame.padded() && frame.padding_payload_len > 0) {
+    std::string padding(frame.padding_payload_len, 0);
+    if (!builder.WriteBytes(padding.data(), padding.length())) {
+      return false;
+    }
+  }
+  return true;
+}
+
+inline bool SerializePriorityFrame(const PriorityFrame& frame,
+                                   SpdyFrameBuilder& builder) {
+  if (!builder.BeginNewFrame(SpdyFrameType::PRIORITY, 0, frame.stream_id, 5)) {
+    return false;
+  }
+  int weight = ClampHttp2Weight(frame.priority.weight);
+  if (!builder.WriteUInt32(PackStreamDependencyValues(
+          frame.priority.exclusive, frame.priority.parent_stream_id))) {
+    return false;
+  }
+  return builder.WriteUInt8(static_cast<uint8_t>(weight - 1));
+}
+
+inline bool SerializeRstStreamFrame(const RstStreamFrame& frame,
+                                    SpdyFrameBuilder& builder) {
+  if (!builder.BeginNewFrame(SpdyFrameType::RST_STREAM, 0, frame.stream_id,
+                             4)) {
+    return false;
+  }
+  return builder.WriteUInt32(static_cast<uint32_t>(frame.error_code));
+}
+
+inline bool SerializeSettingsFrame(const SettingsFrame& frame,
+                                   SpdyFrameBuilder& builder) {
+  uint8_t flags = frame.is_ack ? SETTINGS_FLAG_ACK : 0;
+  size_t payload_len =
+      frame.is_ack ? 0 : (frame.values.size() * kSettingsOneSettingSize);
+  if (!builder.BeginNewFrame(SpdyFrameType::SETTINGS, flags, 0, payload_len)) {
+    return false;
+  }
+  if (!frame.is_ack) {
+    for (const auto& param : frame.values) {
+      if (!builder.WriteUInt16(param.id) || !builder.WriteUInt32(param.value)) {
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+inline bool SerializePushPromiseFrame(const PushPromiseFrame& frame,
+                                      SpdyFrameBuilder& builder) {
+  uint8_t flags = frame.flags;
+  if (frame.end_headers()) {
+    flags |= PUSH_PROMISE_FLAG_END_PUSH_PROMISE;
+  }
+  if (frame.padded()) {
+    flags |= PUSH_PROMISE_FLAG_PADDED;
+  }
+  size_t payload_len = 4 + frame.hpack_block.size() +
+                       (frame.padded() ? (1 + frame.padding_payload_len) : 0);
+  if (!builder.BeginNewFrame(SpdyFrameType::PUSH_PROMISE, flags,
+                             frame.stream_id, payload_len)) {
+    return false;
+  }
+  if (frame.padded()) {
+    if (!builder.WriteUInt8(frame.padding_payload_len)) {
+      return false;
+    }
+  }
+  if (!builder.WriteUInt32(frame.promised_stream_id & 0x7fffffff)) {
+    return false;
+  }
+  if (!builder.WriteBytes(frame.hpack_block.data(), frame.hpack_block.size())) {
+    return false;
+  }
+  if (frame.padded() && frame.padding_payload_len > 0) {
+    std::string padding(frame.padding_payload_len, 0);
+    if (!builder.WriteBytes(padding.data(), padding.length())) {
+      return false;
+    }
+  }
+  return true;
+}
+
+inline bool SerializePingFrame(const PingFrame& frame,
+                               SpdyFrameBuilder& builder) {
+  uint8_t flags = frame.is_ack ? PING_FLAG_ACK : 0;
+  if (!builder.BeginNewFrame(SpdyFrameType::PING, flags, 0, 8)) {
+    return false;
+  }
+  return builder.WriteUInt64(frame.opaque_data);
+}
+
+inline bool SerializeGoAwayFrame(const GoAwayFrame& frame,
+                                 SpdyFrameBuilder& builder) {
+  size_t payload_len = 8 + frame.debug_data.size();
+  if (!builder.BeginNewFrame(SpdyFrameType::GOAWAY, 0, 0, payload_len)) {
+    return false;
+  }
+  if (!builder.WriteUInt32(frame.last_good_stream_id & 0x7fffffff)) {
+    return false;
+  }
+  if (!builder.WriteUInt32(static_cast<uint32_t>(frame.error_code))) {
+    return false;
+  }
+  if (!frame.debug_data.empty()) {
+    if (!builder.WriteBytes(frame.debug_data.data(), frame.debug_data.size())) {
+      return false;
+    }
+  }
+  return true;
+}
+
+inline bool SerializeWindowUpdateFrame(const WindowUpdateFrame& frame,
+                                       SpdyFrameBuilder& builder) {
+  if (!builder.BeginNewFrame(SpdyFrameType::WINDOW_UPDATE, 0, frame.stream_id,
+                             4)) {
+    return false;
+  }
+  return builder.WriteUInt32(frame.delta & 0x7fffffff);
+}
+
+inline bool SerializeContinuationFrame(const ContinuationFrame& frame,
+                                       SpdyFrameBuilder& builder) {
+  uint8_t flags = frame.flags;
+  if (frame.end_headers()) {
+    flags |= HEADERS_FLAG_END_HEADERS;
+  }
+  if (!builder.BeginNewFrame(SpdyFrameType::CONTINUATION, flags,
+                             frame.stream_id, frame.hpack_block.size())) {
+    return false;
+  }
+  return builder.WriteBytes(frame.hpack_block.data(), frame.hpack_block.size());
+}
+
+inline bool SerializeAltSvcFrame(const AltSvcFrame& frame,
+                                 SpdyFrameBuilder& builder) {
+  size_t payload_len = 2 + frame.origin.size() + frame.value.size();
+  if (!builder.BeginNewFrame(SpdyFrameType::ALTSVC, 0, frame.stream_id,
+                             payload_len)) {
+    return false;
+  }
+  if (!builder.WriteUInt16(frame.origin.size())) {
+    return false;
+  }
+  if (!builder.WriteBytes(frame.origin.data(), frame.origin.size())) {
+    return false;
+  }
+  return builder.WriteBytes(frame.value.data(), frame.value.size());
+}
+
+inline bool SerializePriorityUpdateFrame(const PriorityUpdateFrame& frame,
+                                         SpdyFrameBuilder& builder) {
+  size_t payload_len = 4 + frame.priority_field_value.size();
+  if (!builder.BeginNewFrame(SpdyFrameType::PRIORITY_UPDATE, 0, 0,
+                             payload_len)) {
+    return false;
+  }
+  if (!builder.WriteUInt32(frame.prioritized_stream_id & 0x7fffffff)) {
+    return false;
+  }
+  return builder.WriteBytes(frame.priority_field_value.data(),
+                            frame.priority_field_value.size());
+}
+
+inline bool SerializeAcceptChFrame(const AcceptChFrame& frame,
+                                   SpdyFrameBuilder& builder) {
+  size_t payload_len = 0;
+  for (size_t i = 0; i < frame.num_entries && i < frame.entries.size(); ++i) {
+    payload_len +=
+        4 + frame.entries[i].origin.size() + frame.entries[i].value.size();
+  }
+  if (!builder.BeginNewFrame(SpdyFrameType::ACCEPT_CH, 0, 0, payload_len)) {
+    return false;
+  }
+  for (size_t i = 0; i < frame.num_entries && i < frame.entries.size(); ++i) {
+    if (!builder.WriteUInt16(frame.entries[i].origin.size()) ||
+        !builder.WriteBytes(frame.entries[i].origin.data(),
+                            frame.entries[i].origin.size()) ||
+        !builder.WriteUInt16(frame.entries[i].value.size()) ||
+        !builder.WriteBytes(frame.entries[i].value.data(),
+                            frame.entries[i].value.size())) {
+      return false;
+    }
+  }
+  return true;
+}
+
+inline bool SerializeUnknownFrame(const UnknownFrame& frame,
+                                  SpdyFrameBuilder& builder) {
+  // Handles other unknown frame types, where the payload is opaque.
+  if (!builder.BeginNewUncheckedFrame(frame.type, frame.flags, frame.stream_id,
+                                      frame.payload.size())) {
+    return false;
+  }
+  return builder.WriteBytes(frame.payload.data(), frame.payload.size());
+}
+
+// Serializes a strongly-typed HTTP/2 frame representation directly into the
+// provided `SpdyFrameBuilder`.
+//
+// This generic template writes HTTP/2 wire-format frames (RFC 9113) without
+// allocating intermediate heap objects. Returns true on success, or false if
+// the buffer cannot accommodate the frame or serialization fails.
+template <Http2FrameConcept T>
+bool SerializeFrame(const T& frame, SpdyFrameBuilder& builder) {
+  if constexpr (std::is_same_v<T, DataFrame>) {
+    return SerializeDataFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, HeadersFrame>) {
+    return SerializeHeadersFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, PriorityFrame>) {
+    return SerializePriorityFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, RstStreamFrame>) {
+    return SerializeRstStreamFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, SettingsFrame>) {
+    return SerializeSettingsFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, PushPromiseFrame>) {
+    return SerializePushPromiseFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, PingFrame>) {
+    return SerializePingFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, GoAwayFrame>) {
+    return SerializeGoAwayFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, WindowUpdateFrame>) {
+    return SerializeWindowUpdateFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, ContinuationFrame>) {
+    return SerializeContinuationFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, AltSvcFrame>) {
+    return SerializeAltSvcFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, PriorityUpdateFrame>) {
+    return SerializePriorityUpdateFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, AcceptChFrame>) {
+    return SerializeAcceptChFrame(frame, builder);
+  } else if constexpr (std::is_same_v<T, UnknownFrame>) {
+    return SerializeUnknownFrame(frame, builder);
+  }
+  return false;
+}
+
+// Serializes a unified `SpdyFrame` variant (containing any standard-layout
+// HTTP/2 frame type) into the provided `SpdyFrameBuilder` using `std::visit`.
+// Returns true on success, or false if serialization fails.
+inline bool SerializeSpdyFrame(const SpdyFrame& frame,
+                               SpdyFrameBuilder& builder) {
+  return std::visit(
+      [&builder](const auto& f) { return SerializeFrame(f, builder); }, frame);
+}
+
 }  // namespace spdy
 
 #endif  // QUICHE_HTTP2_CORE_SPDY_FRAME_BUILDER_H_
diff --git a/quiche/http2/core/spdy_framer.cc b/quiche/http2/core/spdy_framer.cc
index 9f400c4..699eb96 100644
--- a/quiche/http2/core/spdy_framer.cc
+++ b/quiche/http2/core/spdy_framer.cc
@@ -27,17 +27,6 @@
 
 namespace {
 
-// Pack parent stream ID and exclusive flag into the format used by HTTP/2
-// headers and priority frames.
-uint32_t PackStreamDependencyValues(bool exclusive,
-                                    SpdyStreamId parent_stream_id) {
-  // Make sure the highest-order bit in the parent stream id is zeroed out.
-  uint32_t parent = parent_stream_id & 0x7fffffff;
-  // Set the one-bit exclusivity flag.
-  uint32_t e_bit = exclusive ? 0x80000000 : 0;
-  return parent | e_bit;
-}
-
 // Used to indicate no flags in a HTTP2 flags field.
 const uint8_t kNoFlags = 0;
 
diff --git a/quiche/http2/core/spdy_protocol.h b/quiche/http2/core/spdy_protocol.h
index a5ec174..8ee1af2 100644
--- a/quiche/http2/core/spdy_protocol.h
+++ b/quiche/http2/core/spdy_protocol.h
@@ -9,6 +9,8 @@
 #ifndef QUICHE_HTTP2_CORE_SPDY_PROTOCOL_H_
 #define QUICHE_HTTP2_CORE_SPDY_PROTOCOL_H_
 
+#include <array>
+#include <concepts>
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
@@ -17,10 +19,12 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <type_traits>
 #include <utility>
 #include <variant>
 #include <vector>
 
+#include "absl/container/inlined_vector.h"
 #include "absl/strings/string_view.h"
 #include "quiche/http2/core/spdy_alt_svc_wire_format.h"
 #include "quiche/http2/core/spdy_bitmasks.h"
@@ -447,6 +451,367 @@
 
 typedef StreamPrecedence<SpdyStreamId> SpdyStreamPrecedence;
 
+// =============================================================================
+// Value-Semantic POD Frame Structs
+// =============================================================================
+
+// Contains the fields specific to any priority-conveying frames.
+struct QUICHE_EXPORT PriorityFields {
+  SpdyStreamId parent_stream_id = 0;
+  uint16_t weight = kHttp2DefaultStreamWeight;
+  bool exclusive = false;
+  constexpr bool operator==(const PriorityFields&) const = default;
+};
+
+// A container for a single SETTINGS field.
+struct QUICHE_EXPORT SettingParameter {
+  SpdySettingsId id = 0;
+  uint32_t value = 0;
+  constexpr bool operator==(const SettingParameter&) const = default;
+};
+
+// A container for a single ACCEPT_CH entry.
+struct QUICHE_EXPORT AcceptChEntryView {
+  absl::string_view origin;
+  absl::string_view value;
+  constexpr bool operator==(const AcceptChEntryView&) const = default;
+};
+
+struct QUICHE_EXPORT DataFrame {
+  SpdyStreamId stream_id = 0;
+  uint8_t flags = 0;
+  uint8_t padding_payload_len = 0;
+  absl::string_view data;
+
+  constexpr bool HasFin() const { return (flags & DATA_FLAG_FIN) != 0; }
+  ABSL_DEPRECATED("Use HasFin() instead.") constexpr bool fin() const {
+    return HasFin();
+  }
+  constexpr bool IsPadded() const {
+    return (flags & DATA_FLAG_PADDED) != 0 || padding_payload_len > 0;
+  }
+  ABSL_DEPRECATED("Use IsPadded() instead.") constexpr bool padded() const {
+    return IsPadded();
+  }
+  constexpr bool operator==(const DataFrame&) const = default;
+};
+static_assert(sizeof(DataFrame) == (sizeof(void*) == 8 ? 24 : 16));
+
+struct QUICHE_EXPORT HeadersFrame {
+  SpdyStreamId stream_id = 0;
+  uint8_t flags = 0;
+  uint8_t padding_payload_len = 0;
+  bool has_priority = false;
+  PriorityFields priority{};
+  absl::string_view hpack_block;
+
+  constexpr bool HasFin() const { return (flags & CONTROL_FLAG_FIN) != 0; }
+  ABSL_DEPRECATED("Use HasFin() instead.") constexpr bool fin() const {
+    return HasFin();
+  }
+  constexpr bool HasEndHeaders() const {
+    return (flags & HEADERS_FLAG_END_HEADERS) != 0;
+  }
+  ABSL_DEPRECATED("Use HasEndHeaders() instead.")
+  constexpr bool end_headers() const { return HasEndHeaders(); }
+  constexpr bool IsPadded() const {
+    return (flags & HEADERS_FLAG_PADDED) != 0 || padding_payload_len > 0;
+  }
+  ABSL_DEPRECATED("Use IsPadded() instead.") constexpr bool padded() const {
+    return IsPadded();
+  }
+  constexpr bool operator==(const HeadersFrame&) const = default;
+};
+static_assert(sizeof(HeadersFrame) == (sizeof(void*) == 8 ? 32 : 24));
+
+struct QUICHE_EXPORT PriorityFrame {
+  SpdyStreamId stream_id = 0;
+  PriorityFields priority{};
+  constexpr bool operator==(const PriorityFrame&) const = default;
+};
+static_assert(sizeof(PriorityFrame) == 12);
+
+struct QUICHE_EXPORT RstStreamFrame {
+  SpdyStreamId stream_id = 0;
+  SpdyErrorCode error_code = ERROR_CODE_NO_ERROR;
+  constexpr bool operator==(const RstStreamFrame&) const = default;
+};
+static_assert(sizeof(RstStreamFrame) == 8);
+
+struct QUICHE_EXPORT SettingsFrame {
+  static constexpr size_t kMaxInlineSettings = 5;
+  bool is_ack = false;
+  absl::InlinedVector<SettingParameter, kMaxInlineSettings> values;
+  bool operator==(const SettingsFrame&) const = default;
+};
+static_assert(sizeof(SettingsFrame) == (sizeof(void*) == 8 ? 56 : 48));
+
+struct QUICHE_EXPORT PushPromiseFrame {
+  SpdyStreamId stream_id = 0;
+  SpdyStreamId promised_stream_id = 0;
+  uint8_t flags = 0;
+  uint8_t padding_payload_len = 0;
+  absl::string_view hpack_block;
+
+  constexpr bool HasEndHeaders() const {
+    return (flags & PUSH_PROMISE_FLAG_END_PUSH_PROMISE) != 0;
+  }
+  ABSL_DEPRECATED("Use HasEndHeaders() instead.")
+  constexpr bool end_headers() const { return HasEndHeaders(); }
+  constexpr bool IsPadded() const {
+    return (flags & PUSH_PROMISE_FLAG_PADDED) != 0 || padding_payload_len > 0;
+  }
+  ABSL_DEPRECATED("Use IsPadded() instead.") constexpr bool padded() const {
+    return IsPadded();
+  }
+  constexpr bool operator==(const PushPromiseFrame&) const = default;
+};
+static_assert(sizeof(PushPromiseFrame) == (sizeof(void*) == 8 ? 32 : 20));
+
+struct QUICHE_EXPORT PingFrame {
+  uint64_t opaque_data = 0;
+  bool is_ack = false;
+  constexpr bool operator==(const PingFrame&) const = default;
+};
+static_assert(sizeof(PingFrame) == 16);
+
+struct QUICHE_EXPORT GoAwayFrame {
+  SpdyStreamId last_good_stream_id = 0;
+  SpdyErrorCode error_code = ERROR_CODE_NO_ERROR;
+  absl::string_view debug_data;
+  constexpr bool operator==(const GoAwayFrame&) const = default;
+};
+static_assert(sizeof(GoAwayFrame) == (sizeof(void*) == 8 ? 24 : 16));
+
+struct QUICHE_EXPORT WindowUpdateFrame {
+  SpdyStreamId stream_id = 0;
+  uint32_t delta = 0;
+  constexpr bool operator==(const WindowUpdateFrame&) const = default;
+};
+static_assert(sizeof(WindowUpdateFrame) == 8);
+
+struct QUICHE_EXPORT ContinuationFrame {
+  SpdyStreamId stream_id = 0;
+  uint8_t flags = 0;
+  absl::string_view hpack_block;
+
+  constexpr bool HasEndHeaders() const {
+    return (flags & HEADERS_FLAG_END_HEADERS) != 0;
+  }
+  ABSL_DEPRECATED("Use HasEndHeaders() instead.")
+  constexpr bool end_headers() const { return HasEndHeaders(); }
+  constexpr bool operator==(const ContinuationFrame&) const = default;
+};
+static_assert(sizeof(ContinuationFrame) == (sizeof(void*) == 8 ? 24 : 16));
+
+struct QUICHE_EXPORT AltSvcFrame {
+  SpdyStreamId stream_id = 0;
+  absl::string_view origin;
+  absl::string_view value;
+  constexpr bool operator==(const AltSvcFrame&) const = default;
+};
+static_assert(sizeof(AltSvcFrame) == (sizeof(void*) == 8 ? 40 : 20));
+
+struct QUICHE_EXPORT PriorityUpdateFrame {
+  SpdyStreamId prioritized_stream_id = 0;
+  absl::string_view priority_field_value;
+  constexpr bool operator==(const PriorityUpdateFrame&) const = default;
+};
+static_assert(sizeof(PriorityUpdateFrame) == (sizeof(void*) == 8 ? 24 : 12));
+
+struct QUICHE_EXPORT AcceptChFrame {
+  static constexpr size_t kMaxInlineEntries = 1;
+  uint8_t num_entries = 0;
+  std::array<AcceptChEntryView, kMaxInlineEntries> entries{};
+  constexpr bool operator==(const AcceptChFrame&) const = default;
+};
+static_assert(sizeof(AcceptChFrame) == (sizeof(void*) == 8 ? 40 : 20));
+
+struct QUICHE_EXPORT UnknownFrame {
+  SpdyStreamId stream_id = 0;
+  uint8_t type = 0;
+  uint8_t flags = 0;
+  absl::string_view payload;
+  constexpr bool operator==(const UnknownFrame&) const = default;
+};
+static_assert(sizeof(UnknownFrame) == (sizeof(void*) == 8 ? 24 : 16));
+
+// Compile-Time Type Traits
+template <typename T>
+struct FrameTraits;
+
+#define REGISTER_FRAME_TRAITS(Type, EnumVal, HasStreamId, HasFin, HasPadding, \
+                              IsFixed, FixedSize, ConsumesFC)                 \
+  template <>                                                                 \
+  struct FrameTraits<Type> {                                                  \
+    static constexpr SpdyFrameType frame_type = EnumVal;                      \
+    static constexpr bool has_stream_id = HasStreamId;                        \
+    static constexpr bool has_fin = HasFin;                                   \
+    static constexpr bool has_padding = HasPadding;                           \
+    static constexpr bool is_fixed_size = IsFixed;                            \
+    static constexpr size_t fixed_payload_size = FixedSize;                   \
+    static constexpr bool consumes_flow_control = ConsumesFC;                 \
+  };
+
+REGISTER_FRAME_TRAITS(DataFrame, SpdyFrameType::DATA, true, true, true, false,
+                      0, true)
+REGISTER_FRAME_TRAITS(HeadersFrame, SpdyFrameType::HEADERS, true, true, true,
+                      false, 0, false)
+REGISTER_FRAME_TRAITS(PriorityFrame, SpdyFrameType::PRIORITY, true, false,
+                      false, true, 5, false)
+REGISTER_FRAME_TRAITS(RstStreamFrame, SpdyFrameType::RST_STREAM, true, false,
+                      false, true, 4, false)
+REGISTER_FRAME_TRAITS(SettingsFrame, SpdyFrameType::SETTINGS, false, false,
+                      false, false, 0, false)
+REGISTER_FRAME_TRAITS(PushPromiseFrame, SpdyFrameType::PUSH_PROMISE, true,
+                      false, true, false, 0, false)
+REGISTER_FRAME_TRAITS(PingFrame, SpdyFrameType::PING, false, false, false, true,
+                      8, false)
+REGISTER_FRAME_TRAITS(GoAwayFrame, SpdyFrameType::GOAWAY, false, false, false,
+                      false, 0, false)
+REGISTER_FRAME_TRAITS(WindowUpdateFrame, SpdyFrameType::WINDOW_UPDATE, true,
+                      false, false, true, 4, false)
+REGISTER_FRAME_TRAITS(ContinuationFrame, SpdyFrameType::CONTINUATION, true,
+                      false, false, false, 0, false)
+REGISTER_FRAME_TRAITS(AltSvcFrame, SpdyFrameType::ALTSVC, true, false, false,
+                      false, 0, false)
+REGISTER_FRAME_TRAITS(PriorityUpdateFrame, SpdyFrameType::PRIORITY_UPDATE,
+                      false, false, false, false, 0, false)
+REGISTER_FRAME_TRAITS(AcceptChFrame, SpdyFrameType::ACCEPT_CH, false, false,
+                      false, false, 0, false)
+REGISTER_FRAME_TRAITS(UnknownFrame, SpdyFrameType::DATA, true, false, false,
+                      false, 0, false)
+
+#undef REGISTER_FRAME_TRAITS
+
+template <typename T>
+inline constexpr SpdyFrameType frame_type_v = FrameTraits<T>::frame_type;
+
+template <typename T>
+inline constexpr bool is_fixed_size_v = FrameTraits<T>::is_fixed_size;
+
+template <typename T>
+inline constexpr bool has_stream_id_v = FrameTraits<T>::has_stream_id;
+
+template <typename T>
+inline constexpr bool has_fin_v = FrameTraits<T>::has_fin;
+
+template <typename T>
+inline constexpr bool has_padding_v = FrameTraits<T>::has_padding;
+
+template <typename T>
+inline constexpr bool consumes_flow_control_v =
+    FrameTraits<T>::consumes_flow_control;
+
+template <typename T>
+concept Http2FrameConcept = requires {
+  { FrameTraits<T>::frame_type } -> std::convertible_to<SpdyFrameType>;
+  { FrameTraits<T>::is_fixed_size } -> std::convertible_to<bool>;
+  { FrameTraits<T>::has_stream_id } -> std::convertible_to<bool>;
+  { FrameTraits<T>::has_fin } -> std::convertible_to<bool>;
+  { FrameTraits<T>::has_padding } -> std::convertible_to<bool>;
+  { FrameTraits<T>::consumes_flow_control } -> std::convertible_to<bool>;
+};
+
+// Generic Size Calculation
+template <Http2FrameConcept T>
+constexpr size_t FrameSize(const T& frame) {
+  if constexpr (is_fixed_size_v<T>) {
+    return kFrameHeaderSize + FrameTraits<T>::fixed_payload_size;
+  } else if constexpr (std::is_same_v<T, DataFrame>) {
+    return kDataFrameMinimumSize + frame.data.size() +
+           (frame.padded() ? (1 + frame.padding_payload_len) : 0);
+  } else if constexpr (std::is_same_v<T, HeadersFrame>) {
+    size_t size = kHeadersFrameMinimumSize + frame.hpack_block.size();
+    if (frame.padded()) {
+      size += 1 + frame.padding_payload_len;
+    }
+    if (frame.has_priority) {
+      size += 5;
+    }
+    return size;
+  } else if constexpr (std::is_same_v<T, SettingsFrame>) {
+    if (frame.is_ack) {
+      return kSettingsFrameMinimumSize;
+    }
+    return kSettingsFrameMinimumSize +
+           (frame.values.size() * kSettingsOneSettingSize);
+  } else if constexpr (std::is_same_v<T, PushPromiseFrame>) {
+    size_t size = kPushPromiseFrameMinimumSize + frame.hpack_block.size();
+    if (frame.padded()) {
+      size += 1 + frame.padding_payload_len;
+    }
+    return size;
+  } else if constexpr (std::is_same_v<T, GoAwayFrame>) {
+    return kGoawayFrameMinimumSize + frame.debug_data.size();
+  } else if constexpr (std::is_same_v<T, ContinuationFrame>) {
+    return kContinuationFrameMinimumSize + frame.hpack_block.size();
+  } else if constexpr (std::is_same_v<T, AltSvcFrame>) {
+    return kGetAltSvcFrameMinimumSize + frame.origin.size() +
+           frame.value.size();
+  } else if constexpr (std::is_same_v<T, PriorityUpdateFrame>) {
+    return kPriorityUpdateFrameMinimumSize + frame.priority_field_value.size();
+  } else if constexpr (std::is_same_v<T, AcceptChFrame>) {
+    size_t total_size = kAcceptChFrameMinimumSize;
+    for (size_t i = 0; i < frame.num_entries && i < frame.entries.size(); ++i) {
+      total_size += frame.entries[i].origin.size() +
+                    frame.entries[i].value.size() +
+                    kAcceptChFramePerEntryOverhead;
+    }
+    return total_size;
+  } else if constexpr (std::is_same_v<T, UnknownFrame>) {
+    return kFrameHeaderSize + frame.payload.size();
+  } else {
+    return kFrameHeaderSize;
+  }
+}
+
+// Forward declares SpdyFrameBuilder for SerializeFrame.
+class SpdyFrameBuilder;
+
+template <Http2FrameConcept T>
+bool SerializeFrame(const T& frame, SpdyFrameBuilder& builder);
+
+// Unified variant.
+using SpdyFrame =
+    std::variant<DataFrame, HeadersFrame, PriorityFrame, RstStreamFrame,
+                 SettingsFrame, PushPromiseFrame, PingFrame, GoAwayFrame,
+                 WindowUpdateFrame, ContinuationFrame, AltSvcFrame,
+                 PriorityUpdateFrame, AcceptChFrame, UnknownFrame>;
+
+static_assert(sizeof(SpdyFrame) <= 64,
+              "SpdyFrame must fit within a 64-byte cache line");
+
+inline SpdyStreamId GetFrameStreamId(const SpdyFrame& frame) {
+  return std::visit(
+      [](const auto& f) -> SpdyStreamId {
+        using T = std::decay_t<decltype(f)>;
+        if constexpr (has_stream_id_v<T>) {
+          return f.stream_id;
+        }
+        return 0;
+      },
+      frame);
+}
+
+inline SpdyFrameType GetFrameType(const SpdyFrame& frame) {
+  return std::visit(
+      [](const auto& f) -> SpdyFrameType {
+        using T = std::decay_t<decltype(f)>;
+        if constexpr (std::is_same_v<T, UnknownFrame>) {
+          return static_cast<SpdyFrameType>(f.type);
+        }
+        return frame_type_v<T>;
+      },
+      frame);
+}
+
+inline size_t GetFrameSize(const SpdyFrame& frame) {
+  return std::visit([](const auto& f) { return FrameSize(f); }, frame);
+}
+
+bool SerializeSpdyFrame(const SpdyFrame& frame, SpdyFrameBuilder& builder);
+
 class SpdyFrameVisitor;
 
 // Intermediate representation for HTTP2 frames.
diff --git a/quiche/http2/core/spdy_protocol_test.cc b/quiche/http2/core/spdy_protocol_test.cc
index 1adef29..82100e3 100644
--- a/quiche/http2/core/spdy_protocol_test.cc
+++ b/quiche/http2/core/spdy_protocol_test.cc
@@ -6,9 +6,12 @@
 
 #include <iostream>
 #include <string>
+#include <type_traits>
 #include <utility>
 
 #include "absl/strings/string_view.h"
+#include "quiche/http2/core/spdy_frame_builder.h"
+#include "quiche/http2/core/spdy_framer.h"
 #include "quiche/common/platform/api/quiche_expect_bug.h"
 #include "quiche/common/platform/api/quiche_test.h"
 
@@ -282,5 +285,408 @@
   EXPECT_EQ(frame.end(), frame.data() + frame.size());
 }
 
+// =============================================================================
+// Modern HTTP/2 Frame Representations Tests
+// =============================================================================
+
+TEST(ModernFrameTest, StandardLayoutAndTriviallyCopyable) {
+  static_assert(std::is_standard_layout_v<PriorityFields>);
+  static_assert(std::is_trivially_copyable_v<PriorityFields>);
+
+  static_assert(std::is_standard_layout_v<SettingParameter>);
+  static_assert(std::is_trivially_copyable_v<SettingParameter>);
+
+  static_assert(std::is_standard_layout_v<AcceptChEntryView>);
+  static_assert(std::is_trivially_copyable_v<AcceptChEntryView>);
+
+  static_assert(std::is_standard_layout_v<DataFrame>);
+  static_assert(std::is_trivially_copyable_v<DataFrame>);
+
+  static_assert(std::is_standard_layout_v<HeadersFrame>);
+  static_assert(std::is_trivially_copyable_v<HeadersFrame>);
+
+  static_assert(std::is_standard_layout_v<PriorityFrame>);
+  static_assert(std::is_trivially_copyable_v<PriorityFrame>);
+
+  static_assert(std::is_standard_layout_v<RstStreamFrame>);
+  static_assert(std::is_trivially_copyable_v<RstStreamFrame>);
+
+  static_assert(std::is_standard_layout_v<SettingsFrame>);
+
+  static_assert(std::is_standard_layout_v<PushPromiseFrame>);
+  static_assert(std::is_trivially_copyable_v<PushPromiseFrame>);
+
+  static_assert(std::is_standard_layout_v<PingFrame>);
+  static_assert(std::is_trivially_copyable_v<PingFrame>);
+
+  static_assert(std::is_standard_layout_v<GoAwayFrame>);
+  static_assert(std::is_trivially_copyable_v<GoAwayFrame>);
+
+  static_assert(std::is_standard_layout_v<WindowUpdateFrame>);
+  static_assert(std::is_trivially_copyable_v<WindowUpdateFrame>);
+
+  static_assert(std::is_standard_layout_v<ContinuationFrame>);
+  static_assert(std::is_trivially_copyable_v<ContinuationFrame>);
+
+  static_assert(std::is_standard_layout_v<AltSvcFrame>);
+  static_assert(std::is_trivially_copyable_v<AltSvcFrame>);
+
+  static_assert(std::is_standard_layout_v<PriorityUpdateFrame>);
+  static_assert(std::is_trivially_copyable_v<PriorityUpdateFrame>);
+
+  static_assert(std::is_standard_layout_v<AcceptChFrame>);
+  static_assert(std::is_trivially_copyable_v<AcceptChFrame>);
+
+  static_assert(std::is_standard_layout_v<UnknownFrame>);
+  static_assert(std::is_trivially_copyable_v<UnknownFrame>);
+
+  static_assert(sizeof(SpdyFrame) <= 64,
+                "SpdyFrame must fit in a 64-byte cache line");
+}
+
+TEST(ModernFrameTest, Http2FrameConcept) {
+  static_assert(Http2FrameConcept<DataFrame>);
+  static_assert(Http2FrameConcept<HeadersFrame>);
+  static_assert(Http2FrameConcept<PriorityFrame>);
+  static_assert(Http2FrameConcept<RstStreamFrame>);
+  static_assert(Http2FrameConcept<SettingsFrame>);
+  static_assert(Http2FrameConcept<PushPromiseFrame>);
+  static_assert(Http2FrameConcept<PingFrame>);
+  static_assert(Http2FrameConcept<GoAwayFrame>);
+  static_assert(Http2FrameConcept<WindowUpdateFrame>);
+  static_assert(Http2FrameConcept<ContinuationFrame>);
+  static_assert(Http2FrameConcept<AltSvcFrame>);
+  static_assert(Http2FrameConcept<PriorityUpdateFrame>);
+  static_assert(Http2FrameConcept<AcceptChFrame>);
+  static_assert(Http2FrameConcept<UnknownFrame>);
+
+  // Non-frame types should not satisfy the concept.
+  static_assert(!Http2FrameConcept<int>);
+  static_assert(!Http2FrameConcept<std::string>);
+  static_assert(!Http2FrameConcept<PriorityFields>);
+  static_assert(!Http2FrameConcept<SpdyDataIR>);
+}
+
+TEST(ModernFrameTest, FrameTraits) {
+  EXPECT_EQ(SpdyFrameType::DATA, frame_type_v<DataFrame>);
+  EXPECT_EQ(SpdyFrameType::HEADERS, frame_type_v<HeadersFrame>);
+  EXPECT_EQ(SpdyFrameType::PRIORITY, frame_type_v<PriorityFrame>);
+  EXPECT_EQ(SpdyFrameType::RST_STREAM, frame_type_v<RstStreamFrame>);
+  EXPECT_EQ(SpdyFrameType::SETTINGS, frame_type_v<SettingsFrame>);
+  EXPECT_EQ(SpdyFrameType::PUSH_PROMISE, frame_type_v<PushPromiseFrame>);
+  EXPECT_EQ(SpdyFrameType::PING, frame_type_v<PingFrame>);
+  EXPECT_EQ(SpdyFrameType::GOAWAY, frame_type_v<GoAwayFrame>);
+  EXPECT_EQ(SpdyFrameType::WINDOW_UPDATE, frame_type_v<WindowUpdateFrame>);
+  EXPECT_EQ(SpdyFrameType::CONTINUATION, frame_type_v<ContinuationFrame>);
+  EXPECT_EQ(SpdyFrameType::ALTSVC, frame_type_v<AltSvcFrame>);
+  EXPECT_EQ(SpdyFrameType::PRIORITY_UPDATE, frame_type_v<PriorityUpdateFrame>);
+  EXPECT_EQ(SpdyFrameType::ACCEPT_CH, frame_type_v<AcceptChFrame>);
+
+  // Fixed size checks
+  EXPECT_TRUE(is_fixed_size_v<PriorityFrame>);
+  EXPECT_TRUE(is_fixed_size_v<RstStreamFrame>);
+  EXPECT_TRUE(is_fixed_size_v<PingFrame>);
+  EXPECT_TRUE(is_fixed_size_v<WindowUpdateFrame>);
+  EXPECT_FALSE(is_fixed_size_v<DataFrame>);
+  EXPECT_FALSE(is_fixed_size_v<HeadersFrame>);
+  EXPECT_FALSE(is_fixed_size_v<SettingsFrame>);
+
+  // Stream ID presence
+  EXPECT_TRUE(has_stream_id_v<DataFrame>);
+  EXPECT_TRUE(has_stream_id_v<HeadersFrame>);
+  EXPECT_TRUE(has_stream_id_v<PriorityFrame>);
+  EXPECT_TRUE(has_stream_id_v<RstStreamFrame>);
+  EXPECT_TRUE(has_stream_id_v<PushPromiseFrame>);
+  EXPECT_TRUE(has_stream_id_v<WindowUpdateFrame>);
+  EXPECT_FALSE(has_stream_id_v<SettingsFrame>);
+  EXPECT_FALSE(has_stream_id_v<PingFrame>);
+  EXPECT_FALSE(has_stream_id_v<GoAwayFrame>);
+
+  // Fin flag presence
+  EXPECT_TRUE(has_fin_v<DataFrame>);
+  EXPECT_TRUE(has_fin_v<HeadersFrame>);
+  EXPECT_FALSE(has_fin_v<RstStreamFrame>);
+  EXPECT_FALSE(has_fin_v<SettingsFrame>);
+
+  // Padding presence
+  EXPECT_TRUE(has_padding_v<DataFrame>);
+  EXPECT_TRUE(has_padding_v<HeadersFrame>);
+  EXPECT_TRUE(has_padding_v<PushPromiseFrame>);
+  EXPECT_FALSE(has_padding_v<PingFrame>);
+
+  // Flow control consumption
+  EXPECT_TRUE(consumes_flow_control_v<DataFrame>);
+  EXPECT_FALSE(consumes_flow_control_v<HeadersFrame>);
+  EXPECT_FALSE(consumes_flow_control_v<SettingsFrame>);
+}
+
+TEST(ModernFrameTest, FrameSizeCalculation) {
+  DataFrame data{.stream_id = 1, .data = "hello"};
+  EXPECT_EQ(14u, FrameSize(data));
+  data.flags = DATA_FLAG_PADDED;
+  data.padding_payload_len = 3;
+  EXPECT_EQ(18u, FrameSize(data));
+
+  HeadersFrame headers{.stream_id = 1, .hpack_block = "0123456789"};
+  EXPECT_EQ(19u, FrameSize(headers));
+  headers.has_priority = true;
+  EXPECT_EQ(24u, FrameSize(headers));
+
+  PriorityFrame priority{.stream_id = 1};
+  EXPECT_EQ(14u, FrameSize(priority));
+
+  RstStreamFrame rst{.stream_id = 1};
+  EXPECT_EQ(13u, FrameSize(rst));
+
+  SettingsFrame settings_ack{.is_ack = true, .values = {}};
+  EXPECT_EQ(9u, FrameSize(settings_ack));
+  SettingsFrame settings_data{
+      .is_ack = false,
+      .values = {{SETTINGS_HEADER_TABLE_SIZE, 4096},
+                 {SETTINGS_MAX_CONCURRENT_STREAMS, 100}}};
+  EXPECT_EQ(21u, FrameSize(settings_data));
+
+  PushPromiseFrame push{
+      .stream_id = 1, .promised_stream_id = 2, .hpack_block = "12345678"};
+  EXPECT_EQ(21u, FrameSize(push));
+
+  PingFrame ping{};
+  EXPECT_EQ(17u, FrameSize(ping));
+
+  GoAwayFrame goaway{.debug_data = "abcd"};
+  EXPECT_EQ(21u, FrameSize(goaway));
+
+  WindowUpdateFrame win{.stream_id = 1, .delta = 100};
+  EXPECT_EQ(13u, FrameSize(win));
+
+  ContinuationFrame cont{.stream_id = 1, .hpack_block = "abcdef"};
+  EXPECT_EQ(15u, FrameSize(cont));
+
+  AltSvcFrame altsvc{.stream_id = 1, .origin = "foo", .value = "h2=\":443\""};
+  EXPECT_EQ(9u + 2u + 3u + 9u, FrameSize(altsvc));
+
+  PriorityUpdateFrame prio_up{.prioritized_stream_id = 3,
+                              .priority_field_value = "u=3,i"};
+  EXPECT_EQ(9u + 4u + 5u, FrameSize(prio_up));
+
+  AcceptChFrame accept_ch{.num_entries = 1,
+                          .entries = {{{.origin = "foo", .value = "bar"}}}};
+  EXPECT_EQ(9u + 4u + 3u + 3u, FrameSize(accept_ch));
+
+  UnknownFrame unknown{.type = 0x99, .payload = "payload"};
+  EXPECT_EQ(16u, FrameSize(unknown));
+}
+
+TEST(ModernFrameTest, SerializeDataFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  DataFrame df{.stream_id = 3, .flags = DATA_FLAG_FIN, .data = "hello world"};
+  SpdyFrameBuilder builder(FrameSize(df));
+  EXPECT_TRUE(SerializeFrame(df, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyDataIR data_ir(3, "hello world");
+  data_ir.set_fin(true);
+  SpdySerializedFrame expected = framer.SerializeData(data_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializePriorityFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  PriorityFrame pf{
+      .stream_id = 5,
+      .priority = {.parent_stream_id = 1, .weight = 32, .exclusive = true}};
+  SpdyFrameBuilder builder(FrameSize(pf));
+  EXPECT_TRUE(SerializeFrame(pf, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyPriorityIR prio_ir(5, 1, 32, true);
+  SpdySerializedFrame expected = framer.SerializePriority(prio_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializeRstStreamFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  // 3. RST_STREAM frame
+  RstStreamFrame rf{.stream_id = 7, .error_code = ERROR_CODE_CANCEL};
+  SpdyFrameBuilder builder(FrameSize(rf));
+  EXPECT_TRUE(SerializeFrame(rf, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyRstStreamIR rst_ir(7, ERROR_CODE_CANCEL);
+  SpdySerializedFrame expected = framer.SerializeRstStream(rst_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializeSettingsFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  // SETTINGS ack.
+  {
+    SettingsFrame sf_ack{.is_ack = true, .values = {}};
+    SpdyFrameBuilder builder(FrameSize(sf_ack));
+    EXPECT_TRUE(SerializeFrame(sf_ack, builder));
+    SpdySerializedFrame serialized = builder.take();
+
+    SpdySettingsIR settings_ack_ir;
+    settings_ack_ir.set_is_ack(true);
+    SpdySerializedFrame expected = framer.SerializeSettings(settings_ack_ir);
+
+    EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+  }
+  // SETTINGS with values.
+  {
+    SettingsFrame sf_vals{.is_ack = false,
+                          .values = {{SETTINGS_HEADER_TABLE_SIZE, 4096},
+                                     {SETTINGS_MAX_CONCURRENT_STREAMS, 100}}};
+    SpdyFrameBuilder builder(FrameSize(sf_vals));
+    EXPECT_TRUE(SerializeFrame(sf_vals, builder));
+    SpdySerializedFrame serialized = builder.take();
+
+    SpdySettingsIR settings_ir;
+    settings_ir.AddSetting(SETTINGS_HEADER_TABLE_SIZE, 4096);
+    settings_ir.AddSetting(SETTINGS_MAX_CONCURRENT_STREAMS, 100);
+    SpdySerializedFrame expected = framer.SerializeSettings(settings_ir);
+
+    EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+  }
+}
+
+TEST(ModernFrameTest, SerializePingFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  PingFrame ping{.opaque_data = 0x0102030405060708ULL, .is_ack = true};
+  SpdyFrameBuilder builder(FrameSize(ping));
+  EXPECT_TRUE(SerializeFrame(ping, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyPingIR ping_ir(0x0102030405060708ULL);
+  ping_ir.set_is_ack(true);
+  SpdySerializedFrame expected = framer.SerializePing(ping_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializeGoAwayFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  GoAwayFrame goaway{.last_good_stream_id = 9,
+                     .error_code = ERROR_CODE_PROTOCOL_ERROR,
+                     .debug_data = "protocol error occurred"};
+  SpdyFrameBuilder builder(FrameSize(goaway));
+  EXPECT_TRUE(SerializeFrame(goaway, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyGoAwayIR goaway_ir(9, ERROR_CODE_PROTOCOL_ERROR,
+                         "protocol error occurred");
+  SpdySerializedFrame expected = framer.SerializeGoAway(goaway_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializeWindowUpdateFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  WindowUpdateFrame win{.stream_id = 11, .delta = 65535};
+  SpdyFrameBuilder builder(FrameSize(win));
+  EXPECT_TRUE(SerializeFrame(win, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyWindowUpdateIR win_ir(11, 65535);
+  SpdySerializedFrame expected = framer.SerializeWindowUpdate(win_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializeContinuationFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  ContinuationFrame cont{.stream_id = 13,
+                         .flags = HEADERS_FLAG_END_HEADERS,
+                         .hpack_block = "continuation_block"};
+  SpdyFrameBuilder builder(FrameSize(cont));
+  EXPECT_TRUE(SerializeFrame(cont, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyContinuationIR cont_ir(13);
+  cont_ir.set_end_headers(true);
+  cont_ir.take_encoding("continuation_block");
+  SpdySerializedFrame expected = framer.SerializeContinuation(cont_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializePriorityUpdateFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  PriorityUpdateFrame prio_up{.prioritized_stream_id = 5,
+                              .priority_field_value = "u=1,i"};
+  SpdyFrameBuilder builder(FrameSize(prio_up));
+  EXPECT_TRUE(SerializeFrame(prio_up, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyPriorityUpdateIR prio_up_ir(0, 5, "u=1,i");
+  SpdySerializedFrame expected = framer.SerializePriorityUpdate(prio_up_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializeAcceptChFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  AcceptChFrame accept_ch{
+      .num_entries = 1,
+      .entries = {{{.origin = "example.com", .value = "sec-ch-ua"}}}};
+  SpdyFrameBuilder builder(FrameSize(accept_ch));
+  EXPECT_TRUE(SerializeFrame(accept_ch, builder));
+  SpdySerializedFrame serialized = builder.take();
+
+  SpdyAcceptChIR accept_ch_ir({{"example.com", "sec-ch-ua"}});
+  SpdySerializedFrame expected = framer.SerializeAcceptCh(accept_ch_ir);
+
+  EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+}
+
+TEST(ModernFrameTest, SerializeUnknownFrame) {
+  SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+
+  // 11. UNKNOWN frame
+  {
+    UnknownFrame unknown{.stream_id = 1,
+                         .type = 0xfe,
+                         .flags = 0x05,
+                         .payload = "custom_payload"};
+    SpdyFrameBuilder builder(FrameSize(unknown));
+    EXPECT_TRUE(SerializeFrame(unknown, builder));
+    SpdySerializedFrame serialized = builder.take();
+
+    SpdyUnknownIR unknown_ir(1, 0xfe, 0x05, "custom_payload");
+    SpdySerializedFrame expected = framer.SerializeUnknown(unknown_ir);
+
+    EXPECT_EQ(absl::string_view(expected), absl::string_view(serialized));
+  }
+}
+
+TEST(ModernFrameTest, SpdyFrameVariant) {
+  SpdyFrame frame = DataFrame{.stream_id = 42, .data = "payload"};
+  EXPECT_EQ(42u, GetFrameStreamId(frame));
+  EXPECT_EQ(SpdyFrameType::DATA, GetFrameType(frame));
+  EXPECT_EQ(16u, GetFrameSize(frame));
+
+  frame = PingFrame{.opaque_data = 123};
+  EXPECT_EQ(0u, GetFrameStreamId(frame));
+  EXPECT_EQ(SpdyFrameType::PING, GetFrameType(frame));
+  EXPECT_EQ(17u, GetFrameSize(frame));
+
+  SpdyFrameBuilder builder(GetFrameSize(frame));
+  EXPECT_TRUE(SerializeSpdyFrame(frame, builder));
+  EXPECT_EQ(17u, builder.length());
+}
+
 }  // namespace test
 }  // namespace spdy