Transcendentals: Domains and Fixups

Real call chain: gamma decoding in linear-srgb

The following complete chain is taken from zen/linear-srgb at 6bae33ee657d0cc29eaae6fd869894ec78a41b1c: gamma_to_linear_slice → incant! → gamma_to_linear_slice_tier, then pow_midp for full vectors and the scalar tail helper. Only the scalar helper's name/path is changed to make this excerpt standalone. This example requires the default w512 feature; avx512 enables its native V4 variant. The other tiers polyfill the same logical 16-lane shape.

use archmage::prelude::*;

fn gamma_to_linear_scalar(encoded: f32, gamma: f32) -> f32 {
    if encoded <= 0.0 {
        0.0
    } else if encoded >= 1.0 {
        1.0
    } else {
        encoded.powf(gamma)
    }
}

#[archmage::magetypes(define(f32x16), v4(cfg(avx512)), v3, neon, wasm128, scalar)]
fn gamma_to_linear_slice_tier(token: Token, values: &mut [f32], gamma: f32) {
    let (chunks, remainder) = values.as_chunks_mut::<16>();
    for chunk in chunks {
        let v = f32x16::from_array_t(token, *chunk);
        let clamped = v.max(f32x16::zero_t(token)).min(f32x16::splat_t(token, 1.0));
        *chunk = clamped.pow_midp(gamma).to_array();
    }
    for v in remainder {
        *v = gamma_to_linear_scalar(*v, gamma);
    }
}

pub fn gamma_to_linear_slice(values: &mut [f32], gamma: f32) {
    incant!(
        gamma_to_linear_slice_tier(values, gamma),
        [v4, v3, neon, wasm128, scalar]
    )
}


let mut values = [0.5; 19];
gamma_to_linear_slice(&mut values, 2.2);
for value in values { assert!((value - 0.5_f32.powf(2.2)).abs() < 1e-4); }

For finite normalized samples and positive gamma, this decodes a simple power transfer curve. It is not the piecewise sRGB transfer function. The vector body uses a polynomial approximation, while the tail uses scalar powf: do not promise bit identity between them. Test the consumer's error budget, especially around chunk boundaries. The code and all lengths through two chunks plus a tail are tested in magetypes/tests/doc_examples.rs.

Exact calls and runtime repair

CallAdditional work versus its unchecked polynomialDomain / behavior
v.log2_midp()Three compare/blend stagesRepair zero to negative infinity, negatives to NaN, positive infinity to infinity. This is not a universal guarantee of NaN payload or subnormal accuracy.
v.exp2_midp()Two min/max clamps, two comparisons, two blendsClamp the polynomial input, return zero below −126 and infinity at/above 128. Subnormal outputs are deliberately not constructed. NaN behavior inherits backend min/max differences.
v.ln_midp() / v.log10_midp()log2_midp repairs plus scalingSame logarithm domain restrictions.
v.exp_midp()Input scaling plus exp2_midp repairsSame clamping policy after scaling.
v.pow_midp(gamma)Composes repaired log/exp and scalingNot a full scalar powf replacement over all negative bases, exponents, NaNs and infinities.
v.sigmoid_midp() / v.silu_midp()Their documented exponential arithmetic and divisionUse division internally; do not infer raw reciprocal-estimate special cases.
_unchecked variantsOmit the corresponding domain repairMemory-safe methods with numerical preconditions; the name does not authorize raw memory access.

These counts describe the expression graph. Inlining, constant folding, width polyfills, and ISA lowering change actual instructions and CPU cost. Isolated checked-versus-unchecked timings for these transcendental methods are not yet recorded in the ISA explorer. Do not substitute the reciprocal repair percentages for transcendental overhead.

_lowp and _midp select approximation families, not one cross-ISA ULP guarantee for every input. FMA, range reduction, conversion behavior, and the input domain all matter. See precision levels and the ISA contracts.

Width and backend selection

The complete example above uses f32x16 and requires w512 (enabled by default). Native V4 additionally needs avx512; other listed tiers use polyfills. These are the supported listed tokens, not every token in the registry. The f32 and f64 method sets also differ; follow the selected type's API reference.

For f32x4 / f32x8 transcendental bodies, use V3, NEON, WASM, and scalar. Current V4-family narrow conversion bounds are not supplied merely by the hardware's superset features. Keeping V3 on AVX-512 hardware is valid; widening to sixteen lanes is an algorithm and performance decision, not automatic macro behavior.

Polyfills can split elementwise arithmetic efficiently, but reductions, shuffles, and live temporaries have different costs. There is no established universal 1.5–2× overhead or a guarantee that widening is free. Inspect assembly and measure representative inputs before changing the kernel's shape.

Found an error or it needs a clarification? Open an issue on GitHub.
Substantiated corrections will be incorporated with attribution.