Search code examples
pythonmultithreadingpygletconcurrent.futures

Is there a faster way to draw on screen using pyglet and threading?


The three functions below show how I'm using the pyglet library to draw squares on the screen. The code works fine. But I want to make it run faster.

With my novice understanding of threading, I think the for-loop in draw_grid() can be made faster by using threads, as it waits for one square to draw using pos and then draws a new square. Since all_positions is already provided, is there a way to draw all squares simultaneously?

def draw_square(single_location):
    # draws a single square
    pyglet.graphics.draw_indexed(4, pyglet.graphics.gl.GL_TRIANGLES,
                             [0,1,2,1,2,3],
                             ('v2i', single_location))

def position_array():
    # returns an 2D array with each row representing the coordinates of the rectangle to draw
    ...relevant code...
    return all_positions

def draw_grid(all_positions):
    # uses draw_square function and all_positions to draw multiple squares at the specified locations.
    for pos in all_positions:
        draw_square(pos)

After looking up some videos on threading, I found the concurrent.futures library. So I tried to implement it and it gave me errors. So now I am stuck.

Here's how I used concurrent.futures.ThreadPoolExecutor() inside draw_grid()

def draw_grid(all_positions):
    # uses draw_square function and all_positions to draw multiple squares at the specified locations.
    with concurrent.futures.ThreadPoolExecutor as executor:
        for pos in all_positions:
        f1 = executor.submit(draw_square, pos)
        f1.result()

Solution

  • As a general rule, never mix threads and graphical rendering. It is a recepie for disaster. It is possible to utilize threads in combination with rendering (GL). But again, it's not recommended.

    Instead, what you probably want to have a look at is batched rendering.
    There's great documentation on Batched rendering which you'll probably find easy to understand.

    Just remember that, if you want to modify the vertices after adding them to a patch, you need to store them and modify the return from the batch, don't try to manipulate the batch directly.

    vertex_list = batch.add(2, pyglet.gl.GL_POINTS, None,
        ('v2i', (10, 15, 30, 35)),
        ('c3B', (0, 0, 255, 0, 255, 0))
    )
    # vertex_list.vertices <-- Modify here
    

    The reason for why you don't want to use threads, is that there's almost a 99.9% guarantee that you'll end up with a segmentation fault due to a race condition. Where something is trying to update the screen while you're rendering the things you're trying to manipulate.

    More info in the comments here: Update and On_Draw Pyglet in Thread

    So instead, add all your squares to one batch, and do batch.draw() and it will simulatainiously draw all squares in one pass. Instead of wasting CPU cycles calling functions and re-create the squares each time and then render them one by one.

    batch = pyglet.graphics.Batch()
    batch.add(2, pyglet.gl.GL_TRIANGLES, None,
        ('v2i', [0,1,2,1,2,3])
    )
    
    def on_draw():
        batch.draw()
    

    Something along the lines of this. But obviously you'd wanat to create squares in different positions etc. But as a guideline, create the batch and the squares outside the rendering logic, and then call .draw() on the batch in the render cycle.