That is a great finding, thanks for delving into it and sharing!! On 26 May 2018 at 17:01, Daniel Gillet <[email protected]> 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 > > -- > 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. > -- 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.
