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:
SelfandSelf::Reprmust have the same size.Selfmust 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:
- Enabling SIMD alignment via
Scalar::Repr. - Overriding math function implementations via
ScalarBackend.
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§
Sourcetype Repr: ScalarRepr
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:
SelfandSelf::Reprmust have the same size.Selfmust 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.