diff --git a/packages/cultnet-rs/src/media_fec.rs b/packages/cultnet-rs/src/media_fec.rs index ec1b1230..38525db5 100644 --- a/packages/cultnet-rs/src/media_fec.rs +++ b/packages/cultnet-rs/src/media_fec.rs @@ -337,11 +337,25 @@ fn encoding_rows(k: usize, rows: impl Iterator) -> Vec> { } /// Byte `j` of the result is the sum over `i` of `coefficients[i] * shards[i][j]`. +/// +/// The hot loop of both encode and recovery: each coefficient's 256 products +/// are tabulated once, so a shard byte costs one lookup and one XOR. fn combine(coefficients: &[u8], shards: &[&[u8]], shard_bytes: usize) -> Vec { let mut out = vec![0_u8; shard_bytes]; for (&coefficient, shard) in coefficients.iter().zip(shards) { - for (byte, &input) in out.iter_mut().zip(shard.iter()) { - *byte ^= gf_mul(coefficient, input); + match coefficient { + 0 => {} + 1 => { + for (byte, &input) in out.iter_mut().zip(shard.iter()) { + *byte ^= input; + } + } + _ => { + let products: [u8; 256] = std::array::from_fn(|x| gf_mul(coefficient, x as u8)); + for (byte, &input) in out.iter_mut().zip(shard.iter()) { + *byte ^= products[usize::from(input)]; + } + } } } out diff --git a/packages/cultnet-rs/tests/media_fec_bench.rs b/packages/cultnet-rs/tests/media_fec_bench.rs new file mode 100644 index 00000000..40c9de78 --- /dev/null +++ b/packages/cultnet-rs/tests/media_fec_bench.rs @@ -0,0 +1,88 @@ +//! Timings for the media FEC codec, without a bench framework. +//! +//! Ignored by default; run in release, where the numbers mean something: +//! +//! ```text +//! cargo test --release --test media_fec_bench -- --ignored --nocapture +//! ``` +//! +//! Each line is nanoseconds per call through the public surface, so record +//! validation and cloning are included. The geometries are the ones the codec +//! meets: standard audio, the widest standard video block at a datagram, and +//! the worst recovery the policy admits (255 + 1). + +use std::hint::black_box; +use std::time::Instant; + +use cultnet_rs::{ + GAMECULT_MEDIA_MAX_WIRE_BYTES, GameCultMediaAudioPacketRecord, MediaFecPolicy, + MediaWireProvenance, protect_audio_block, recover_audio_block, +}; + +fn provenance() -> MediaWireProvenance<'static> { + MediaWireProvenance { + stored_at: "unix:1700000000000", + runtime_id: "bench", + role: "bench.media", + producer: "bench", + } +} + +fn packets(k: usize, len: usize) -> Vec { + let mut state = 0x2545_f491_u32; + (0..k as u64) + .map(|index| GameCultMediaAudioPacketRecord { + stream_id: "bench".to_string(), + session_id: "bench".to_string(), + packet_id: index, + codec: "opus".to_string(), + pts_ticks: 960 * index as i64, + duration_ticks: 960, + timebase_num: 1, + timebase_den: 48_000, + deadline_ticks: 960 * index as i64 + 5_000, + payload: (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + state as u8 + }) + .collect(), + }) + .collect() +} + +/// Mean nanoseconds per call over at least `min_ms`, after three warm-up calls. +fn time(mut f: impl FnMut(), min_ms: u128) -> f64 { + for _ in 0..3 { + f(); + } + let start = Instant::now(); + let mut calls = 0_u64; + while start.elapsed().as_millis() < min_ms { + f(); + calls += 1; + } + start.elapsed().as_nanos() as f64 / calls as f64 +} + +#[test] +#[ignore = "timing, not a test: run in release with --ignored --nocapture"] +fn media_fec_timings() { + for (k, m, len) in [(4, 2, 160), (4, 2, 1300), (16, 4, 1200), (255, 1, 1300)] { + let policy = MediaFecPolicy { + audio_data_shards: k as u16, + audio_parity_shards: m as u16, + max_wire_bytes: GAMECULT_MEDIA_MAX_WIRE_BYTES, + ..MediaFecPolicy::STANDARD + }; + let block = packets(k, len); + let encode = time(|| drop(black_box(protect_audio_block(black_box(&block), &policy, provenance()))), 300); + let parity = protect_audio_block(&block, &policy, provenance()).unwrap(); + let lost = m.min(k); + let survivors = &block[lost..]; + let decode = time(|| drop(black_box(recover_audio_block(black_box(&parity), survivors))), 300); + println!("k={k:3} m={m:3} len={len:5}: protect {encode:12.0} ns | recover {lost} lost {decode:12.0} ns"); + } +}