My Experience With Jai (Part 3)

Alright, with an actual file converted into Jai, I think we have a good idea of what these conversions are going to look like, so I’ll try to get through a few more in a single post this time.

gy_rectangles.h

gy_rectangles.h is one of the largest files I have in Gylib because it ended up doing more than just “rectangles.” In this context, rectangles are anything in 2D or 3D that has perpendicular flat faces. So that includes AABBs in both 2D (rec) and 3D (box) as well as OBB in both 2D (obb2) and 3D (obb3). I also have integer variants for rec and box called reci and boxi. So that gives us 6 distinct types that are somewhat similar but are actually, in reality, quite different than each other. What I mean by this, is a lot of functions that work just fine on axis-aligned rectangles in 2D do not make sense on an axis-aligned 3D box, or even a rotated 2D rectangle. So although some functions have 6 variants, many of them only have 2-3 variants. Because of this, the file will probably get split someday. But for now, we’ll move on.

As before, HERE is the (mostly) completed gy_rectangles.jai and I’ll point out some of the interesting things that happened with this file

Positive 1: #place and Operator overloads worked wonderfully

Rectangles and boxes are not normally pointed to as “standard” mathematical types for most CS or Linear Algebra discussions. However, I’ve found that they serve as fundamental a role in game development as vectors which means that proper member aliases and some simple operator overloads go a long way. For example, being able to say rectangle.topLeft to get a v2 that represents the x and y members of the rectangle is super useful. And I can still do rectangle.x instead of rectangle.topLeft.x. Also operator overloads being fully available in user space means that I can do things like rec + v2 to indicate I want to “offset” my rectangle by some amount.

Problem 1: No Way to Represent Derived Values (afaik)

This is not something I had in C/C++ but it is something I would find useful and is one of the things I miss from my C# days. Being able to treat simple derived values as if they are actual members of the struct. For example, being able to say rectangle.center as a shorthand for rectangle.topLeft + rectangle.size/2 would be mighty convenient. C# has the option to make “Properties” that have a getter and a setter that define what should happen if the derived member is read or written to. This has a host of downsides, since it can be easily abused to execute an arbitrary amount of code inside what seems to be a harmless read at the calling side. However, if there was some way that I could get a very restricted form of this functionality, I would be a lot happier for it.

gy_directions.h

This file serves as a standard way for me to talk about cardinal (or diagonal) directions in an enumeration like way. Because of the implicit assumptions about handedness and direction order, I feel like this is one of those things that would never make it into a standard library in a satisfactory way. I don’t want to have to specify if my coordinate system is right-handed or which axis is “up” all over the place. I want those sorts of assumptions to be baked into my library of tools and therefore uniform across the code-base. So that’s what this file does, it defines 4 enums: Dir2, Dir2Ex, Dir3 and Dir3Ex. The Dir2’s are for 2D directions (up, down, left, right) and Dir2’s are for 3D directions (up, down, left, right, forward, backward). The “Ex” variants are enumerations that allow for diagonal directions as well as cardinal ones. To be honest, the Dir3Ex enumeration rarely sees use but the Dir2Ex enumeration was widely used in Princess Castle Quest since we had some things that moved diagonally. All of these enumerations are designed to be bitwise or’d together so they can be used like flags.

You can download the completed file HERE.

Positive 1: enum_flags helps implicitly assign bitwise or-able enumeration values

In the old code we had to make sure each value of our enumeration got assigned a proper power of two since we intended each one to be a single bit in a larger field of flags. Jai has a feature called “enum_flags” which allows us to do the following:

Dir2 :: enum_flags u8
{
	Right; //+x ( 1,  0)
	Down;  //+y ( 0,  1)
	Left;  //-x (-1,  0)
	Up;    //-y ( 0, -1)
}

Positive 2: Enumerations can have a forced backing type

Given that enumerations like this can serve as the backbone for many different systems, having the option to specify the size (and signed-ness) of the integer that backs them is pretty useful. It makes serialization a bit less hairy because we get a guarantee that the enumeration won’t accidentally grow in size and push members of a struct following it down. However, the biggest win by far with this feature is the guarantee that the binary NOT operator will behave like we want. Doing something like ~Dir2.Right is guaranteed to give us 0xFE (not 0xFFFFFE or anything else). This is a big deal for bitwise or-able enumerations where we often want to perform the following operations

IsFlagSet(field, bit) -> ((field & bit) != 0)
FlagSet(fieldPntr, bit) -> *fieldPntr |= bit;
FlagUnset(fieldPntr, bit) -> *fieldPntr &= ~bit;
FlagSetTo(fieldPntr, bit, value) *fieldPntr = (*fieldPntr & ~bit) | (value ? bit : 0);

As a side note, I implemented these Flag macros in Jai using the #expand functions thinking they would be important for readability, but it turns out that the type safety and other guarantees the language gives us are enough to make doing the bitwise operations out in plain code feel pretty normal. So the following code feels pretty mundane:

if (directions & .Right)
{
	directions &= ~.Left;
	directions |= .Up;
}

Problem 1: non-pure enums are more obvious because of Type Metadata

This is more of my own problem but these enumerations in C would often include derived or “meta” values that were related to the enumeration but not actually a proper enumeration value. For example Dir2_Count is not supposed to be used as a “valid” value for the enumeration. In fact it directly “conflicts” with Dir2_Left since they both have a value of 4. And now that we have functions like enum_values_as_s64 and enum_names it seemed weird to include them as part of the actual enumeration. Serendipitously we can just pop them out of the enumeration and declare them as constants that have the enumeration type. Or for the _Count case we define them with a regular integer type

Dir2 :: enum_flags u8
{
	Right; //+x ( 1,  0)
	Down;  //+y ( 0,  1)
	Left;  //-x (-1,  0)
	Up;    //-y ( 0, -1)
}
Dir2_None:     Dir2 : 0x00;
Dir2_All:      Dir2 : (Dir2.Right|.Down|.Left|.Up);
Dir2_Count:    u8   : 4;

This makes it possible to do things like:

directionFlags := Dir2_None;
for 0..Dir2_Count-1
{
	side := cast(Dir2)(1 << it);
	if (somethingOnSide(side)) { directionFlags |= side; }
}

Problem 2: My GetDir#Str functions didn’t go away

I imagine this won’t be true for many of my enums but in this case, I opted to keep my hand-typed stringification functions rather than rely on the enum_names function that’s built into the language. This is largely due to the special case handling we do for stuff like .Left|.Right being “Horizontal” or handing Dir2_All like it’s a proper member of the enum. However, there’s also a problem of ergonomics of these enum functions. They are list providers but they don’t help me go from enum value to name in a direct way. If I wanted to use the enum functions to get a name from a enum value I’d have to do something like:

GetDir2Str :: (direction: Dir2) -> string
{
	names := enum_names(Dir2);
	for enum_values_as_s64(Dir2)
	{
		if (it == cast(s64)direction) { return names[it_index]; }
	}
	return UNKNOWN_ENUM_STR;
}

This is not terrible. It’s only a few lines. But a switch case statement feels a little more in-line with how I’d like this function to operate, since the compiler could decide to turn it into a jump table. The real solution here would be if I specifically could ask the compiler to generate a jump table for me. This isn’t important here, but for larger enumerations (like EntityType) this could be a very useful feature. I imagine this might already be possible, given some metaprogramming. I will have to revisit this when I get more familiar with the compile-time code generation features of the language.

Conclusion

Those are all the files I’ve fully converted so far. I will write some more blog posts as I get farther. As you may have noticed, gy_intrinsics.jai is somewhat finished (things like CosF32 are already being used in these files, but I have yet to alias all the “intrinsic” like functions that I use). That and gy_colors.h will probably be next