changeset 418:63f59be04a54

Replace Pyglet with SDL2 for window creation and events; disables framerate control/display and sound.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 16 Jul 2013 21:07:15 +0200
parents efae61ad6efe
children 1c92721f8e49
files .hgignore README pytouhou/lib/__init__.py pytouhou/lib/sdl.pxd pytouhou/lib/sdl.pyx pytouhou/lib/sdl.pyxbld pytouhou/ui/gamerunner.py setup.py
diffstat 8 files changed, 282 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore
+++ b/.hgignore
@@ -1,1 +1,8 @@
-.pyc
+\.pyc$
+\.pyxbldc$
+\.c$
+\.o$
+\.so$
+\.pyd$
+^build$
+^scripts$
--- a/README
+++ b/README
@@ -15,6 +15,7 @@ Running:
     * Python2 (>= 2.6)
     * Cython
     * Pyglet
+    * SDL2
 
 
 Building sample data:
@@ -27,7 +28,7 @@ Documentation:
 
 The code should be sufficiently documented for anyone interested to learn how the EoSD engine work,
 but additional documentation is available at:
-http://linkmauve.fr/doc/touhou/
+http://pytouhou.linkmauve.fr/
 
 
 
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/pytouhou/lib/sdl.pxd
@@ -0,0 +1,88 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## 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.
+##
+
+cdef extern from "SDL.h":
+    ctypedef unsigned int Uint32
+    ctypedef unsigned short Uint16
+    ctypedef unsigned char Uint8
+
+    int SDL_INIT_VIDEO
+
+    int SDL_Init(Uint32 flags)
+    void SDL_Quit()
+
+
+cdef extern from "SDL_error.h":
+    const char *SDL_GetError()
+
+
+cdef extern from "SDL_video.h":
+    ctypedef enum SDL_GLattr:
+        SDL_GL_CONTEXT_MAJOR_VERSION
+        SDL_GL_CONTEXT_MINOR_VERSION
+        SDL_GL_DOUBLEBUFFER
+        SDL_GL_DEPTH_SIZE
+
+    ctypedef enum SDL_WindowFlags:
+        SDL_WINDOWPOS_CENTERED
+        SDL_WINDOW_OPENGL
+        SDL_WINDOW_SHOWN
+
+    ctypedef struct SDL_Window:
+        pass
+
+    ctypedef void *SDL_GLContext
+
+    int SDL_GL_SetAttribute(SDL_GLattr attr, int value)
+    SDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags)
+    SDL_GLContext SDL_GL_CreateContext(SDL_Window *window)
+    void SDL_GL_SwapWindow(SDL_Window *window)
+    void SDL_GL_DeleteContext(SDL_GLContext context)
+    void SDL_DestroyWindow(SDL_Window *window)
+
+
+cdef extern from "SDL_scancode.h":
+    ctypedef enum SDL_Scancode:
+        SDL_SCANCODE_Z
+        SDL_SCANCODE_X
+        SDL_SCANCODE_LSHIFT
+        SDL_SCANCODE_UP
+        SDL_SCANCODE_DOWN
+        SDL_SCANCODE_LEFT
+        SDL_SCANCODE_RIGHT
+        SDL_SCANCODE_LCTRL
+        SDL_SCANCODE_ESCAPE
+
+
+cdef extern from "SDL_events.h":
+    ctypedef enum SDL_EventType:
+        SDL_KEYDOWN
+        SDL_QUIT
+
+    ctypedef struct SDL_Keysym:
+        SDL_Scancode scancode
+
+    ctypedef struct SDL_KeyboardEvent:
+        Uint32 type
+        SDL_Keysym keysym
+
+    ctypedef union SDL_Event:
+        Uint32 type
+        SDL_KeyboardEvent key
+
+    int SDL_PollEvent(SDL_Event *event)
+
+
+cdef extern from "SDL_keyboard.h":
+    const Uint8 *SDL_GetKeyboardState(int *numkeys)
new file mode 100644
--- /dev/null
+++ b/pytouhou/lib/sdl.pyx
@@ -0,0 +1,96 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## 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.
+##
+
+INIT_VIDEO = SDL_INIT_VIDEO
+
+GL_CONTEXT_MAJOR_VERSION = SDL_GL_CONTEXT_MAJOR_VERSION
+GL_CONTEXT_MINOR_VERSION = SDL_GL_CONTEXT_MINOR_VERSION
+GL_DOUBLEBUFFER = SDL_GL_DOUBLEBUFFER
+GL_DEPTH_SIZE = SDL_GL_DEPTH_SIZE
+
+WINDOWPOS_CENTERED = SDL_WINDOWPOS_CENTERED
+WINDOW_OPENGL = SDL_WINDOW_OPENGL
+WINDOW_SHOWN = SDL_WINDOW_SHOWN
+
+SCANCODE_Z = SDL_SCANCODE_Z
+SCANCODE_X = SDL_SCANCODE_X
+SCANCODE_LSHIFT = SDL_SCANCODE_LSHIFT
+SCANCODE_UP = SDL_SCANCODE_UP
+SCANCODE_DOWN = SDL_SCANCODE_DOWN
+SCANCODE_LEFT = SDL_SCANCODE_LEFT
+SCANCODE_RIGHT = SDL_SCANCODE_RIGHT
+SCANCODE_LCTRL = SDL_SCANCODE_LCTRL
+SCANCODE_ESCAPE = SDL_SCANCODE_ESCAPE
+
+KEYDOWN = SDL_KEYDOWN
+QUIT = SDL_QUIT
+
+
+class SDLError(Exception):
+    pass
+
+
+cdef class Window:
+    cdef SDL_Window *window
+    cdef SDL_GLContext context
+
+    def __init__(self, const char *title, int x, int y, int w, int h, Uint32 flags):
+        self.window = SDL_CreateWindow(title, x, y, w, h, flags)
+        if self.window == NULL:
+            raise SDLError(SDL_GetError())
+
+    def destroy_window(self):
+        SDL_DestroyWindow(self.window)
+
+    def gl_create_context(self):
+        self.context = SDL_GL_CreateContext(self.window)
+
+    def gl_swap_window(self):
+        SDL_GL_SwapWindow(self.window)
+
+    def gl_delete_context(self):
+        SDL_GL_DeleteContext(self.context)
+
+
+def init(Uint32 flags):
+    if SDL_Init(flags) < 0:
+        raise SDLError(SDL_GetError())
+
+
+def quit():
+    SDL_Quit()
+
+
+def gl_set_attribute(SDL_GLattr attr, int value):
+    if SDL_GL_SetAttribute(attr, value) < 0:
+        raise SDLError(SDL_GetError())
+
+
+def poll_events():
+    cdef SDL_Event event
+    ret = []
+    while SDL_PollEvent(&event):
+        if event.type == SDL_KEYDOWN:
+            ret.append((event.type, event.key.keysym.scancode))
+        elif event.type == SDL_QUIT:
+            ret.append((event.type,))
+    return ret
+
+
+def get_keyboard_state():
+    cdef int numkeys
+    cdef bint k
+    cdef const Uint8 *state
+    state = SDL_GetKeyboardState(&numkeys)
+    return tuple([k is not False for k in state[:numkeys]])
new file mode 100644
--- /dev/null
+++ b/pytouhou/lib/sdl.pyxbld
@@ -0,0 +1,30 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2013 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+##
+## 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.
+##
+
+""" Build instructions for the SDL module. """
+
+from distutils.extension import Extension
+from subprocess import check_output
+
+COMMAND = 'pkg-config'
+LIBRARIES = ['sdl2']
+
+def make_ext(modname, pyxfilename):
+    """ Compile and link with the corrects options. """
+    compile_args = check_output([COMMAND, '--cflags'] + LIBRARIES).split()
+    link_args = check_output([COMMAND, '--libs'] + LIBRARIES).split()
+    return Extension(name=modname,
+                     sources=[pyxfilename],
+                     extra_compile_args=compile_args,
+                     extra_link_args=link_args)
--- a/pytouhou/ui/gamerunner.py
+++ b/pytouhou/ui/gamerunner.py
@@ -12,8 +12,7 @@
 ## GNU General Public License for more details.
 ##
 
-import pyglet
-import traceback
+from pytouhou.lib import sdl
 
 from pyglet.gl import (glMatrixMode, glLoadIdentity, glEnable, glDisable,
                        glHint, glEnableClientState, glViewport, glScissor,
@@ -37,20 +36,29 @@ from ctypes import c_uint, byref
 logger = get_logger(__name__)
 
 
-class GameRunner(pyglet.window.Window, GameRenderer):
+class GameRunner(GameRenderer):
     def __init__(self, resource_loader, game=None, background=None, replay=None, double_buffer=True, fps_limit=60, fixed_pipeline=False, skip=False):
         GameRenderer.__init__(self, resource_loader, game, background)
 
-        config = pyglet.gl.Config(double_buffer=double_buffer)
-        width, height = (game.interface.width, game.interface.height) if game else (None, None)
-        pyglet.window.Window.__init__(self, width=width, height=height,
-                                      caption='PyTouhou', resizable=False,
-                                      config=config)
+        sdl.init(sdl.INIT_VIDEO)
+        sdl.gl_set_attribute(sdl.GL_CONTEXT_MAJOR_VERSION, 2)
+        sdl.gl_set_attribute(sdl.GL_CONTEXT_MINOR_VERSION, 1)
+        sdl.gl_set_attribute(sdl.GL_DOUBLEBUFFER, int(double_buffer))
+        sdl.gl_set_attribute(sdl.GL_DEPTH_SIZE, 24)
+
+        self.width, self.height = (game.interface.width, game.interface.height) if game else (640, 480)
+        self.win = sdl.Window('PyTouhou',
+                              sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED,
+                              self.width, self.height,
+                              sdl.WINDOW_OPENGL | sdl.WINDOW_SHOWN)
+        self.win.gl_create_context()
 
         self.fps_limit = fps_limit
         self.use_fixed_pipeline = fixed_pipeline
         self.replay_level = None
         self.skip = skip
+        self.has_exit = False
+        self.keystate = 0
 
         if not self.use_fixed_pipeline:
             self.game_shader = GameShader()
@@ -64,7 +72,7 @@ class GameRunner(pyglet.window.Window, G
         if game:
             self.load_game(game, background, replay)
 
-        self.clock = pyglet.clock.get_default()
+        #self.clock = pyglet.clock.get_default()
 
 
     def load_game(self, game=None, background=None, bgms=None, replay=None, save_keystates=None):
@@ -85,14 +93,16 @@ class GameRunner(pyglet.window.Window, G
 
     def set_input(self, replay=None):
         if not replay or not replay.levels[self.game.stage-1]:
-            self.keys = pyglet.window.key.KeyStateHandler()
-            self.push_handlers(self.keys)
             self.replay_level = None
         else:
             self.replay_level = replay.levels[self.game.stage-1]
             self.keys = self.replay_level.iter_keystates()
 
 
+    def set_size(self, width, height):
+        self.win.set_window_size(width, height)
+
+
     def start(self, width=None, height=None):
         width = width or (self.game.interface.width if self.game else 640)
         height = height or (self.game.interface.height if self.game else 480)
@@ -115,16 +125,14 @@ class GameRunner(pyglet.window.Window, G
         self.game_mvp = game_view * self.proj
         self.interface_mvp = ortho_2d(0., float(self.width), float(self.height), 0.)
 
-        if self.fps_limit > 0:
-            pyglet.clock.set_fps_limit(self.fps_limit)
+        #if self.fps_limit > 0:
+        #    pyglet.clock.set_fps_limit(self.fps_limit)
         while not self.has_exit:
             if not self.skip:
-                pyglet.clock.tick()
-                self.dispatch_events()
                 self.update()
                 self.render_game()
                 self.render_interface()
-                self.flip()
+                self.win.gl_swap_window()
             else:
                 self.update()
 
@@ -132,50 +140,42 @@ class GameRunner(pyglet.window.Window, G
             vbo_array = (c_uint * 2)(self.vbo, self.back_vbo)
             glDeleteBuffers(2, vbo_array)
 
-
-    def _event_text_symbol(self, ev):
-        # XXX: Ugly workaround to a pyglet bug on X11
-        #TODO: fix that bug in pyglet
-        try:
-            return pyglet.window.Window._event_text_symbol(self, ev)
-        except Exception as exc:
-            logger.warn('Pyglet error: %s', traceback.format_exc(exc))
-            return None, None
-
-
-    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)
+        self.win.gl_delete_context()
+        self.win.destroy_window()
+        sdl.quit()
 
 
     def update(self):
         if self.background:
             self.background.update(self.game.frame)
+        for event in sdl.poll_events():
+            type_ = event[0]
+            if type_ == sdl.KEYDOWN:
+                scancode = event[1]
+                if scancode == sdl.SCANCODE_ESCAPE:
+                    self.has_exit = True #TODO: implement the pause.
+            elif type_ == sdl.QUIT:
+                self.has_exit = True
         if self.game:
             if not self.replay_level:
                 #TODO: allow user settings
+                keys = sdl.get_keyboard_state()
                 keystate = 0
-                if self.keys[pyglet.window.key.W] or self.keys[pyglet.window.key.Z]:
+                if keys[sdl.SCANCODE_Z]:
                     keystate |= 1
-                if self.keys[pyglet.window.key.X]:
+                if keys[sdl.SCANCODE_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]:
+                if keys[sdl.SCANCODE_LSHIFT]:
                     keystate |= 4
-                if self.keys[pyglet.window.key.UP]:
+                if keys[sdl.SCANCODE_UP]:
                     keystate |= 16
-                if self.keys[pyglet.window.key.DOWN]:
+                if keys[sdl.SCANCODE_DOWN]:
                     keystate |= 32
-                if self.keys[pyglet.window.key.LEFT]:
+                if keys[sdl.SCANCODE_LEFT]:
                     keystate |= 64
-                if self.keys[pyglet.window.key.RIGHT]:
+                if keys[sdl.SCANCODE_RIGHT]:
                     keystate |= 128
-                if self.keys[pyglet.window.key.LCTRL]:
+                if keys[sdl.SCANCODE_LCTRL]:
                     keystate |= 256
             else:
                 try:
@@ -208,7 +208,7 @@ class GameRunner(pyglet.window.Window, G
 
     def render_interface(self):
         interface = self.game.interface
-        interface.labels['framerate'].set_text('%.2ffps' % self.clock.get_fps())
+        #interface.labels['framerate'].set_text('%.2ffps' % self.clock.get_ticks())
 
         if self.use_fixed_pipeline:
             glMatrixMode(GL_MODELVIEW)
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,7 @@ from distutils.extension import Extensio
 from distutils.command.build_scripts import build_scripts
 from distutils.dep_util import newer
 from distutils import log
+from subprocess import check_output
 
 # Cython is needed
 try:
@@ -16,6 +17,9 @@ except ImportError:
     sys.exit(1)
 
 
+COMMAND = 'pkg-config'
+LIBRARIES = ['sdl2']
+
 packages = []
 extension_names = []
 extensions = []
@@ -47,8 +51,16 @@ for directory, _, files in os.walk('pyto
         if filename.endswith('.pyx'):
             extension_name = '%s.%s' % (package, os.path.splitext(filename)[0])
             extension_names.append(extension_name)
+            if extension_name == 'pytouhou.lib.sdl':
+                compile_args = check_output([COMMAND, '--cflags'] + LIBRARIES).split()
+                link_args = check_output([COMMAND, '--libs'] + LIBRARIES).split()
+            else:
+                compile_args = None
+                link_args = None
             extensions.append(Extension(extension_name,
-                                        [os.path.join(directory, filename)]))
+                                        [os.path.join(directory, filename)],
+                                        extra_compile_args=compile_args,
+                                        extra_link_args=link_args))