changeset 131:fab7ad2f0d8b

Use Cython, improve performances!
author Thibaut Girka <thib@sitedethib.com>
date Sun, 11 Sep 2011 02:02:59 +0200
parents 11ab06f4c4c6
children fba45c37ec99
files README eclviewer.py pytouhou/opengl/gamerenderer.py pytouhou/opengl/gamerenderer.pyx pytouhou/opengl/gamerunner.py pytouhou/opengl/sprite.pxd pytouhou/opengl/sprite.py pytouhou/opengl/sprite.pyx pytouhou/utils/bitstream.py pytouhou/utils/bitstream.pyx pytouhou/utils/interpolator.py pytouhou/utils/interpolator.pyx pytouhou/utils/matrix.pxd pytouhou/utils/matrix.py pytouhou/utils/matrix.pyx
diffstat 10 files changed, 206 insertions(+), 195 deletions(-) [+]
line wrap: on
line diff
--- a/README
+++ b/README
@@ -11,10 +11,16 @@ Additionally, this project aims for a pe
 Dependencies:
 -------------
 
+Running:
     * Python2 (>= 2.6)
+    * Cython
     * Pyglet
 
 
+Building sample data:
+    * thtk (http://code.google.com/p/thtk/)
+
+
 
 Documentation:
 --------------
--- a/eclviewer.py
+++ b/eclviewer.py
@@ -16,9 +16,12 @@
 import sys
 import os
 
+import pyximport
+pyximport.install()
+
 from pytouhou.resource.loader import Loader
 from pytouhou.game.background import Background
-from pytouhou.opengl.gamerenderer import GameRenderer
+from pytouhou.opengl.gamerunner import GameRunner
 from pytouhou.game.games import EoSDGame
 from pytouhou.game.player import PlayerState
 
@@ -39,8 +42,8 @@ def main(path, stage_num):
     print(stage.name)
 
     # Main loop
-    renderer = GameRenderer(resource_loader, game, background)
-    renderer.start()
+    runner = GameRunner(resource_loader, game, background)
+    runner.start()
 
 
 try:
rename from pytouhou/opengl/gamerenderer.py
rename to pytouhou/opengl/gamerenderer.pyx
--- a/pytouhou/opengl/gamerenderer.py
+++ b/pytouhou/opengl/gamerenderer.pyx
@@ -12,116 +12,55 @@
 ## GNU General Public License for more details.
 ##
 
-import struct
-from itertools import chain
+from libc.stdlib cimport malloc, free
+
 import ctypes
 
-import pyglet
+import struct
+
 from pyglet.gl import *
 
 from pytouhou.opengl.texture import TextureManager
-from pytouhou.opengl.sprite import get_sprite_rendering_data
+from pytouhou.opengl.sprite cimport get_sprite_rendering_data
 from pytouhou.opengl.background import get_background_rendering_data
 
 
 MAX_ELEMENTS = 10000
 
 
-class GameRenderer(pyglet.window.Window):
+cdef struct Vertex:
+    float x, y, z
+    float u, v
+    unsigned char r, g, b, a
+
+
+cdef class GameRenderer:
+    cdef public texture_manager
+    cdef public game
+    cdef public background
+
+    cdef Vertex *vertex_buffer
+
+
+    def __cinit__(self, resource_loader, game=None, background=None):
+        # Allocate buffers
+        self.vertex_buffer = <Vertex*> malloc(MAX_ELEMENTS * sizeof(Vertex))
+
+
+    def __dealloc__(self):
+        free(self.vertex_buffer)
+
+
     def __init__(self, resource_loader, game=None, background=None):
-        pyglet.window.Window.__init__(self, caption='PyTouhou', resizable=False)
-        self.keys = pyglet.window.key.KeyStateHandler()
-        self.push_handlers(self.keys)
-
         self.texture_manager = TextureManager(resource_loader)
 
-        self.fps_display = pyglet.clock.ClockDisplay()
-
         self.game = game
         self.background = background
 
 
-    def start(self, width=384, height=448):
-        self.set_size(width, height)
-
-        # Initialize OpenGL
-        glMatrixMode(GL_PROJECTION)
-        glLoadIdentity()
-        gluPerspective(30, float(width)/float(height),
-                       101010101./2010101., 101010101./10101.)
-
-        glEnable(GL_BLEND)
-        glEnable(GL_TEXTURE_2D)
-        glEnable(GL_FOG)
-        glHint(GL_FOG_HINT, GL_NICEST)
-        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
-        glEnableClientState(GL_COLOR_ARRAY)
-        glEnableClientState(GL_VERTEX_ARRAY)
-        glEnableClientState(GL_TEXTURE_COORD_ARRAY)
-
-        # Allocate buffers
-        buff = ctypes.c_buffer(MAX_ELEMENTS * 4 * (3 * 4 + 2 * 4 + 4))
-        self.buffers = (buff,
-                        ctypes.byref(buff, 3 * 4),
-                        ctypes.byref(buff, 3 * 4 + 2 * 4))
-
-        # Use our own loop to ensure 60 (for now, 120) fps
-        pyglet.clock.set_fps_limit(120)
-        while not self.has_exit:
-            pyglet.clock.tick()
-            self.dispatch_events()
-            self.update()
-            self.on_draw()
-            self.flip()
-
-
-    def on_resize(self, width, height):
-        glViewport(0, 0, width, height)
-
+    cdef render_elements(self, elements):
+        cdef unsigned short nb_vertices = 0
 
-    def on_key_press(self, symbol, modifiers):
-        if symbol == pyglet.window.key.ESCAPE:
-            self.has_exit = True
-        # XXX: Fullscreen will be enabled the day pyglet stops sucking
-        elif symbol == pyglet.window.key.F11:
-            self.set_fullscreen(not self.fullscreen)
-
-
-    def update(self):
-        if self.background:
-            self.background.update(self.game.game_state.frame)
-        if self.game:
-            #TODO: allow user settings
-            keystate = 0
-            if self.keys[pyglet.window.key.W]:
-                keystate |= 1
-            if self.keys[pyglet.window.key.X]:
-                keystate |= 2
-            #TODO: on some configurations, LSHIFT is Shift_L when pressed
-            # and ISO_Prev_Group when released, confusing the hell out of pyglet
-            # and leading to a always-on LSHIFT...
-            if self.keys[pyglet.window.key.LSHIFT]:
-                keystate |= 4
-            if self.keys[pyglet.window.key.UP]:
-                keystate |= 16
-            if self.keys[pyglet.window.key.DOWN]:
-                keystate |= 32
-            if self.keys[pyglet.window.key.LEFT]:
-                keystate |= 64
-            if self.keys[pyglet.window.key.RIGHT]:
-                keystate |= 128
-            if self.keys[pyglet.window.key.LCTRL]:
-                keystate |= 256
-            self.game.run_iter(keystate) #TODO: self.keys...
-
-
-    def render_elements(self, elements):
-        texture_manager = self.texture_manager
-
-        pack_data = struct.Struct('fff ff BBBB' * 4).pack_into
-        _vertices, _uvs, _colors = self.buffers
-
-        nb_vertices = 0
         indices_by_texture = {}
 
         for element in elements:
@@ -135,22 +74,10 @@ class GameRenderer(pyglet.window.Window)
                 (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), (x4, y4, z4) = vertices
                 r1, g1, b1, a1, r2, g2, b2, a2, r3, g3, b3, a3, r4, g4, b4, a4 = colors
                 u1, v1, u2, v2, u3, v3, u4, v4 = uvs
-                pack_data(_vertices, nb_vertices * (3 * 4 + 2 * 4 + 4),
-                                            x1 + ox, y1 + oy, z1,
-                                            u1, v1,
-                                            r1, g1, b1, a1,
-
-                                            x2 + ox, y2 + oy, z2,
-                                            u2, v2,
-                                            r2, g2, b2, a2,
-
-                                            x3 + ox, y3 + oy, z3,
-                                            u3, v3,
-                                            r3, g3, b3, a3,
-
-                                            x4 + ox, y4 + oy, z4,
-                                            u4, v4,
-                                            r4, g4, b4, a4)
+                self.vertex_buffer[nb_vertices] = Vertex(x1 + ox, y1 + oy, z1, u1, v1, r1, g1, b1, a1)
+                self.vertex_buffer[nb_vertices+1] = Vertex(x2 + ox, y2 + oy, z2, u2, v2, r2, g2, b2, a2)
+                self.vertex_buffer[nb_vertices+2] = Vertex(x3 + ox, y3 + oy, z3, u3, v3, r3, g3, b3, a3)
+                self.vertex_buffer[nb_vertices+3] = Vertex(x4 + ox, y4 + oy, z4, u4, v4, r4, g4, b4, a4)
 
                 # Add indices
                 index = nb_vertices
@@ -158,19 +85,19 @@ class GameRenderer(pyglet.window.Window)
 
                 nb_vertices += 4
 
-        glVertexPointer(3, GL_FLOAT, 24, _vertices)
-        glTexCoordPointer(2, GL_FLOAT, 24, _uvs)
-        glColorPointer(4, GL_UNSIGNED_BYTE, 24, _colors)
+        glVertexPointer(3, GL_FLOAT, 24, <long> &self.vertex_buffer[0].x)
+        glTexCoordPointer(2, GL_FLOAT, 24, <long> &self.vertex_buffer[0].u)
+        glColorPointer(4, GL_UNSIGNED_BYTE, 24, <long> &self.vertex_buffer[0].r)
 
         for (texture_key, blendfunc), indices in indices_by_texture.items():
             nb_indices = len(indices)
             indices = struct.pack(str(nb_indices) + 'H', *indices)
             glBlendFunc(GL_SRC_ALPHA, (GL_ONE_MINUS_SRC_ALPHA, GL_ONE)[blendfunc])
-            glBindTexture(GL_TEXTURE_2D, texture_manager[texture_key].id)
+            glBindTexture(GL_TEXTURE_2D, self.texture_manager[texture_key].id)
             glDrawElements(GL_QUADS, nb_indices, GL_UNSIGNED_SHORT, indices)
 
 
-    def on_draw(self):
+    def render(self):
         glClear(GL_DEPTH_BUFFER_BIT)
 
         back = self.background
@@ -227,10 +154,3 @@ class GameRenderer(pyglet.window.Window)
             self.render_elements(game.players)
             glEnable(GL_FOG)
 
-        #TODO
-        glMatrixMode(GL_MODELVIEW)
-        glLoadIdentity()
-        gluLookAt(192., 224., 835.979370,
-                  192, 224., 0., 0., 1., 0.)
-        self.fps_display.draw()
-
new file mode 100644
--- /dev/null
+++ b/pytouhou/opengl/gamerunner.py
@@ -0,0 +1,111 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+
+import pyglet
+from pyglet.gl import *
+
+from pytouhou.opengl.texture import TextureManager
+from pytouhou.opengl.sprite import get_sprite_rendering_data #TODO: cimport?
+from pytouhou.opengl.background import get_background_rendering_data
+from pytouhou.opengl.gamerenderer import GameRenderer
+
+
+class GameRunner(pyglet.window.Window, GameRenderer):
+    def __init__(self, resource_loader, game=None, background=None):
+        GameRenderer.__init__(self, resource_loader, game, background)
+        pyglet.window.Window.__init__(self, caption='PyTouhou', resizable=False)
+        self.keys = pyglet.window.key.KeyStateHandler()
+        self.push_handlers(self.keys)
+
+        self.fps_display = pyglet.clock.ClockDisplay()
+
+
+    def start(self, width=384, height=448):
+        self.set_size(width, height)
+
+        # Initialize OpenGL
+        glMatrixMode(GL_PROJECTION)
+        glLoadIdentity()
+        gluPerspective(30, float(width)/float(height),
+                       101010101./2010101., 101010101./10101.)
+
+        glEnable(GL_BLEND)
+        glEnable(GL_TEXTURE_2D)
+        glEnable(GL_FOG)
+        glHint(GL_FOG_HINT, GL_NICEST)
+        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
+        glEnableClientState(GL_COLOR_ARRAY)
+        glEnableClientState(GL_VERTEX_ARRAY)
+        glEnableClientState(GL_TEXTURE_COORD_ARRAY)
+
+        # Use our own loop to ensure 60 (for now, 120) fps
+        pyglet.clock.set_fps_limit(120)
+        while not self.has_exit:
+            pyglet.clock.tick()
+            self.dispatch_events()
+            self.update()
+            self.on_draw()
+            self.flip()
+
+
+    def on_resize(self, width, height):
+        glViewport(0, 0, width, height)
+
+
+    def on_key_press(self, symbol, modifiers):
+        if symbol == pyglet.window.key.ESCAPE:
+            self.has_exit = True
+        # XXX: Fullscreen will be enabled the day pyglet stops sucking
+        elif symbol == pyglet.window.key.F11:
+            self.set_fullscreen(not self.fullscreen)
+
+
+    def update(self):
+        if self.background:
+            self.background.update(self.game.game_state.frame)
+        if self.game:
+            #TODO: allow user settings
+            keystate = 0
+            if self.keys[pyglet.window.key.W]:
+                keystate |= 1
+            if self.keys[pyglet.window.key.X]:
+                keystate |= 2
+            #TODO: on some configurations, LSHIFT is Shift_L when pressed
+            # and ISO_Prev_Group when released, confusing the hell out of pyglet
+            # and leading to a always-on LSHIFT...
+            if self.keys[pyglet.window.key.LSHIFT]:
+                keystate |= 4
+            if self.keys[pyglet.window.key.UP]:
+                keystate |= 16
+            if self.keys[pyglet.window.key.DOWN]:
+                keystate |= 32
+            if self.keys[pyglet.window.key.LEFT]:
+                keystate |= 64
+            if self.keys[pyglet.window.key.RIGHT]:
+                keystate |= 128
+            if self.keys[pyglet.window.key.LCTRL]:
+                keystate |= 256
+            self.game.run_iter(keystate) #TODO: self.keys...
+
+
+    def on_draw(self):
+        GameRenderer.render(self)
+
+        #TODO
+        glMatrixMode(GL_MODELVIEW)
+        glLoadIdentity()
+        gluLookAt(192., 224., 835.979370,
+                  192, 224., 0., 0., 1., 0.)
+        self.fps_display.draw()
+
new file mode 100644
--- /dev/null
+++ b/pytouhou/opengl/sprite.pxd
@@ -0,0 +1,1 @@
+cpdef object get_sprite_rendering_data(object sprite)
rename from pytouhou/opengl/sprite.py
rename to pytouhou/opengl/sprite.pyx
--- a/pytouhou/opengl/sprite.py
+++ b/pytouhou/opengl/sprite.pyx
@@ -15,10 +15,12 @@
 
 from math import pi
 
-from pytouhou.utils.matrix import Matrix
+from pytouhou.utils.matrix cimport Matrix
 
 
-def get_sprite_rendering_data(sprite):
+cpdef object get_sprite_rendering_data(object sprite):
+    cdef Matrix vertmat
+
     if not sprite._changed:
         return sprite._rendering_data
 
@@ -52,7 +54,7 @@ def get_sprite_rendering_data(sprite):
     if sprite.corner_relative_placement: # Reposition
         vertmat.translate(width / 2., height / 2., 0.)
     if sprite.allow_dest_offset:
-        vertmat.translate(*sprite.dest_offset)
+        vertmat.translate(sprite.dest_offset[0], sprite.dest_offset[1], sprite.dest_offset[2])
 
     x_1 = 1. / sprite.anm.size[0]
     y_1 = 1. / sprite.anm.size[1]
rename from pytouhou/utils/bitstream.py
rename to pytouhou/utils/bitstream.pyx
--- a/pytouhou/utils/bitstream.py
+++ b/pytouhou/utils/bitstream.pyx
@@ -12,8 +12,12 @@
 ## GNU General Public License for more details.
 ##
 
-class BitStream(object):
-    def __init__(self, io):
+cdef class BitStream:
+    cdef public io
+    cdef public int bits
+    cdef public int byte
+
+    def __init__(BitStream self, io):
         self.io = io
         self.bits = 0
         self.byte = 0
@@ -27,21 +31,21 @@ class BitStream(object):
         return self.io.__exit__(type, value, traceback)
 
 
-    def seek(self, offset, whence=0):
+    def seek(BitStream self, offset, whence=0):
         self.io.seek(offset, whence)
         self.byte = 0
         self.bits = 0
 
 
-    def tell(self):
+    def tell(BitStream self):
         return self.io.tell()
 
 
-    def tell2(self):
+    def tell2(BitStream self):
         return self.io.tell(), self.bits
 
 
-    def read_bit(self):
+    cpdef unsigned char read_bit(BitStream self):
         if not self.bits:
             self.byte = ord(self.io.read(1))
             self.bits = 8
@@ -49,14 +53,15 @@ class BitStream(object):
         return (self.byte >> self.bits) & 0x01
 
 
-    def read(self, nb_bits):
+    def read(BitStream self, nb_bits):
+        cdef unsigned int value
         value = 0
         for i in range(nb_bits - 1, -1, -1):
             value |= self.read_bit() << i
         return value
 
 
-    def write_bit(self, bit):
+    cpdef write_bit(BitStream self, bit):
         if self.bits == 8:
             self.io.write(chr(self.byte))
             self.bits = 0
@@ -66,12 +71,12 @@ class BitStream(object):
         self.bits += 1
 
 
-    def write(self, bits, nb_bits):
+    def write(BitStream self, bits, nb_bits):
         for i in range(nb_bits):
             self.write_bit(bits >> (nb_bits - 1 - i) & 0x01)
 
 
-    def flush(self):
+    def flush(BitStream self):
         self.io.write(chr(self.byte))
         self.bits = 0
         self.byte = 0
rename from pytouhou/utils/interpolator.py
rename to pytouhou/utils/interpolator.pyx
--- a/pytouhou/utils/interpolator.py
+++ b/pytouhou/utils/interpolator.pyx
@@ -56,6 +56,6 @@ class Interpolator(object):
             self.start_frame = frame
         else:
             coeff = self._formula(float(frame - self.start_frame) / float(self.end_frame - self.start_frame))
-            self.values = tuple(start_value + coeff * (end_value - start_value)
-                                for (start_value, end_value) in zip(self.start_values, self.end_values))
+            self.values = [start_value + coeff * (end_value - start_value)
+                           for (start_value, end_value) in zip(self.start_values, self.end_values)]
 
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/matrix.pxd
@@ -0,0 +1,10 @@
+cdef class Matrix:
+    cdef public data
+
+    cpdef flip(Matrix self)
+    cpdef scale(Matrix self, x, y, z)
+    cpdef scale2d(Matrix self, x, y)
+    cpdef translate(Matrix self, x, y, z)
+    cpdef rotate_x(Matrix self, angle)
+    cpdef rotate_y(Matrix self, angle)
+    cpdef rotate_z(Matrix self, angle)
rename from pytouhou/utils/matrix.py
rename to pytouhou/utils/matrix.pyx
--- a/pytouhou/utils/matrix.py
+++ b/pytouhou/utils/matrix.pyx
@@ -12,35 +12,28 @@
 ## GNU General Public License for more details.
 ##
 
-#TODO: find/learn to use a proper lib
+from libc.math cimport sin, cos
+
 
-from math import sin, cos
-
-class Matrix(object):
-    def __init__(self, data=None):
+cdef class Matrix:
+    def __init__(Matrix self, data=None):
         self.data = data or [[0] * 4 for i in xrange(4)]
 
 
-    def mult(self, other_matrix):
-        d1 = self.data
-        d2 = other_matrix.data
-        return Matrix([[sum(d1[i][a] * d2[a][j] for a in xrange(4)) for j in xrange(4)] for i in xrange(4)])
-
-
-    def flip(self):
+    cpdef flip(Matrix self):
         data = self.data
         a, b, c, d = data[0]
         data[0] = [-a, -b, -c, -d]
 
 
-    def scale(self, x, y, z):
+    cpdef scale(Matrix self, x, y, z):
         d1 = self.data
         d1[0] = [a * x for a in d1[0]]
         d1[1] = [a * y for a in d1[1]]
         d1[2] = [a * z for a in d1[2]]
 
 
-    def scale2d(self, x, y):
+    cpdef scale2d(Matrix self, x, y):
         data = self.data
         d1a, d1b, d1c, d1d = data[0]
         d2a, d2b, d2c, d2d = data[1]
@@ -48,7 +41,7 @@ class Matrix(object):
         data[1] = [d2a * y, d2b * y, d2c * y, d2d * y]
 
 
-    def translate(self, x, y, z):
+    cpdef translate(Matrix self, x, y, z):
         data = self.data
         a, b, c = data[3][:3]
         a, b, c = a * x, b * y, c * z
@@ -60,7 +53,7 @@ class Matrix(object):
         data[2] = [d3a + c, d3b + c, d3c + c, d3d + c]
 
 
-    def rotate_x(self, angle):
+    cpdef rotate_x(Matrix self, angle):
         d1 = self.data
         cos_a = cos(angle)
         sin_a = sin(angle)
@@ -68,7 +61,7 @@ class Matrix(object):
                         [sin_a * d1[1][i] + cos_a * d1[2][i] for i in range(4)])
 
 
-    def rotate_y(self, angle):
+    cpdef rotate_y(Matrix self, angle):
         d1 = self.data
         cos_a = cos(angle)
         sin_a = sin(angle)
@@ -76,50 +69,10 @@ class Matrix(object):
                         [- sin_a * d1[0][i] + cos_a * d1[2][i] for i in range(4)])
 
 
-    def rotate_z(self, angle):
+    cpdef rotate_z(Matrix self, angle):
         d1 = self.data
         cos_a = cos(angle)
         sin_a = sin(angle)
         d1[0], d1[1] = ([cos_a * d1[0][i] - sin_a * d1[1][i] for i in range(4)],
                         [sin_a * d1[0][i] + cos_a * d1[1][i] for i in range(4)])
 
-
-    @classmethod
-    def get_translation_matrix(cls, x, y, z):
-        return cls([[1., 0., 0., x],
-                    [0., 1., 0., y],
-                    [0., 0., 1., z],
-                    [0., 0., 0., 1.]])
-
-
-    @classmethod
-    def get_scaling_matrix(cls, x, y, z):
-        return cls([[x,  0., 0., 0.],
-                    [0., y,  0., 0.],
-                    [0., 0., z,  0.],
-                    [0., 0., 0., 1.]])
-
-
-    @classmethod
-    def get_rotation_matrix(cls, angle, axis):
-        """Only handles axis = x, y or z."""
-        cos_a = cos(angle)
-        sin_a = sin(angle)
-        if axis == 'x':
-            return Matrix([[    1.,     0.,     0.,     0.],
-                           [    0.,  cos_a, -sin_a,     0.],
-                           [    0.,  sin_a,  cos_a,     0.],
-                           [    0.,     0.,     0.,     1.]])
-        elif axis == 'y':
-            return Matrix([[ cos_a,     0.,  sin_a,     0.],
-                           [    0.,     1.,     0.,     0.],
-                           [-sin_a,     0.,  cos_a,     0.],
-                           [    0.,     0.,     0.,     1.]])
-        elif axis == 'z':
-            return Matrix([[ cos_a, -sin_a,     0.,     0.],
-                           [ sin_a,  cos_a,     0.,     0.],
-                           [    0.,     0.,     1.,     0.],
-                           [    0.,     0.,     0.,     1.]])
-        else:
-            raise Exception
-