Experimental Cures for Flattened Register Definitions in vr_ad, Part 2

We've already talked about how to handle flattened register definitions from a modeling point of view in this post. This other post also showed us that accessing multiply instantiated registers is a bit of a challenge, even when they are defined properly. Let's add the missing piece of the puzzle now and have a look at how to easily access flattened registers.

Let's apply the same idea from the previous post and use macros. This post is actually less experimental as I've already used this approach on my current project.

Let's start out with the register definitions. We'll use our trusty graphics processing engine that can handle triangles:

<'
extend vr_ad_reg_file_kind : [ GRAPHICS ];
extend GRAPHICS vr_ad_reg_file {
keep size == 256;
post_generate() is also {
reset();
};
};

reg_def TRIANGLE0 GRAPHICS 0x00 {
reg_fld SIDE0 : uint(bits : 8);
reg_fld SIDE1 : uint(bits : 8);
reg_fld SIDE2 : uint(bits : 8);
};

reg_def TRIANGLE1 GRAPHICS 0x4 {
reg_fld SIDE0 : uint(bits : 8);
reg_fld SIDE1 : uint(bits : 8);
reg_fld SIDE2 : uint(bits : 8);
};

reg_def TRIANGLE2 GRAPHICS 0x8 {
reg_fld SIDE0 : uint(bits : 8);
reg_fld SIDE1 : uint(bits : 8);
reg_fld SIDE2 : uint(bits : 8);
};
'>

Here's how we would access a single triangle:

<'
extend MAIN vr_ad_sequence {
!triangle0 : TRIANGLE0 vr_ad_reg;

body() @driver.clock is only {
write_reg triangle0 {
.SIDE0 == 1;
.SIDE1 == 2;
.SIDE2 == 3;
};
};
};
'>

If we would want to access TRIANGLE1 we would need to use a field of type TRIANGLE1 vr_ad_reg, but the code would otherwise stay the same. We don't need to use any static_item, because each register type is unique (which is exactly our problem). You can already see that creating a generic sequence that can handle any triangle is going to become a mess of case statements and doubled up code for the constraints.

We can fix that by using a macro on top of write_reg. We can call this macro on any field of type TRIANGLE0, TRIANGLE1, etc. and pass the desired instance as an argument. This is how we would write to TRIANGLE1:

<'
extend MAIN vr_ad_sequence {
!triangle : TRIANGLE0 vr_ad_reg;

body() @driver.clock is only {
write_triangle_reg 1 triangle {
.SIDE1 == 1;
};
};
};
'>

The macro would need to generate triangle with the appropriate constraint and execute an access to TRIANGLE1. Here's the macro body:

<'
define <write_triangle_reg'action>
"write_triangle_reg <idx'exp> <reg'exp>[ <any>]" as computed
{
var los : list of string;
los.add( "{");
los.add(appendf("var temp_triangle : typeof(%s);", <reg'exp>));

if <any> != "" {
los.add(appendf("gen temp_triangle keeping %s;", <any>));
}
else {
los.add( "gen temp_triangle;");
};

los.add( "var access_triangle : vr_ad_reg = new with {;");
los.add( append(".kind = appendf(\"TRIANGLE%d\", ", <idx'exp>, ").as_a(vr_ad_reg_kind);"));
los.add( "};");
los.add( "write_reg access_triangle val temp_triangle.read_reg_rawval();");
los.add( "};");

result = str_join(los, "\n");
};
'>

We generate a temporary register of the same type as the one we get passed in. Since all triangles have the same fields, it's irrelevant if the triangle field we passed in is of type TRIANGLE0, TRIANGLE1, etc. For the call to write_reg we need to use a variable of the appropriate type. We extract this type from the <idx'exp> argument by concatenating it to the string "TRIANGLE" and converting that to a vr_ad_reg_kind. As the write value we use the contents of the temporary field we just generated.

This should remind us that in this form our macro won't work in all cases, particularly when trying to use the val <val> syntax:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
write_triangle_reg 1 triangle val 0x010101;
};
};
'>

This will give us a cryptic compile error. To do away with it we need to fix our macro body:

<'
define <write_triangle_reg'action>
"write_triangle_reg <idx'exp> <reg'exp>[ <any>]" as computed
{
var los : list of string;
var is_val : bool = str_match(<any>, "/^val /");

los.add( "{");
los.add(appendf("var temp_triangle : typeof(%s) = new;", <reg'exp>));

if not is_val {
if <any> != ""{
los.add(appendf("gen temp_triangle keeping %s;", <any>));
}
else {
los.add( "gen temp_triangle;");
};
};

los.add( "var access_triangle : vr_ad_reg = new with {;");
los.add( append(".kind = appendf(\"TRIANGLE%d\", ", <idx'exp>, ").as_a(vr_ad_reg_kind);"));
los.add( "};");
if not is_val {
los.add( "write_reg access_triangle val temp_triangle.read_reg_rawval();");
}
else {
los.add(appendf("write_reg access_triangle %s;", <any>));
};
los.add( "};");

result = str_join(los, "\n");
};
'>

We need to filter out the case when using the val <val> syntax. In that case we don't need to generate our temporary register to use it as the write value. "This macro is getting a bit too complicated", you might say and you would be right, but this is the price we pay for not doing things properly from the start (i.e. not having flattened register definitions).

As we've previously seen, the code for the write_reg_fields and read_reg flavors of the macro will be pretty similar, so it makes sense to encapsulate and generalize the macro code inside a global function:

<'
extend global {
vgm__access_triangle_reg_body(operation : string,
idx : string, reg : string, block : string = "") : string is
{
var los : list of string;
var is_val : bool = str_match(block, "/^val /");

los.add( "{");
los.add(appendf("var temp_triangle : typeof(%s) = new;", reg));

if operation == "write_reg" {
if not is_val {
if block != ""{
los.add(appendf("gen temp_triangle keeping %s;", block));
}
else {
los.add( "gen temp_triangle;");
};
};
}
else if operation == "write_reg_fields" {
los.add(appendf("temp_triangle = new with %s;", block));
};

los.add( "var access_triangle : vr_ad_reg = new with {;");
los.add( append(".kind = appendf(\"TRIANGLE%d\", ", idx, ").as_a(vr_ad_reg_kind);"));
los.add( "};");

// writing/reading
if operation != "read_reg" {
if operation != "write_reg" or not is_val {
los.add( "write_reg access_triangle val temp_triangle.read_reg_rawval();");
}
else {
los.add(appendf("write_reg access_triangle %s;", block));
};
}
else {
los.add( "read_reg access_triangle;");
los.add(appendf("if %s == NULL {", reg));
los.add(appendf("%s = new;", reg));
los.add( "};");
los.add(appendf("%s.write_reg_rawval(access_triangle.read_reg_rawval());", reg));
};

los.add( "};");

result = str_join(los, "\n");
};
};
'>

Our macro bodies will just contain calls to this function:

<'
define <write_triangle_reg'action>
"write_triangle_reg <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_triangle_reg_body("write_reg", <idx'exp>, <reg'exp>, <any>);
};

define <write_triangle_reg_fields'action>
"write_triangle_reg_fields <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_triangle_reg_body("write_reg_fields", <idx'exp>, <reg'exp>, <any>);
};

define <read_triangle_reg'action>
"read_triangle_reg <idx'exp> <reg'exp>" as computed
{
result = vgm__access_triangle_reg_body("read_reg", <idx'exp>, <reg'exp>);
};
'>

Here's the write_reg_fields flavor in action:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
write_triangle_reg 1 triangle val 0x010101;
};
};
'>

And here's the read_reg flavor in action:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
read_triangle_reg 2 triangle;
};
};
'>

What we have up to now works fine for triangles, but as you can remember our graphics engine can also process circles:

<'
reg_def CIRCLE0 GRAPHICS 0x10 {
reg_fld RADIUS : uint(bits : 8);
};

reg_def CIRCLE1 GRAPHICS 0x14 {
reg_fld RADIUS : uint(bits : 8);
};

reg_def CIRCLE2 GRAPHICS 0x18 {
reg_fld RADIUS : uint(bits : 8);
};
'>

We need to generalize our macro to handle any type of register. We can use string matching to separate the register type from its instance number. For example, CIRCLE0 is composed of CIRCLE and 0. Once we extract the 0 from the end we can append the appropriate index given as an input to the macro. Here's a snippet that does exactly this:

<'
var reg_kind_str := reg.kind.as_a(string);
assert str_match(reg_kind_str, "/(.*)(\\d+)$/");
reg_kind_str = appendf("%s%d", $1, idx);
'>

We match everything until the end of the string, where we expect to see at least one numeric character. The first match group is stored in $1 (a built-in variable), to which we append the desired index. We integrate this code into the global function that returns the macro body:

<'
extend global {
vgm__access_graphics_reg_body(operation : string,
idx : string, reg : string, block : string = "") : string is
{
var los : list of string;
var is_val : bool = str_match(block, "/^val /");

los.add( "{");
los.add(appendf("var temp_reg : typeof(%s) = new;", reg));

if operation == "write_reg" {
if not is_val {
if block != ""{
los.add(appendf("gen temp_reg keeping %s;", block));
}
else {
los.add( "gen temp_reg;");
};
};
}
else if operation == "write_reg_fields" {
los.add(appendf("temp_reg = new with %s;", block));
};

los.add(appendf("if %s == NULL {", reg));
los.add(appendf("%s = new;", reg));
los.add( "};");
los.add(appendf("var reg_kind_str := %s.kind.as_a(string);", reg));
los.add( "assert str_match(reg_kind_str, \"/(.*)(\\d+)$/\");");
los.add( append("reg_kind_str = appendf(\"%s%d\", $1, ", idx, ");"));
los.add( "var access_reg : vr_ad_reg = new with {;");
los.add( ".kind = reg_kind_str.as_a(vr_ad_reg_kind);");
los.add( "};");

// writing/reading
if operation != "read_reg" {
if operation != "write_reg" or not is_val {
los.add( "write_reg access_reg val temp_reg.read_reg_rawval();");
}
else {
los.add(appendf("write_reg access_reg %s;", block));
};
}
else {
los.add( "read_reg access_reg;");
los.add(appendf("%s.write_reg_rawval(access_reg.read_reg_rawval());", reg));
};

los.add( "};");

result = str_join(los, "\n");
print result;
};
};
'>

As before, the macros just call this function:

<'
define <write_graphics_reg'action>
"write_graphics_reg <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_graphics_reg_body("write_reg", <idx'exp>, <reg'exp>, <any>);
};

define <write_graphics_reg_fields'action>
"write_graphics_reg_fields <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_graphics_reg_body("write_reg_fields", <idx'exp>, <reg'exp>, <any>);
};

define <read_graphics_reg'action>
"read_graphics_reg <idx'exp> <reg'exp>" as computed
{
result = vgm__access_graphics_reg_body("read_reg", <idx'exp>, <reg'exp>);
};
'>

Here's the macro being used to write to CIRCLE1:

<'
extend MAIN vr_ad_sequence {
!circle : CIRCLE0 vr_ad_reg;

body() @driver.clock is only {
write_graphics_reg 1 circle { .RADIUS == 5 };
};
};
'>

One of the main reasons why we wanted a generic way of accessing the registers is, of course, being able to do loops. Using the macro we can write all triangle registers:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
for i from 0 to 2 {
write_graphics_reg i triangle {
.SIDE0 == 3;
.SIDE1 == 3;
.SIDE2 == 3;
};
};
};
};
'>

We can also easily read all circle registers:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
for i from 0 to 2 {
read_graphics_reg i circle;
};
};
};
'>

The macros could also be converted to operate in multiple dimensions (i.e. to handle multiple instances of the GRAPHICS register file, etc.) by just updating the extraction of the vr_ad_reg_kind variable. We won't look at that here. If you want to get started with them, you can find the code on SourceForge, including the complete testing harness I used for development.

Using this approach we can easily access flattened registers in a generic way, allowing us to write portable sequences to complement the nice reference models we've learned to code last time. We pay for having flattened definitions with more complicated macro code, but the internal implementation of these macros is easy to change once we swap out the flattened model for a correctly defined one.

Happy register accesses and see you next time!

Comments