[Pre-RFC] Native support for linear algebra primitives: vectors, matrices, ...
Looking for comments here to not open RFC for something that could be instantly rejected / can be easily improved. I am somewhat new to Rust - sorry if i missed some features this conflicts with. Closest existing RFCs/rustc PRs to this i found were all about SIMD
Summary
Add built-in vector, matrix and quaternion types with swizzles (.xyzw/.rgba), indexing, math overloads and SIMD backend.
Motivation
Fragmented ecosystem (nalgebra, glam, vek, cgmath, dozen others and manual implementations (to not add dependencies)) cause duplication, conversion overhead (cognitive and performance), extra deps, inconsistent and inconvenient syntax. Vectors match SIMD. Builtins would unify, optimize, cut compile times for graphics (realtime, offline for 3d software, image processing, game engines)/physics (both 2d and 3d)/ML.
This would make Rust a step closer to being adopted by gamedev industry, including its usage as GPU shading language.
Value of syntax sugar for mathematics is high*, some features of e.g. C++ (templates/overloads) are often the only used features and used for math (e.g. GLM).
* Mathematicians/physicists invent new symbols just to shorten expressions all the time, we use one-letter notations to type less, etc.
Guide-level explanation
New types; aliases f32vec4=vec4, vec4=f32vec4.
let v: vec4<f32> = vec4(1 as isize, 2.0, 3.0_f64, 4u32); // implicit casts
let swizzled: vec4 = v.xyzw; // .xyzw swizzled in same order as original elements
let mut sw2: f32vec3 = v.xyx; // .xyx swizzled first three elements
let v2: f32vec4 = vec4(v); // v2 is constructed from entire v
let v3: f32vec4 = vec4(v.xx, v.yy); // v3 is constructed from two copies of first and two copies second elements of v
let v4: f32vec4 = vec4(v.zzz, 42); // v4 is constructed from three copies of third elements of v and 42_f32
sw2.xz += 100.0; // add 100.0 to first and third elements
let color = v.rgba; // alternative syntax for swizzles, corresponding to .xyzw. Represents color
let elem = v[1]; // access second element with indexing syntax
let sum = v + vec4(5.0, 6, 7, 8.0); // element-wide math
let dot = v.dot(sum); // method `dot` represents mathematical dot product
let m: mat4x4<f32> = mat4x4::identity();
let col = m[2];
let val = m[2][1];
let mul = m * v; // multiplies vector by matrix, e.g. 3d geometry transformations in rasterization
let q: quat<f32> = quat(0.0,0.0,0.0,1.0);
let rotated = q * v; // rotation by quaternion
let casted: vec4<i32> = v as i32vec4; // same as per-element `as` cast
Reference-level explanation
N=2-4; T=f32/f64/i8-i64/u8-u64
vecN<T> (aliases to TvecN); matMxN<T> (major?, aliases to TmatMxN); quat<T> (aliases to Tquat)
- writable/readable swizzles; indexing; element-wise ops; matrix/quat mul and other overloads
- casts: 'as' same-size; From/Into arrays
- backend: std::simd
Drawbacks
Extra complexity, syntactical exceptions, "magic", compiler speed/size, some work needs to be done to existing vector libraries.
Rationale and alternatives
Built-ins enable syntax that libs can't have. Fixed 2-4D covers 99% of usecases. In many projects linear linal is primary reason for operator overloads usage (many C-like C++ projects with GLM). Pretty much every Rust game/graphics/physics lib depends on one or more linal libs, and when combining those you have to "glue" them together. Adding this feature would make gamedev a lot more appealing in Rust, make graphics programming less painful, help projects like Rust-GPU (and integrate with things like std::offload).
This proposal looking unexpected is understandable. But consider something you are very familiar with - floats. Floats existed in before IEEE 754 standard, and pretty much every language has them. As well as integer mathematics. Even if operations on those do not end up as trivial assembly (e.g. integer division in Rust produces a branch with zero-division check). Yet everyone understands and expects "builtin sugary" syntax for both floats and integers.
Imagine the ecosystem if there were no builtin floats. "Yes, we have instructions that map directly to float operations, but we also have vector instructions - so what? There is million ways one could implement floating point numbers - have or not have checks, precision, comparisons, pretend as if it associative or not, NaN policy, zero policy, etc. Someone could want 128 integer fixed point floats, someone could want packed 24-bit floats in range from 0 to 1. Something as complex and variable as floats could and should be implemented as a library"
Could this be done in a library or macro instead? Yes, this is mostly syntactical change. I do think, however, this syntactical sugar is worth way more than added complexity. Alternatively, something could be done to:
- allow creation of
ascasts / implicit casts - allow writeable swizzles to exist (e.g. struct that contains compile-time known zero-size mutable reference to memory sections of its owner)
- allow some sort of overloads (or deal with macro expansions)
Currently, if someone wants nice linal syntax, they can:
- use some other language (complicated build, potentially glue code). That is what most people currently do with GLSL, HLSL, WGSL, etc.
- create a preprocessing system for desugaring, that would run before compilation (complicated build, error-prone, slow, no rust-analyzer integration)
- pay compile & runtime fee (for half the syntax) but keep everything in Rust
Prior art
| Language | Swizzles syntax sugar | Constructors | Elementwise operations | Matrix * Vector Mul | Vector casts |
|---|---|---|---|---|---|
| Odin | Yes, with writes | explicit elements | Yes | Yes (m * v) | explicit per-element |
| GLSL | Yes, with writes | overloaded fill, implicit casts, from others | Yes | Yes (m * v) | explicit constructors, downcasts |
| WGSL | Yes, reads + single-component writes | overloaded fill, from others | Yes | Yes (m * v) | explicit constructors, down/up casts |
| HLSL | Yes, with writes | implicit casts, from others | Yes | Yes (mul(m, v)) | implicit/explicit, downcasts |
| Metal | Yes, with writes | overloaded fill, implicit casts, from others | Yes | Yes (m * v) | explicit |
| C (GCC/Clang extensions) | No | expli cit elements | Yes | No (manual) | implicit/explicit same-size |
| Zig | No | explicit elements | Yes | No (manual) | explicit |
Odin (which many people know as gamedev-oriented language). Shading languages have obvious success (no one really writes their shaders in C, even when programming a language that supports SPIR-V via LLVM).
It is hard to compare languages in that regard because of unique differences and the fact we are comparing syntax.
Unresolved questions
Dims limit; quats/complex; alignment (possibly just std::simd?); (row/column) major; at which level is this implemented.
Future possibilities
Arbitrary-sized integers/floats in vectors and integer vector sizes.