perf(cmaf): enable AES-NI + zero-alloc decrypt loop
CMAF decrypt was the bottleneck for offline-cache gapless. Live
testing showed a 208 MB HiRes bundle took ~22 seconds to decrypt end-
to-end, which made gapless for anything longer than a 3-minute track
impossible (the gapless pre-emptive window is ~5s before track end,
so a 22s decrypt can't land in time regardless of how early we start).
Two changes, complementary:
1. `.cargo/config.toml` with `target-cpu=x86-64-v3` (Haswell baseline).
The `aes` crate ships both a software implementation and an AES-NI
hardware path, runtime-dispatched via `cpufeatures` — but the
AES-NI path is only compiled in when the appropriate target-feature
is enabled at build time. Default dev builds compile ONLY the
software path, which hits ~10 MB/s on a modern CPU. Haswell+ (2013
and up, every realistic QBZ user) supports AES-NI and clears
400 MB/s+. For prebuilt distribution binaries we'll want runtime-
dispatch via subfunction build or dynamic linking, but for self-
built and release-source-from-tag scenarios this is the right
default.
2. `decrypt_segments_into` no longer allocates a Vec per frame. Was:
frame = seg_data[..].to_vec(); // alloc
decrypt_frame(&mut frame); // in-place
output.extend_from_slice(&frame); // copy back
Now:
let start = output.len();
output.extend_from_slice(&seg_data[..]); // single copy
decrypt_frame(&mut output[start..]); // in-place
For a HiRes FLAC with ~50K frames this saves 50K small allocs +
50K double-copies. Output Vec is already pre-sized via
with_capacity(total), so no reallocation growth during extend.
Combined expected impact: 22s decrypt on a 208 MB track → under 500ms,
which fits inside the 5s gapless window with headroom to spare.
cc
This commit is contained in:
2 files changed
+29
-3
No files matched your search
@@ -0,0 +1,15 @@
|
||||
# Enable CPU intrinsics for AES-NI + SSSE3.
|
||||
#
|
||||
# The qbz-cmaf crate does AES-128-CTR decrypt on every offline-cached
|
||||
# audio frame. Without AES-NI the decrypt falls back to a software
|
||||
# implementation that hits ~10 MB/s on a modern CPU; with AES-NI it
|
||||
# clears 400+ MB/s. For HiRes (200 MB+ tracks) this is the difference
|
||||
# between a 20-second gap and a sub-second one.
|
||||
#
|
||||
# `target-cpu=x86-64-v3` = Haswell baseline (2013+). Every CPU that
|
||||
# QBZ realistically runs on supports it. If we later ship prebuilt
|
||||
# binaries for older CPUs we can switch to a runtime-dispatch path,
|
||||
# but for self-built and workstation-compiled binaries this is the
|
||||
# right default.
|
||||
[build]
|
||||
rustflags = ["-C", "target-cpu=x86-64-v3"]
|
||||
@@ -424,6 +424,17 @@ async fn fetch_all_segments(
|
||||
/// This is the common decryption logic shared between the full-download
|
||||
/// path (decrypt-then-return) and the offline playback path (decrypt-from-
|
||||
/// disk-then-feed-player).
|
||||
///
|
||||
/// Hot-path note: the previous implementation allocated a `Vec<u8>` per
|
||||
/// frame, copied the encrypted bytes into it, decrypted in place, then
|
||||
/// copied again into `output` via `extend_from_slice`. For a HiRes FLAC
|
||||
/// this is tens of thousands of small heap allocations + double copies
|
||||
/// per track. Now we extend `output` with the encrypted bytes directly
|
||||
/// and decrypt the just-appended slice in place — one copy instead of
|
||||
/// three, zero per-frame allocations. Combined with AES-NI codegen
|
||||
/// (enabled via `target-cpu=x86-64-v3` in `.cargo/config.toml`) this
|
||||
/// is the difference between a 20-second offline-cache gap on track
|
||||
/// transitions and a sub-second one.
|
||||
pub fn decrypt_segments_into(
|
||||
segments: &[Vec<u8>],
|
||||
content_key: &[u8; 16],
|
||||
@@ -441,11 +452,11 @@ pub fn decrypt_segments_into(
|
||||
if frame_end > seg_data.len() {
|
||||
return Err(format!("CMAF seg {} frame overflow", log_idx));
|
||||
}
|
||||
let mut frame = seg_data[data_pos..frame_end].to_vec();
|
||||
let output_start = output.len();
|
||||
output.extend_from_slice(&seg_data[data_pos..frame_end]);
|
||||
if entry.flags != 0 {
|
||||
qbz_cmaf::decrypt_frame(content_key, &entry.iv, &mut frame);
|
||||
qbz_cmaf::decrypt_frame(content_key, &entry.iv, &mut output[output_start..]);
|
||||
}
|
||||
output.extend_from_slice(&frame);
|
||||
data_pos = frame_end;
|
||||
}
|
||||
if data_pos < crypto.mdat_end && crypto.mdat_end <= seg_data.len() {
|
||||
|
||||
Reference in new issue
Block a user