Skip to main content

Scalar

Trait Scalar 

Source
pub unsafe trait Scalar:
    Copy
    + ScalarBackend<2, Aligned>
    + ScalarBackend<3, Aligned>
    + ScalarBackend<4, Aligned>
    + ScalarBackend<2, Unaligned>
    + ScalarBackend<3, Unaligned>
    + ScalarBackend<4, Unaligned> {
    type Repr: ScalarRepr;
}
Expand description

A trait for elements of math types.

Types that implement Scalar can be used as the T in math types like Vector.

All scalars must implement Copy.

In order to make SIMD optimizations possible, implementing Scalar comes with boilerplate. See the example below for a simple implementation.

§Safety

Scalar is only unsafe for SIMD implementations. Implementations where Repr = () (like the example below) are safe.

If Scalar::Repr is a signed integer:

  • Self and Self::Repr must have the same size.
  • Self must have no uninitialized bytes.

§Example

Implementing without SIMD optimizations:

use ggmath::{Alignment, Length, Scalar, ScalarBackend, SupportedLength};

#[derive(Clone, Copy)]
struct Foo(f32);

// SAFETY: `Scalar` is only unsafe for SIMD implementations. Implementations
// where `Repr = ()` are safe.
unsafe impl Scalar for Foo {
    type Repr = ();
}

impl<const N: usize, A: Alignment> ScalarBackend<N, A> for Foo
where
    Length<N>: SupportedLength,
{
}

§Making SIMD Optimizations

Scalar implementations can enable SIMD optimizations for their scalar’s math types.

This is done by:

Scalar::Repr is a marker type that controls SIMD alignment. It can be either:

  • (): There won’t be any SIMD alignment.
  • A signed integer: There will be SIMD alignment appropriate for that integer’s size, where the integer must have the size of the scalar type.

Let’s make a scalar named Foo which is a wrapper around f32:

use ggmath::{Alignment, Length, Scalar, ScalarBackend, SupportedLength};

#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq)]
struct Foo(f32);

// SAFETY: `Foo` and `i32` are both 4-bytes long, and `Foo` has no
// uninitialized bytes because its a simple wrapper around `f32`.
unsafe impl Scalar for Foo {
    type Repr = i32;
}

impl<const N: usize, A: Alignment> ScalarBackend<N, A> for Foo
where
    Length<N>: SupportedLength,
{
}

Because Scalar::Repr is i32, math types of Foo will have SIMD alignment appropriate for Foo’s size, 4-bytes.

Lets implement Add for Foo:

use std::ops::Add;

impl Add for Foo {
    type Output = Self;

    #[inline]
    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0 + rhs.0)
    }
}

Our implementation simply adds the internal f32 values and returns the result. Implementing Add for Foo automatically implements Add for Foo vectors:

use ggmath::{Vec2, vec2};

let result: Vec2<Foo> = vec2!(Foo(1.0), Foo(2.0)) + vec2!(Foo(3.0), Foo(4.0));

assert_eq!(result, vec2!(Foo(1.0 + 3.0), Foo(2.0 + 4.0)));

But currently the implementation for Foo vectors doesn’t use SIMD. To fix this, we can override the default implementation for ScalarBackend::vec_add which controls vector addition:

use ggmath::Vector;

impl<const N: usize, A: Alignment> ScalarBackend<N, A> for Foo
where
    Length<N>: SupportedLength,
{
    #[inline]
    fn vec_add(
        vec: Vector<N, Self, A>,
        rhs: Vector<N, Self, A>,
    ) -> Vector<N, Self, A> {
        // TODO: implement this function.
    }
}

To make a SIMD implementation we need to know the underlying representation of Foo vectors.

Its guaranteed that vectors of scalars with the same Repr have the same memory layout. Vector::to_repr can be used to reinterpret the bits of vectors to different scalar types where appropriate.

Lets implement extension methods for Foo and f32 vectors to convert between the two:

use ggmath::Vector;

trait ToF32 {
    type Output;

    fn to_f32(self) -> Self::Output;
}

trait ToFoo {
    type Output;

    fn to_foo(self) -> Self::Output;
}

impl<const N: usize, A: Alignment> ToF32 for Vector<N, Foo, A>
where
    Length<N>: SupportedLength,
{
    type Output = Vector<N, f32, A>;

    #[inline]
    fn to_f32(self) -> Self::Output {
        // SAFETY: `f32` accepts all bit-patterns.
        unsafe { self.to_repr() }
    }
}

impl<const N: usize, A: Alignment> ToFoo for Vector<N, f32, A>
where
    Length<N>: SupportedLength,
{
    type Output = Vector<N, Foo, A>;

    #[inline]
    fn to_foo(self) -> Self::Output {
        // SAFETY: `Foo` accepts all bit-patterns.
        unsafe { self.to_repr() }
    }
}

Vector::to_repr is unsafe because the input arguments must be valid for the output type. In our case both Foo and f32 accept all bit-patterns and so any conversion is safe.

Lets implement vec_add using these methods:

impl<const N: usize, A: Alignment> ScalarBackend<N, A> for Foo
where
    Length<N>: SupportedLength,
{
    #[inline]
    fn vec_add(
        vec: Vector<N, Self, A>,
        rhs: Vector<N, Self, A>,
    ) -> Vector<N, Self, A> {
        (vec.to_f32() + rhs.to_f32()).to_foo()
    }
}

Foo vector addition now uses the same implementation as f32 vector addition. Because of this, any SIMD optimization the f32 implementation has, the Foo implementation will have as well.

This pattern can then be expanded for all vector functions to make a fully optimized scalar.

SIMD optimizations can only be made using math types of primitives. Implementations that directly use intrinsics aren’t supported because the exact representation of math types isn’t stable.

Required Associated Types§

Source

type Repr: ScalarRepr

Controls the representation of math types.

Scalar::Repr is a marker type that controls what SIMD alignment math types have.

If Repr is (), math types won’t have any SIMD alignment.

If Repr is the signed integer with the size of Self, math types will have SIMD alignment appropriate for your scalar’s size.

For more information, see the documentation for Scalar.

§Safety

Scalar implementations where Repr = () are safe.

If Repr is a signed integer:

  • Self and Self::Repr must have the same size.
  • Self must have no uninitialized bytes.
§Stability

The Repr of primitive types is stable and is the appropriate signed integer for the primitive type’s size. The Repr of externally defined scalar types might be changed silently without breaking SemVer, unless their documentation explicitly guarantees a stable Repr.

Dyn Compatibility§

This trait is not dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.

Implementations on Foreign Types§

Source§

impl Scalar for bool

Source§

impl Scalar for f32

Source§

impl Scalar for f64

Source§

impl Scalar for i8

Source§

impl Scalar for i16

Source§

impl Scalar for i32

Source§

impl Scalar for i64

Source§

impl Scalar for i128

Source§

impl Scalar for isize

Source§

impl Scalar for u8

Source§

impl Scalar for u16

Source§

impl Scalar for u32

Source§

impl Scalar for u64

Source§

impl Scalar for u128

Source§

impl Scalar for usize

Implementors§