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

-- 
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