/// @file dpf/net/round_lane.hpp /// @brief Round→lane mapping and wire framing when rounds share lanes. /// @details A fixed pool of duplex streams can carry any number of exchange /// rounds: lane = `round % n_lanes`. A framed lane prefixes each write /// with a little-endian `{u16 round, u32 nbytes}` header, and the /// receiver appends those bytes to that round's inbox. Unframed lanes /// carry one round each (round == lane) with no header. #ifndef LIBDPF_INCLUDE_DPF_NET_ROUND_LANE_HPP__ #define LIBDPF_INCLUDE_DPF_NET_ROUND_LANE_HPP__ #include #include #include #include #include #include #include "dpf/net/policy.hpp" namespace dpf { namespace net { /// @brief On-wire header for a framed round payload on a shared lane. struct round_lane_hdr { static constexpr std::size_t size = 6; std::uint16_t round = 0; std::uint32_t nbytes = 0; void pack(std::uint8_t * dst) const noexcept { dst[0] = static_cast(round & 0xffu); dst[1] = static_cast((round >> 8) & 0xffu); dst[2] = static_cast(nbytes & 0xffu); dst[3] = static_cast((nbytes >> 8) & 0xffu); dst[4] = static_cast((nbytes >> 16) & 0xffu); dst[5] = static_cast((nbytes >> 24) & 0xffu); } static round_lane_hdr unpack(const std::uint8_t * src) noexcept { round_lane_hdr h; h.round = static_cast(src[0] | (src[1] << 8)); h.nbytes = static_cast(src[2] | (src[3] << 8) | (src[4] << 16) | (src[5] << 24)); return h; } }; /// @brief Lane count used when a caller passes `0`. inline constexpr std::size_t default_lanes = 8; /// @brief Request one stream per round. inline constexpr std::size_t lanes_one_per_round = static_cast(-1); /// @brief Round ids travel as `u16`. inline constexpr std::size_t max_rounds = 0xffffu; /// @brief Streams to open for `n_rounds`. `requested` 0 → `default_lanes`. inline std::size_t lane_count_for_rounds(std::size_t n_rounds, std::size_t requested = 0) { if (n_rounds == 0) return 1; if (requested == lanes_one_per_round) return n_rounds; std::size_t want = requested == 0 ? default_lanes : requested; if (want > n_rounds) want = n_rounds; return want == 0 ? 1 : want; } /// @brief Lane index and whether the wire uses round headers. struct round_lane_map { std::size_t n_lanes = 1; std::size_t n_rounds = 0; bool framed = false; round_lane_map() = default; round_lane_map(std::size_t streams, std::size_t rounds, framing_mode mode = framing_mode::automatic) : n_lanes(streams == 0 ? 1 : streams), n_rounds(rounds) { if (rounds > max_rounds) throw std::invalid_argument("round_lane_map: " + std::to_string(rounds) + " rounds exceed the u16 round id"); switch (mode) { case framing_mode::automatic: framed = rounds > n_lanes; break; case framing_mode::always: framed = true; break; case framing_mode::never: if (rounds > n_lanes) throw std::invalid_argument("round_lane_map: framing never needs " "one lane per round (" + std::to_string(rounds) + " rounds, " + std::to_string(n_lanes) + " lanes)"); framed = false; break; } } std::size_t lane(std::uint16_t round) const { if (static_cast(round) >= n_rounds && n_rounds != 0) throw std::out_of_range("round_lane_map round"); if (!framed) return static_cast(round); return static_cast(round) % n_lanes; } }; /// @brief Build `hdr || payload` for a framed write. inline std::vector pack_round_frame(std::uint16_t round, const std::uint8_t * payload, std::size_t n) { std::vector out(round_lane_hdr::size + n); round_lane_hdr{round, static_cast(n)}.pack(out.data()); if (n != 0) std::memcpy(out.data() + round_lane_hdr::size, payload, n); return out; } /// @brief FNV-1a over a slot-width vector (plan shape fingerprint). inline std::uint64_t slots_fingerprint(const std::vector & slots) { std::uint64_t h = 14695981039346656037ull; for (auto s : slots) { std::uint64_t v = static_cast(s); for (int i = 0; i < 8; ++i) { h ^= (v >> (8 * i)) & 0xffu; h *= 1099511628211ull; } } return h; } } // namespace net } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_NET_ROUND_LANE_HPP__