Skip to content

Commit

Permalink
Merge pull request #2987 from pygame-community/ankith26-update-iterable
Browse files Browse the repository at this point in the history
Support iterable in `display.update`
  • Loading branch information
damusss authored Jul 21, 2024
2 parents 13fe906 + aabeed9 commit 6d8007c
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 25 deletions.
4 changes: 2 additions & 2 deletions buildconfig/stubs/pygame/display.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, Tuple, Union, overload, Literal
from typing import Dict, List, Optional, Tuple, Union, overload, Literal, Iterable
from typing_extensions import deprecated # added in 3.13

from pygame.constants import FULLSCREEN
Expand Down Expand Up @@ -48,7 +48,7 @@ def get_surface() -> Surface: ...
def flip() -> None: ...
@overload
def update(
rectangle: Optional[Union[RectValue, Sequence[Optional[RectValue]]]] = None, /
rectangle: Optional[Union[RectValue, Iterable[Optional[RectValue]]]] = None, /
) -> None: ...
@overload
def update(x: int, y: int, w: int, h: int, /) -> None: ...
Expand Down
10 changes: 6 additions & 4 deletions docs/reST/ref/display.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ required).

| :sl:`Update all, or a portion, of the display. For non-OpenGL displays.`
| :sg:`update(rectangle=None, /) -> None`
| :sg:`update(rectangle_list, /) -> None`
| :sg:`update(rectangle_iterable, /) -> None`
For non OpenGL display Surfaces, this function is very similar to
``pygame.display.flip()`` with an optional parameter that allows only
Expand All @@ -285,21 +285,23 @@ required).
updated. Whereas ``display.update()`` means the whole window is
updated.

You can pass the function a single rectangle, or a sequence of rectangles.
Generally you do not want to pass a sequence of rectangles as there is a
You can pass the function a single rectangle, or an iterable of rectangles.
Generally you do not want to pass an iterable of rectangles as there is a
performance cost per rectangle passed to the function. On modern hardware,
after a very small number of rectangles passed in, the per-rectangle cost
will exceed the saving of updating less pixels. In most applications it is
simply more efficient to update the entire display surface at once, it also
means you do not need to keep track of a list of rectangles for each call
to update.

If passing a sequence of rectangles it is safe to include None
If passing an iterable of rectangles it is safe to include None
values in the list, which will be skipped.

This call cannot be used on ``pygame.OPENGL`` displays and will generate an
exception.

.. versionchanged:: 2.5.1 Added support for passing an iterable, previously only sequence was allowed

.. ## pygame.display.update ##
.. function:: get_driver
Expand Down
67 changes: 49 additions & 18 deletions src_c/display.c
Original file line number Diff line number Diff line change
Expand Up @@ -1694,48 +1694,78 @@ pg_update(PyObject *self, PyObject *arg)
SDL_UpdateWindowSurfaceRects(win, &sdlr, 1);
}
else {
PyObject *seq;
PyObject *r;
Py_ssize_t loop, num;
PyObject *iterable, *single_arg, *r;
Py_ssize_t num;
int count;
SDL_Rect *rects;
if (PyTuple_Size(arg) != 1)
return RAISE(
PyExc_ValueError,
"update requires a rectstyle or sequence of rectstyles");
seq = PyTuple_GET_ITEM(arg, 0);
if (!seq || !PySequence_Check(seq))
"update requires a rectstyle or an iterable of rectstyles");

single_arg = PyTuple_GET_ITEM(arg, 0);
num = PyObject_Size(single_arg);
if (num == -1) {
/* Either __len__ errored, or object doesn't have __len__.
* In this case we can assume a length arbitrarily, and keep
* scaling it as needed. */
PyErr_Clear();
num = 8;
}
iterable = PyObject_GetIter(single_arg);
if (!iterable)
return RAISE(
PyExc_ValueError,
"update requires a rectstyle or sequence of rectstyles");
"update requires a rectstyle or an iterable of rectstyles");

num = PySequence_Length(seq);
rects = PyMem_New(SDL_Rect, num);
if (!rects)
if (!rects) {
Py_DECREF(iterable);
return NULL;
}
count = 0;
for (loop = 0; loop < num; ++loop) {
SDL_Rect *cur_rect = (rects + count);

/*get rect from the sequence*/
r = PySequence_GetItem(seq, loop);
while (1) {
r = PyIter_Next(iterable);
if (!r) {
if (PyErr_Occurred()) {
/* forward error */
Py_DECREF(iterable);
PyMem_Free((void *)rects);
return NULL;
}
/* End of sequence, break loop */
break;
}
if (r == Py_None) {
Py_DECREF(r);
continue;
}
gr = pgRect_FromObject(r, &temp);
Py_XDECREF(r);
if (!gr) {
PyMem_Free((char *)rects);
Py_DECREF(iterable);
PyMem_Free((void *)rects);
return RAISE(PyExc_ValueError,
"update_rects requires a single list of rects");
}

if (gr->w < 1 && gr->h < 1)
continue;

/*bail out if rect not onscreen*/
if (!pg_screencroprect(gr, wide, high, cur_rect))
if (count >= num) {
/* About to overstep boundary, need reallocing */
num *= 2;
SDL_Rect *new_rects = PyMem_Resize(rects, SDL_Rect, num);
if (!new_rects) {
Py_DECREF(iterable);
PyMem_Free((void *)rects);
return NULL;
}
rects = new_rects;
}

/* bail out if rect not onscreen */
if (!pg_screencroprect(gr, wide, high, &rects[count]))
continue;

++count;
Expand All @@ -1747,7 +1777,8 @@ pg_update(PyObject *self, PyObject *arg)
Py_END_ALLOW_THREADS;
}

PyMem_Free((char *)rects);
Py_DECREF(iterable);
PyMem_Free((void *)rects);
}
Py_RETURN_NONE;
}
Expand Down
2 changes: 1 addition & 1 deletion src_c/doc/display_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#define DOC_DISPLAY_SETMODE "set_mode(size=(0, 0), flags=0, depth=0, display=0, vsync=0) -> Surface\nInitialize a window or screen for display"
#define DOC_DISPLAY_GETSURFACE "get_surface() -> Surface\nGet a reference to the currently set display surface"
#define DOC_DISPLAY_FLIP "flip() -> None\nUpdate the full display Surface to the screen"
#define DOC_DISPLAY_UPDATE "update(rectangle=None, /) -> None\nupdate(rectangle_list, /) -> None\nUpdate all, or a portion, of the display. For non-OpenGL displays."
#define DOC_DISPLAY_UPDATE "update(rectangle=None, /) -> None\nupdate(rectangle_iterable, /) -> None\nUpdate all, or a portion, of the display. For non-OpenGL displays."
#define DOC_DISPLAY_GETDRIVER "get_driver() -> name\nGet the name of the pygame display backend"
#define DOC_DISPLAY_INFO "Info() -> VideoInfo\nCreate a video display information object"
#define DOC_DISPLAY_GETWMINFO "get_wm_info() -> dict\nGet information about the current windowing system"
Expand Down
56 changes: 56 additions & 0 deletions test/display_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,9 @@ def test_update_negative(self):
r3 = pygame.Rect(-10, 0, -100, -100)
pygame.display.update(r3)

# random point in rect
self.assertEqual(self.screen.get_at((50, 50)), (0, 255, 0))

self.question("Is the screen green in (0, 0, 100, 100)?")

def test_update_sequence(self):
Expand All @@ -728,6 +731,47 @@ def test_update_sequence(self):
pygame.display.update(rects)
pygame.event.pump() # so mac updates

# random points in rect
for random_point in ((50, 50), (150, 50), (250, 50), (350, 350)):
self.assertEqual(self.screen.get_at(random_point), (0, 255, 0))

self.question(f"Is the screen green in {rects}?")

def test_update_dict_values(self):
"""only updates the part of the display given by the rects."""
self.screen.fill("green")
rects = {
"foo": pygame.Rect(0, 0, 100, 100),
"foobar": pygame.Rect(100, 0, 100, 100),
"hello": pygame.Rect(200, 0, 100, 100),
"hi": pygame.Rect(300, 300, 100, 100),
}
pygame.display.update(rects.values())
pygame.event.pump() # so mac updates

# random points in rect
for random_point in ((50, 50), (150, 50), (250, 50), (350, 350)):
self.assertEqual(self.screen.get_at(random_point), (0, 255, 0))

self.question(f"Is the screen green in {' '.join(map(str, rects.values()))}?")

def test_update_generator(self):
"""only updates the part of the display given by the rects."""
self.screen.fill("green")
rects = [
pygame.Rect(0, 0, 100, 100),
pygame.Rect(100, 0, 100, 100),
pygame.Rect(200, 0, 100, 100),
pygame.Rect(300, 300, 100, 100),
]
# make a generator from list, pass list with rect duplicates
pygame.display.update(i for i in (rects * 5))
pygame.event.pump() # so mac updates

# random points in rect
for random_point in ((50, 50), (150, 50), (250, 50), (350, 350)):
self.assertEqual(self.screen.get_at(random_point), (0, 255, 0))

self.question(f"Is the screen green in {rects}?")

def test_update_none_skipped(self):
Expand All @@ -743,6 +787,10 @@ def test_update_none_skipped(self):
pygame.display.update(rects)
pygame.event.pump() # so mac updates

# random points in rect
for random_point in ((50, 50), (150, 50), (250, 50), (350, 350)):
self.assertEqual(self.screen.get_at(random_point), (0, 255, 0))

self.question(f"Is the screen green in {rects}?")

def test_update_none(self):
Expand All @@ -757,6 +805,11 @@ def test_update_no_args(self):
self.screen.fill("green")
pygame.display.update()
pygame.event.pump() # so mac updates

# random points in rect
for random_point in ((50, 50), (150, 50), (250, 50), (350, 350)):
self.assertEqual(self.screen.get_at(random_point), (0, 255, 0))

self.question(f"Is the WHOLE screen green?")

def test_update_args(self):
Expand All @@ -766,6 +819,9 @@ def test_update_args(self):
pygame.event.pump() # so mac updates
self.question("Is the screen green in (100, 100, 100, 100)?")

# random points in rect
self.assertEqual(self.screen.get_at((150, 150)), (0, 255, 0))

def test_update_incorrect_args(self):
"""raises a ValueError when inputs are wrong."""

Expand Down

0 comments on commit 6d8007c

Please sign in to comment.