/** * @file src/pyrowave_policy.h * @brief Pure PyroWave host policy: frame framing and the per-frame byte budget. * * Nothing here touches a GPU or Sunshine's runtime, so it is shared by the * Windows encoder, the unit tests and the standalone encoder harness. See * docs/pyrowave-protocol.md for the wire format. */ #pragma once #include #include #include #include #include #include #include #include namespace pyrowave::policy { /// Frame payload capacity from the negotiated packet size and video header fields. std::size_t max_frame_bytes(int packetsize, bool critical_fec = false); /// Codec budget leaving enough space for compatibility packet length prefixes. std::size_t max_bitstream_bytes(int packetsize, bool length_prefixed, bool critical_fec = false); /// Packets per pacing quantum, using the routed link speed or stream bitrate. std::size_t pacing_packets_per_ms( std::uint64_t link_bps, int bitrate_kbps, std::size_t payload_bytes, std::size_t wire_bytes, std::size_t frame_bytes = 0, int framerate = 0 ); enum class framing_e { records, ///< Concatenated PyroWave records with padding records aligned to RTP shards. length_prefixed, ///< `[u32 count] { [u32 size] [packet] }`, for the azafrob/dimizago clients. }; /** * @brief Choose the frame framing for a PyroWave session. * @param client_sent_adaptive_fec The client sent `x-ss-video[0].pyrowaveAdaptiveFec` (any value). * @param client_features Value of `x-ss-video[0].pyrowaveFeatures`, if sent. */ framing_e select_framing(bool client_sent_adaptive_fec, std::optional client_features); /** * @brief Frame bytes carried by one RTP shard for the negotiated `packetSize`. * @return 0 when record alignment is impossible (unknown size, too small, or not * a multiple of four bytes); the frame is then written without padding. */ std::size_t shard_payload_bytes(int packetsize); /// A PyroWave packet inside an encoded bitstream. Layout-compatible with `pyrowave_packet`. struct packet_t { std::size_t offset; std::size_t size; }; struct record_frame_stats_t { std::size_t block_records = 0; std::size_t padding_records = 0; std::size_t padding_bytes = 0; std::size_t oversized_records = 0; ///< Records too large to share a shard with padding, which span shards. std::size_t reordered_records = 0; ///< Records placed ahead of an earlier record to fill a shard. /// Frame bytes through the last record of the coarsest wavelet level (at least the /// sequence header). The shards holding them are the frame's critical shards. std::size_t critical_bytes = 0; }; /** * @brief Number of blocks in PyroWave's coarsest wavelet level for a frame size. * * PyroWave indexes these first (block indices below this count). A receiver cannot * decode a frame that lost any of them, so they are sent before finer blocks. */ std::uint32_t coarse_block_count(std::uint32_t width, std::uint32_t height); /** * @brief Append a record-framed frame to `out`. * * `bitstream` is one PyroWave packet holding the whole frame: the 8-byte sequence * header followed by every block record (`pyrowave_encoder_packetize` with an * unlimited boundary). The layout lets a receiver decode a frame that lost packets * (docs/pyrowave-protocol.md): * * 1. The sequence header. * 2. The coarsest wavelet level, which a receiver cannot do without: its shards * (`critical_bytes`) are the ones stream.cpp protects with parity. * 3. Every other record. * * Within groups 2 and 3, oversized records (too large to share a shard with a * padding record) come first, in frame order. They span shards; a minimal padding * record goes before one that would otherwise end 4 bytes before a boundary. The * group's other records follow, packed first-fit: a shard remainder takes the * earliest record of the group that fits it, and a padding record fills it only * when none fits. None of them crosses a shard boundary and no 4-byte remainder * (too small for a padding record) is left, so every shard after the finer * level's oversized records starts with a record. * * Measured padding is well under 1% of the frame, against about 20% when records * keep strict order. * * @param shard_payload Result of shard_payload_bytes(); 0 copies the bitstream unchanged. * @param frame_limit If set, omit padding when it would exceed this capacity. * @return Statistics, or std::nullopt when `bitstream` is not a valid PyroWave frame * (nothing is appended then). */ std::optional write_record_frame( std::span bitstream, std::size_t shard_payload, std::vector &out, std::size_t frame_limit = 0 ); /// Append a length-prefixed frame (`[u32 LE count] { [u32 LE size] [bytes] }`) to `out`. void write_length_prefixed_frame( std::span packets, const std::uint8_t *bitstream, std::vector &out ); /// Result of parsing a record-framed frame; used by tests and diagnostics. struct record_frame_info_t { bool valid = false; std::string error; bool has_sequence_header = false; std::uint32_t width = 0; std::uint32_t height = 0; bool chroma444 = false; std::uint32_t total_blocks = 0; ///< From the sequence header. std::size_t block_records = 0; std::size_t padding_records = 0; std::size_t padding_bytes = 0; std::size_t misaligned_records = 0; ///< Ordinary block or padding records that cross a shard boundary. std::size_t out_of_order_records = 0; ///< Block records whose block_index is lower than the previous one. std::size_t oversized_records = 0; ///< Records too large to share a shard with a padding record. std::size_t late_oversized_records = 0; ///< Oversized records after an ordinary record of their level. std::size_t late_coarse_records = 0; ///< Coarsest-level records after a finer one. std::size_t critical_bytes = 0; ///< Frame bytes through the last coarsest-level record. std::vector stripped; ///< The frame with padding records removed. }; /** * @brief Which shards of a record-framed frame start with a record. * * Shard 0 holds the frame from its first byte (after the frame header) and so starts * with the sequence header; shard s > 0 starts at frame byte s * shard_payload - * FRAME_HEADER_BYTES. A record here is a sequence header, block or padding record. * @return One entry per shard, or empty when the frame cannot be walked. */ std::vector record_start_shards(std::span frame, std::size_t shard_payload); /** * @brief Parse a record-framed frame the way a receiver does. * @param shard_payload Shard size used for the alignment statistics; 0 skips them. */ record_frame_info_t inspect_record_frame(std::span frame, std::size_t shard_payload); /// One FEC block of a PyroWave frame. struct fec_block_t { std::size_t data_shards; int fec_percentage; }; /// stream.cpp sends a frame in at most this many FEC blocks (2 bits in the header). constexpr std::size_t MAX_FEC_BLOCKS = 4; /// Data shards one FEC block can index (10 bits in the header). constexpr std::size_t MAX_FEC_BLOCK_SHARDS = 1023; /// Data plus parity shards one Reed-Solomon block (GF(2^8)) can hold. constexpr std::size_t MAX_REED_SOLOMON_SHARDS = 255; /// Stop buying detail protection once parity costs half the protected data. constexpr int MAX_DETAIL_FEC_PERCENTAGE = 50; /** * @brief Split a PyroWave frame into FEC blocks. * * By default, only the frame's first `critical_shards` (the sequence header and the coarsest * wavelet level) get parity, as block 0 at `critical_fec_percentage` with at least * `min_parity_shards` parity shards. Losing any of them costs the whole frame, * while a lost finer record only blurs its area. The rest is split evenly into up * to three blocks without FEC. Parity is skipped when the percentage is 0, there is * nothing critical, or block 0 would exceed a Reed-Solomon block; the frame is then * split as before, into up to four blocks without FEC. Optional detail protection * spends at most `extra_parity_budget` additional shards. Large frames receive * partial coverage within the same four blocks. * * Blocks are listed in order and their shards add up to `total_shards`. A frame * too large for the blocks gets blocks over MAX_FEC_BLOCK_SHARDS, which the caller * reports; the encoder's frame budget keeps that from happening. */ std::vector plan_fec_blocks(std::size_t total_shards, std::size_t critical_shards, int critical_fec_percentage, std::size_t min_parity_shards, int detail_fec_percentage = 0, std::size_t extra_parity_budget = 0); /// Parity shards stream.cpp's FEC encoder adds to a block of `data_shards`. std::size_t parity_shards(std::size_t data_shards, int fec_percentage, std::size_t min_parity_shards); struct detail_fec_t { int percentage = 0; std::size_t frame_wire_budget = 0; }; /// Conservative activity estimate from record contents, ignoring sequence bits /// and padding. No GPU readback; quantization changes count as picture changes. /// Extra protection requires 250 ms of similar frames below negotiated FPS. /// The cadence shortfall determines the request, capped at 50% extra parity. /// Only unused bitrate can pay for the resulting parity. class detail_fec_controller_t { public: explicit detail_fec_controller_t(int framerate); detail_fec_t observe(std::span frame, std::chrono::steady_clock::time_point when, int bitrate_kbps); private: double nominal_interval; std::unordered_map previous_blocks; std::size_t previous_record_bytes = 0; std::uint64_t previous_format = 0; std::optional previous_time; double interval = 0.0; double stable_seconds = 0.0; }; /** * @brief Per-frame byte budget for an intra-only codec. * * Each submitted frame, including repeats, gets the bytes earned since the * previous submission. The initial budget uses the negotiated frame rate. * With stable_frame_size, the encoded image budget never exceeds one negotiated * frame's allowance at the current bitrate, leaving slower cadence's savings for FEC. * The transport capacity bounds accumulation after stalls in either mode. */ class budget_t { public: budget_t(int framerate, int bitrate_kbps, std::size_t max_frame_bytes, bool stable_frame_size = false); void set_bitrate(int bitrate_kbps); /// Report an encoding attempt, including repeats of the previous capture. void on_frame(std::chrono::steady_clock::time_point when); /// Current budget in bytes, aligned to codec words; zero means skip this frame. [[nodiscard]] std::size_t bytes_per_frame() const { return budget; } [[nodiscard]] double frame_fps() const { return frame_interval > 0.0 ? 1.0 / frame_interval : 0.0; } private: void update(); int bitrate_kbps; std::size_t max_frame_bytes; double nominal_interval; bool stable_frame_size; double frame_interval; std::optional last_frame; std::size_t budget = 0; }; } // namespace pyrowave::policy