0

I'm trying to change my screen in Pygame pixel by pixel. I know I shouldn't be doing this because it is not efficient, but I'm just trying to build my own version of a 3D engine for (fun) learning purposes.

In the code below I noticed that Pygame can change the entire screen very quickly (METHOD=1), the pixel change methods are significantly slower. I tried using directly Surface.fill (METHOD=2) but that was VERY slow. Then I tried (METHOD=3) pygame.surfarray.blit_array and blit is very fast but my double loop in python is quite slow. It feels strange to me because it is a simple double loop filling a numpy array with integers, I assumed it wouldn't be the bottleneck.


EDIT: By @BlackJack suggestion, I used METHOD=4 that basically sends the loop into a Cython module. I followed the instalation as in: Cython with pythonxy and I followed the optimization techniques showed in http://docs.cython.org/src/userguide/numpy_tutorial.html

The loop timing improved radically! 100 X's faster! If I look at how many FPS I can manage I see:

  • METHOD 2: 01.89 fps
  • METHOD 3: 04.83 fps
  • METHOD 4: 444.4 fps

Below is my working example (assuming Cython is properly installed and working, which is not a trivial thing). There is no need to precompile the module because I'm using pyximport


PyGameTest.py

from __future__ import division
import pygame, random, time
import numpy as np
import pyximport
pyximport.install(setup_args={'include_dirs': np.get_include()})
import FastLoops

#n = r*256^2 + g*256 + b
IRED=256*256*150
IGRN=256*150
IBLU=150
arr=[IRED,IGRN,IBLU]

BLK=0

RESX=600
RESY=600

TARGFPS=60

# initialize game engine
pygame.init()
# set screen width/height and caption
Surface = pygame.display.set_mode([RESX, RESY])
pygame.display.set_caption('My Game')
# initialize clock. used later in the loop.
clock = pygame.time.Clock()
 
METHOD=4

i0=0
t0=time.time()
nl=0
# Loop until the user clicks close button
done = False
while done == False:
    # write event handlers here
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    if METHOD==1:   
        col=arr[random.randint(0,2)]
        Surface.fill(col)
        Surface.fill(BLK,pygame.Rect(i0,i0,10,10))
    
    elif METHOD==2:
        for i in range(RESX):
            if i<i0:
                col=IBLU
            else:
                col=IGRN
            for j in range(RESY):
                Surface.fill(col,pygame.Rect(i,j,1,1))
      
    elif METHOD==3:
        myarray = np.zeros([RESX, RESY], dtype=np.int32)
        for i in range(RESX):
            if i<i0:
                col=IBLU
            else:
                col=IGRN
            for j in range(RESY):
                myarray[i,j]=col
        pygame.surfarray.blit_array(Surface,myarray)

        
    elif METHOD==4:
        myarray = FastLoops.Loop(RESX,RESY,IBLU,IRED,i0)
        pygame.surfarray.blit_array(Surface,myarray)

    nl+=1     
    i0+=RESX/TARGFPS
    if i0>=RESX:
        t1=time.time()
        fps=round(nl/(t1-t0),2)
        print 'METHOD '+str(METHOD)+':'
        print '    Time = ' + str(round(t1-t0,2)) + 's for ' + str(nl) + ' frames'
        print '    FPS  = '+str(fps)
        i0=0
        t0=time.time()
        nl=0
        
    # display what’s drawn. this might change.
    pygame.display.update()
    # run at 20 fps
    clock.tick(TARGFPS)
 
# close the window and quit
pygame.quit()

FastLoops.pyx

from __future__ import division
import numpy as np
cimport numpy as np
cimport cython

DTYPE = np.int32
ctypedef np.int32_t DTYPE_t

@cython.boundscheck(False) # turn of bounds-checking for entire function
def Loop(int RESX, int RESY, long IBLU, long IGRN, int i0):
    cdef int i, j
    cdef np.ndarray[DTYPE_t, ndim=2] myarray = np.zeros([RESX, RESY], dtype=DTYPE)
    for i in range(RESX):
        if i<i0:
            col=IBLU
        else:
            col=IGRN
        for j in range(RESY):
            myarray[i,j]=col
            
            
    return myarray
Community
  • 1
  • 1
Miguel
  • 1,293
  • 1
  • 13
  • 30

2 Answers2

1

It is to be expected that a pure python loop is much slower than the calls to functions or methods implemented in C and maybe even accelerated by graphics hardware.

Try to use Numpy methods if possible because those are implemented in C or use libraries implemented in C or Fortran.

If you really need single pixel level access you may try to write the slow parts in Cython and compile that code to an extension module for Python.

BlackJack
  • 4,476
  • 1
  • 20
  • 25
  • Thank you for your answer. I think Cython will be the way to go if I still want to stay within the python world. I'm having trouble getting it to work but I'll open a new question for it here: http://stackoverflow.com/questions/32171369/cython-with-pythonxy – Miguel Aug 23 '15 at 20:46
0

Disclaimer: I am one of the developers of the Pythran project.

If Cython performance is not enough, you can give Pythran a try. On your particular test case, the following code:

import numpy as np

#pythran export Loop(int, int, int, int, int)
def Loop(RESX, RESY, IBLU, IGRN, i0):
    myarray = np.empty((RESX, RESY), dtype=np.int32)
    for i in range(RESX):
        if i < i0:
            col = IBLU
        else:
            col = IGRN
        myarray[i, :] = IBLU if i < i0 else IGRN

    return myarray

gave me a x2 speedup on Cython.

Using numpy style (and it could be improved)

import numpy as np

#pythran export Loop(int, int, int, int, int)
def Loop(RESX, RESY, IBLU, IGRN, i0):
    myarray = np.empty((RESX, RESY), dtype=np.int32)
    for i in range(RESX):
        myarray[:, :] = IBLU if i < i0 else IGRN

    return myarray

gave me a x3 speedup.

Samuel Lelièvre
  • 3,212
  • 1
  • 14
  • 27
serge-sans-paille
  • 2,109
  • 11
  • 9