Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thoroughly test performance of pygame.display.update(list_of_rects) vs pygame.display.flip() #2514

Closed
5 tasks done
MyreMylar opened this issue Oct 10, 2023 · 1 comment · Fixed by #2532
Closed
5 tasks done
Assignees
Labels
display pygame.display docs

Comments

@MyreMylar
Copy link
Member

MyreMylar commented Oct 10, 2023

This was a performance optimisation that was relevant twenty plus years ago, but with the passage of time, and the change to modern versions of SDL - my intuition is that it is no longer providing any performance benefit. The idea of it being effective is baked into a lot of pygame-ce functionality (with drawing and rendering functions returning rectangles for use in this performance optimisation). If we could prove it was, or was not, effective in the modern world we could make better decisions on what to optimise in the code, document in the docs and include in tutorials.

However, we should test it before taking any action. A to do list.

  • create a base test program with a standard game loop that draws/blits something to about about 25% of the display surface every loop. Rather than a while loop just have it loop something like 100 times.
  • split the base test program into two versions - one that flips the display surface to the screen with pygame.display.flip(), and one that collects all the rectangles returned from the draws/blits into a list of rectangles and passes that list to pygame.display.update().
  • test the performance of the two programs and see which is faster/slower/whether it makes any difference at all.
  • present the results and see if anyone else has any ideas for changes to the performance test that might change the results.
  • open a PR to update the docs & tutorials regarding the usefulness (or not) of the rect_list - unless anyone else has any better uses for it?
@MyreMylar MyreMylar self-assigned this Oct 10, 2023
@MyreMylar
Copy link
Member Author

OK, I made a test program using a (reasonably) modern sized display surface at 1920x1080.

It seems like one of the main issues with passing a rect_list to display.update() is that there is some significant per rectangle overhead.

If each rectangle is only a pixel or two in size, but there are 50 of them, then calling display.update(fifty_rects) is always slower than display.flip().

There is also some scaling cost depending on the area updated, so if you have some reasonably sized Rects, say 64x64, then 25 of them passed in to display.update(twenty_five_64x64_rects) is slower than a basic display.flip().

The best case for display.update(rect_list) is a list of one rect that is significantly smaller than the full display. The worst case is a list containing more than 50 rectangles of any size larger than a pixel.

The other issue I'm currently seeing is that I seem to get some artifacting using the rect list that I don't see when flipping the whole display each time, but that may just be my poor coding.

Performance test coding:

from sys import stdout
from pstats import Stats
from cProfile import Profile

import random
import pygame

pygame.init()

display_surf = pygame.display.set_mode((1920, 1080))

NUM_CIRCLES = 25
CIRCLES_RADIUS = 32


def update_w_rects_test():
    global display_surf

    old_rect_list = []

    for loops in range(0, 1000):
        pygame.event.pump()
        display_surf.fill((0, 0, 0))
        new_rect_list = []
        for _ in range(0, NUM_CIRCLES):
            new_rect_list.append(
                pygame.draw.circle(
                    display_surf,
                    pygame.Color("red"),
                    (random.randint(0, 1920), random.randint(0, 1080)),
                    CIRCLES_RADIUS,
                )
            )

        update_rect_list = old_rect_list + new_rect_list
        pygame.display.update(update_rect_list)
        old_rect_list = new_rect_list.copy()


def flip_test():
    global display_surf

    for loops in range(0, 1000):
        pygame.event.pump()
        display_surf.fill((0, 0, 0))
        for _ in range(0, NUM_CIRCLES):
            pygame.draw.circle(
                display_surf,
                pygame.Color("red"),
                (random.randint(0, 1920), random.randint(0, 1080)),
                CIRCLES_RADIUS,
            )

        pygame.display.flip()


if __name__ == "__main__":
    print("\nJust display.flip()")

    profiler = Profile()
    profiler.runcall(flip_test)
    stats = Stats(profiler, stream=stdout)
    stats.strip_dirs()
    stats.sort_stats("cumulative")
    stats.print_stats()

    print("display.update() w rects")
    profiler = Profile()
    profiler.runcall(update_w_rects_test)
    stats = Stats(profiler, stream=stdout)
    stats.strip_dirs()
    stats.sort_stats("cumulative")
    stats.print_stats()

On the basis of this I would advise that we keep the option available, but make a note in the docs for display,update() that there is a per rectangle cost for passing in a list of rectangles and that you are unlikely to see any benefit at all unless you are updating less than 50 small (smaller than 64x64) areas on a display per frame/loop of the game loop.

I would also de-emphasise this as a general performance optimisation in any tutorial docs we have. A full display surface display.flip() call is costing around 1ms, and the best, unrealistic, case for this optimisation (a single 1 pixel rect) only reduces its cost by 65% saving you around 0.65ms a frame - not a massive win for the added complexity.

Then, in the average, single screen game (no scrolling camera game can make use of this) - something like asteroids (on average 10-20 asteroids, bullets and ships) or space invaders (55 invaders) using a rect list in the most obvious way to update the moving objects would make the performance worse because of the number of rects you would be updating is too high.

The only 'good' uses for this feature (where a small list of reasonably sized rects could be passed in) I can think of are unlikely to need the small performance boost in the first place.

Anyone else have any good use cases - or can fix up my test program to get rid of that artefacting?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
display pygame.display docs
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant