lordmauve / pgzero

A zero-boilerplate games programming framework for Python 3, based on Pygame.
https://pygame-zero.readthedocs.io/
GNU Lesser General Public License v3.0
530 stars 190 forks source link

Is `numpy` a necessary dependency? #270

Open lgautier opened 2 years ago

lgautier commented 2 years ago

numpy is a required dependency but it only appears to be used in one place in the code: https://github.com/lordmauve/pgzero/blob/38cb6021496bd2f9dae0b37ef3ee6304dee71e2f/pgzero/ptext.py#L348

numpy is a rather large package. Is the dependency really necessary? The same could be implemented in pure Python is there is no significant loss of performance as a result, or a minimal C-extension would just do the job (and could eventually be pushed upstream to pygame if this is something general for writing games in Python).

lordmauve commented 2 years ago

It's also used in screen.py.

ptext is vendored from elsewhere, and needs this for the outline feature (and others?) so this isn't easy to fix. We could submit a patch, or we could actually go ahead and fork it.

It is a huge dependency and I was reluctant to add it in the first place, but the situation we had where it was optional was not healthy because some stuff only worked when it was installed. Requiring it was a simpler solution.

Note that the amount of numpy usage has gone down a lot now we use pyfxr to generate tones in tone.py.

lordmauve commented 2 years ago

Looks like the current version of ptext doesn't need numpy. But it has changed significantly, so merging that would need some care.

lgautier commented 2 years ago

Thanks. The dependency on numpy in screen.py is for gradients, and it uses an optional pygame capability that wraps memory regions in numpy arrays (getting rid of numpy in pygame zero would not suppress the need for numpy in the end). Interestingly the current ptext also has an implementation of vertical gradients in rectangles: https://github.com/cosmologicon/pygame-text/blob/master/ptext.py#L487

Overall it seems that gradients are generally useful, and arguably should not require numpy and be part of pygame (and in a C-extension module).

lgautier commented 2 years ago

It seems that there is no need to go for a C-extension and get decent performances without numpy. Running the test below shows a candidate alternative implementation without numpy that is 4.7 times faster:

import timeit
import pygame

# colors.
gradient_start = (0, 50, 100)
gradient_stop = (255, 250, 200)

# modest screen size.
WIDTH = 800
HEIGHT = 600

def blit_gradient(start, stop, dest_surface):
    """ New proposed implementation."
    surface_compact = pygame.Surface((2, 2))
    pixelarray = pygame.PixelArray(surface_compact)
    pixelarray[0][0] = start
    pixelarray[0][1] = stop
    pixelarray[1][0] = start
    pixelarray[1][1] = stop
    pygame.transform.smoothscale(surface_compact,
                                 pygame.PixelArray(dest_surface).shape,
                                 dest_surface=dest_surface)

def old_blit_gradient(start, stop, dest_surface):
    """The current implementation requiring numpy."""
    import numpy as np
    pixs = pygame.surfarray.pixels3d(dest_surface)

    gradient = np.dstack(
        [np.linspace(a, b, HEIGHT) for a, b in zip(start, stop)][:3]
    )

    pixs[...] = gradient

# Speed benchmark.

# Proposed new implementation.
setup_test = """
import pygame
dest_surface = pygame.Surface((WIDTH, HEIGHT))
"""
test = 'blit_gradient(gradient_start, gradient_stop, dest_surface)'

t_new = min(
    timeit.Timer(
        test, setup_test,
        globals=locals()).repeat(
            repeat=5,
            number=1000)
)

# Current implementation requiring numpy.
setup_test_old = """
import pygame
import numpy as np

dest_surface = pygame.Surface((WIDTH, HEIGHT))"""
test_old = 'old_blit_gradient(gradient_start, gradient_stop, dest_surface)'

t_current = min(
    timeit.Timer(
        test_old, setup_test_old,
    globals=locals()).repeat(
        repeat=5,
        number=1000)
)

print(f'Speedup: {t_current/t_new:.2f}x')