android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
apple / swift (push) Has been cancelled
arch / build-publish (push) Has been cancelled
audit / bun-audit (push) Failing after 13s
audit / cargo-audit (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
deb / build-publish (push) Has been cancelled
deb / build-publish-host (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Successful in 10m1s
windows-host / package (push) Successful in 11m20s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m29s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 4m14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 22m16s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 22m27s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 5m28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 6m39s
Lifts the ~100 Mbps decrypt ceiling on clients without hardware AES — the armv7 soft-AES targets (webOS TVs), where AES-128-GCM resolves to fixsliced software AES + software GHASH (~50-100 cpb) while ChaCha20-Poly1305's ARX construction runs ~10-17 cpb portable, a 4-7x lift that PyroWave-on-TV needs (design/chacha20-session-cipher.md). Phase 1 (core crypto, no wire change): SessionKey merges cipher choice and key material (invalid combinations unrepresentable, zeroize + redacted-Debug discipline kept); SessionCrypto dispatches both aead-0.5 ciphers per call — the salt||seq nonce scheme, per-direction salts, seq-as-AAD and replay window carry over verbatim (same 96-bit nonce / 16-byte tag, const-asserted). Config.key becomes SessionKey; validate's zero-key rejection follows the active variant. The C ABI keeps its fixed 16-byte key mapped to AES — no ABI_VERSION bump. Phase 2 (negotiation): VIDEO_CAP_CHACHA20 (0x40) — support-plus-request in one bit, the VIDEO_CAP_444 precedent. Welcome grows cipher@68 + key_chacha@69..101, emitted only when non-zero so an AES session's Welcome stays byte-identical to the pre-cipher form; decode is fail-closed (short key or unknown id -> Err, never a silent AES fallback). No WIRE_VERSION bump; downgrade resistance inherited from the pinned-TLS control channel. Phase 3 (host): grant only when the client advertised the bit and the PUNKTFUNK_CHACHA20 kill-switch (default on, documented) allows; fresh 32-byte per-session key from the same RNG discipline, legacy key field stays independently random; resolved cipher logged at session start. Verification: seal/open suites parameterized over both ciphers + a cross-cipher tamper case; Welcome roundtrip/truncation/fail-closed tests; ChaCha lossy-loopback soak (loss/replay is cipher-independent); bench gains _chacha20 series (AES ids unchanged for CI history) — host-side sealing line-rate-trivial on both x86 (~640 MiB/s) and Apple Silicon (~535 MiB/s). punktfunk-probe drives the interop matrix via PUNKTFUNK_CLIENT_CHACHA20=1 and logs the negotiated cipher. Phase 4 (pf-webos pin bump + unconditional cap bit) follows the next core release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
127 lines
5.5 KiB
Rust
127 lines
5.5 KiB
Rust
//! Tier-1 microbenchmarks for the punktfunk/1 hot path — GPU-free, so they run in normal CI.
|
|
//!
|
|
//! Two layers:
|
|
//! - `crypto/*` — the isolated AEAD primitives (AES-128-GCM + the negotiated
|
|
//! ChaCha20-Poly1305) on one ~MTU shard.
|
|
//! - `pipeline/*`— a whole frame through the real per-frame path end to end over the in-process
|
|
//! loopback transport: FEC encode → AES-GCM seal → packetize → (loopback) → reassemble →
|
|
//! FEC decode → open. This is what a throughput/latency regression in the core would show up in.
|
|
//!
|
|
//! The GPU capture/NVENC encode path is deliberately out of scope here (no GPU in CI) — that's the
|
|
//! Tier-3 stream benchmark on a self-hosted GPU runner. Run locally with `cargo bench -p punktfunk-core`.
|
|
|
|
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
|
use punktfunk_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
|
|
use punktfunk_core::crypto::{SessionCrypto, SessionKey};
|
|
use punktfunk_core::session::Session;
|
|
use punktfunk_core::transport::loopback_pair;
|
|
|
|
const TAG_LEN: usize = 16; // AEAD authentication tag (GCM and Poly1305 share the size)
|
|
const SHARD: usize = punktfunk_core::config::mtu1500_shard_payload(); // one MTU-safe data shard
|
|
|
|
fn cfg(role: Role, scheme: FecScheme) -> Config {
|
|
Config {
|
|
role,
|
|
phase: match scheme {
|
|
FecScheme::Gf8 => ProtocolPhase::P1GameStream,
|
|
FecScheme::Gf16 => ProtocolPhase::P2Punktfunk,
|
|
},
|
|
fec: FecConfig {
|
|
scheme,
|
|
fec_percent: 25,
|
|
// GF(2^8) is capped at ≤255 shards/block (Moonlight-compatible); GF(2^16) Leopard goes
|
|
// far higher. Use a realistic, valid block size for each.
|
|
max_data_per_block: match scheme {
|
|
FecScheme::Gf8 => 128,
|
|
FecScheme::Gf16 => 4096,
|
|
},
|
|
},
|
|
shard_payload: SHARD,
|
|
max_frame_bytes: 8 * 1024 * 1024,
|
|
encrypt: true, // bench the real path — crypto is always on for punktfunk/1
|
|
key: SessionKey::Aes128Gcm([7u8; 16]),
|
|
salt: [1, 2, 3, 4],
|
|
loopback_drop_period: 0, // throughput run: no induced loss (loss-harness covers recovery)
|
|
}
|
|
}
|
|
|
|
fn bench_crypto(c: &mut Criterion) {
|
|
let mut g = c.benchmark_group("crypto");
|
|
g.throughput(Throughput::Bytes(SHARD as u64));
|
|
// Both negotiated session AEADs. On the x86 / Apple Silicon this runs on, both must be
|
|
// line-rate-trivial — the chacha20 series is the host-side sealing-cost check for the
|
|
// negotiated soft-AES-armv7 path (design/chacha20-session-cipher.md §7). The AES series
|
|
// keeps its unsuffixed names so the CI regression compare retains its history.
|
|
for (suffix, key) in [
|
|
("", SessionKey::Aes128Gcm([7u8; 16])),
|
|
("_chacha20", SessionKey::ChaCha20Poly1305([7u8; 32])),
|
|
] {
|
|
let host = SessionCrypto::new(&key, [1, 2, 3, 4], Role::Host);
|
|
let client = SessionCrypto::new(&key, [1, 2, 3, 4], Role::Client);
|
|
let payload = vec![0xABu8; SHARD];
|
|
let sealed = host.seal(0, &payload).unwrap();
|
|
|
|
g.bench_function(format!("seal{suffix}"), |b| {
|
|
let mut seq = 0u64;
|
|
b.iter(|| {
|
|
let ct = host.seal(seq, black_box(&payload)).unwrap();
|
|
seq += 1;
|
|
black_box(ct)
|
|
})
|
|
});
|
|
g.bench_function(format!("seal_in_place{suffix}"), |b| {
|
|
let mut seq = 0u64;
|
|
let mut buf = vec![0xABu8; SHARD + TAG_LEN];
|
|
b.iter(|| {
|
|
host.seal_in_place(seq, black_box(&mut buf)).unwrap();
|
|
seq += 1;
|
|
})
|
|
});
|
|
g.bench_function(format!("open{suffix}"), |b| {
|
|
b.iter(|| black_box(client.open(0, black_box(&sealed)).unwrap()))
|
|
});
|
|
g.bench_function(format!("open_in_place{suffix}"), |b| {
|
|
// In-place open consumes the buffer, so each iteration restores the ciphertext first —
|
|
// one memcpy, mirroring what the recv ring does when the next datagram lands in the slot.
|
|
let mut buf = sealed.clone();
|
|
b.iter(|| {
|
|
buf.copy_from_slice(black_box(&sealed));
|
|
black_box(client.open_in_place(0, &mut buf).unwrap());
|
|
})
|
|
});
|
|
}
|
|
g.finish();
|
|
}
|
|
|
|
fn bench_pipeline(c: &mut Criterion) {
|
|
let mut g = c.benchmark_group("pipeline");
|
|
// 64 KB ≈ a steady-state P-frame; 1 MB ≈ a keyframe/scene-cut. Both FEC schemes (GF(2^8)
|
|
// GameStream-compat vs GF(2^16) Leopard, the wall-breaker).
|
|
for scheme in [FecScheme::Gf8, FecScheme::Gf16] {
|
|
let label = match scheme {
|
|
FecScheme::Gf8 => "gf8",
|
|
FecScheme::Gf16 => "gf16",
|
|
};
|
|
for &size in &[64 * 1024usize, 1024 * 1024] {
|
|
g.throughput(Throughput::Bytes(size as u64));
|
|
g.bench_with_input(BenchmarkId::new(label, size), &size, |b, &size| {
|
|
let (h, cl) = loopback_pair(0, 0);
|
|
let mut host = Session::new(cfg(Role::Host, scheme), Box::new(h)).unwrap();
|
|
let mut client = Session::new(cfg(Role::Client, scheme), Box::new(cl)).unwrap();
|
|
let frame = vec![0x5Au8; size];
|
|
let mut seq = 0u64;
|
|
b.iter(|| {
|
|
host.submit_frame(black_box(&frame), seq, 0).unwrap();
|
|
let f = client.poll_frame().unwrap();
|
|
seq += 1;
|
|
black_box(f)
|
|
})
|
|
});
|
|
}
|
|
}
|
|
g.finish();
|
|
}
|
|
|
|
criterion_group!(benches, bench_crypto, bench_pipeline);
|
|
criterion_main!(benches);
|