On Tue, Oct 01, 2013 at 10:34:54AM +0200, laurent bernabe wrote:
> But I don't think I can avoid repainting all screen at each frame,
> as the balls move, so according to me, the performance gained this
> way, in the case of my application, won't be that high. Maybe, I am
> wrong.

Yes, you're wrong.  ;-)

The performance gain is of the order of six times.  Using top in 20
second sample period, the CPU usage dropped from 30% to 5%, just for
the game process.  There was a similar drop for the X process, which
is the display handler.

Please find attached five patches, which you can apply in order.

The patches change the drawing functions to return sequences
containing rectangles, which are then used by Pygame to update only
the parts of the screen that need to be repainted.

If the patches do not apply (git am), then perhaps you have made other
changes, and I can rebase them.  Push your changes and let me know.

These changes will make the activity less energy intensive.

-- 
James Cameron
http://quozl.linux.org.au/
>From cfaeaddb7b6699b14aefb38339954a4c7928e7bb Mon Sep 17 00:00:00 2001
From: James Cameron <qu...@laptop.org>
Date: Tue, 1 Oct 2013 20:36:52 +1000
Subject: [PATCH 1/5] add test harness

Add a main() function that can be used to test the activity without
using Sugar, by typing this at a shell prompt:

	python main_game.py
---
 main_game.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/main_game.py b/main_game.py
index 0e23fd9..411649a 100644
--- a/main_game.py
+++ b/main_game.py
@@ -321,3 +321,11 @@ class Game:
                                 30,
                                 self._levels[selected_level_index])
             self._clock.tick(self._FPS)
+def main():
+    pygame.init()
+    pygame.display.set_mode((0, 0), pygame.RESIZABLE)
+    game = Game() 
+    game.show_menu()
+
+if __name__ == '__main__':
+    main()
-- 
1.8.1.2

>From 91f9631543e8cbddac0ed312be08c9e0e41a0a5e Mon Sep 17 00:00:00 2001
From: James Cameron <qu...@laptop.org>
Date: Tue, 1 Oct 2013 20:39:34 +1000
Subject: [PATCH 2/5] use fixed slower update rate in menus and end of round

The high frame rate is necessary when playing a round of the game, but
not as necessary while the menus are displayed.  This reduces the CPU
cycle cost of the menu from 40% to 5% on OLPC XO-1.5 measured with top
at 20 second interval.
---
 main_game.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/main_game.py b/main_game.py
index 411649a..2a976d9 100644
--- a/main_game.py
+++ b/main_game.py
@@ -255,6 +255,7 @@ class Game:
                 for ball in the_balls:
                     ball.move()
                 balls_collision.manage_colliding_balls(the_balls)
+                self._clock.tick(self._FPS)
             else:
                 paint_results(balls_area, the_balls, self._screen)
                 # Blinks the status text.
@@ -277,7 +278,7 @@ class Game:
                     elif event.type == MOUSEBUTTONUP:
                         if event.button == self._LEFT_BUTTON:
                             return
-            self._clock.tick(self._FPS)
+                self._clock.tick(5)
 
     def show_menu(self):
         """
@@ -320,7 +321,8 @@ class Game:
                             self._play_game(
                                 30,
                                 self._levels[selected_level_index])
-            self._clock.tick(self._FPS)
+            self._clock.tick(5)
+
 def main():
     pygame.init()
     pygame.display.set_mode((0, 0), pygame.RESIZABLE)
-- 
1.8.1.2

>From f45fb2b84bfbfe37cbe2e2cd9a49f96f1fb13b38 Mon Sep 17 00:00:00 2001
From: James Cameron <qu...@laptop.org>
Date: Tue, 1 Oct 2013 20:40:23 +1000
Subject: [PATCH 3/5] remove the loading screen, it was not visible

---
 main_game.py | 12 ------------
 1 file changed, 12 deletions(-)

diff --git a/main_game.py b/main_game.py
index 2a976d9..d8ad3b0 100644
--- a/main_game.py
+++ b/main_game.py
@@ -174,19 +174,7 @@ class Game:
             => list of
             OperationConfig.
         """
-        # Shows loading screen
         self._screen.fill(self._MENU_BACKGROUND)
-        loading_txt = _("Loading...")
-        loading_txt_surface = self._end_font.render(
-            loading_txt,
-            True,
-            self._BLUE
-        )
-        txt_size = self._end_font.size(loading_txt)
-        self._screen.blit(loading_txt_surface, (
-            (self._size[0] - txt_size[0]) / 2,
-            (self._size[1] - txt_size[1]) / 2,
-        ))
         pygame.display.update()
 
         game_state = GameState.NORMAL
-- 
1.8.1.2

>From 508fc1a16d014d99d8306b0a51c106926708add7 Mon Sep 17 00:00:00 2001
From: James Cameron <qu...@laptop.org>
Date: Tue, 1 Oct 2013 20:41:22 +1000
Subject: [PATCH 4/5] return sequence of dirty rectangles for all drawing
 operations

Every drawing operation is to return a sequence of rectangles that
have been drawn on, for update optimisation.
---
 elements_painter.py | 44 ++++++++++++++++++++++++++++++++------------
 1 file changed, 32 insertions(+), 12 deletions(-)

diff --git a/elements_painter.py b/elements_painter.py
index 63e01a6..e32bf33 100644
--- a/elements_painter.py
+++ b/elements_painter.py
@@ -30,10 +30,13 @@ def paint_ball(ball, surface):
     Draws a ball onto the given PyGame surface.
     ball : the ball to draw => Ball instance
     surface : the destination surface => PyGame.Surface
+    (returns sequence of Rect, or an empty sequence)
     """
+    s = []
     if ball.is_visible():
-        pygame.draw.circle(surface, ball.get_bg_color(), ball.get_center(),
-                           int(ball.get_diameter() / 2))
+        r = pygame.draw.circle(surface, ball.get_bg_color(), ball.get_center(),
+                               int(ball.get_diameter() / 2))
+        s.append(r)
         ball_center = ball.get_center()
         txt_width, txt_height = ball.get_txt_font().size(ball.get_operation().
                                                          get_text())
@@ -41,7 +44,9 @@ def paint_ball(ball, surface):
                         int(ball_center[1] - txt_height / 2))
         txt_surface = ball.get_txt_font().render(
             ball.get_operation().get_text(), 1, ball.get_txt_color())
-        surface.blit(txt_surface, txt_position)
+        r = surface.blit(txt_surface, txt_position)
+        s.append(r)
+    return s
 
 
 def paint_time_bar(time_bar, surface):
@@ -49,20 +54,25 @@ def paint_time_bar(time_bar, surface):
     Draws a time bar onto the given PyGame surface.
     time_bar : the time bar => TimeBar
     surface : the destination surface => PyGame.Surface
+    (returns sequence of Rect)
     """
+    s = []
     edge = time_bar.get_edge()
     dead_rect = (edge[0], edge[1],
                  time_bar.get_width(), time_bar.get_height())
-    pygame.draw.rect(surface, time_bar.get_dead_color(), dead_rect)
+    r = pygame.draw.rect(surface, time_bar.get_dead_color(), dead_rect)
+    s.append(r)
     try:
         value = time_bar.get_value()
         max_value = time_bar.get_max_value()
         active_rect = (edge[0], edge[1],
                        int(time_bar.get_width() * value / max_value),
                        time_bar.get_height())
-        pygame.draw.rect(surface, time_bar.get_active_color(), active_rect)
+        r = pygame.draw.rect(surface, time_bar.get_active_color(), active_rect)
     except NameError:
-        pygame.draw.rect(surface, time_bar.get_active_color(), dead_rect)
+        r = pygame.draw.rect(surface, time_bar.get_active_color(), dead_rect)
+    s.append(r)
+    return s
 
 
 def paint_result_bar(result_bar, surface):
@@ -70,20 +80,25 @@ def paint_result_bar(result_bar, surface):
     Draws a result bar onto a given PyGame surface.
     result_bar : the result bar => ResultBar
     surface : the destination surface => PyGame.Surface
+    (returns sequence of Rect)
     """
+    s = []
     edge = result_bar.get_edge()
     rect = (edge[0], edge[1],
             result_bar.get_width(), result_bar.get_height())
-    pygame.draw.rect(surface, result_bar.get_background(), rect)
+    r = pygame.draw.rect(surface, result_bar.get_background(), rect)
+    s.append(r)
     try:
         text = result_bar.get_header() + str(result_bar.get_result())
     except NameError:
         text = result_bar.get_header()
     text_surface = result_bar.get_text_font().render(
         text, 1, result_bar.get_foreground())
-    surface.blit(text_surface,
-                 (edge[0] + result_bar.get_insets(),
-                  edge[1] + result_bar.get_insets()))
+    r = surface.blit(text_surface,
+                     (edge[0] + result_bar.get_insets(),
+                      edge[1] + result_bar.get_insets()))
+    s.append(r)
+    return s
 
 
 def paint_results(game_area, balls_list, surface):
@@ -92,7 +107,9 @@ def paint_results(game_area, balls_list, surface):
     game_area : area of the balls => tuple of 4 integer
     balls_list : list of balls => list of Ball
     surface : the destination surface => PyGame.Surface
+    (returns sequence of Rect)
     """
+    s = []
     font = pygame.font.Font(None, 40)
     #font = PangoFont(family='Helvetica', size=16)
     LINE_HEIGHT = font.size("0123456789")[1]
@@ -100,14 +117,17 @@ def paint_results(game_area, balls_list, surface):
     ball_index = 0
     BLACK = (0, 0, 0)
     for ball in balls_list:
-        pygame.draw.circle(surface, ball.get_bg_color(),
+        r = pygame.draw.circle(surface, ball.get_bg_color(),
                           (game_area[0] + CIRCLES_RADIUS,
                            game_area[
                                1] + ball_index * LINE_HEIGHT + CIRCLES_RADIUS),
                            CIRCLES_RADIUS)
+        s.append(r)
         txt = ball.get_operation().get_text() + " = " + str(ball.get_operation().
                                                             get_result())
         txt_surface = font.render(txt, 1, BLACK)
-        surface.blit(txt_surface,
+        r = surface.blit(txt_surface,
                      (game_area[0] + 40, game_area[1] + ball_index * LINE_HEIGHT))
+        s.append(r)
         ball_index += 1
+    return s
-- 
1.8.1.2

>From 711f90f10ad1da32a7fe4160063aec7de6348f8b Mon Sep 17 00:00:00 2001
From: James Cameron <qu...@laptop.org>
Date: Tue, 1 Oct 2013 20:43:26 +1000
Subject: [PATCH 5/5] optimise drawing

Change to update only the parts of the screen that have been affected
by drawin gin the preceeding frame.  This cuts average CPU usage from
30% to 5% on OLPC XO-1.5 during the moving of the balls part of the
game.
---
 main_game.py | 45 +++++++++++++++++++++++++++++++--------------
 1 file changed, 31 insertions(+), 14 deletions(-)

diff --git a/main_game.py b/main_game.py
index d8ad3b0..54e696d 100644
--- a/main_game.py
+++ b/main_game.py
@@ -50,6 +50,7 @@ class Game:
         Constructor.
         """
         self._initialized = False
+        self.s = self.p = []
 
     def _lazy_init(self):
         """
@@ -167,6 +168,19 @@ class Game:
         y_good = point[1] >= rect[1] and point[1] <= rect[1] + rect[3]
         return x_good and y_good
 
+    def _update(self):
+        """
+        Update the screen rectangles that have been changed by drawing
+        actions since the last update (self.s), including rectangles
+        that were cleared the previous update (self.p), then clear the
+        current rectangles (self.s) in preparation for the next cycle.
+        """
+        pygame.display.update(self.s + self.p)
+        self.p = self.s
+        for r in self.s:
+            self._screen.fill(self._GAME_BACKGROUND, r)
+        self.s = []
+
     def _play_game(self, time_seconds, operations_config):
         """ The main game routine
             time_seconds : time limit in seconds => integer
@@ -175,7 +189,7 @@ class Game:
             OperationConfig.
         """
         self._screen.fill(self._MENU_BACKGROUND)
-        pygame.display.update()
+        self._update()
 
         game_state = GameState.NORMAL
 
@@ -210,13 +224,12 @@ class Game:
         while True:
             while Gtk.events_pending():
                 Gtk.main_iteration()
-            pygame.display.update()
-            self._screen.fill(self._GAME_BACKGROUND)
-            paint_result_bar(result_bar, self._screen)
-            paint_time_bar(time_bar, self._screen)
+            self._update()
+            self.s += paint_result_bar(result_bar, self._screen)
+            self.s += paint_time_bar(time_bar, self._screen)
             if game_state == GameState.NORMAL:
                 for ball in the_balls:
-                    paint_ball(ball, self._screen)
+                    self.s += paint_ball(ball, self._screen)
 
                 for event in pygame.event.get():
                     if event.type == QUIT:
@@ -245,7 +258,7 @@ class Game:
                 balls_collision.manage_colliding_balls(the_balls)
                 self._clock.tick(self._FPS)
             else:
-                paint_results(balls_area, the_balls, self._screen)
+                self.s += paint_results(balls_area, the_balls, self._screen)
                 # Blinks the status text.
                 if show_status:
                     if game_state == GameState.WON:
@@ -255,7 +268,7 @@ class Game:
                     end_txt_surface = self._end_font.render(end_txt, True,
                                                             self._BLUE,
                                                             self._RED)
-                    self._screen.blit(end_txt_surface, self._END_TXT_POS)
+                    self.s.append(self._screen.blit(end_txt_surface, self._END_TXT_POS))
 
                 for event in pygame.event.get():
                     if event.type == QUIT:
@@ -273,25 +286,29 @@ class Game:
         Manages the main menu.
         """
         self._lazy_init()
+        self.s = []
+        self.s.append(self._screen.fill(self._MENU_BACKGROUND))
+        self._update()
         while True:
-            self._screen.fill(self._MENU_BACKGROUND)
+            while Gtk.events_pending():
+                Gtk.main_iteration()
+            self._update()
             for box_index in range(len(self._levels)):
                 box_value = self._levels_rect[box_index]
-                pygame.draw.rect(
+                s = pygame.draw.rect(
                     self._screen, self._MENU_LEVELS_RECTS_BG_COLOR,
                     box_value)
+                self.s.append(s)
                 txt = _("Level ") + str(box_index + 1)
                 txt_surface = self._menu_font.render(txt, True,
                                                      self._MENU_LEVELS_RECTS_TXT_COLOR)
-                self._screen.blit(txt_surface,
+                s = self._screen.blit(txt_surface,
                                   (self._levels_rect[box_index][0] +
                                    self._MENU_LEVELS_RECTS_TXT_OFFSET[0],
                                    self._levels_rect[box_index][1] +
                                    self._MENU_LEVELS_RECTS_TXT_OFFSET[1]
                                    ))
-            pygame.display.update()
-            while Gtk.events_pending():
-                Gtk.main_iteration()
+                self.s.append(s)
 
             for event in pygame.event.get():
                 if event.type == QUIT:
-- 
1.8.1.2

_______________________________________________
Sugar-devel mailing list
Sugar-devel@lists.sugarlabs.org
http://lists.sugarlabs.org/listinfo/sugar-devel

Reply via email to