renpy / pygame_sdl2

Reimplementation of portions of the pygame API using SDL2.
GNU Lesser General Public License v2.1
329 stars 66 forks source link

Pygame_SDL2 runs slower when large quantity of sprites are drawn #36

Closed runedevros closed 5 months ago

runedevros commented 8 years ago

Introduction

I noticed that Pygame SDL2 seems to be slower when many sprites are drawn on the screen. I created a program a while ago called bullet stress test in order to test the limits of the number of sprites that can be drawn while keeping the framerate up. In this program, a set of bullet images are drawn on the screen at random positions. Periodically 20 more are added, and the framerate is recorded until 2000 bullets are present. CProfile was used to track which function calls are the most costly.

Experimental Procedure

The test was run on the following system. I'm having some difficulty getting Pygame to run properly due to problems that OSX 10.11 introduced but the same behavior is seen in Pygame_SDL2 on OSX.

Python version:

#Sprite Test

sdl2_mode = True 

if sdl2_mode:
    try:
        import pygame_sdl2
        pygame_sdl2.import_as_pygame()

    except ImportError:
        print "OOPS Pygame SDL2 not available"

[... rest of program ...]

OS and Hardware:

Figure 1 shows the results of the recorded FPS as a function of number of bullets drawn to the screen. For Pygame 1.9.1 we are able to keep 60 FPS for up to 1000 bullets, dropping to 30 FPS once 2000 bullets are on the screen and the experiment is ended. However, for Pygame_SDL2, the FPS declines starting at roughly 250 bullets, and decreases to below 10 FPS by 2000 bullets.

figure_1 Figure 1. FPS as a function of number of bullets on the screen. While Pygame can maintain 60 FPS up to over 1000+ sprites, Pygame_SDL2's frame rate drops precipitously above 250 bullets.

Cprofile stats are shown in Table 1 and Table 2 for Pygame and Pygame_SDL2 respectively. For over 12 million calls to blit, the cumulative time is less than 40 seconds. In contrast, the cumulative time spent in blit for Pygame_SDL2 is on the order of 400 seconds, representing an order of magnitude increase in time spent in this function.

As for the specific implementation details, I do not know why this occurs in Pygame vs. Pygame_SDL2 but I hope this data will help you be able to make the library run faster.

Tue Feb 09 21:14:43 2016    pygame191.cprof

         36564382 function calls (36562557 primitive calls) in 126.736 seconds

   Ordered by: cumulative time
   List reduced from 896 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.005    0.005  126.737  126.737 sprites.py:3(<module>)
        1    0.443    0.443  126.625  126.625 sprites.py:30(main)
     6061   52.406    0.009   52.406    0.009 {pygame.display.update}
 12122020   36.185    0.000   36.185    0.000 {method 'blit' of 'pygame.Surface' objects}
     6061   24.674    0.004   24.674    0.004 {method 'tick' of 'Clock' objects}
     6061    1.512    0.000   23.238    0.004 sprite.py:478(clear)
     6061    6.469    0.001   22.851    0.004 sprite.py:566(draw)
     6061    2.077    0.000    2.574    0.000 sprite.py:452(update)
  6060000    0.691    0.000    0.691    0.000 {method 'union' of 'pygame.Rect' objects}
  6060000    0.474    0.000    0.474    0.000 {method 'colliderect' of 'pygame.Rect' objects}

Table 1. Cprofile results of the script for Pygame 1.9.1. Note the total time spent in blit is less than 1 minute.


Tue Feb 09 21:23:10 2016    pygamesdl2.cprof

         36571201 function calls in 455.481 seconds

   Ordered by: cumulative time
   List reduced from 212 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.005    0.005  455.481  455.481 sprites.py:3(<module>)
        1    6.541    6.541  455.453  455.453 sprites.py:30(main)
 12122020  419.963    0.000  419.963    0.000 {method 'blit' of 'pygame_sdl2.surface.Surface' objects}
     6061    6.467    0.001  252.535    0.042 sprite.py:566(draw)
     6061    1.506    0.000  184.180    0.030 sprite.py:478(clear)
     6061    8.444    0.001    8.444    0.001 {pygame_sdl2.display.update}
  6060000    4.636    0.000    4.636    0.000 {method 'union' of 'pygame_sdl2.rect.Rect' objects}
  6060000    3.411    0.000    3.411    0.000 {method 'colliderect' of 'pygame_sdl2.rect.Rect' objects}
     6061    1.957    0.000    2.429    0.000 sprite.py:452(update)
     6061    0.876    0.000    0.981    0.000 {pygame_sdl2.event.get}

Table 2. Cprofile results of the same script for Pygame_SDL2. The total time spent in blit time is over an order of magnitude greater than in Pygame.

renpytom commented 8 years ago

I don't see a way to fix this.

As best as I can tell, SDL1 had an mmx-optimized alpha blitter, which is gone in SDL2. In fact, the blit semantics have changed so much between versions that we can't use the SDL2 alpha blitter, we generally used our own.

But even when I modified pygame_sdl2 to be as close to pygame as possible - forcing the use of an the SDL blitter, modifying various surfaces so the masks and flags are all the same - I still get a similar slowdown.

I don't know what your way forwards is. If an optimized alpha blitter were to become available, I'd merge it. It might also be possible to use the pygame_sdl2.renderer to do some sort of accelerated blitting - to be honest, I didn't write and haven't looked at that code. Perhaps the best way - but also the most intrusive - would be to come up with a pygame_sdl2.gl API.

I'll leave this open, in the hopes someone will drop by and tell me what I'm doing wrong. But I don't see a simple fix.

pkdawson commented 8 years ago

The renderer module will probably be a little faster, but SDL2 doesn't provide a way to do draw call batching. I'll see if I can add something with OpenGL that would be simple to use.

bitcraft commented 8 years ago

I think the way forward would be retiring the surface/blit concept and start teaching how to use SDL2's excellent Renderer. It will offer a gentle introduction to GPU programming and also make things fast. Holding on to surface/blit rendering is just going to slow down progress.

runedevros commented 8 years ago

So I have a rather extensive codebase tied to the Pygame surface/blit system where our game has been engine-stable for about a year now. How hard would it be to port everything to the proposed SDL2 GPU thing that bitcraft suggested?

bitcraft commented 8 years ago

If you have something similar to pygame's sprite and group system, I don't think it would be too difficult, as SDL2s renderer is not much different conceptually. On the other hand, if you have blits spread out all over, then it may be more difficult. -- you will have to manage and track textures yourself. You can follow any migration guide for SDL 1.2 => 2.x and it will be more or less that same, provided that there is a Renderer api in pygame2...is there?

On Mon, Mar 14, 2016 at 5:48 PM runedevros notifications@github.com wrote:

So I have a rather extensive codebase https://bitbucket.org/featheredmelody/lost-sky-project/wiki/Home tied to the Pygame surface/blit system where our game has been engine-stable for about a year now. How hard would it be to port everything to the proposed SDL2 GPU thing that bitcraft suggested?

— Reply to this email directly or view it on GitHub https://github.com/renpy/pygame_sdl2/issues/36#issuecomment-196553756.

eshikafe commented 8 years ago

I tried experimenting with the pygame_sdl2 renderer and I see a very huge difference in the fps.

image

Modified Code:

#Sprite Test

sdl2_mode = True

if sdl2_mode:
    try:
        import pygame_sdl2
        pygame_sdl2.import_as_pygame()

    except ImportError:
        print "OOPS Pygame SDL2 not available"

import pygame
from pygame.locals import *
import os
import sys
import random

from pygame.render import *

class Bullet(pygame.render.Sprite):
    def __init__(self,img_tex):
        pygame.render.Sprite.__init__(self, img_tex)

def main():
    pygame.init()
    screen = pygame.display.set_mode((840,630))
    renderer = Renderer(None)
    renderer.render_present()

    img = pygame.image.load(os.path.join('images','bullets','02-orangeorb.png')) # SDL_Surface
    img_tex = renderer.load_texture(img) # SDL_Texture
    file = open('output_data.txt','w')

    menu_flag = True
    clock = pygame.time.Clock()
    bullets = []
    counter = 0
    bullet_num = 0
    fps_list = []
    data_list = []

    while menu_flag:

        for event in pygame.event.get():
            if event.type == QUIT:
                exit()
            if event.type == KEYDOWN:
                menu_flag = False
        if counter == 60:
            # spawn 20 new bullets
            for i in xrange(0,20):
                position = (random.randint(0, 840), random.randint(0,630))
                new_bullet = Bullet(img_tex)
                new_bullet.render(position) # SDL_RenderCopy()
                renderer.render_present()   # SDL_RenderPresent() 
                bullet_num += 1
                avg_fps = sum(fps_list)/len(fps_list)

            print (bullet_num,avg_fps)
            data_list.append((bullet_num,avg_fps))

            fps_list = []

            counter = 0

            # End Simulation at 2000 bullets
            if bullet_num > 2000:
                menu_flag = False

        clock.tick(60)
        fps_list.append(clock.get_fps())
        counter += 1

    print "Exporting Data"
    [file.write("%s    %s \n"%(str(data_pt[0]),str(data_pt[1]))) for data_pt in data_list]

if __name__ == "__main__":
    main()

In summary, I believe pygame_sdl2 should take full advantage of the SDL2 render and texture for sprites transformation and blit surface.

http://wiki.libsdl.org/MigrationGuide

bitcraft commented 8 years ago

The way forward is with hardware rendering.

thomanimation commented 7 years ago

Guys, when running eshikafe's modified code (two posts above) using the SDL2 render and texture for sprites: there's a magenta border around every sprite.

kazam1

I've had the same result (magenta borders) in two computers running Xubuntu 16.04. One with intel's integrated graphics and the other with nvidia video card and proprietary drivers. With the default ubuntu package "python-pygame-sdl2" as well as the most recent version built from source (today).

I've opened issue #65 for this. Is it a bug or is it just me?

eshikafe commented 7 years ago

I have just provided a solution for this issue in #65

Here is the result: image

eshikafe commented 7 years ago

I think issue #36 can be closed. This is already solved by using the render module.

Pololot64 commented 7 years ago

How do you use the render module on android? I get the error: File "main.py", line 7, in I/python (26081): from pygame.render import * I/python (26081): File "renpy/display/render.pyx", line 26, in init renpy.display.render (gen/renpy.display.render.c:27950) I/python (26081): ImportError: No module named renpy

I am using rapt to package my game. It is extremely slow. How do you make it use the GPU?

OSA413 commented 7 years ago

Well, actually I left this render method as "PC only" feature, because I couldn't run it on Android. I also tried to import "Renderer" instead of "*" from pygame but it returns the same error.

My code:

try:
    import pygame_sdl2
    pygame_sdl2.import_as_pygame() #It's normally imported on Android
    from pygame.render import Renderer #Returns exception "No module named Renderer" on Android
    SDL2 = True
    SDL2_Render = False
except:
    SDL2 = False
    SDL2_Render = False

If SDL2 is True, User will be offered to choose one of two render methods.

Pololot64 commented 7 years ago

Which render method should I use on android to speed up my game? To see my code, look at the latest issue which I created. I use canvas.blit and Pygame.display.update.