6 Commits
Author SHA1 Message Date
vicrodh 395e537b59 fix(build): drop compile-time x86 target-features — runtime AES dispatch
Users on CPUs without AES-NI (Nehalem i7-860, pre-2010 Intel Macs) or
without SSSE3 (AMD Phenom II) crashed at launch with SIGILL. The
global `-C target-feature=+aes,+ssse3` was kept after the #353/#343
x86-64-v3 incident on the belief that the aes crate's cpufeatures
runtime check would protect CPUs without AES-NI — but a compile-time
target feature makes cpufeatures compile its check to a constant, so
the AES-NI intrinsics ran unconditionally (credentials AES-GCM decrypt
at startup), and +ssse3 let LLVM autovectorize anywhere, breaking
Phenom II. Measured on the qbz-cmaf hot path (Ctr128BE<Aes128>
apply_keystream, 256 MB, release): 2145 MB/s compiled-in vs 2162 MB/s
runtime-dispatched, identical output — the flags bought nothing on
capable CPUs. Removed from .cargo/config.toml, all dev/build scripts
and the release/build workflows; old CPUs now get the software AES
fallback instead of a crash.

Fixes #549

cc
2026-07-05 14:17:46 -06:00
vicrodh 3d42214775 docs(build): credit #354 and document future AVX2 pattern
Expand the .cargo/config.toml comment to reference both reports of
the SIGILL-at-launch bug: issue #353 (Sandy Bridge on Linux) and
discussion #343 (2012 MacBookPro9,1 / Ivy Bridge on macOS). The
macOS Intel case was diagnosed independently by @afonsojramos, who
opened PR #354 with the same fix landed earlier in b54574a7.

Also codify the forward-looking note afonsojramos suggested in that
PR: if a future hot loop needs AVX2/BMI2, the correct pattern is
per-function #[target_feature] with runtime dispatch (what the `aes`
crate already does via cpufeatures), not a global target-cpu bump
that silently breaks older hardware. This way the rationale is in
the file the next time someone is tempted to raise the baseline.

No code change, comment-only.

cc
2026-04-23 22:46:21 -06:00
vicrodh b54574a786 fix(build): drop x86-64-v3 baseline so pre-Haswell CPUs don't SIGILL
Issue #353 reports v1.2.8 dying with "Illegal instruction (core
dumped)" at launch on Sandy Bridge (Intel HD 2000, 2011). The crash
is not in CMAF decrypt — it's the process never reaches user code.
Root cause: .cargo/config.toml set target-cpu=x86-64-v3 which is
the Haswell (2013) microarch level. LLVM then freely emits AVX2,
BMI2, FMA, LZCNT and MOVBE across the whole binary, including the
Rust/Tauri startup path. Any pre-Haswell x86_64 CPU (Sandy Bridge,
Ivy Bridge, AMD Bulldozer/Piledriver) SIGILLs on the first of those
instructions, long before CMAF decrypt or the UI mounts.

The x86-64-v3 baseline was added together with +aes,+ssse3 thinking
it was required to activate AES-NI. It is not. AES-NI is an
orthogonal ISA extension; target-feature=+aes alone is what gets
the aes crate to compile in the AES-NI code path, and its
cpufeatures::new! runtime CPUID check then dispatches to hardware
or software per-process. So the 40x CMAF decrypt speedup (22s ->
<500ms on a 208 MB HiRes bundle) came from +aes, not from v3.

Drop target-cpu=x86-64-v3. Keep target-feature=+aes,+ssse3:
  - Modern CPUs with AES-NI still get the hardware decrypt path.
  - CPUs without AES-NI get the software fallback via cpufeatures
    runtime dispatch — slower but correct, no crash.
  - SSSE3 has been in every x86_64 CPU since 2006 (Core 2).
  - Pre-Haswell CPUs stop SIGILLing at launch.

What we lose: some AVX2/BMI2 auto-vectorization in general codegen
(stdlib, symphonia decode paths). CMAF decrypt — the actual
bottleneck we were optimizing — is unaffected.

cc
2026-04-23 08:43:42 -06:00
vicrodh 2a9f90e3e7 fix(build): scope x86_64 rustflags so aarch64 builds don't break
The .cargo/config.toml used [build] rustflags which applies globally
to every target. On aarch64-apple-darwin this injects
target-cpu=x86-64-v3 and +ssse3 as "ignored features", but the ring
crate's static CPU feature assertions detect the perturbed feature
set and fail to compile with:

    error[E0080]: evaluation panicked:
    assertion failed: CAPS_STATIC == MIN_STATIC_FEATURES
    at ring-0.17.14/src/cpu/arm/darwin.rs:51

Move the flags under [target.'cfg(target_arch = "x86_64")'] so only
x86_64 builds see them. ARM/Apple Silicon targets get their own AES
hardware path automatically via the aes crate's armv8 dispatch — no
explicit flags needed.

cc
2026-04-17 16:31:33 -06:00
vicrodh d5ef97b84f perf(cmaf): explicitly enable +aes target-feature for AES-NI dispatch
Previous commit tried to enable AES-NI via target-cpu=x86-64-v3 but
that microarchitecture level does NOT include AES-NI — it's a separate
ISA extension added on top. The aes crate kept falling back to software
decrypt (~10 MB/s), so the first live test still showed a 6s gap on
offline-cache gapless for a 58 MB track (~10 MB/s).

Fix: add target-feature=+aes,+ssse3 to rustflags. The aes crate's
runtime dispatch via cpufeatures only works if the AES-NI code path
was actually compiled in, which requires the build-time feature flag.

Expected: 58 MB decrypt drops from ~6s to ~150ms, fits inside the 5s
gapless pre-emptive window with headroom.

cc
2026-04-17 13:00:56 -06:00
vicrodh aae685e701 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
2026-04-17 12:45:42 -06:00