/// @file dpf/buffered_prg.hpp /// @brief Buffered, seekable lanes over a libdpf PRG. /// @details The default PRG is `dpf::prg::aes128`. Any PRG with /// `block_type`, `eval(seed, pos)`, and `eval(seed, out, count, pos)` /// can be substituted. A master block is the recorded seed. Role `r` /// owns two streams: values are `PRG::eval(master, r)` and share /// masks are `PRG::eval(tweaked_master, r)`. Element `index` is a /// contiguous run of blocks on that stream, so a forward scan is one /// multi-block `eval`. /// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors) /// @license Released under a GNU General Public v2.0 (GPLv2) license; /// see [LICENSE.md](@ref license) for details. #ifndef LIBDPF_INCLUDE_DPF_BUFFERED_PRG_HPP__ #define LIBDPF_INCLUDE_DPF_BUFFERED_PRG_HPP__ #include #include #include #include #include #include #include #include #include #include "hedley/hedley.h" #include "dpf/aligned_allocator.hpp" #include "dpf/experiment_note.hpp" #include "dpf/prg.hpp" #include "dpf/random.hpp" namespace dpf { namespace randomness { namespace detail { template HEDLEY_NO_THROW typename PRG::block_type mask_master(typename PRG::block_type master) noexcept { unsigned char raw[sizeof(master)]; std::memcpy(raw, &master, sizeof(master)); raw[sizeof(master) - 1] ^= 0x01u; typename PRG::block_type out{}; std::memcpy(&out, raw, sizeof(out)); return out; } /// @brief Master for IT-MAC tag masks. Distinct from `mask_master`. template HEDLEY_NO_THROW typename PRG::block_type tag_master(typename PRG::block_type master) noexcept { unsigned char raw[sizeof(master)]; std::memcpy(raw, &master, sizeof(master)); raw[sizeof(master) - 1] ^= 0x02u; typename PRG::block_type out{}; std::memcpy(&out, raw, sizeof(out)); return out; } template struct lane_codec { static_assert(std::is_trivially_copyable_v, "prg lanes require a trivially copyable value type"); using block_type = typename PRG::block_type; static T at(block_type seed, std::uint64_t index) { T out{}; fill(seed, index, &out, 1u); return out; } /// @brief Element `index` is the packed byte range `[index * sizeof(T), ...)`. /// @param seed the PRG seed /// @param index the index /// @param out the output buffer /// @param count the number of blocks /// @throws std::invalid_argument if `prg lane index is out of range` static void fill(block_type seed, std::uint64_t index, T * out, std::size_t count) { if (count == 0) return; constexpr std::uint64_t block_bytes = sizeof(block_type); std::uint64_t byte_off = index * static_cast(sizeof(T)); std::uint64_t nbytes = static_cast(count) * sizeof(T); std::uint64_t start = byte_off / block_bytes; std::uint64_t end = byte_off + nbytes; std::uint64_t nblocks = (end + block_bytes - 1u) / block_bytes - start; if (start > static_cast(UINT32_MAX) || nblocks > static_cast(UINT32_MAX) || start > static_cast(UINT32_MAX) - nblocks) { throw std::invalid_argument("prg lane index is out of range"); } HEDLEY_PRAGMA(GCC diagnostic push) HEDLEY_PRAGMA(GCC diagnostic ignored "-Wignored-attributes") dpf::aligned_allocator alloc; HEDLEY_PRAGMA(GCC diagnostic pop) auto blocks = alloc.allocate_unique_ptr(static_cast(nblocks)); PRG::eval(seed, blocks.get(), static_cast(nblocks), static_cast(start)); auto * bytes = reinterpret_cast(blocks.get()); std::memcpy(out, bytes + static_cast(byte_off % block_bytes), static_cast(nbytes)); } }; template struct buffered_slot { using block_type = typename PRG::block_type; explicit buffered_slot(block_type seed, std::size_t buffer_elems) : seed_(seed), buffer_(buffer_elems), absolute_pos_(0u), filled_(0u), next_(0u) { refill(0u); } T get() { if (next_ >= filled_) refill(absolute_pos_); T v = buffer_[next_]; ++next_; ++absolute_pos_; return v; } void fill(T * out, std::size_t count) { std::size_t written = 0u; while (written < count) { if (next_ >= filled_) refill(absolute_pos_); std::size_t available = filled_ - next_; std::size_t take = available < count - written ? available : count - written; std::memcpy(out + written, buffer_.data() + next_, take * sizeof(T)); written += take; next_ += take; absolute_pos_ += static_cast(take); } } T at(std::uint64_t index) const { return lane_codec::at(seed_, index); } HEDLEY_NO_THROW std::uint64_t sampled() const noexcept { return absolute_pos_; } private: void refill(std::uint64_t at_elem) { next_ = 0u; filled_ = buffer_.size(); lane_codec::fill(seed_, at_elem, buffer_.data(), buffer_.size()); } block_type seed_; std::vector buffer_; std::uint64_t absolute_pos_; std::size_t filled_; std::size_t next_; }; } // namespace detail template typename PRG::block_type sample_master_seed() { return dpf::uniform_sample(); } /// @brief Forward cursor over one PRG stream per value type. /// /// `get()` and `fill()` consume the cursor. `at(index)` reads an /// absolute index and leaves the cursor where it is. `sampled()` reports /// how far `get` and `fill` have advanced. `per_stream_buffer_elems` is at /// least 1. /// @snippet evaluation/buffered_prg.cpp buffered-prg /// @tparam PRG pseudorandom generator /// @tparam Ts ts template class buffered_prg { public: using prg_type = PRG; using seed_type = typename PRG::block_type; static constexpr std::size_t stream_count = sizeof...(Ts); explicit buffered_prg(std::size_t per_stream_buffer_elems = 1024u) : seed_(sample_master_seed()), buffers_(make_buffers(per_stream_buffer_elems)) { note_experiment_seed("buffered_prg", seed_); } explicit buffered_prg(seed_type seed, std::size_t per_stream_buffer_elems = 1024u) : seed_(seed), buffers_(make_buffers(per_stream_buffer_elems)) { note_experiment_seed("buffered_prg", seed_); } HEDLEY_NO_THROW const seed_type & seed() const noexcept { return seed_; } template auto get() { static_assert(I < stream_count, "stream index out of range"); return std::get(buffers_).get(); } template void fill(std::tuple_element_t> * out, std::size_t count) { static_assert(I < stream_count, "stream index out of range"); std::get(buffers_).fill(out, count); } template auto at(std::uint64_t index) const { static_assert(I < stream_count, "stream index out of range"); return std::get(buffers_).at(index); } template HEDLEY_NO_THROW std::uint64_t sampled() const noexcept { static_assert(I < stream_count, "stream index out of range"); return std::get(buffers_).sampled(); } private: template std::tuple...> make_buffers_impl(std::size_t n, std::index_sequence) const { return std::tuple...>( detail::buffered_slot( PRG::eval(seed_, static_cast(I)), n)...); } std::tuple...> make_buffers(std::size_t n) const { if (n == 0) throw std::invalid_argument("per_stream_buffer_elems must be positive"); return make_buffers_impl(n, std::make_index_sequence{}); } seed_type seed_; std::tuple...> buffers_; }; template using aes_buffered_prg = buffered_prg; /// @brief Seekable value and mask streams for a runtime set of roles. /// /// `value_at(role, index)` and `mask_at(role, index)` are independent of /// call order. A repeated index returns the same element. `window` is at /// least 1. /// @snippet evaluation/buffered_prg.cpp lane-table /// @tparam T value type /// @tparam PRG pseudorandom generator template class lane_table { public: using prg_type = PRG; using seed_type = typename PRG::block_type; using value_type = T; explicit lane_table(std::size_t window = 256u) : lane_table(sample_master_seed(), window) { } explicit lane_table(seed_type seed, std::size_t window = 256u) : seed_(seed), mask_seed_(detail::mask_master(seed)), window_(window) { if (window_ == 0) throw std::invalid_argument("prg lane window must be positive"); note_experiment_seed("lane_table", seed_); } lane_table(const lane_table &) = delete; lane_table & operator=(const lane_table &) = delete; lane_table(lane_table &&) = default; lane_table & operator=(lane_table &&) = default; HEDLEY_NO_THROW const seed_type & seed() const noexcept { return seed_; } T value_at(std::uint32_t role, std::uint64_t index) const { return cached(role, true, index); } T mask_at(std::uint32_t role, std::uint64_t index) const { return cached(role, false, index); } void fill_values(std::uint32_t role, std::uint64_t index, T * out, std::size_t n) const { if (n == 0) return; detail::lane_codec::fill(slot(role).value_seed, index, out, n); } void fill_masks(std::uint32_t role, std::uint64_t index, T * out, std::size_t n) const { if (n == 0) return; detail::lane_codec::fill(slot(role).mask_seed, index, out, n); } private: using block_type = typename PRG::block_type; struct slot_pair { block_type value_seed{}; block_type mask_seed{}; std::vector value_cache; std::vector mask_cache; std::uint64_t value_base = 0; std::uint64_t mask_base = 0; bool value_hot = false; bool mask_hot = false; }; slot_pair & slot(std::uint32_t role) const { auto it = slots_.find(role); if (it != slots_.end()) return it->second; slot_pair created; created.value_seed = PRG::eval(seed_, role); created.mask_seed = PRG::eval(mask_seed_, role); created.value_cache.resize(window_); created.mask_cache.resize(window_); auto inserted = slots_.emplace(role, std::move(created)); return inserted.first->second; } T cached(std::uint32_t role, bool values, std::uint64_t index) const { slot_pair & s = slot(role); std::vector & buf = values ? s.value_cache : s.mask_cache; std::uint64_t & base = values ? s.value_base : s.mask_base; bool & hot = values ? s.value_hot : s.mask_hot; block_type stream = values ? s.value_seed : s.mask_seed; if (hot && index >= base && index < base + buf.size()) return buf[static_cast(index - base)]; base = index; hot = true; detail::lane_codec::fill(stream, index, buf.data(), buf.size()); return buf[0]; } seed_type seed_; seed_type mask_seed_; std::size_t window_; mutable std::map slots_; }; } // namespace randomness } // namespace dpf #endif // LIBDPF_INCLUDE_DPF_BUFFERED_PRG_HPP__