[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[pygame] Re: Strange performance in blit



After some more study I have refined the behavior. Thanks to DR0ID for helping me reduce the problem.

In any blit(surf, (x,y)), if x is aligned on a multiple of 16 then blit is fast; otherwise it is slow.

Is this a SDL quirk?

If you have a faster computer than my i3 laptop you may be able to see the problem more clearly with larger tile sizes and screen width.

I've attached another program that is easier to play with, though a bit more complicated to read due to all the run-time controls. You can set the tile size on the fly, and toggle the camera step. Culling is an option just to show rendering off screen has no impact.

Gumm

On 4/26/2014 20:47, bw wrote:
Whoops. Forgive my haste and loss of focus right before sending that. There is a mistake.

Change line 89:
costs = []
to
costs_per_screen = []

Gumm

On 4/26/2014 18:01, bw wrote:
Howdy, folks,

I am getting some strange behavior in Surface.blit(). This is a mystery.

I blit a collection of images to to fill the screen. If I pan left or right even one pixel, blit consumes nearly 4x the CPU. It is only left or right: if I pan up or down performance is not impacted.

I've attached a program that attempts to demonstrate this. Hopefully you'll find it minimal and easy to comprehend. Please let me know:

- Am I doing something wrong, and how?
- Is this a legitimate issue, and how might I work around it?


Please read the program description for the steps to reproduce the issue:

"""strange.py - demonstrating strange performane in Surface.blit

This program fills a screen with sprites to produce a checkerboard background,
and demonstrates a problem.

Controls:

    UP,DOWN,LEFT,RIGHT  pan the background
    SPACE               center the background
    ESCAPE              quit

Reproducing the problem:

The times indicated in these steps are on a Intel i3 running Windows 7 64-bit. Times will vary on other machines. The time cost measured is in blit ONLY.

1. Observe the cost per blit in the window's caption. On my machine it costs
   about 0.0000015 seconds per blit.
2. Pan LEFT one or more pixels. Note the cost goes up nearly 4x (0.0000056). 3. Press SPACE to center the background. Note the cost goes back down to the
   original value.
4. Pan RIGHT just one or more pixels. Note the costs goes up as in step 2.
5. Press SPACE to center the background.
6. Pan UP or DOWN: the cost is not impacted.
"""

Gumm


"""strange.py - demonstrating strange performane in Surface.blit

This program fills a screen with sprites to produce a checkerboard background,
and demonstrates a problem: slow blits when the x-position of a surface is not
aligned on a multiple of 16 (0, 16, 32, ...).

Runtime Controls:

    UP,DOWN,LEFT,RIGHT  pan the background
    0 - 9               set tile size
    C                   toggle culling on and off
    S                   toggle step between 1 and 16
    SPACE               center the background
    ESCAPE              quit

Reproducing the problem:

The times indicated in these steps are on a Intel i3 running Windows 7 64-bit.
Times will vary on other machines. The time cost measured is in blit ONLY.

To see the issue perform the following with the default settings.

1. Observe the cost per blit in the window's caption. On my machine it costs
   about 0.0000015 seconds per blit.
2. Press S to choose step=1.
3. Pan LEFT one pixel. Note the cost goes up nearly 4x (0.0000056).
4. Press SPACE to center the background. Note the cost goes back down to the
   original value.
5. Pan RIGHT just one pixel. Note the costs goes up as in step 3.
6. Press SPACE to center the background.
7. Pan UP or DOWN: the cost is not impacted.
"""

# Configurables.
resolution = (800, 600)
max_fps = 100

import sys
import time
import pygame
from pygame.locals import *

if sys.version_info[0] == 3:
    xrange = range

pygame.init()
screen = pygame.display.set_mode(resolution)
screen_rect = screen.get_rect()
clock = pygame.time.Clock()

# graphics
colors = [
    Color('red'),       # check 0
    Color('orange'),    # check 1
    Color('black'),     # screen erase
]
# images are shared among the sprites
image_sizes = (
    (16, 16),
    (32, 32),   # best performance when rect.x is a multiple of 16
    (35, 35),   # all-around bad performance; no matter what rect.x is
    (48, 48),   # best performance when rect.x is a multiple of 16
    (64, 64),   # best performance when rect.x is a multiple of 16
)
image_sizei = 4
image_size = image_sizes[image_sizei]

# checkboard background
images = {}
sprites = []
sign = {0:1, 1:-1}
def make_sprites():
    global image_size
    image_size = image_sizes[image_sizei]
    del sprites[:]
    # make the shared images
    for i in (0, 1):
        image = pygame.Surface(image_size)
        image.fill(colors[i])
        images[i] = image
    # make the sprites
    for y in xrange(0, screen_rect.h, image_size[1]):
        for x in xrange(0, screen_rect.w, image_size[0]):
            sprite = pygame.sprite.Sprite()
            sprite.rect = Rect((x, y), image_size)
            # the next two lines produce the checkerboard pattern
            yd = y // image_size[1] % 2
            xd = yd + sign[yd] * (x // image_size[0] % 2)
            sprite.image = images[xd]
            sprites.append(sprite)
make_sprites()

# camera
camera_rect = Rect(screen_rect)
camera_move = [0, 0]  # x, y

step = 16 #image_size[0] / 2
cull_sprites = False
sizes_keys = range(K_0, K_0 + len(image_sizes))

def do_events(events):
    global cull_sprites, step, image_sizei
    for e in events:
        if e.type == KEYDOWN:
            if e.key == K_ESCAPE: quit()
            elif e.key == K_RIGHT: camera_move[0] += step
            elif e.key == K_LEFT: camera_move[0] -= step
            elif e.key == K_DOWN: camera_move[1] += step
            elif e.key == K_UP: camera_move[1] -= step
            elif e.key == K_SPACE: camera_rect.center = screen_rect.center
            elif e.key == K_c: cull_sprites = not cull_sprites
            elif e.key == K_s: step = 16 if step == 1 else 1
            elif e.key in sizes_keys:
                i = e.key - K_0
                image_sizei = i
                make_sprites()
        elif e.type == QUIT: quit()

# main loop
blit = screen.blit
costs_per_screen = []
avg_costs = []
r = [0, 0]
while True:
    screen.fill(colors[2])
    
    # Do keys, and move camera.
    clock.tick(max_fps)
    do_events(pygame.event.get())
    camera_rect.x += camera_move[0]
    camera_rect.y += camera_move[1]
    camera_move[0] = 0
    camera_move[1] = 0
    
    # Draw camera-translated sprites. Collect the timing of blit for each
    # sprite on one screen.
    del costs_per_screen[:]
    cx, cy = camera_rect.topleft
    for sprite in sprites:
        r = sprite.rect.move(-cx, -cy)
        
        ## Curious: attempt to cull left or right columns doesn't matter.
        ## CPU cost still quadruples.
        if cull_sprites:
            if r.right > screen_rect.right or r.left < screen_rect.x:
                continue
        
        ## timing blit: panning left or right just one pixel causes CPU
        ## cost to nearly quadruple.
        t = time.time()
        blit(sprite.image, r)
        costs_per_screen.append(time.time() - t)
        ### done timing blit
    
    # Calc average cost of blit over a one-second span, and set caption.
    avg_costs.append(float(sum(costs_per_screen)) / len(costs_per_screen))
    while len(avg_costs) > max_fps:
        avg_costs.pop(0)
    pygame.display.set_caption(
        'X: {6} | Step: {3} | Cull: {2} | Tile size: {4} {5} | FPS: {0:.0f} | Avg cost per blit: {1:0.7f}'.format(
            clock.get_fps(),
            float(sum(avg_costs)) / len(avg_costs),
            cull_sprites,
            step,
            image_sizei,
            image_size,
            camera_rect.x))
    
    pygame.display.flip()