-- Leo's gemini proxy

-- Connecting to gemlog.blue:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini

         _.-~--~.
       .'.:::::::`.   Petros Katiforis (Πέτρος Κατηφόρης)
      /.::::::    /
     /.:::  .---=*
     ;.::  /  _~~_    Want to ask any question or perhaps share your thoughts?
     ;    |   C ..\   Feel free to contact me! <pkatif@mail.com>
     |    ;   \  _.)
      \   |   /  \    This post was published on the 6th of August, 2023
       *~. \ / \)\)
          `-|   ) /
            '--*-*

Back to Home


Optimizing Drawing Software


About a month ago I created a simple pixel art drawing program using C and SDL2. In the process, as I had intended, I taught myself quite a lot about optimizing code, using the correct data structures and trying to squeeze as much performance as possible from a rendering library. This is an overview of what I learned.


Introduction to Psyfidot


I named the software "Psyfidot" and released it under the GPLv3 license as a brand new codeberg repository[1]. Frankly, the initial version was laggy. When the user was trying to draw in a big canvas it would freeze up and its framerate would drop drastically. What is more, the initial implementation of the bucket fill tool was recursive and thus it would just error out with a "max recursion depth exceeded" error when applied to images of more than a certain amount of pixels.


Psyfidot's Codeberg Repository


Optimizations

Avoid redrawing the whole display


After many hours and days of trying to figure out how I could fix it and plenty of paper diagrams, I decided to introduce a new queue data structure that would hold the indices of the pixels that were modified during the last action. So, right after any tool was successfully applied and the image data had been altered, instead of looping over the whole array of pixels and re-rendering every single one of them, I just deleted the line that cleared the screen and made it so I only draw the modified pixels on top of their former color. This is what it looks like in code:


// canvas.h
typedef struct
{
    queue_t undrawn_modifications;

    // Storing canvas data as an array of pixels
    // In the actual program the current state is held in the canvas history array
    // but that is completely irrelevant right now
    pixel_t *canvas_state;
} canvas_t;

// main.c
for (;;)
{
    // ... event handling and palette rendering code

    // Notice how there is no call to SDL_RenderClear()
    while (!queue_is_empty(&canvas.undrawn_modifications))
    {
        render_canvas_pixel(&canvas.canvas_state, queue_pop(&canvas.undrawn_modifications));
    }

    SDL_RenderPresent(renderer);
}

Only apply the brush when it is absolutely needed


I also optimized the mouse motion handling code quite a fair bit. Naively, my code used to apply the brush continuously when the mouse moved inside a zoomed pixel but didn't yet exit from its boundaries. This caused brushes being applied a hundred times a second without any visual changes whatsoever! As a solution, I introduced a new variable to keep track of the index of the last pixel in focus and prevent the handler from executing code if no substantial movement was done. Again, as simple as that may seem and is, it offered a great performance boost in later versions.


void handle_mouse_motion(SDL_Event *event)
{
    // Check if mouse is outside the pixel grid
    if (!is_on_canvas(event->motion.x, event->motion.y)) return;

    size_t index = window_to_pixel_index(event->motion.x, event->motion.y);

    // If the user hasn't yet moved to a new pixel, don't bother applying the tool
    if (latest_pixel_index == index)
    {
	return;
    }

    latest_pixel_index = index;

    // Don't apply the brushes if the left mouse is not being held
    if (!is_holding_left_mouse) return;

    drawing_while_holding_mouse = true;

    canvas_apply_brush(&canvas, canvas.active_brush, index);

    return;
}

-- Response ended

-- Page fetched on Sun Jun 2 09:42:41 2024