archmage 0.9.31-beta.2

Safely invoke your intrinsic power, using the tokens granted to you by the CPU. Cast primitive magics faster than any mage alive.
Documentation

archmage

Guide · Intrinsics browser · Archmage API · Magetypes API

Archmage lets you write SIMD code in Rust without unsafe — your crate can keep #![forbid(unsafe_code)] while calling intrinsics directly.

You pick a CPU tier, prove it's present once with summon(), and the type system keeps every intrinsic call sound. Rust already makes most intrinsics safe inside a function compiled for their CPU features; archmage supplies the proof that the CPU has them.

It runs on x86-64, AArch64 and WASM, is no_std + alloc, and needs Rust 1.89 or later.

0.9.31 beta: unified macros

This prerelease adds #[attune], attuned!, and reattune!. Existing attributes and incant! remain supported. Use matching beta versions of archmage and magetypes so their tokens come from the same archmage package.

use archmage::prelude::*;

// Generate direct tier functions and an ordinary callable dispatcher.
#[attune(_*, dispatch)]
fn scale(values: &mut [f32], factor: f32) {
    for value in values {
        *value *= factor;
    }
}

let mut values = [2.0, 4.0];
scale(&mut values, 0.5);
assert_eq!(values, [1.0, 2.0]);

#[attune(v3)] adds features to one function; #[attune(wrap)] creates a proof wrapper using its token parameter. Bare #[attune] infers a direct _v3 or proof-wrapper _v3_t suffix. A token parameter alone does not imply wrap. The beta guide covers the selector grammar, call and proof rules, inline policies, a legacy-attribute mapping, and current restrictions.

Quick start

[dependencies]
archmage = "=0.9.31-beta.2"
magetypes = "=0.9.31-beta.2"   # vector types such as f32x8

Multiply a buffer by a gain, using AVX2, NEON or WASM SIMD where available:

#![forbid(unsafe_code)]
use archmage::prelude::*;

#[magetypes(define(f32x8), v3, neon, wasm128, scalar)]
fn gain_impl(token: Token, plane: &mut [f32], gain: f32) {
    let factor = f32x8::splat_t(token, gain);
    let (chunks, tail) = f32x8::partition_slice_mut_t(token, plane);
    for chunk in chunks {
        (f32x8::load_t(token, chunk) * factor).store(chunk);
    }
    for value in tail {
        *value *= gain;
    }
}

pub fn apply_gain(plane: &mut [f32], gain: f32) {
    incant!(gain_impl(plane, gain), [v3, neon, wasm128, scalar])
}

let mut plane = [2.0; 11];
apply_gain(&mut plane, 0.5);
assert_eq!(plane, [1.0; 11]);
  • #[magetypes] compiles gain_impl once per tier in its list and names each copy for its tier: gain_impl_v3 (AVX2 and FMA), gain_impl_neon, gain_impl_wasm128 and gain_impl_scalar. In each copy, Token is that tier's token type and f32x8 is that tier's vector.
  • incant! finds the copies by those names. It calls summon() for each tier, best first, and runs the first copy the CPU supports. Call it around your loop, as here, not inside it.
  • Constructors ending in _t take the token as their first argument.

The kernel is adapted from zenfilters; Reusable generic kernels links the production source. If you are upgrading, 0.9.30 deprecates the older constructor names (splat, load, …): the migration guide has the mapping.

Calling intrinsics directly

For code tied to one instruction set, write a function per tier and let incant! choose:

use archmage::prelude::*;

#[arcane(import_intrinsics)]
fn multiply_v3(_token: X64V3Token, data: &[f32; 8]) -> [f32; 8] {
    let v = _mm256_loadu_ps(data);
    let mut out = [0.0; 8];
    _mm256_storeu_ps(&mut out, _mm256_mul_ps(v, _mm256_set1_ps(2.0)));
    out
}

fn multiply_scalar(_token: ScalarToken, data: &[f32; 8]) -> [f32; 8] {
    data.map(|v| v * 2.0)
}

pub fn multiply(data: &[f32; 8]) -> [f32; 8] {
    incant!(multiply(data), [v3, scalar])
}

assert_eq!(multiply(&[3.0; 8]), [6.0; 8]);
  • #[arcane] compiles the function with its token's CPU features and makes it safe to call from ordinary code.
  • import_intrinsics brings the architecture's intrinsics into scope. Loads and stores take references instead of raw pointers.
  • The names do the dispatch. incant!(multiply(data), [v3, scalar]) calls multiply_v3 where the CPU has the v3 features and multiply_scalar everywhere else.

Mark helper functions with #[rite] instead. They get the same features and inline into the kernel that calls them. The intrinsics browser shows which intrinsics each token unlocks.

Naming: <name>_<tier>

incant!(gain_impl(…), [v3, scalar]) calls gain_impl_v3(token, …) or gain_impl_scalar(token, …). That is the whole contract: the tier as a suffix, and that tier's token as the first argument. #[magetypes] and #[autoversion] generate functions of that shape.

Name your own tier functions the same way:

  • They join the same family. Leave v3 out of a #[magetypes] list, write gain_impl_v3 by hand, and incant! still finds it: hand-tuned variants.
  • Inside a tier function, incant! needs no CPU check. In a v3 function, incant!(helper(…), [v3, scalar]) compiles to a direct call to helper_v3: calls inside a tier.
  • The suffix shows, at every call site, which CPU features a function needs.

Which macro

You want Write Call it
One kernel with vector types, for every CPU #[magetypes(v3, neon, scalar)] on fn kernel(token: Token, …) incant!(kernel(…), [v3, neon, scalar])
A plain loop the compiler vectorizes for each tier #[autoversion] on fn sum(data: &[f32]) -> f32 sum(data). The macro writes the dispatcher
The intrinsics of one instruction set #[arcane] on fn kernel_v3(token: X64V3Token, …) incant!(kernel(…), [v3, scalar])
A helper inside SIMD code #[rite(v3)] on fn helper(…) helper(…), from a function that has the v3 features
An algorithm shared between kernels A generic helper: #[inline(always)] fn helper<T: F32x8Backend>(token: T, …) helper(token, …), from a #[magetypes] kernel
The tier chosen by hand X64V3Token::summon() and an if let kernel_v3(token, …), behind #[cfg(target_arch = "x86_64")]: manual dispatch

The guide also covers tokens and tiers, testing every tier and AVX-512.

Features

Feature Crate Default Effect
std both on Runtime CPU detection. Without it, summon() sees only the features enabled at compile time.
avx512 both off archmage: AVX-512 intrinsics through import_intrinsics. magetypes: native AVX-512 vectors.
w512 magetypes on The 512-bit vector types. They run as narrower vectors where native AVX-512 is not in use.

AVX-512 is opt-in from your own crate. Give it an avx512 feature:

[features]
avx512 = ["archmage/avx512", "magetypes/avx512"]

Then write the kernel with a 512-bit type and name the tier in both lists. Its copy is compiled only when that feature is on:

#[magetypes(define(f32x16), v4(cfg(avx512)), v3, neon, wasm128, scalar)]
incant!(gain_impl(plane, gain), [v4(cfg(avx512)), v3, neon, wasm128, scalar])

The other tiers run the same f32x16 as a polyfill: two 256-bit operations on AVX2, four 128-bit operations on NEON and WASM. In the two kernels we inspected, each polyfilled operation compiled to those native operations and nothing else (assembly results). The magetypes README has the runnable example and what polyfills cost.

Listing v4 on an f32x8 kernel does not widen it: f32x8 stays eight lanes there.

Installation has the no_std setup and the remaining features.

Safety

You write no unsafe, so your crate can keep #![forbid(unsafe_code)].

Rust does most of the work. It checks every call into a #[target_feature] function, and since 1.87 it lets safe code call most intrinsics inside one. Archmage adds the proof that the CPU has the features: a token such as X64V3Token. Safe code gets one only once the features are confirmed, normally by summon(). The macros generate the one unsafe call into feature-enabled code, and the token is what makes it sound.

magetypes builds on the same tokens and adds its own compile-time checks. What is left for unsafe there is a few one-line blocks that load, store, gather and scatter vector storage.

The safety model has the details, and SOUNDNESS.md lists every unsafe in both crates.

Limits

  • There is no SVE tier: stable Rust has no SVE intrinsics yet. Targets other than x86-64, AArch64 and WASM, including 32-bit x86, run the scalar fallback.
  • Calling an #[arcane] function once per loop iteration is slow. On two small kernels it measured 4.1× and 6.2× slower than one call around the whole loop (docs/PERFORMANCE.md). Dispatch once, outside the loop.
  • A vector wider than the CPU's registers is a polyfill. It runs as two or four native operations, and each value takes that many registers: an f32x8 on NEON is two f32x4s. Polyfills has the measured costs.
  • With the avx512 feature, AVX-512 tokens also support the 128- and 256-bit float and integer types. These narrower backend, conversion and bitcast implementations delegate to V3; 512-bit types use AVX-512.
  • Some floating-point results differ between backends. mul_add_relaxed rounds once where the hardware fuses (x86 v3/v4, NEON) and twice on the scalar backend and strict WASM; mul_add_fused rounds once everywhere, in software where needed. mul_add is deprecated and becomes the fused form in 0.11. ISA quirks and fixups lists every difference, and Transcendentals gives each function's domain and precision.

License

MIT OR Apache-2.0

Image tech I maintain

Codecs ¹ zenjpeg · zenpng · zenwebp · zengif · zenavif · zenjxl · zenbitmaps · heic · zentiff · zenpdf · zensvg · zenjp2 · zenraw · ultrahdr
Codec internals zenjxl-decoder · jxl-encoder · zenrav1e · rav1d-safe · zenavif-parse · zenavif-serialize
Compression zenflate · zenzop · zenzstd
Processing zenresize · zenquant · zenblend · zenfilters · zensally · zentone
Pixels & color zenpixels · zenpixels-convert · linear-srgb · garb
Pipeline & framework zenpipe · zencodec · zencodecs · zenlayout · zennode · zenwasm · zentract
Metrics zensim · fast-ssim2 · butteraugli · zenmetrics · resamplescope-rs
Pickers & ML zenanalyze · zenpredict · zenpicker
Products Imageflow image engine (.NET · Node · Go) · Imageflow Server · ImageResizer (C#)

¹ pure-Rust, #![forbid(unsafe_code)] codecs, as of 2026

General Rust awesomeness

zenbench · archmage · magetypes · enough · whereat · cargo-copter

Open source · @imazen · @lilith · lib.rs/~lilith