Contributing to Pygame

In the past I’ve used python to create small programs to interface with my hardware projects like EOGee and ShArc. I use the Matplotlib animation class to plot live data, however I’ve found this to be very slow, typically only updating a few times a second. To help remedy this I’ve begun using the Pygame library. Pygame is an open source python library for developing games – it provides a simple structure for running a main loop while drawing to the screen, and update rates of 60Hz are easily achievable.

While using the Pygame library for developing a ShArc demo, I came across a shortcoming in the function for drawing arcs, which I was using the visualise the shape of the sensor.

When drawing an arc of very large radius, I found that the arc typically would not be completely drawn. This is illustrated in the following image which shows four arcs which should be connecting to each other, however clearly there are large gaps between them. If the arc was short enough, with large enough radius, it may not be drawn at all.

Four arcs do not connect due to the bug

Looking on the Pygame GitHub, the bug was more or less exactly where I expected it to be. The function for drawing arcs was actually a C-function in a python wrapper and was located in draw.c:

static void
draw_arc(SDL_Surface *surf, int x, int y, int radius1, int radius2,
         double angle_start, double angle_stop, Uint32 color, int *drawn_area)
{
    double aStep;  // Angle Step (rad
    double a;      // Current Angle (rad)
    int x_last, x_next, y_last, y_next;

    // Angle step in rad
    if (radius1 < radius2) {
        if (radius1 < 1.0e-4) {
            aStep = 1.0;
        }
        else {
            aStep = asin(2.0 / radius1);
        }
    }
    else {
        if (radius2 < 1.0e-4) {
            aStep = 1.0;
        }
        else {
            aStep = asin(2.0 / radius2);
        }
    }

    if (aStep < 0.05) {
        aStep = 0.05;
    }

    x_last = (int)(x + cos(angle_start) * radius1);
    y_last = (int)(y - sin(angle_start) * radius2);
    for (a = angle_start + aStep; a <= angle_stop; a += aStep) {
        int points[4];
        x_next = (int)(x + cos(a) * radius1);
        y_next = (int)(y - sin(a) * radius2);
        points[0] = x_last;
        points[1] = y_last;
        points[2] = x_next;
        points[3] = y_next;
        draw_line(surf, points[0], points[1], points[2], points[3],
                  color, drawn_area);
        x_last = x_next;
        y_last = y_next;
    }
}

The function takes the parameters you might expect a function for drawing arcs to require – the location of the arc, the radius of the arc and the start and end angles of the section of the arc to draw. The code then tries to select a reasonable value for aStep which is the angular step used to create the arc. This is because the arc is actually constructed from a number of straight lines, each spanning a small section of the arc.

We can see that if the angular step is too small (less than 0.05 radians) then the angle will be rounded up to 0.05 radians. In most cases this is probably fine, but if you are drawing a short arc with a large radius the total arc may be less than 0.05 radians in length.

The function then loops over the value a from the start angle value to the end angle value in steps of aStep. However, if the total angular span of the desired arc is less than aStep then this loop would never execute, resulting in nothing being drawn. In a less extreme case, if the arc was a non-integer multiple of aStep, the arc would only be partially drawn as the final segment of the arc would never be drawn. This is what is happening in the image above – the final segment of each arc is never drawn, resulting in gaps at the end of each arc.

My solution to this only changed three lines. The first was to change the for loop:

for (a = angle_start + aStep; a < aStep + angle_stop; a += aStep)

Rather than ending if a was larger than angle_stop, my function will continue past angle_stop to a value within aStep larger than angle_stop. This allows the value of a to overshoot angle_stop slightly and ensures that the loop is completed enough times to draw the full arc (always at least once).

However if this overshoot occurred, we would then be drawing an arc that is too long.

x_next = (int)(x + cos(MIN(a, angle_stop)) * radius1);
y_next = (int)(y - sin(MIN(a, angle_stop)) * radius2);

In order to resolve this I added a MIN function to the lines where a was used to ensure that if a was larger than angle_stop we use the value of angle_stop instead. This ensures that the arc ends where it is expected to end.

In order to implement this fix I forked the Pygame GitHub repo and cloned a local copy. After installing a few necessary dependencies listed on wiki page, I ran the setup.py script included in the GitHub. This installed my modified version of Pygame to my python installation and I verified that the code was successful.

Surprisingly this worked first time and my arcs were now drawing correctly

Arcs no longer have a gap between them

I could then submit a pull-request to pull my fix back into the main Pygame repository. After a few days this was approved and now my code is in the main repository.

My fix is in no way impressive or complex, but it was quite rewarding to go from finding a problem, to fixing the problem, to submitting the fix and it being accepted all within a few days – this is the first time I’ve contributed to an open source library.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s