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:

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.

So, you want to make a gui in C

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...

Polygon visualization

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.

<< Back

Sound interesting? See if we're hiring!