Dynamically Swizzled Type Equivalent Goodness

Following on from the vector factory, I’ve gone a step further and created the generic ‘matrix’ factory. The drama here is the realization that a ‘vector’ is nothing more than a ‘matrix’ with a single dimension. Once you make that leap, you can do all sorts of generalized matrix math, and just specialize for those cases where you truly need a ‘vector’ such as a cross product or something.

But, this is where things get really fun. Here’s the beginning and ending of the matrix factory:

local matrix_kinds = {}

local function make_matrix_kind(ct, rows, columns)
	rows = rows or 1;
	columns = columns or 1;

	local typemoniker = string.format("%s:[%d,%d]", tostring(ct), rows, columns);
	if matrix_kinds[typemoniker] then
		return matrix_kinds[typemoniker];
	end
.
.
.
	-- Save the fact that we've created the type so that
	-- we can return that same type next time it is asked for
	matrix_kinds[typemoniker] = mat_kind;

	return ffi.metatype(mat_kind, mat_mt);

I started writing this article just because I wanted to say the phrase “type equivalence”. What the heck is that? Well, in the case of these matrices, I have decided that two kinds of matrices are the same if they use the same base type for their elements, and they have the same dimensions. I’m not saying two instances of different matrices are the same, just their types. As such, I won’t bother creating a new type for the same thing.

New type? Yah, remember, I’m actually creating new structures types whenever you call this ‘factory’ function. A small digression:

local t1 = ffi.typeof("struct {float x,y,z;}");
local t2 = ffi.typeof("struct {float x,y,z;}");

print("t1: ", t1);
> ctype<struct 1310>

print("t2: ", t2);
> ctype<struct 1314>

print("t1 == t2: ", t1 == t2);
> t1 == t2: false

Just to prove that although two things might look the same, they can be very different. I’ve create an ‘anonymous type’ if you will in ‘t1’. Then, I create another anonymous type, with the exact same structure, and name it ‘t2’. Well, these ‘types’ are first class data elements in themselves, and they definitely are not the same data element, even if they look exactly the same.

For the case of the matrix, I want to use the same type, so that I don’t have a proliferation of these anonymous type objects running around. For convenience, the matrix class has the following already created:

return {
  make_matrix_kind = make_matrix_kind;

  mat3 = make_matrix_kind(ffi.typeof("double"), 3,3);
  mat4 = make_matrix_kind(ffi.typeof("double"), 4,4);

  mvec3 = make_matrix_kind(ffi.typeof("float"), 1, 3);
  mvec4 = make_matrix_kind(ffi.typeof("float"), 1, 4);
}

A couple of vector types (mvec3, mvec4), and a couple of square matrix types (mat3, mat4). So, if anyone wants to use them in their code, there they are. The ‘make_matrix_kind’ factory function is also exposed though. This allows anyone to come along and create another matrix type that may not already be defined. That’s got to be a good thing right? But, what if two different parts of the code create the exact same type?

vec7 = math_matrix.make_matrix_kind(ffi.typeof("float"), 1, 7);
v1 = vec7();

if ffi.typeof(v1) == ffi.typeof(vec7) then
  -- do super duper stuff
end

Then some enterprising developer comes along and does a similiar thing…

myvec7 = math_matrix.make_matrix_kind(ffi.typeof("float"), 1, 7);

myvec7processor = function(avec)
  if ffi.typeof(avec) ~= ffi.typeof(myvec7) then
    -- we don't process anything but 'myvec7'
    return false, "failed type check"
  end

  -- we made it past the type check
  -- do super duper stuff
  return true;
end

This would be a very rude awakening indeed. Your code is generic enough that it should accept the vec7 declared object from above, but alas, it would not, unless of course you do the trick that make_matrix_kind() does, which is to capture type equivalence, and simply hand out the same kind whenever someone tries to create something like it.

Hmmm, how else can I use that? Well, one of the more fascinating language features that I find in glsl is this concept of ‘swizzling’. Basically, what that means is you can create a vector instance, like a vec3 for example, and you can get various fields out of it by using ‘.’ (dot) notation. For example, the following can be done:

local color1 = mvec3(255, 13, 200);
print(color1);
> 255, 13, 200

local bgr = color1.bgr;
print(bgr)
> 200, 13, 255

The explicit ordering of a color value is ‘r,g,b’, with the ‘r’ at the zeroth byte, ‘g’ at 1 and ‘b’ at 2. But, I want to get the value out in the reverse order, ‘bgr’. So, I just specify that using the dot notation, and out comes the data in the value I specified. You can even have more fun with this as it does not restrict how many or what order you do things. So, the following are also valid:

print(color1.ggrr);
> 13, 13, 255, 255

But, you’ve got to ask yourself, having just read the above stuff about type equivalence, or lack thereof, what kind of types are being generated here? Let’s look at the guts of the thing to find out. First, the __index metamethod of the matrix kind has to catch the fact that you’re typing some random string and call the swizzling routine. It looks like this:

__index = function(self, key)
  if type(key) == "number" then
    -- Return a pointer to the specified column
    return self:asPointer() + key*rows;
  elseif  mat_t[key] then
    -- perhaps it's a function call  
    return mat_t[key];
  elseif type(key) == "string" then
    -- might be swizzling
    return self:swizzle(key);
  end
end;

So, this will catch the case where you type in ‘color1.bbgr’, and call the swizzle() function. And what does that look like?

local swizzlemap = {
x = 0;
y = 1;
z = 2;
w = 3;

r = 0;
g = 1;
b = 2;
a = 3;

s = 0;
t = 1;
p = 2;
q = 3;
}

local swizzlit = function(val)
  local offset = 0;
  local closure = function()
    offset = offset + 1
    if offset > #val then
      return nil
    end

    return swizzlemap[val:sub(offset,offset)];
  end

  return closure;
end

swizzle = function(self, val)
  -- construct a row matrix based on our type
  local kind = make_matrix_kind(ct, 1, #val);
  
  local res = kind();
  local offset = 0;
  for idx in swizzlit(val) do
    res:set(0,offset, self.data[idx]);
    offset = offset + 1;
  end
  return res;
end

First off, the swizzlemap table is the mapping between the single character symbols, and the array offsets within a data structure. This is according to glsl convention. I’ve implemented the swizzlit() thing as an iterator. It will basically just map from a single character in the string to the index value from the table. At first I was going to do this as a table that was returned, but I figured, why bother creating a new table each time through this loop. It might be done a lot!

Pulling the swilling together, and the whole point of this article is the swizzle() function. The first thing it does is try to create a new kind of row vector! Thank goodness this has been taken care of previously. If not, then every single time through this function you’d get a new anonymous type. Just imagine swizzling the values of a bitmap image, converting from rgb to bgr, and how many millions of new anonymous types you’d generate! So this is why to type equivalence is dealt with up front.

The proof is in the pudding:

local mvec7 = math_matrix.make_matrix_kind(ffi.typeof("float"), 1, 7);
print("mvec7 kind: ", ffi.typeof(mvec7));
>mvec7 kind: ctype<struct 1306>


local swizzled = color1.xyrgstz;
print(swizzled);
>255, 13, 255, 13, 255, 13, 200

print("TYPE of SWIZ: ", ffi.typeof(swizzled));
>TYPE of SWIZ: ctype<struct 1306>

You can see that the type of the ffi.typeof(mvec7) == ffi.typeof(swizzled);

This is a very good thing indeed. There’s all sorts of equivalence I deal with in my day to day programming. Mostly it’s value equivalence, but sometimes it’s structural equivalence, and sometimes it’s type equivalence. Know which is what, and how to deal with each of them appropriately and thoughtfully has been quite an exercise for me. Dealing with the issues correctly though can mean the difference between poorly performant, buggy code, and highly performant ‘correct’ code.



Leave a comment