-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Comments
Related: the accepted proposal to add a SPIR-V target backend for zig: #2683 |
Take my upvote. This is a pretty solid extension which would make gamedev and other linalg topics in zig a lot easier.
I agree, this would be my only use case if we had operator overloading
This would contradict the "only one way to do things" zen as types are always constructed with Vector creation is just initialized like any other type in zig: var a: i32x3 = i32x3 { 1, 2, 3 }; |
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 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. |
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. |
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.
Regarding cross product I sort of agree. It might be a good idea to not have 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 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. |
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.
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. |
Other than a handful of builtins, what does this give us that our existing SIMD support doesn't have? |
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 saw your edit, but I would like to add to this) There's also more optimizations to
Where Beyond that it's also a question about syntax. From the proposal:
But I just did a quick search through the zig documentation and realized I had completely missed 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
Examples of bad/terrible syntax for the
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 This proposal adds many new primitives:
Out of all these new primitives, only very few actually makes sense for SIMD at all, i.e. Overall, I would expect that |
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 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. |
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. :) |
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. |
We already have |
@Sizik How would element access work in this case? Would you be able to use |
@zigazeljko Element access would be just through
|
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 |
Instead of defining new vector types and extending common operators for those types, you should probably look at Julia's
|
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.
As I'm sure you know, GLSL and GLM assume that matrix multiplication is the more important use-case for 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
Just for some clarification, do you think this should this include the I've also seen Sorry for the long post .. I even cut out less important notes. 😳 |
How about
It would fit nicely with all the other types such as slices which are also expressed as a prefix of the type
rather than this:
which feels kinda backwards I would personally expand it to matrices too like this: |
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. |
With the The functionality of swizzling is currently achievable via Perhaps a more concise subset of the 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}; |
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):
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. |
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++:
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
i32
,f32
,u8
, etc)+
,-
,*
,/
,==
,!=
, etc) for these primitive types@dot()
)Non-goals
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
forN
-dimensionalf32
vectors, andivecN
anduvecN
fori32
andu32
vectors respectively. Examplesvec3
,ivec2
, etc.In HLSL (and CUDA) vectors are named
typeN
where type is the normal type and N the dimension. Examplesfloat3
,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
wheret
isi
,u
orf
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) andN
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:
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: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 tou8x4
. 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:
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:
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:Specifically, for operator
*
it's possible to:scalar * vector
andvector * scalar
. But for operator/
it's only possible to dovector / scalar
. Some vector libraries implementscalar / 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:@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.:
Can potentially be called
norm()
ornorm2()
, butlength()
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:
@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:
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! :)
The text was updated successfully, but these errors were encountered: