GDB Custom Commands: Polygon Visualization
Introduction
It's late. You're sitting hunched over a monitor in a cold, dark room. It's been hours since you started investigating a mysterious bug, but you've tracked it down to an arcane function performing various geometric transformations. Some of the polygons don't quite seem right. But wait! It's an array of vertices, so we can use the command from last time to examine them.
(gdb) pa vertices
$2 = { {x = 337.106201, y = -179.005585}, {x = 304.106201, y = -179.005585},
{x = 304.106201, y = -219.666870}, {x = 246.719818, y = -219.666870},
{x = 246.719818, y = -246.666870}, {x = 264.499939, y = -246.666870},
{x = 264.499939, y = -279.666870}, {x = 364.106201, y = -279.666870},
{x = 364.106201, y = -156.887070}, {x = 337.106201, y = -156.887070} }
If, like me, you have not evolved the capability of immediately understanding this output, you might have a few options:
- Tracing out the path with your finger. Low chance of success, high chance of soliciting bewildered stares from your colleagues.
- Drawing out the path on paper. Unfortunately, you have no paper, since it would ruin the minimalist modern aesthetic of your chic co-working space.
- Uh, use a computer? Supposedly those are good at rendering polygons (well, triangles, but close enough).
So, let's begin a new project by muttering the magical incantation "there must be a better way to do this..."
The Bridge
In order to do interesting things to help us understand this data, we're first going to get it out of Python. At TestFit, we've already built tons of utilities for math operations and rendering in C, so it'd be frustrating to rebuild (or embed) everything in Python. Thus, our custom command looks pretty simple:
import os
class VisPoly(gdb.Command):
def __init__(self):
gdb.Command.__init__(self, "vis_poly", gdb.COMMAND_DATA, gdb.COMPLETE_SYMBOL, True)
def invoke(self, arg, from_tty):
args = gdb.string_to_argv(arg)
if len(args) < 1:
print("Need polygon to print")
return
polygon = gdb.parse_and_eval(args[0])
os.system('echo %s | ./vis_poly &' % polygon)
VisPoly()
Nothing here should be surprising for those familiar with the previous command we built. Instead of processing the data in Python and hooking back into GDB, we're going to farm it out using stdin/stdout
to a separate executable we've built. This means we'll have to add a build step, but dynamic languages drive me towards insanity, so I think it's an ok trade-off. Note that we're going to run this program in the background so that it doesn't block further execution of GDB, allowing us to run this command multiple times for different polygons.
Finally back in C
Oh, wait, not so fast.
Unfortunately, there's no standard when it comes to making GUI's in C. It would be infeasible to both keep this post concise and provide a full code sample. I also don't want to spend time explaining all the math, since that's not really the point here. And, as is customary, error handling has been left as an exercise for the reader. With that in mind, here are the key pieces we'll need.
First up, my humble attempt at a terse parser for GDB's print format.
array(v2f) vertices = array_create();
v2f v = {0};
char buf[4] = {0};
assert(fgetc(stdin) == '{'); /* TODO: replace asserts with real error handling */
do {
assert(fscanf(stdin, "{x = %f, y = %f}", &v.x, &v.y) == 2);
array_append(vertices, v);
} while (fgets(buf, 3, stdin) && strcmp(buf, ", ") == 0);
assert(buf[0] == '}');
assert(array_sz(vertices) >= 3);
It's not much, but it's honest work. I'm sure that TODO
will never make it into production, right?
v2f border = {20, 20};
box2f bbox = polygon_bounding_box(vertices);
v2f window_dimensions = v2f_add(box2f_get_extent(bbox), border);
Creating a full screen window probably isn't ideal. The preceding snippet should provide appropriate window dimensions that include a small border, ensuring the polygon's edges aren't drawn too close to the edges of the window. Additionally, while the polygon may be anywhere in 2D space, our gui framework has the origin in the bottom left corner of the window. You may need to change this part if your coordinate system has the origin in the top right corner or the center of the window.
polygon_translate(poly, v2f_inverse(box2f_get_center(bbox)));
polygon_translate(poly, v2f_scale(window_dimensions, 0.5f));
After creating a window in your framework of choice, all that's left is to render the polygon.
for (u32 i = 0, n = array_size(vertices); i < n; ++i) {
v2f v0 = vertices[i], v1 = vertices[(i+1)%n];
sprintf(buf, "%u", i);
gui_line(gui, v0.x, v0.y, v1.x, v1.y, orange);
gui_text(gui, v0.x, v0.y, buf, white);
}
Showing the vertex indices alongside to the polygon edges helps inspect specific vertices by index, and it gives us an easy way to check the winding.
The Waterworks
(gdb) pa vertices
$1 = 10
$2 = { {x = 337.106201, y = -179.005585}, {x = 304.106201, y = -179.005585},
{x = 304.106201, y = -219.666870}, {x = 246.719818, y = -219.666870},
{x = 246.719818, y = -246.666870}, {x = 264.499939, y = -246.666870},
{x = 264.499939, y = -279.666870}, {x = 364.106201, y = -279.666870},
{x = 364.106201, y = -156.887070}, {x = 337.106201, y = -156.887070} }
(gdb) vis_poly $2
Now, if everything worked correctly, a new window should pop up and...
Tears begin to roll. You're not sure if it's due to the pure joy at how much better your life will be going forward or from the abject horror at how stupid you were to wait this long. Whatever your tech stack is, invest in your tools, folks.
I didn't cover them here, but we've made a few additions to the visualization over time. We're using the ID's from GDB ($1, $2, etc) as the window's title so that multiple visualizations can be mapped back to the source data more easily. To help when examining small polygons, the polygon will expand (without changing the aspect ratio) if the window is resized.
Sound interesting? See if we're hiring!