How to use multiprocessing in pygame

Viewed 64

I am trying to print a million rectangles in 2-3 seconds, however, I can not understand how to use parallelization with GUI.

    class Rectangle:
        def __init__(self, pos, color, size):
            self.pos = pos
            self.color = color
            self.size = size
        def draw(self):
            pygame.draw.rect(screen, self.color, Rect(self.pos, self.size))

I want to use something like open MP here to make this process faster

    rectangles = []
    for i in range(1000000):
        random_color = (randint(0,255), randint(0,255), randint(0,255))
        random_pos = (randint(0,639), randint(0,479))
        random_size = (639-randint(random_pos[0], 639), 479-randint(random_pos[1],479))
    
        rectangles.append(Rectangle(random_pos, random_color, random_size))
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
        screen.lock()
        for rectangle in rectangles:
          rectangle.draw()
        screen.unlock()
        pygame.display.update()
1 Answers

Here's an example that generates random rectangles in sub-processes. It's done this way, because it's not possible to draw to the main PyGame surface from anything other than the main thread/process. So if you want to generate items in real-time, do so, but then notify the main-thread somehow to include them. Perhaps by posting a PyGame Event.

The approach taken is to declare a shared subprocess.manager.list for the rectangles, then split the generation. The function rectGenerator() is given a position in the list from which to start, and a count to stop at.

With rectGenerator() as the target, /N/ sub-processes are created. We then loop waiting for the processes to all finish. A nicer approach might be to smarter about this and only paint the blocks that are complete.

Running on my workstation, it takes a while to paint one million rectangles (~10 seconds). To see for yourself, change BLOCK_SIZE to 100000. Probably this could be optimised (it was my first time using multiprocessing for some years), but I'd rather make the code illustrative.

So as I said in my comment, for simple rectangles it's much faster to just do it without sub-processes. But if each sub-process was doing some kind of CPU-bound complex rendering (fractal generating, image filtering, ray-tracing, etc. etc.) then this is a good way to spread the workload amongst your CPUs/cores.

Further reading: The python GIL.

one million rectangles (colour-space reduced)

#! /usr/bin/env python3

import pygame
import random
import multiprocessing

CHILDREN    = 10       # processes to use
BLOCK_SIZE  = 1000     # rectangles per process
RECT_COUNT  = BLOCK_SIZE * CHILDREN  # ensure an even factor / subprocess

# Window size
WINDOW_WIDTH      = 800
WINDOW_HEIGHT     = 800

def randomColour():
    """ Generate a random "non dark" colour """
    MIN = 80  # minimum brightness of colour channel
    MAX = 255
    return ( random.randint( MIN, MAX ), 
             random.randint( MIN, MAX ),
             random.randint( MIN, MAX ) )

def randomRect( border=5 ):
    """ Generate a random PyGame Rect, within the window bounds """
    MAX_HEIGHT = WINDOW_HEIGHT - border 
    MAX_WIDTH  = WINDOW_WIDTH  - border 
    # Generate a rect that stays within the screen bounds
    x1 = random.randint( border, MAX_WIDTH )
    y1 = random.randint( border, MAX_HEIGHT )
    x2 = random.randint( border, MAX_WIDTH )
    y2 = random.randint( border, MAX_HEIGHT )
    if ( x2 < x1 ):
        xswap = x2
        x2 = x1
        x1 = xswap
    if ( y2 < y1 ):
        yswap = y2
        y2 = y1
        y1 = yswap
    return pygame.Rect( x1, y1, x2-x1, y2-y1 )


class colourRect:
    """ Simple class for holding components of a coloured rectangle """
    def __init__( self ):
        self.rect   = randomRect()
        self.colour = randomColour()

    def draw( self, surface ):
        pygame.draw.rect( surface, self.colour, self.rect, 1 )

    def __str__( self ):
        s = "rect[ %d,%d w=%d, h=%d ], colour[ %d, %d, %d ]" % ( self.rect.x, self.rect.y, self.rect.width, self.rect.height, self.colour[0], self.colour[1], self.colour[2] )
        return s


###
### Multiprocessing Target Function
###
def rectGenerator( mp_list, index_from, count ):
    """ Populate a section of the mp_list with randomly sized and coloured 
        rectangles, starting from the index, for the given count """
    for i in range( count ):
        mp_list[ index_from + i ] = colourRect()



###
### MAIN
###

pygame.init()
window  = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.HWSURFACE )
pygame.display.set_caption("Plenty o' Rectangles")

# Make a *Huge* list of rectangles
# Note: this runs much faster than using multiprocessing
#all_rects = [ None for i in range( RECT_COUNT ) ]   # pre-allocate list
#rectGenerator( all_rects, 0, RECT_COUNT )

print( "### Setting-up Generation" )
manager = multiprocessing.Manager()
all_rects = manager.list( range( RECT_COUNT ) )   # pre-allocate list
subprocesses = []    # keep the process handles so we can watch them

print( "### Starting Generation" )

for i in range( CHILDREN ):
    # each subprocess populates a separate region of the rectangle array
    p = multiprocessing.Process( target=rectGenerator, args=( all_rects, i*BLOCK_SIZE, BLOCK_SIZE ) ) 
    p.start()
    subprocesses.append( p )

print( "### Waiting for Generation" )

# Wait for the generation to complete
found_running = True
while ( found_running ):
    found_running = False
    for p in subprocesses:
        if ( not p.is_alive() ):
            p.join()
        else:
            found_running = True        

print( "### Generation Complete" )
print( "--------------------------" )

        
    

# Main loop
clock = pygame.time.Clock()
running = True
while running:
    time_now = pygame.time.get_ticks()
    # Handle user-input
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            running = False

    # Paint the window with rects, a lot of rects takes a while
    print( "### Painting starts" )
    window.fill( ( 0,0,0 ) )
    for r in all_rects:
        r.draw( window )
    pygame.display.flip()
    print( "### Painting ends" )

    # Clamp FPS
    clock.tick(2)  #slow

pygame.quit()
Related