Tearing / dropped frames in pygame

Viewed 73

The following code draws a small white disc orbiting the center of the screen. There is noticeable tearing on my machine (macOS Monterey) and 6 or 7 frames are dropped on average on each revolution. Is there any way to avoid that? I've tried adding flags such as vsync, fullscreen, scaled etc, nothing works. The opengl flag doesn't seem to work on macos.

import pygame
import sys
import math

pygame.init()

screen = pygame.display.set_mode((1920,1080))
clock = pygame.time.Clock()

while True:

    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            sys.exit()

    t = pygame.time.get_ticks()

    screen.fill('Black')
    pygame.draw.circle(screen, 'White', (
        960 + 300 * math.cos(t / 800),
        540 + 300 * math.sin(t / 800)),
        20)

    pygame.display.update()
    clock.tick(60)

To be clear: IMHO the frames are not dropping because of heavy or background CPU usage. A modified version of this code runs approximately 1000 such circles all over the screen pretty smoothly, with only the regular hiccup here and there (same as above). I only start to get real slowdowns when I go over 5000 circles.

1 Answers

Firstly - check that there is not some CPU-heavy task running on your machine. For the moment I would just write your program, and then worry about if/when it's dropping frames later. If it's still an issue at completion, then optimise it.

One way to speed this up is to pass the rectangle for changed screen area to the pygame.display.update( dirty_rectangle ) function. That way only the part of the screen that has been "dirtied" by changes will be updated. This means that typically a smaller part of the screen will be re-drawn.

I also modified the code to draw the circle to an off-screen surface, and just blit() that image to the screen, rather than re-calculate the circle each loop. That makes it easy to work out the corners of the changed area too.

For your example, it's possible to calculate this rectangle based on the position of the circle, and where it has been previously. So before repositioning the circle to the new co-ordinate, we make a copy of circle_rect into previous_rect. The new position is calculated, the drawing position updated. The maximum extent of the update is then the minimum top-left corner of both rectangles, down to the maximum of the bottom-right corners. The min & max is used to populate the update rectangle.

import pygame
import sys
import math

pygame.init()

screen = pygame.display.set_mode((1920,1080))
clock = pygame.time.Clock()

# Create a sprite for the circle
CIRCLE_RAD=20
circle = pygame.Surface( (CIRCLE_RAD*2, CIRCLE_RAD*2), pygame.SRCALPHA, 32 )
circle.fill( (0,0,0) )
pygame.draw.circle( circle, (255,255,255), (CIRCLE_RAD,CIRCLE_RAD), CIRCLE_RAD )
circle_rect = circle.get_rect()
update_rect = circle_rect.copy()

while True:

    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            sys.exit()

    previous_rect = circle_rect.copy()
    t = pygame.time.get_ticks()
    t800 = t/800
    circle_rect.center = ( 960 + 300 * math.cos(t800), 540 + 300 * math.sin(t800) )

    # work out the size of the update
    min_x = min( previous_rect.x, circle_rect.x )
    min_y = min( previous_rect.y, circle_rect.y )
    max_x = max( previous_rect.x + previous_rect.width, circle_rect.x + circle_rect.width )
    max_y = max( previous_rect.y + previous_rect.height, circle_rect.y + circle_rect.height )
    update_rect.update( min( previous_rect.x, circle_rect.x ),
                        min( previous_rect.y, circle_rect.y ),
                        max_x-min_x,
                        max_y-min_y )


    screen.fill('Black')
    screen.blit( circle, circle_rect )
    pygame.display.update( update_rect )

    ms_since_previous = clock.tick(60)
    if ( ms_since_previous > 17 ):      # 60FPS is about 17ms between frames
        print( "Slow update: %d milliseconds" % ( ms_since_previous ) )
Related