Closed TyOverby closed 10 years ago
cgmath-rs uses column major matrices, which makes it more expensive to compute the inverse. Column major matrices are also more confusing since row major is mathematical standard.
It is correct that people might need to include both cgmath and vecmath, but no such case has yet occurred. I believe that's optimizing for something that might not happen, or happen less frequently that it outweighs the benefit; of having a simple flat vector library without assumptions about structure and to be used mostly internally.
When I am writing libraries, I know the math and what operations I need to make it work. I don't have time to figure out which trait I need to import. I don't want operator overloading, I want to see what it does as a thin layer on top of the raw computations. I know what assumptions I can make. I need to make those shortcuts quickly without added complexity, knowing what it will cost me in the future. A flat design with global function will work, because it has been tested out before.
This library is intended as an experiment to see if it justifies the use case. It will be tested for Rust-Graphics.
Just let me work on it. I won't give in for peer pressure if it gets in the way of experimenting.
I need to correct something: cgmath-rs uses AffineMatrix3
to do what Matrix3x4
does. The inverse is not more expensive to compute for that matrix.
Interesting.. some comments: [1] i strongly favour Column major, it has a desirable property for game-programming: you can extract the objects' axes &position directly.. "which way is this object looking"..etc..
[2] +1 to a simple 'prefixed' free function library: Without a proper IDE, it shows up quickly with simple text completion. ( I'd argue for choosing 'v3... ' v4..' - vector maths are so pervasive in a graphics source base that the 'v' prefix is easily worth taking ... not a big deal though. vec3_ is fine)
[3] +1 to vecmaths' use of [T,..N] - completely decouples the description of data in memory from any specific vector maths library. One uncertainty is what will be SIMD-friendly later? [T,..N] probably will be?
This is a huge reason i was interested in Rust in the first place.. the hazard in C++ of coupling the simple description of x,y,z to every single operation you're going to do on it in a class. I've seen people go round in circles throwing source bases away over changes to the base types.
However, Rust should be better though with traits, the ability to layer more operations or conversions over an existing library.
Maybe this will be a double-edge sword , as you say, "don't want to be hunting through the traits". Maybe it'll all be easier if the documentation is generated?
[4] NIH... For my own experiments in Rust I'm rolling my own aswell.
I've just changed over to tuple-structs simply because I prefer the constructors.. I've never been a fan of rusts' convention TypeName::new(..).
I ended up rolling a trait XYZW { x(),y()..} accessors for that switch.. unfortunate verbosity.
But it seems you basically have 4 possibilities for representing vecmath, which will all appear i'm sure :)
[T,..3] struct Vec3{x,y,z} (T,T,T) Vec3(T,T,T)
by going with acessors I can change later or roll accessors for someone else's library later.
I wonder if they'll keep tuple-structs? this decision seems like some 'non-minimalism' in the language, but they seem nice with enums?
I hope rust gets' swifts .0 .1 .2 accusers for tuples
Actually parameterising individual fields might be interesting, e.g. you could have a scalar which is semantically a 3d axis, or create Point/Axis types by introducing One and Zero types.. but I haven't done this yet.. Point3D = Vec4(T,T,T,One_t) Origin= Vec4(Zero_t,Zero_t,Zero_t,One_t) ..
a more likely use of that is a matrix with different precision between axes & position - e.g. axes in f32, pos in f64
I've rolled the operator overloads with indirection traits for matrix_matrix, matrix_vector .. and it does seem very verbose , unfortunately. But it works better than I thought it would. This might all change if rust gets' "multi parameter type classes".
I haven't tried wrapping it all in macros yet.. but that makes comprehending source later harder, its' messy in its own way
@dobkeratops Row major is mathematical standard and also gives natural dot product when doing transformation. This means if you want to compute the transformation 2 out of 3 axis you can do so with 2 dot products. The left side of a matrix product naturally belongs to rows, but the right side naturally belongs to columns. They are completely balanced, but row major has the benefit of being laid out in the source the same way it is represented in mathematical notation.
The benefit with column major is base vectors, which vecmath will support conversion to and from. We can add methods for transformations using base vector matrices and use them as column major matrices. If you reverse order of the matrix product you get the column major product. The only difference is the type alias will be Base4x3
instead of Matrix3x4
.
I prefer vec3
to v3
because easier to understand if you don't know what v
stands for. However, I might change my mind later, so this is a good idea to keep for later.
[T, ..3]
can be indexed and all the members are known to be of the same type. You can also take a slice of it. This is why I prefer it.
Well this just highlights why we have multiple maths libraries in the first place. No way to please everyone :)
Any performance decision depends on context. If you're going to multiply a matrix by lots of vertices in a batch, and your hardware has fast dot-product, then row-major is good. (the ability to store 4x3 aligned is nice).
However most other situations.. I've simply found the ease of getting axis/position directly a much bigger draw.
Any sort of code setting up , comparing, manipulating objects. axis planes for OOBB's.. direction vectors for AI. Any sort of bulk transformation is usually going on on the GPU, not the CPU.
well if you always go through an abstraction layer you can change later.. you can always have an acessor 'matrix_getaxes()->(,,_) I guess. 'matrix_fromaxes(,,)->_' .. etc
I've worked on one project where someone decided to make it platform dependant. The xbox 360 had fast dot product instruction, so could handle row-major. The ps3 didn't, so column-major was the only sensible choice. There's even a difference between throughput and latency when comparing them. Any fine-tuning there depends on the specifics of the machine..
Alternative: If we rename Base4x3
to Matrix4x3
and use mat3x4_mul_row
and mat4x3_mul_col
. We distinguish the format by the functions and not by the type. The transform_pos
and transform_vec
functions are unambiguous, so there should be no problem.
maybe you could namespace it.. mod row_major { ...all the matrix stuff implemented row major.. }
mod col_major {... col-major implementations}
if you're going to work predominantly one way or the other - bring in the namespace that suits you.
Why on earth would you need both row major and column major versions? Extracting an axis or a position from the matrix is unlikely ever be revealed by a profiler - there are lots of heavier operations, regardless of the representation (unless you go quaternions, which are another matter).
Besides, why did you write your own math library in the first place? cgmath-rs is pretty rich and welcoming any changes in case you are not happy with something.
Why on earth would you need both row major and column major versions?
[1] personal preference . I happen to prefer column major but bvssvni has a preference for the opposite :) he might want to get data to & from other libraries using the opposite [2] one or the other might be more efficient depending on the specific use case. I've seen people motivated to make it platform dependant. (e.g. xbox360 has dot, ps3 doesn't)
There is the case for saving memory holding the affine matrix (the most common type)as a 4x3 row-major. Alternatively, having fast access to position & axis vectors directly loadable into SIMD registers is also compelling.
Extracting an axis or a position from the matrix is unlikely ever be revealed by a profiler
there are platforms where this sort of thing can go badly wrong and you need to ensure data stays in one register set or the other
@dobkeratops personal preference is a bit flaky, since both row/column major representations can be see as a hidden implementation detail with the same interface.
one or the other might be more efficient depending on the specific use case.
My point is - it doesn't matter if one or the other is more efficient, as long as the operations we are talking about are cheap anyways. Have you ever seen a get_row()
method (or something like it) appearing at all in the profiler result? If not, then efficiency is not a problem, while code maintainability is always a concern - joining efforts with one library (like cgmath-rs) is beneficial to the community.
There is the case for saving memory holding the affine matrix
How many of these are you going to hold? 1000? 10000? Unless you are writing a high-profile scientific application, this difference in memory is nothing (we are talking about graphics here: Piston, gfx-rs, whatever).
The only place where these matrices can be a bottleneck - for skinning, where you need to send array of them to GPU, choosing a 4x3 matrix is not the most efficient solution to begin with. Passing an offset (vec3) and quaternion (vec4) takes way less bandwidth and allows better interpolation in the end.
Besides (as I believe was mentioned), if that becomes a concern, and your row/major doesn't fit the need, you just pass a transposed matrix and reverse the multiplication order.
See, whatever internal implementation you choose - doesn't really that matter in the end. This race for perfection has a negative impact on the community by fragmenting the ecosystem, and that's what should matter.
@dobkeratops personal preference is a bit flaky, since both row/column major representations can be see as a hidden implementation detail with the same interface.
Whilst i disagree on row/col, I completely sympathise with @bvssvni 's motivation here: The representation you choose makes the code more comprehendible 'out of the box' for one case without having to dig through lots of wrappers/abstraction layers to figure out what its' doing.
Its just his preference is opposite to mine:) I like the simple, direct access to axis vectors.. thats' pleasant to me, because thats how I think. and writing lots of noisy boilerplate wrappers is the kind of nonsense we're trying to escape from other languages :)
have you ever seen a method like
get_row
appear on the profiler result Unless you are writing a high-profile scientific application, this difference in memory is nothing
we dealt with this sort of detail continuously on the 360/ps3 .. small differences in data layout were routinely critical.. packing as much as possible into the cache lines (overstepping a cache line and you get 2 600 cycle stalls instead of one..), and avoiding pathological hazards in accessing data to ensure it goes either SIMD or float regs but not both. It's not a trivial issue. So basically 'get_row' doesn't appear on the profiler result, The enclosing code does .. and the optimisation to 'the enclosing code' is to fine tune the data layout.
A specific example of this i've dealt with is a skeleton animation system with constraints/driven bones. One approach might involve if statements calling abstractions.(select axis for a constraints) But arrange the data differently, and you get to index into an array of homogeneous data (now the axis vectors are just raw data vectors), and avoid the if's. Neither the 'get_row' or 'if' individually show up on the profiler, but the enclosing routine does, and eliminating the branches allows much better pipelining. The abstraction is what killed it originally.
we are talking about graphics here: Piston, gfx-rs, whatever).
graphics IS a high performance application :) framerate matters. and if you're running on a game console , you have a tiny/crippled CPU relative to a monster GPU, unlike a desktop computer which is more balanced. i.e. cpu side is relatively less of the total 'processing', but paradoxically harder to optimise & still critical: - you never want the GPU waiting for the CPU - all this fine tuning is a big deal. Rust will face prejudice from C++ users and competing in benchmarks will be good 'marketing'
This race for perfection has a negative impact on the community by fragmenting the ecosystem, and that's what should matter.
but equally you can't please everyone. I count 4 options for representing vectors in rust. Then there's this row/col idea. There are many choices, and I'm sure all will get explored.
Rust is still young& changing, who really knows what the best practices are yet?
At the same time, perhaps the experience here might suggest directions the language can take. Is the fact that there are 4 possibilities for representing a vector a sign of a language feature that could be improved ?
And again ... I agree with @bvssvni reasoning that [T,..3]
is a good choice... (decouple operations from data), I just personally find it less comfortable because I'm more used to [i] for indexing into a collection and having that look distinct from .x/.y/.z/ for accessing components. (the 'Vec{x,y,z}' is a lower-level thing to me than the array holding it, and it should usually just be one register..)
Besides, why did you write your own math library in the first place?
same reasons. I want as much control over my code as I can get, I don't want to have to debate anything. (e.g. have someone else impose on me "these axis vectors are abstracted, so you can't index them", or whatever other issues might appear 5,10 years from now) Digging through all the interfaces takes time. I know it better if i wrote it. It makes more sense to me if its the names I thought up first. Do I really know the language well? Implementing something where I'm familiar with the desired end result is a good way to learn it.
@dobkeratops Thanks for taking your time to write such a good answer! I was worried that my posts were a bit too emotional, and I'm glad you responded without any offense taken.
I lack X360/PS3 experience, used to work on PCs and lately next-gen consoles. The weakest HW I worked with was an integrated VIA board, and even there math operations (in the 3D engine with games we shipped) almost didn't show up in the profile, IIRC. I believe it's not that important how you arrange columns/raws as it is to just feed data in a predictable manner with as much locality as possible.
May I wonder what projects/titles did you happen to optimize for the matrix representation?
As a general note on rust game-dev ecosystem - I think we should keep in mind the need to cooperate as much as possible. Yes, we may not know yet what works best, but sometimes an inferior solution adopted more wildly is better (in the long run) than a perfect solution not adopted at all. Think about D with its alternative standard libraries, or even KDE/Gnome fight (hell, even the amount of Linux distributions these days). That all could be way more advanced and efficient if not fragmenting, all happened because developers were egoistic and didn't look far enough.
My main issue is that with fragmentation. I don't want to be converting between 3 different matrix representations on the inside of a hot loop.
If we do have multiple matrix math libraries, I at the very least want to perform an unsafe byte-for-byte cast between them.
These are all good concerns, but I don't see what answers we get now, so I suggest more experimentation. Vecmath is an experiment to see if it is possible to remove the trait stuff and still have something acceptable. When I get something working I will share my thoughts and experiences. As far as I know there is an agreement that [T, ..n]
is a good choice for data structure. If we support both row and column major without much mental overhead there will be plenty of room to make one data structure fit with any math library.
I believe the concern of fragmentation is a bit hyped up and I also think using cgmath-rs everywhere will be the cause of the concern, not the solution. If you rely on cgmath-rs internally in a library and force the end user to depend on it too, you are causing the frustration you wanted to avoid in the first place. Because there is no guarantee that there will be compatibility with other math libraries, for example designed for general linear algebra. The Rust gamedev community already got this problem between cgmath-rs and nalgebra and it is the extra abstraction that causes it.
Think of Vecmath as a test bed for now. It should only be used internally in libraries and top level executables until we know more about the consequences of the design.
@bvssvni Thanks for expressing your vision on Vecmath! I agree that forcing the user to stick with any particular math lib may not be optimal. However, I see the way Vecmath doing it as fixing a small issue by introducing a bigger one - partial type erasure.
That's how I see it. You've got a fixed-size array of 4 elements and you treat it as a vector, different functions may treat it differently (as a case for column/row major), and the type system gives you no protection from misusing these. There is plenty of things that can be represented by a 4-elements fixed size array: a 4D vector, a 4D point, a 3D homogeneous vector, a quaternion (hyper-complex number). You give the user no type protection on them.
gfx-rs will use a compatible structure with Vecmath, at least it is listed as one of its goals. I take this as a sign that other people also are thinking in the direction that they want their library to be math library agnostic.
You got the sign wrong. gfx-rs does no math operations, hence it doesn't need any type protection in regards to math primitives. It doesn't have anything to do with math.
My point is - once you want to do any operations on data, you've got to type it strongly. Sacrificing type safety for the fragmentation concern (which you believe to be hyped) is a bad trade.
@kvark I am aware of the problem with type safety. The way this is solved is separating the operations by name, so you can look at the code and see what it does, but it does not have the type enforced by the compiler. The problem with having too much type information is that sometimes it is irrelevant to what you do.
For example, a Matrix4x3
, is is a row matrix with 4 rows and 3 columns? Is it a column matrix with 4 columns or 3 rows? In most cases this is used as an affine transform. If it is used as an affine transform, then it will use mat4x3_transform_pos
and mat4x3_transform_vec
for points and vectors respectively. There is no type safety, but there is no ambiguity either. It does not matter whether is a column or row matrix.
There are many concepts in linear algebra where you can name differently and there will still be problems. There are isomorphisms everywhere. In the case of you example with 4 elements, you can treat it as a Matrix4x1
or a Matrix1x4
. The type system can't deal with it, because when you have decided to call something a Vector4
you already have made an choice that "I don't care" that might break the semantic rules in the proof.
If you want something rigorous, use Clifford algebra. For everyday use this notation is unpractical. You have to make tradeoffs. The design of Vecmath is no exception.
This is a deep old problem where I am a fan of Wittgenstein's quote "look at the use of words". If you try to model language as "is" blocks where all it can do are defined by concepts about what things are, it will generate dependencies on these concepts. If you try to model language as "does" blocks where all it can do are defined by the usage, then you have more freedom, but you live in the danger of doing more errors. In human language we largely depend on detecting the "does" blocks in the language and derive meaning from context, so we reduce the consequences of error. In programming languages and library design this becomes a spectrum, where you can separate the parts a compiler need to know from what you are communicating toward the user.
A complete type system that prevents you from doing any error is a platonic ideal. If you are standing inside the platonic ideal and looking outside, you see imperfections. If you are standing outside and looking in, you see the attributes that you loose. This problem is older than 2 millennia, but Wittgenstein applied it recursively and I think his message was: If you understand the use of words, you can see how that problem appears, but if you are following a platonic ideal, you are making a mistake by taking your view as the truth and the problem remains intrinsic mysterious.
So I am saying I am aware of the problem, but I don't see it as a problem I can solve perfectly, and I am not willing to give my freedom to experiment to commit to an ideal I understand just too well. I want to be able to pick cgmath-rs when it makes sense and use something else when the type system gets in the way.
Thanks for the discussion because it made me think more deeply about these concerns. I think we have made our points good enough that continuing will repeat them over and over. Will close this, but if there are new concerns, feel free to reopen.
https://github.com/bjz/cgmath-rs
There is no real punishment for importing cgmath and reexporting parts of it, because compilers are pretty good at not keeping around unused code.
Besides, if people are using cgmath-rs, they will actually get hurt for it if they need to include both cgmath and vecmath. Plus, converting between the different types will be a pain.
I'm strongly in favor of just using cgmath and declaring that as the standard.