Search code examples
pythonopenglmemory-leakspyopengl

How to solve Python OpenGL Memory Leakage problem?


I am trying to draw some traingles and render some texts in screen. But I've observed that memory(RAM) is gradually increasing just only for 6 draw calls. I've 8 GB RAM. When I run the program memory usage goes from 4.2 to 6 within 1 minute. Here is the full code.

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GL import shaders

from shader import *

import glfw
import freetype
import glm

import numpy as np
from PIL import Image
import math

class CharacterSlot:
    def __init__(self, texture, glyph):
        self.texture = texture
        self.textureSize = (glyph.bitmap.width, glyph.bitmap.rows)

        if isinstance(glyph, freetype.GlyphSlot):
            self.bearing = (glyph.bitmap_left, glyph.bitmap_top)
            self.advance = glyph.advance.x
        elif isinstance(glyph, freetype.BitmapGlyph):
            self.bearing = (glyph.left, glyph.top)
            self.advance = None
        else:
            raise RuntimeError('unknown glyph type')

def _get_rendering_buffer(xpos, ypos, w, h, zfix=0.0):
    return np.asarray([
        xpos,     ypos + h, 0, 0,
        xpos,     ypos,     0, 1,
        xpos + w, ypos,     1, 1,
        xpos,     ypos + h, 0, 0,
        xpos + w, ypos,     1, 1,
        xpos + w, ypos + h, 1, 0
    ], np.float32)

def init_chars(shaderProgram,window_height,window_width,font_size=24,fontfile = "Vera.ttf"):
    glUseProgram(shaderProgram)
    
    #get projection
    shader_projection = glGetUniformLocation(shaderProgram, "projection")
    W = window_width
    H = window_height
    projection = glm.ortho(-W/2, W/2, -H/2, H/2)
    glUniformMatrix4fv(shader_projection, 1, GL_FALSE, glm.value_ptr(projection))
    
    #disable byte-alignment restriction
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1)

    face = freetype.Face(fontfile)
    face.set_char_size(font_size*64 )

    #load first 128 characters of ASCII set
    Characters = dict()
    for i in range(0,128):
        face.load_char(chr(i))
        glyph = face.glyph
        
        #generate texture
        texture = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, texture)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, glyph.bitmap.width, glyph.bitmap.rows, 0,
                     GL_RED, GL_UNSIGNED_BYTE, glyph.bitmap.buffer)

        #texture options
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

        #now store character for later use
        Characters[chr(i)] = CharacterSlot(texture,glyph)

    glBindTexture(GL_TEXTURE_2D, 0)    
    glUseProgram(0)
    
    return Characters
    
def render_text(window,shaderProgram,text,x,y,scale,Characters,color=(170,250,255)):
    r,g,b = color
    
    glUseProgram(shaderProgram)

    #configure VAO/VBO for texture quads
    VAO = glGenVertexArrays(1)
    glBindVertexArray(VAO)
    
    VBO = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, 6 * 4 * 4, None, GL_STATIC_DRAW)
    glEnableVertexAttribArray(0)
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    glBindVertexArray(0)
    
    glUniform3f(glGetUniformLocation(shaderProgram, "textColor"),r/255,g/255,b/255) 
    glActiveTexture(GL_TEXTURE0)
    glBindVertexArray(VAO)
    
    for c in text:
        ch = Characters[c]
        w, h = ch.textureSize
        w = w*scale
        h = h*scale
        vertices = _get_rendering_buffer(x,y,w,h)

        #render glyph texture over quad
        glBindTexture(GL_TEXTURE_2D, ch.texture)
        #update content of VBO memory
        glBindBuffer(GL_ARRAY_BUFFER, VBO)
        glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)

        glBindBuffer(GL_ARRAY_BUFFER, 0)
        #render quad
        glDrawArrays(GL_TRIANGLES, 0, 6)
        #now advance cursors for next glyph (note that advance is number of 1/64 pixels)
        x += (ch.advance>>6)*scale

    glBindTexture(GL_TEXTURE_2D, 0);
    glUseProgram(0)

    #UNBIND and DELETE VAO/VBO
    glBindVertexArray(0)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    glDeleteBuffers(1, id(VBO))
    glDeleteBuffers(1, id(VAO))
    

def triangle(shaderProgram,window,x=0,y=0):
    vertices = [-0.5, -0.5, 0.0,
                 0.5, -0.5, 0.0,
                 0.0, 0.5, 0.0]
    vertices = np.array(vertices, dtype=np.float32)

    VBO = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
    glEnableVertexAttribArray(0)

    #use shader program
    glUseProgram(shaderProgram)

    #accessing ourColor variable from shaderProgram
    vertexColorLoc = glGetUniformLocation(shaderProgram, "ourColor")
    glUniform4f(vertexColorLoc, 255, 28/255.0, 20/255.0, 0.7);

    #transform matrix
    transform = glm.mat4(1)
    transform = glm.translate(transform,glm.vec3(x,y,0))
    MVP = glGetUniformLocation(shaderProgram, "MVP")
    glUniformMatrix4fv(MVP, 1, GL_FALSE, glm.value_ptr(transform))

    #drawing trangle 
    glLineWidth(3)
    glDrawArrays(GL_TRIANGLES, 0, 3)
    
    glUseProgram(0)

    #UNBIND and DELETE VAO/VBO
    glBindVertexArray(0)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    glDeleteBuffers(1, id(VBO))
   

def main():
    glfw.init()
    window = glfw.create_window(640, 640,"EXAMPLE PROGRAM",None,None)    
    glfw.make_context_current(window)
    
    #initliaze shader programs
    shaderProgram = get_shaderProgram()
    text_shaderProgram = get_text_shaderProgram()

    #load characters and VAO/VBO for text rendering
    Characters = init_chars(text_shaderProgram,640,640)
    #window loop
    while not glfw.window_should_close(window):
        glfw.poll_events()

        #screen
        glClearColor(0, 0, 0, 1)
        glClear(GL_COLOR_BUFFER_BIT)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

        #draw functions
        
        render_text(window,text_shaderProgram,"TRIANGLE",-50,-200,1,Characters)
        render_text(window,text_shaderProgram,"A",0,180,1,Characters)
        render_text(window,text_shaderProgram,"B",-160,-180,1,Characters)
        render_text(window,text_shaderProgram,"C",150,-180,1,Characters)
        
        triangle(shaderProgram,window)
        triangle(shaderProgram,window,x=0.5,y=0.5)
        #swap buffers 
        glfw.swap_buffers(window)
        glfw.swap_interval(1)
        
    glfw.terminate()
    
if __name__ == '__main__':
    main()

The shader program is here. But I think the problems is in buffer object. I've tried to unbind VAO/VBO and delete buffers by following code. But I see no change.

    #UNBIND and DELETE VAO/VBO
    glBindVertexArray(0)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    glDeleteBuffers(1, id(VBO))
    glDeleteBuffers(1, id(VAO))

Here is the related problem where accepted answer suggested that glGenBuffers causes memory leak. The alternate function glCreateBuffers is not available in pyopengl. How can I solve this issue?


Solution

  • I can't see any good reason for recreating the Vertex Array Object and Array Buffer Object every time when render_text respectively triangle is called. The vertex specification and the number of vertices doesn't change, so it would be sufficient to update the content of the buffer.

    Create the Vertex Array Object and the Array Buffer Object once at initialization:

    def init_buffers():
        global text_VAO, text_VBO, triangle_VAO, triangle_VBO
    
        text_VAO = glGenVertexArrays(1)
        glBindVertexArray(text_VAO)
        
        text_VBO = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, text_VBO)
        glBufferData(GL_ARRAY_BUFFER, 6 * 4 * 4, None, GL_STATIC_DRAW)
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, None)
    
        vertices = [-0.5, -0.5, 0.0,
                     0.5, -0.5, 0.0,
                     0.0, 0.5, 0.0]
        vertices = np.array(vertices, dtype=np.float32)
    
        triangle_VAO = glGenVertexArrays(1)
        glBindVertexArray(triangle_VAO)
    
        triangle_VBO = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, triangle_VBO)
        glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
    
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
        glEnableVertexAttribArray(0)
    

    Use then in the functions render_text and triangle:

    def render_text(window,shaderProgram,text,x,y,scale,Characters,color=(170,250,255)):
        # [...]
    
        glBindVertexArray(text_VAO)
        
        for c in text:
            # [...]
    
            glBindBuffer(GL_ARRAY_BUFFER, text_VBO)
            glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)
    
        # glDeleteBuffers(1, id(VBO)) <--- DELETE
        # glDeleteBuffers(1, id(VAO)) <--- DELETE
    
    def triangle(shaderProgram,window,x=0,y=0):
        
        glBindVertexArray(triangle_VAO)
    
        # [...]
    
        # glDeleteBuffers(1, id(VBO)) <--- DELETE
    

    Invoke init_buffers before the application loop:

    def main():
        # [...]
    
        init_buffers()
        
        while not glfw.window_should_close(window):
            # [...]