So if we take a look at our dependency graph in the last post there’s a bunch of things we “should” implement next if we wanted to continue in a top-down order. Things like gy_assert.h, gy_debug.h, gy_intrinsics.h, etc. However, a lot of this stuff can be sorta stubbed out and side-stepped for the time being. And I think that’s for our benefit since it will allow us to get to the meat of the features we want to try converting to Jai really quickly. So next up on the chopping block:
gy_vectors.h
This file contains, as you might expect, vector structs like Vector2, Vector3, Vector4. Generally, since we were working with C in the past, this file tends to be a massive file of manually duplicated code to handle each vector type for each type of operation we’d like to perform with vectors. Moving into Jai, I had two main questions I wanted to answer:
Do Vectors already exist in the “standard library” of Jai and if so, do they do all the things we want them to do? If they exist, but don’t do everything we want, can we easily expand the functionality to do everything we want? Or do we need to wholesale replace those implementations with our own?
Can and should we try and do polymorphic programming for this kind of problem? At the end of the day, the number of vector variants we actually want is around 6. We want 2D, 3D, and 4D and then we want floating point and integer variants for all 3 of those. Is it worth doing all our programming in a polymorphic way to keep from having 6 of everything?
So we can tackle these questions one at a time. First, do Vector structs already exist. The answer is YES (sorta). In the Math module we have definitions for Vector2, Vector3 and Vector4 (as well as Quaternion and various Matrix sizes). However, on further inspection there is only a limited number of math operations defined for these structures so we at least need to add more functions for these vector implementations to be “complete” in our case. For example, there are dot_product and length functions for all the vector types but there are no functions to round the components of the vector easily (something I do often for UI code and text pixel alignment) or clamp the components between two sets of upper and lower values. Also missing are the integer based vector variants that we want (I call these Vector2i, Vector3i, and Vector4i). So at the very least we’ll need to completely implement those variants and add some functions that operate on the existing structs in common (to us) ways.
Second question: Should we write our structs and functions in a polymorphic way? I’ll admit that I didn’t fully investigate what this would look like before deciding against it. So it may be that my worries are not actually founded. However, the main things that dissuaded me are:
We have a very small set of cases that we actually want. And having properly named functions and types for each variant holds some value when we are debugging and looking at the call stack. Seeing that something is a “Vector2i” instead of a Vector{2, s32} might seem like a small distinction, but it makes a big difference when you are dealing with these types all day, every day.
We don’t actually need anything above 4 dimensions, nor below 2 dimensions. A lot of functions would degenerate in the 1D case and make implementation a bit noisy (i.e. checking for this edge case and erroring, or early returning). Also a lot of functions that could be generalized to an N-dimensional case are actually only ever used for 3D cases. For example, cross product can be generalized to more dimensions besides 3, but we literally would never use the other variants in our games.
Naming our functions specifically to 2D and 3D is often useful. For example Vec2PerpRight is a very common function I use that makes no sense in 3D or 4D. You could make somewhat similar functions that take a couple more parameters than the 2D case but we would basically never use these variants because they don’t make sense to be called “PerpRight.” They would be called something like “RotateAroundAxis” for 3D (and who knows what for 4D)
So we don’t want to do things entirely polymorphic but we do we want to try implementing some “generalizable” operations in a polymorphic way? When I first started writing the file I figured I would say yes to this question. After all, having one VecAdd function seems quite a bit better than 6 VecAdd variants. However, given the brevity of some of the syntax, I found that almost ALL of my functions could be collapsed to a single line and not lose much readability. So I opted to do everything in a bespoke way (mostly for the sake of debugging readability later).
So I went ahead and converted my gy_vector.h to gy_vector.jai. For the sake of brevity, I’m going to upload the result of that process HERE and then point to some of the interesting aspects instead of explaining the process of conversion from beginning to end.
Positive 1: Constant Value Definitions are awesome looking (Curly Bracket Initialization)
One of the things I like having for vectors are “Simple Value Definitions” (see lines 107-181). Stuff like Vec2_One or Vec2_Right. Jai has support for default initialization of a struct which solves half the problem (i.e. Vec2_Zero is probably going to be referenced very rarely). In C these value definitions took the form of #defines that are really just function calls to NewVec2.
#define Vec2_Zero NewVec2( 0.0f, 0.0f)
#define Vec2_One NewVec2( 1.0f, 1.0f)
#define Vec2_Half NewVec2( 0.5f, 0.5f)
#define Vec2_Left NewVec2(-1.0f, 0.0f)
#define Vec2_Right NewVec2( 1.0f, 0.0f)
#define Vec2_Up NewVec2( 0.0f, -1.0f)
#define Vec2_Down NewVec2( 0.0f, 1.0f)
This meant that our value “definitions” were not actually compile time constants like we wanted them to be. However, the alternative was to do curly bracket initialization, but this came with a HOST of downsides because C’s bracket initialization is under-defined and not very strict.
In jai we have a few more promises from the implementation of this feature. Namely the curly bracket expression is strongly typed, with stuff like “v2.” before the open bracket forcing the expression to always be a v2. And the expression errors if the number of items passed does not match the number of items in the struct exactly. With those things in mind, we’re able to switch to curly bracket initialization for these definitions and reap the benefits of having truly constant values for our vector structures!
Vec2_Zero :: v2.{ 0, 0 };
Vec2_One :: v2.{ 1, 1 };
Vec2_Half :: v2.{ 0.5, 0.5 };
Vec2_Right :: v2.{ 1, 0 };
Vec2_Left :: v2.{-1, 0 };
Vec2_Down :: v2.{ 0, 1 };
Vec2_Up :: v2.{ 0,-1 };
Also, curly bracket initialization allows me to make many of my basic vector functions into single line implementations that are still relatively readable. For example:
Vec2Floor :: (vector: v2) -> v2 { return v2.{ floor(vector.x), floor(vector.y) }; }
Positive 2: I’m getting rid of _t suffix on my Types
For a long time now I’ve opted to add _t to the end of all my types in C. I forget exactly what the main impetus was for this but I suspect it has a lot to do with the fact that types are not treated the same as other constructs in C/C++. So normally my vectors would be defined like so:
union Vector2_t
{
r32 values[2];
struct { r32 x, y; };
struct { r32 width, height; };
};
typedef Vector2_t v2;
So we’d put a _t on the end of the type name to make it clear it was a type. However, for really common mathematical types (like vectors) I would also make an super shorthand alias like “v2” (something I never did for higher level types like Color_t or Textbox_t for example).
With types being treated with more “first-classiness” in Jai, it seems like having this suffix is entirely redundant now, so I’ve opted to try getting rid of them and see where that takes me.
Problem 1: I can’t Augment Vector2, Vector3, and Vector4
If we look at one of our integer vector implementations, there is a feature that I like having that the Math module’s implementation does not have. And that is aliasing x, y, and z components as “width”, “height” and “depth.”
Vector2i :: struct
{
x, y: s32;
#place x;
component: [2]s32 = ---;
#place x;
width: s32 = ---;
#place y;
height: s32 = ---;
}
v2i :: Vector2i;
This can be done by using the #place directive, which is great! However, as far as I can tell there is no way to do this after the structure has been defined. So if we want to actually use the Vector structures from the Math module (and it’s associated functions and operator overloads) we have to forego these aliases for floating point vectors. This is a common case of “If it doesn’t do everything I want out of the box, I can’t use it at all.” This isn’t a deal breaker for now, but I may change my mind later if there ends up being any other missing features that I haven’t noticed yet.
Problem 2: The Unary Negative Operator was missing
This is a small thing to fix, but worth mentioning. There is no definition for negating any of the Vector structs in the Math module, so we had to implement those ourselves.
Problem 3: Dead Code Elimination and Float32 is a “long” name
Since this is the first real file of substance that we’ve started bringing into Jai, there are 2 problems that aren’t Vector specific but are interesting to mention. The first one is that many of the mistakes I made would not get caught by the compiler until the function was actually being called by some external code. At first I solved this by writing a TestVectors function and #running it during compilation. I’m not a TDD proponent so this is very abnormal for me. It ended up wasting enough time that I had to take a brief break and go figure out how to make the compiler more strict. Turns out that the compiler does “Dead Code Elimination” by default, which was definitely my suspicion. There’s actually a standard command-line option -no_dce to turn this off that I had completely skipped over when reading the debug output of “jai -help”. Turning this elimination off definitely helped, but I feel like I still hit a few cases where my function would compile until I called it from somewhere. This might be anecdotal though, I’ll try and keep an example if I come across another instance of this happening. The other “problem” is that I distinctly feel the difference between a 3 letter type name of “s32” and a 7 letter type name for “float32.” I put problem in quotes because it’s entirely aesthetic and it’s not hard for me to fix as a user. But for now I simply noted my preference and continued on with my day.