Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Short Vector Primitives #7295

Open
PetorSFZ opened this issue Dec 3, 2020 · 21 comments
Open

Proposal: Short Vector Primitives #7295

PetorSFZ opened this issue Dec 3, 2020 · 21 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@PetorSFZ
Copy link

PetorSFZ commented Dec 3, 2020

Before I get started, I would like to thank the people behind Zig for their awesome work. Zig is the first language in a long time I can even potentially see replacing C/C++ for me, which is exciting. :) There are still some issues and maturing left to do, but I have faith these will be fixed with time. Anyway, I figured I should create a proposal that would fix my primary showstopper. I apologize if this has already been proposed before and this is just spam.

Proposal: Short Vector Primitives

In game development and computer graphics we use a lot of linear algebra to do all sorts of stuff. The most important thing from linear algebra we use are vectors, specifically short vectors (2-4 elements). It's no exaggeration to say that we use them A LOT, they are incredibly versatile and useful.

Let's say you have a size of an image, store it in a 2D vector. A position in 2D or 3D? Store it in a 2D or 3D vector respectively. A velocity for something? 2D or 3D vector. An axis-aligned box? That's just two 3D (or 2D) vectors of course. A coordinate for the mouse position on the screen? 2D vector. An RGB color? A 3D vector. I could go on.

We also use vectors a lot when defining low-level data-structures with explicit memory layouts (e.g. for sharing between CPU and GPU), an example of a simple vertex in C++:

struct Vertex {
	float3 position;
	float3 normal;
	float2 texcoord;
};
static_assert(sizeof(Vertex) == sizeof(float) * 8, "Accidental padding");

// Note that Vertex has the exact same memory layout as float[8], and we reinterpret cast between them liberally.

In the most common game development languages (C++ and C#) we can simply create our own vector primitives which behave just as built-in primitives thanks to operator overloading. In shading languages such as HLSL and GLSL there is no operator overloading, so instead there are native built-in vector primitives.

This is just speculation, but I would wager 99% of all requests for operator overloading from the game and graphics communities is purely to implement vector primitives (and other linear algebra constructs such as matrices and quaternions while at it). Implementing native vector primitives would fix this need and make the language substantially more valuable to us while not opening the floodgates with general purpose operator overloading.

It's also worth mentioning that having native vector primitives would make Zig a substantially better candidate as a shading language in the future. As far as I know there is currently no shading language without vector primitives (or capability of implementing them yourself), I very much doubt anyone would bother using a shading language without them. Being able to share code between the CPU and GPU similar to what CUDA is doing would be extremely powerful and a very good selling feature for Zig, as we would no longer need separate external compilers that compile our shading code to binary blobs which we need to bundle along with our app.

Goals and non-goals

Goals

  • New primitive types in addition to existing ones (i32, f32, u8, etc)
  • New operators (+, -, *, /, ==, !=, etc) for these primitive types
  • Some built-in compiler functions (e.g. @dot())
  • Some vector primitive specific functionality, e.g. swizzling.
  • Be as "uncontroversial" as possible, avoid things which differ among common vector libraries

Non-goals

  • Quaternions and Matrices (and other Linear Algebra constructs than vectors)
  • Long vectors (more than 4 elements)
  • SIMD
  • General purpose operator overloading, keep it simple

Quaternions and matrices are not relevant for this proposal because they don't have an obvious implementation (unlike vectors). They differ quite a bit between different libraries, both in terms of memory layout, syntax and functionality. Making a choice here could easily make someone who needs something else quite mad. The good thing is that it's not at all as limiting to not have overloaded operators for matrices and quaternions as it is for vectors, so this is fine. Let someone else make a 3rd party library for these.

Vectors longer than 4 elements are not considered because it's no longer as obvious how they should work. I.e., for a 4D vector we can refer to individual elements using .x, .y, .z and .w. But there is no obvious letter for e.g. the 5th coordinate.

Many linear algebra vector libraries have SIMD implementations for some vector primitives, but this is mostly bikeshedding to be honest. I have seen examples of compilers generating the same (and at times even better) code when vector primitives are not explicitly implemented using SIMD. And if you really need highly optimized SIMD code you should write it explicitly yourself and not trust in the primitives anyway.

There's also the problem where forcing SIMD characteristics upon vector primitives can make them work weirdly in other contexts. E.g. a 4D float vector would need to have 16-byte alignment for SIMD, but that means it's easier to introduce accidental padding when placing them in structs (e.g. for memory shared between CPU and GPU). We also use 3D vectors more than 4D vectors, should they be forced to have an element of padding so that they can be 16-byte aligned? But that's terrible if we have an array of them which will then take up 25% more memory.

Overall the reason we want vector primitives is because it makes our code substantially easier to read and write, not specifically because of performance.

What

The new primitive types

In GLSL vectors are named vecN for N-dimensional f32 vectors, and ivecN and uvecN for i32 and u32 vectors respectively. Examples vec3, ivec2, etc.

In HLSL (and CUDA) vectors are named typeN where type is the normal type and N the dimension. Examples float3, int2, char4, etc.

In general I prefer the HLSL approach because it doesn't actually imply that the contents is specifically a vector (which tend to make mathematicians sad at times). It's just multiple elements of a given primitive and it's up to the user to specify meaning.

My proposed naming for the vector primitives is: tAAxN where t is i,u or f for signed, unsigned or floating point respectively, AA is the number of bits for each element (fine if only 8, 16, 32 and 64 is allowed) and N is the number of elements (2, 3 or 4). Some examples: f32x3, f16x4, u8x4, i32x3, etc.

Construction

I don't know if this syntax is the one most suited for Zig, but something very similar to this should be available:

var a: i32x3 = i32x3(1, 2, 3);
var b: f32x2 = f32x2(1.0f); // Compiler-error, not all elements specified

In many vector libraries (such as GLSL) if you only specify one element in the constructor, e.g. vec3(1.0f);, then the value is assigned to every element in the vector. This is somewhat counter-intuitive (and not standardized among all vector libraries), so it should be avoided. There should however be some compact equivalent to do the same thing. My suggestion:

var c: i32x4 = i32x4.fill(42); // Results in the vector [42, 42, 42, 42]

We also need to be able to convert between vectors with different types. I.e, we might have values in an f32x4 which we need to convert to u8x4. Or any other types of casts. I suggest extending existing cast infrastructure to also work with vector primitives (element-wise).

Swizzling and component-wise access

Swizzling should work basically the same as it does in GLSL or HLSL. Some (exaggerated) examples:

fn foo(a: f32x2) f32x3 {
	return a.xyy;
}

fn bar(a: f32x4, b: f32x2) : f32x3 {
	return a.wyx + b.xxy;
}

Operators (vector, vector)

In general, all operators work exactly the same as they would for a normal primitive, except applied to all elements. In other words, 100% element-wise. Below is a snippet with a "fake implementation" of addition between two vectors:

fn add(a: i32x4, b: i32x4) i32x4 {
    var c: i32x4 = undefined;
    c.x = a.x + b.x;
    c.y = a.y + b.y;
    c.z = a.z + b.z;
    c.w = a.w + b.w;
    return c;
}

Operators +, -, * and / are defined as above. The last two are potentially a bit controversial outside of game and graphics fields, but it's basically what makes most sense and what is most consistent when you are actually using these things in practice. If there is a big opposition to implementing * and / as element-wise operations they can be skipped entirely, but it would be a shame because it does reduce the value a bit.

Operators == and != return a single bool if all elements in the vectors are the same or not.

These operators only work when both sides of the operator have the exact same vector type, i.e. no mixing and matching.

Operators (vector, scalar)

In addition to the vector-vector operators it's also possible to multiply/divide (* and /) vectors with scalars of the same type. Example implementation:

fn mult(a: f32, b: f32x2): f32x2 {
    var c: f32x2 = undefined;
    c.x = a * b.x;
    c.y = a * b.y;
    return c;
}

Specifically, for operator * it's possible to: scalar * vector and vector * scalar. But for operator / it's only possible to do vector / scalar. Some vector libraries implement scalar / vector and implicitly expand the scalar to a vector, but this is not at all obvious and should probably be avoided.

Built-in functions

There are a number of standard functions for vectors that should be implemented. As far as I have understood it Zig does not support function overloading, which means that these need to be implemented using the special @func() syntax.

@dot()

dot(vector, vector) is probably the most common and important function. Dot product. In math syntax this is often written using the dot operator, this is however very confusing in code so game/graphics engineers tend to prefer to have it defined as a function instead. Example syntax:

var a: f32x3 = /* ... */
var b: f32x3 = /* ... */
var c: f32 = @dot(a, b);

// Example implementation for 4D vectors (need function overloading here)
fn dot(a: v4, b: v4) {
	return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
}

@cross()

cross(vector3, vector3) is another very common function. Cross product. Unlike the dot product it is defined only for 3D vectors, and the result is another 3D vector. Unfortunately the result of the cross product depends on if you are using a right-handed or left-handed coordinate system, so this could potentially cause a bit of friction. However, I think it should be safe to define this to use a right-handed coordinate system and maybe also provide a @crossLH() for left-handed cross product.

@length()

Returns the euclidean length of a vector. I.e.:

fn length(a: f32x4) f32 {
	return @sqrt(a.x * a.x + a.y * a.y + a.z * a.z + a.w * a.w);
}

Can potentially be called norm() or norm2(), but length() is probably the most common naming.

@normalize()

Normalizes a vector so that it's length is 1. In most vector libraries you will get back a vector filled with NaNs if you enter a zero-vector, this should probably be caught by Debug builds in Zig.

Example implementation:

fn normalize (a: f32x4) f32x4 {
    const len: f32 = @length(a);
    // Trap if len == 0 in debug builds only
    return a / len;
}

@min() and @max()

It's very common to need to clamp the values in vectors for various reasons. What's interesting here is that we often only want to specify a scalar as the bound, example:

const unclamped: f32x3 = /* ... */
const clamped: f32x3 = @max(0, @min(unclamped, 1.0));

Conclusion

This got a lot bigger and longer than I was intending, but I think I caught most important aspects. There are of course many other things one might want for vectors (a bunch more built-in functions for one). But I think the above proposal covers the most important parts so that the rest of the linear algebra we need can be implemented on top as a 3rd party library.

Thanks for taking the time reading this! :)

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Dec 3, 2020
@andrewrk andrewrk added this to the 0.8.0 milestone Dec 3, 2020
@andrewrk
Copy link
Member

andrewrk commented Dec 3, 2020

Related: the accepted proposal to add a SPIR-V target backend for zig: #2683

@ikskuh
Copy link
Contributor

ikskuh commented Dec 4, 2020

Take my upvote. This is a pretty solid extension which would make gamedev and other linalg topics in zig a lot easier.

This is just speculation, but I would wager 99% of all requests for operator overloading from the game and graphics communities is purely to implement vector primitives (and other linear algebra constructs such as matrices and quaternions while at it).

I agree, this would be my only use case if we had operator overloading

var a: i32x3 = i32x3(1, 2, 3);

This would contradict the "only one way to do things" zen as types are always constructed with T{ }. So a minor change request:

Vector creation is just initialized like any other type in zig:

var a: i32x3 = i32x3 { 1, 2, 3 };

@auwi-nordic
Copy link

I think it's a good proposal. I'd be a bit worried that the implementation would be too "arbitrary". I like how Zig decided to implement "any" bit width for uint/int instead of whatever common CPUs/languages have chosen. If you extend this idea to vector primitives, you could argue for implementing Geometric Algebra for up to 3 dimensions

https://bivector.net

As far as I've seen it's the best generalisation of vectors, complex numbers etc., and we'd have to make fewer "arbitrary" decisions about what to implement.

But perhaps doing something like that would be too ambitious for a language like Zig. I mean, there's already similar decisions made on which kind of scalars to support (int, floating point, but no fixed point). And it'd have to be a different proposal. So just food for thought.

@Sobeston
Copy link
Contributor

Sobeston commented Dec 4, 2020

While I agree that zig is in need of something to accommodate for writing linear algebra code, I don't think this is it. While this allows for the most common linear algebra code to be written more concisely, I think it is too narrow, too vague (why have right handed cross product by default, why are there no matrices?), and not general purpose enough to excuse the added wart or complexity to the language.

Ultimately I feel we should experiment with something that strikes a good balance between having and not having operator overloading.

@PetorSFZ
Copy link
Author

PetorSFZ commented Dec 5, 2020

I feel like I might have been a bit unclear, so to clarify: This is not a proposal to add linear algebra support to Zig. This is a proposal to add new primitives, which in some aspects behave like vectors.

You don't even have to think about them as lin alg vectors, they are useful anyway. There are tons of peoples in games which use vector primitives without knowing any linear algebra. They also expose low-level features which are not exposed by the language otherwise (such as shuffle through swizzling).

Even if some sort of more general linear algebra support is added later on, this proposal would still be useful to us. Not having a standardized/built-in vector primitive has hurt interoperability between different libraries in C++ for quite a while. And swizzling is probably not something the user of a language should be able to implement themselves.

(why have right handed cross product by default, why are there no matrices?)

Regarding cross product I sort of agree. It might be a good idea to not have @cross() and instead do @crossRH() and @crossLH(). Or perhaps none of them. Doesn't matter all that much, cross is easily implemented in 3rd party code.

Regarding matrices I feel like I touched upon that in the proposal, but I can clarify a bit more. The problem here is that there are many ways of doing matrices, some C++ examples:

struct Matrix4x4_RowMajor {
    float4 row0;
    float4 row1;
    float4 row2;
    float4 row3;
};

struct Matrix4x4_ColumnMajor {
    float4 column0;
    float4 column1;
    float4 column2;
    float4 column3;
};

struct Matrix4x4_Compact {
    float4 row0;
    float4 row1;
    float4 row2;
    // row3 is omitted because it is always [0, 0, 0, 1]
};

And the above doesn't even describe how the contents should be interpreted, i.e. the whole row-vector vs column-vector thing. In addition, how would you define operators such as matrix * matrix? Both matrix multiplication and element-wise multiplication are common for that operator.

Thing is, none of the above approaches are "more correct" than the others. If Zig where to choose one as the "default one" it would be a pain for people who prefer something else. As a general purpose low-level programming language I don't think Zig should impose on the user that way. With vector primitives we don't really have that same problem, because there aren't that many options.

@SpexGuy
Copy link
Contributor

SpexGuy commented Dec 5, 2020

This is a really well written proposal, thanks for putting the effort into it! After reading through the spir-v spec, I think this might be a decent candidate to be in the language. Specifically, the operators and swizzling correspond to spir-v instructions that would be difficult for the compiler to identify otherwise.

I think the builtins should probably be in the standard library instead of in the language. Even spir-v doesn't encode these operations as opcodes, and it's not clear to me that the optimizer benefits from knowing that an operation is a dot/cross/length operation.

There is an existing proposal that covers matrices separately (#4960), so I agree that this one should just focus on vectors. However if both are accepted we should make them consistent with each other. Either both should be limited to max 4 items on any axis or both should allow an arbitrary comptime-known length.

Edit: I was wrong, spir-v does indeed have OpDot and OpOuterProduct. So maybe these should be builtins.

@ghost
Copy link

ghost commented Dec 5, 2020

Other than a handful of builtins, what does this give us that our existing SIMD support doesn't have?

@PetorSFZ
Copy link
Author

PetorSFZ commented Dec 5, 2020

I will admit that I'm a complete Zig noob, any weirdness might be because I don't fully understand all parts of the language yet.

I think the builtins should probably be in the standard library instead of in the language. Even spir-v doesn't encode these operations as opcodes, and it's not clear to me that the optimizer benefits from knowing that an operation is a dot/cross/length operation.

(I saw your edit, but I would like to add to this) There's also more optimizations to normalize() that are sort of hardware dependent. An ideal implementation would be:

fn normalize(v) v {
	return rsqrt(dot(v, v)) * v;
}

Where rsqrt() is the reciprocal square root, i.e. 1/sqrt(x) which is available on some CPUs and probably all GPUs. But the above solution is probably not wanted in a debug build because it might be harder to trap errors if v == 0.

Beyond that it's also a question about syntax. From the proposal:

As far as I have understood it Zig does not support function overloading, which means that these need to be implemented using the special @func() syntax.

But I just did a quick search through the zig documentation and realized I had completely missed anytype, if we could just write dot(a, b) instead of @dot(a, b) then of course that would be better. My bad. :)

Really, it's all about avoiding the stupid syntax I (and many others) had to deal with when we started out in Java many years ago. Examples of good syntax for the dot() function:

  • dot(a, b)

Examples of bad/terrible syntax for the dot() function:

  • dot(u32x2, a, b)
  • i16x3.dot(a, b)
  • Math.dot(a, b)
  • a.dot(b)

Other than a handful of builtins, what does this give us that our existing SIMD support doesn't have?

I'm going to admit I don't know much about how Zig's SIMD implementation works, and the docs is just a big TODO. My answer assumes it works similarly to __m128 or __m128i.

This proposal adds many new primitives:

// Unsigned
u8x2, u8x3, u8x4, u16x2, u16x3, u16x4, u32x2, u32x3, u32x4, u64x2, u64x3, u64x4

// Signed
i8x2, i8x3, i8x4, i16x2, i16x3, i16x4, i32x2, i32x3, i32x4, i64x2, i64x3, i64x4

// Floating point
f16x2, f16x3, f16x4, f32x2, f32x3, f32x4, f64x2, f64x3, f64x4

// The most common ones to use (imo)
u8x4, u32x2, u32x3, u32x4, i32x2, i32x3, i32x4, f32x2, f32x3, f32x4

Out of all these new primitives, only very few actually makes sense for SIMD at all, i.e. u32x4, i32x4 and f32x4. And even then, these ones with potential SIMD capabilities probably shouldn't be 16-byte aligned because that would mess up structs with padding and such. (Or maybe that's not a thing? I just assumed Zig used C's struct layout so you could share memory between CPU/GPU easily...)

Overall, I would expect that @alignof(i32x4) == @alignof(i32) and @sizeof(f32xN) == @sizeof(f32) * N for all vector primitive variants.

@ghost
Copy link

ghost commented Dec 5, 2020

That's pretty much how existing Zig SIMD works, except with flexible rather than hardcoded length. Alignment may not work exactly like that, but that can be fixed if it's a problem -- we are pre-1.0, after all. (Also, the compiler is free to represent structs however it likes: reordering fields, inserting padding etc. You can force it to use native ABI struct layout with extern.)

The problem I have with this proposal is that it doesn't enable anything that we can't already do, and hardcodes many details that are not universal. Much like we have arbitrary-width integer types, which we trust the compiler to represent and operate on in the most optimal format, we can do the same with the existing vector primitives.

@PetorSFZ
Copy link
Author

PetorSFZ commented Dec 5, 2020

It sounds like there might be a good opportunity to merge the two then. Take the good parts from this proposal and the good parts from the SIMD one. Though I would humbly suggest not calling it SIMD if it's not guaranteed hardware SIMD. ;)

Anyway, now I'm way out of my league and this discussion is probably better taken over by people who actually work on Zig 😅 Thanks for taking the time with my request. I'm of course still available if there are any questions about this proposal itself or anything related. :)

@lonjil
Copy link

lonjil commented Dec 5, 2020

SIMD types are very different from what is proposed here. The purpose of SIMD types is to tell the compiler to use SIMD registers and instructions, and for most use cases of the proposed short vectors, that would actually be quite slow, which is also why the current vector type is recommended against unless you're specifically trying to write code to take advantage of SIMD without relying on auto-vectorisation.

@Sizik
Copy link

Sizik commented Dec 7, 2020

We already have [3]u32, [4]f32, and the like, why not have well-defined semantics for doing element-wise math operations on scalars/arrays of compatible types (i.e. same length and element type)? Like how Zig doesn't have an explicit string type but instead uses []u8.

@zigazeljko
Copy link
Contributor

@Sizik How would element access work in this case? Would you be able to use .x, .y, .z, .w?

@Sizik
Copy link

Sizik commented Dec 8, 2020

@zigazeljko Element access would be just through vec[1] instead of vec.x in this case, which is a downside compared to a dedicated struct type. Perhaps some way to assign names to array slots would be nice, like how in C you can use a union to seamlessly convert between treating data as a float vec[3]; and float x; float y; float z;. Zig unions might not be the right thing to use for this, but a new way to declare struct fields might. Here's an example of how it could work (although this is starting to veer a bit into #7311):

const Vec3 = struct {
    elements: [3]f32,
    alias x = elements[0],
    alias y = elements[1],
    alias z = elements[2],
}

@zigazeljko
Copy link
Contributor

Perhaps some way to assign names to array slots would be nice.

This could be solved with something like #793:

const Axis = enum { x, y, z };
const Vec3 = [Axis]f32;

const foo = Vec3{ 3.0, 4.0, 5.0 };
foo[.x] == 3.0;

Additionally, we can add foo.x as syntactic sugar for foo[.x].

@zzzoom
Copy link

zzzoom commented Dec 16, 2020

Instead of defining new vector types and extending common operators for those types, you should probably look at Julia's broadcast and dot operators which solve the general problem. Then you could write something like:

c = a + b;     // scalar math
vc = va .+ vb; // vector math
vc = va .+ b;  // scalar b gets broadcasted

@Cons-Cat
Copy link

Cons-Cat commented Sep 15, 2021

It seems that scientific programmers get shafted here without higher-dimensional vectors (f64x5, f64x12, etc.)

Supposedly, scientific programmers somehow find reasons to use higher dimensions, and I assume Zig would look unattractive compared to the myriad of other languages that are both expressive enough to provide those abstractions and similarly performant to Zig. It's far from my place to guess whether this is a problem or not, I just wanted to point that out since it hasn't been mentioned here yet.

And the above doesn't even describe how the contents should be interpreted, i.e. the whole row-vector vs column-vector thing. In addition, how would you define operators such as matrix * matrix? Both matrix multiplication and element-wise multiplication are common for that operator.

As I'm sure you know, GLSL and GLM assume that matrix multiplication is the more important use-case for *, and HLSL has a matrix multiplication method that is simply .mul(), which doesn't seem any more descriptive to me than the * operator to me. DirectXMath has an XMMatrixMultiply(a, b) function to do this which also not more descriptive than *. The Unity game engine also just uses * for its C# matrices like GLSL does. I am certainly biased, but based on the universal conventions of the overwhelmingly popular technologies used in gamedev and graphics, I think modern coders probably find matrix multiplication to be the intuitive behavior.

Maybe scientific programmers have a different culture here than graphics programmers (I don't know), but if scientific programmers' use-case is not considered important enough to support arbitrary-dimensional vectors, maybe their use-case matters less in other areas too, such as multiplying matrices?

If @crossRH is on the table to solve handed-ness problems, could something like @getTranslationRM (row-major) and @getTranslationCM (column-major) also be a reasonable solution for matrix major-ness problems? In GLM, the default major-ness is determined by a #define, but I suspect that most Ziguanas would not like build-options controlling this. I might be alone, but since I use both HLSL and GLSL, which are row-major and column-major by default respectively, I get slightly annoyed when math library function names assume one or the other instead of being consistently explicit.

Swizzling should work basically the same as it does in GLSL or HLSL. Some (exaggerated) examples:

Just for some clarification, do you think this should this include the stpq and rgba swizzle elements from GLSL? And what about uv for f32x2 vectors? I assume that these don't fit Zig's zen, but the proposal doesn't say.

I've also seen 0 and 1 used in some swizzling implementations, so that const foo: f32x4 = vec3.xyz1; would be valid and essentially appends a 1 to the end of an f32x3. This can be nice both for adding alpha and homogenous coordinates to vectors, for which 0 and 1 are both common values to hold. It could also easily zero-out part a vector, like const foo: f32x3 = vec3.x00; Perhaps this doesn't satisfy the zen of Zig, because it would introduce multiple ways of doing those things.

Sorry for the long post .. I even cut out less important notes. 😳

@andrewrk andrewrk modified the milestones: 0.9.0, 0.10.0 Nov 23, 2021
@andrewrk andrewrk modified the milestones: 0.10.0, 0.11.0 Apr 16, 2022
@ProkopRandacek
Copy link
Contributor

How about v3i32 instead of i32x3? I think it reads better.

  • v3i32 - vector of 3 ints 32 wide
  • i32x3 - int 32 wide vector length 3?

It would fit nicely with all the other types such as slices which are also expressed as a prefix of the type

  • []i32 - slice of i32
  • []v3u8 - slice of vectors of u8
  • []*v3u8 - slice of pointers to vectors of u8

rather than this:

  • []i32x3 - slice of i32 times 3?
  • []*i32x3 - slice of pointers to i32 times 3?

which feels kinda backwards

I would personally expand it to matrices too like this: m44i32 - 4x4 matrix of ints, but you say that this is not goal of this proposal :D

@AdamGoertz
Copy link
Contributor

As a roboticist, I just want to add my support for this proposal. Robotics and game dev have a lot of similar needs from programming languages, and one of those is vector operations (and matrices, but I’ll leave that to the matrix-specific proposal).

Personally, I think zig is almost a perfect language for robotics. Performant (for real-time operation); safe, with explicit error handling (for robustness); and simple (great for multi-disciplinary teams where not everyone is an experienced programmer). The biggest thing it’s missing currently is ergonomics for working with vectors (and matrices), which is something I do pretty much every day.

Unlike game dev, many of my use-cases could benefit from longer vectors, or particularly vectors of arbitrary length. For example, many state estimation algorithms require operating on the state vector of the system, which can easily be 12-20 elements long.

@andrewrk andrewrk modified the milestones: 0.11.0, 0.12.0 Apr 9, 2023
@andrewrk andrewrk modified the milestones: 0.13.0, 0.12.0 Jul 9, 2023
@expikr
Copy link
Contributor

expikr commented Nov 1, 2023

With the @Vector built-in supporting arithmetic operators, it seems like the only usecase wanted not already covered is concise swizzling.

The functionality of swizzling is currently achievable via @shuffle, which is a very powerful built-in that additionally allows for selection from two vectors as well as setting the resulting datatype. The extra power, however, also makes its syntax cumbersome when using it for just one vector.

Perhaps a more concise subset of the @shuffle functionality would satisfy this use case? Something along the lines of:

const oldVec = @Vector(4, f64){ 420 , 69 , 0 , 42 };
const newVec = @swizzle( oldVec, .{2,2,1,3,0,1,1} ); // @TypeOf(newVec) == @Vector(7,f64);

or

const oldVec = @Vector(4, f64){ 420 , 69 , 0 , 42 };
const newVec = oldVec.{2,2,1,3,0,1,1};

@AdamGoertz
Copy link
Contributor

AdamGoertz commented Mar 5, 2024

Just going to tack on one other motivating example for why having this at a language level would be beneficial, because it’s been a pain point recently. In the past week at my job (using C++) I have used the following vector types (counting only 3D vectors, and ignoring single vs double precision float variants):

  1. Eigen::Vector3f
  2. octomap::point3d
  3. pcl::PointXYZ
  4. geometry_msgs::Point
  5. Some guy’s hand-rolled Vector3 implementation from their open-source project.
  6. gtsam::Vector3 (this one is actually a thin wrapper around Eigen but I’m including it because the fact that that wasn’t immediately obvious helps illustrate my point)

Having to convert between all of these different representations introduces both unnecessary complexity and unnecessary runtime cost. Even when the internal memory layout is identical (which it nearly always is), reinterpreting memory to avoid a copy on conversion introduces the potential for bugs later on.

While having a vector type in the standard library could also help avoid the proliferation of alternative types, so long as all of the user-land implemented vector types lack built in arithmetic operations, I’m afraid there will be little incentive to avoid this explosion of alternative implementations, with minor ergonomic preferences becoming the fuel for competing APIs, and fear of depending on someone else’s implementation causes everyone to roll their own.

Having this supported by the language itself would make all software using vectors simpler, faster, more readable, and more interoperable.

One vector type to rule them all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests