2

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()
4
  • 3
    You can't draw from multiple threads. Especially not from multiple processes. It's a seperate limitation in both OpenGL and SDL limitation. However, you can do your physics or logic in a separate thread or process from your rendering. Commented Aug 23, 2022 at 13:29
  • @mousetail how can I parallelize the loop which is storing rectangles in the array "rectangles[]" Commented Aug 23, 2022 at 13:49
  • 1
    You can try to build a separate list of rectangles in each process then concatenate them together at the end, but the performance benefit will be marginal at best. Commented Aug 23, 2022 at 13:50
  • 1
    Unless your rectangles are somehow complex to generate, the overhead of transferring the results back to the main window-loop will be much greater than just generating the rectangles in a single process. But if you want to try, maybe use multiprocessing.Array to create a huge list, then create N processes to populate N sections of that Array. There's a pretty easy example in the Python doco - docs.python.org/3/library/… Commented Aug 23, 2022 at 22:32

1 Answer 1

3

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()
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.