On Saturday, 26 May 2018 09:01:17 CEST Daniel Gillet wrote:
> Hello,
> 
> I recently was trying some code and I found something which was both
> surprising and interesting. I thought I would share my findings with you.
> 
> I wanted to play a bit with moderngl
> <https://github.com/cprogrammer1994/ModernGL>, an OpenGL binding. I started
> initially by using pygame to create the window and handle the events. My
> goal was just to use OpenGL 3 features to see how I could display lots of
> moving and rotating sprites on the screen. I also used numpy for storing my
> arrays and I managed to get 10,000 moving and rotating quads on my screen
> while barely maintaining 60 FPS.
> 
> Then I thought I would try to port that code to pyglet, but still using
> moderngl. And this is where the situation gets interesting. After finishing
> the porting, my performances dropped to 30 FPS. The interesting part was
> that I was not using any OpenGL calls from either pygame (doesn't have any
> anyways) nor pyglet. So I knew the OpenGL stuff was not responsible for
> this drop in performances. I started to profile both examples and both
> showed that the bottleneck was the flip function. This makes sense as this
> is where all the data get loaded on the graphic card. But pygame flip
> function was taking something like 15 ms while pyglet flip function was
> taking twice as much, around 30 ms.
> 
> I went down the rabbit hole and I'll spare you with the details of all the
> things I tried. I'll go straight to my findings. Pygame and Pyglet were not
> giving me the same OpenGL context. Pygame gave me an OpenGL with a 16 bits
> depth buffer and no stencil buffer, while Pyglet was giving me a 24 bits
> depth buffer together with an 8 bits stencil buffer. And obviously swapping
> extra information has a cost.
> 
> I added the following code to my pyglet code:
> 
> config = pyglet.gl.Config(double_buffer=True, depth_size=16)
> window = pyglet.window.Window(
>     width=1366, height=768, resizable=True, vsync=True, config=config
> )
> 
> And guess what? I had the same performances as with Pygame, reaching barely
> 60 FPS with 10,000 textured quads moving and rotating on the screen.
> 
> In conclusion Pyglet offers by default an OpenGL context with better depth
> buffer and a stencil buffer. But if you don't use them, they come at a
> cost. I don't know how many Pyglet app really need the 24 bits depth buffer
> together with an 8 bits stencil buffer. Just by changing the OpenGL config,
> you might get a performance boost if you're hitting heavily the graphics
> card.
> 
> For those interested in my little test code, here it is:
> 
> import time
> import collections
> 
> import pyglet
> 
> import moderngl
> import numpy as np
> from vec_utils import create_orthogonal_projection
> 
> import cProfile, pstats
> 
> pr = cProfile.Profile()
> pr.enable()
> 
> 
> class FPSCounter:
>     def __init__(self):
>         self.time = time.perf_counter()
>         self.frame_times = collections.deque(maxlen=60)
> 
>     def tick(self):
>         t1 = time.perf_counter()
>         dt = t1 - self.time
>         self.time = t1
>         self.frame_times.append(dt)
> 
>     def get_fps(self):
>         return len(self.frame_times) / sum(self.frame_times)
> 
> 
> fps_counter = FPSCounter()
> config = pyglet.gl.Config(double_buffer=True, depth_size=16)
> window = pyglet.window.Window(
>     width=1366, height=768, resizable=True, vsync=True, config=config
> )
> 
> ctx = moderngl.create_context()
> ctx.viewport = (0, 0) + window.get_size()
> prog = ctx.program(
>     vertex_shader='''
>         #version 330
>         uniform mat4 Projection;
> 
>         in vec2 in_vert;
>         in vec2 in_texture;
> 
>         in vec3 in_pos;
>         in float in_angle;
>         in vec2 in_scale;
> 
>         out vec2 v_texture;
> 
>         void main() {
>             mat2 rotate = mat2(
>                         cos(in_angle), sin(in_angle),
>                         -sin(in_angle), cos(in_angle)
>                     );
>             vec3 pos;
>             pos = in_pos + vec3(rotate * (in_vert * in_scale), 0.);
>             gl_Position = Projection * vec4(pos, 1.0);
>             v_texture = in_texture;
>         }
>     ''',
>     fragment_shader='''
>         #version 330
>         uniform sampler2D Texture;
> 
>         in vec2 v_texture;
> 
>         out vec4 f_color;
> 
>         void main() {
>             vec4 basecolor = texture(Texture, v_texture);
> 
>             if (basecolor.a == 0.0){
>                 discard;
>             }
>             f_color = basecolor;
>         }
>     ''',
> )
> vertices = np.array([
>     #  x,    y,   u,   v
>     -1.0, -1.0, 0.0, 0.0,
>     -1.0,  1.0, 0.0, 1.0,
>      1.0, -1.0, 1.0, 0.0,
>      1.0,  1.0, 1.0, 1.0,
>     ], dtype=np.float32
> )
> 
> proj = create_orthogonal_projection(
>     left=0, right=600, bottom=0, top=400, near=-1000, far=100,
> dtype=np.float32
> )
> 
> img = pyglet.image.load('grossinis2.png',
> file=pyglet.resource.file('grossinis2.png'))
> texture = ctx.texture((img.width, img.height), 4, img.get_data("RGBA",
> img.pitch))
> texture.use(0)
> 
> INSTANCES = 10_000
> 
> # pos_scale = np.array([
> #       # pos_x, pos_y,  z, angle,   scale_x,       scale_y
> #         100.0, 150.0, 0., 0., rect.width/2, rect.height/2,
> #         120.5, 200.0, 10., 0., rect.width/2, rect.height/2,
> #     ], dtype=np.float32)
> 
> positions = (np.random.rand(INSTANCES, 3) * 1000).astype('f4')
> angles = (np.random.rand(INSTANCES, 1) * 2 * np.pi).astype('f4')
> sizes = np.tile(np.array([img.width / 2, img.height / 2], dtype=np.float32),
> (INSTANCES, 1)
>                 )
> pos_scale = np.hstack((positions, angles, sizes))
> player_pos = pos_scale[0, :]
> 
> pos_scale_buf = ctx.buffer(pos_scale.tobytes())
> 
> 
> vbo = ctx.buffer(vertices.tobytes())
> vao_content = [
>     (vbo, '2f 2f', 'in_vert', 'in_texture'),
>     (pos_scale_buf, '3f 1f 2f/i', 'in_pos', 'in_angle', 'in_scale')
> ]
> vao = ctx.vertex_array(prog, vao_content)
> ctx.enable(moderngl.BLEND)
> ctx.enable(moderngl.DEPTH_TEST)
> 
> 
> def show_fps(dt):
>     print(f"FPS: {fps_counter.get_fps()}")
> 
> 
> def update(dt):
>     pos_scale[1:, 0] += 0.1
>     pos_scale[1:, 3] += 0.01
>     pos_scale[1::2, 2] += 0.1
> 
> 
> def report(dt):
>     pr.disable()
>     with open("pyglet_stats.txt", "w") as f:
>         sortby = 'tottime'
>         ps = pstats.Stats(pr, stream=f).sort_stats(sortby)
>         ps.print_stats()
>     print("Report written")
> 
> 
> pyglet.clock.schedule_once(report, 5)
> 
> pyglet.clock.schedule_interval(show_fps, 1)
> pyglet.clock.schedule_interval(update, 1 / 60)
> 
> 
> @window.event
> def on_resize(width, height):
>     global proj
>     ctx.viewport = (0, 0, width, height)
>     proj = create_orthogonal_projection(
>         left=0, right=width, bottom=0, top=height, near=-1000, far=100,
> dtype=np.float32
>     )
>     return True
> 
> 
> @window.event
> def on_draw():
>     ctx.clear(0.0, 0.0, 0.0, 0.0, depth=1.0)
> 
>     prog['Texture'].value = 0
>     prog['Projection'].write(proj.tobytes())
>     pos_scale_buf.write(pos_scale.tobytes())
>     vao.render(moderngl.TRIANGLE_STRIP, instances=INSTANCES)
>     pos_scale_buf.orphan()
>     fps_counter.tick()
> 
> 
> pyglet.app.run()
> 
> And the vec_utils.py contains the create_orthogonal_projection function
> which is a straight copy paste from the Pyrr project.
> 
> import numpy as np
> 
> def create_orthogonal_projection(
>     left,
>     right,
>     bottom,
>     top,
>     near,
>     far,
>     dtype=None
> ):
>     """Creates an orthogonal projection matrix.
> 
>     :param float left: The left of the near plane relative to the plane's
> 
> centre.
> 
>     :param float right: The right of the near plane relative to the plane's
> 
> centre.
> 
>     :param float top: The top of the near plane relative to the plane's
> 
> centre.
> 
>     :param float bottom: The bottom of the near plane relative to the
> 
> plane's centre.
> 
>     :param float near: The distance of the near plane from the camera's
> 
> origin.
>         It is recommended that the near plane is set to 1.0 or above to
> avoid rendering issues
>         at close range.
> 
>     :param float far: The distance of the far plane from the camera's
> 
> origin.
> 
>     :rtype: numpy.array
>     :return: A projection matrix representing the specified orthogonal
> 
> perspective.
>     .. seealso::
> http://msdn.microsoft.com/en-us/library/dd373965(v=vs.85).aspx
>     """
> 
>     """
>     A 0 0 Tx
>     0 B 0 Ty
>     0 0 C Tz
>     0 0 0 1
>     A = 2 / (right - left)
>     B = 2 / (top - bottom)
>     C = -2 / (far - near)
>     Tx = (right + left) / (right - left)
>     Ty = (top + bottom) / (top - bottom)
>     Tz = (far + near) / (far - near)
>     """
>     rml = right - left
>     tmb = top - bottom
>     fmn = far - near
> 
>     A = 2. / rml
>     B = 2. / tmb
>     C = -2. / fmn
>     Tx = -(right + left) / rml
>     Ty = -(top + bottom) / tmb
>     Tz = -(far + near) / fmn
> 
>     return np.array((
>         ( A, 0., 0., 0.),
>         (0.,  B, 0., 0.),
>         (0., 0.,  C, 0.),
>         (Tx, Ty, Tz, 1.),
>     ), dtype=dtype)
> 
> Daniel

Hi Daniel,

I notice you're using vsync=True on your window which would explain why it 
drops from 60 FPS all the way to 30 FPS and why the flip function time doubles. 
Good work on figuring out the context differences.


-- 
You received this message because you are subscribed to the Google Groups 
"pyglet-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/pyglet-users.
For more options, visit https://groups.google.com/d/optout.

Reply via email to