SofaPython3 icon indicating copy to clipboard operation
SofaPython3 copied to clipboard

Memory Leak related to unload and rendering

Open ScheiklP opened this issue 3 years ago • 3 comments

Hi, there is another memory leak, this time related to rendering in combination with simulation.unload.

As a testing scene, I took the pygame example from SofaPython3 and added a reload to every tenth step.

import Sofa
import Sofa.Core
import Sofa.Simulation
import Sofa.SofaGL
import SofaRuntime
import os

sofa_directory = os.environ["SOFA_ROOT"]
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *

display_size = (800, 600)


def init_display(node):
    pygame.display.init()
    pygame.display.set_mode(display_size, pygame.DOUBLEBUF | pygame.OPENGL)

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glEnable(GL_LIGHTING)
    glEnable(GL_DEPTH_TEST)
    Sofa.SofaGL.glewInit()
    Sofa.Simulation.initVisual(node)
    Sofa.Simulation.initTextures(node)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45, (display_size[0] / display_size[1]), 0.1, 50.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()


def simple_render(rootNode):
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glEnable(GL_LIGHTING)
    glEnable(GL_DEPTH_TEST)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45, (display_size[0] / display_size[1]), 0.1, 50.0)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    cameraMVM = rootNode.camera.getOpenGLModelViewMatrix()
    glMultMatrixd(cameraMVM)
    Sofa.SofaGL.draw(rootNode)

    pygame.display.get_surface().fill((0, 0, 0))
    pygame.display.flip()


def createScene(root):
    # Register all the common component in the factory.
    SofaRuntime.PluginRepository.addFirstPath(os.path.join(sofa_directory, "bin"))
    root.addObject("RequiredPlugin", name="SofaOpenglVisual")  # visual stuff
    root.addObject("RequiredPlugin", name="SofaLoader")  # geometry loaders
    root.addObject("RequiredPlugin", name="SofaSimpleFem")  # diffusion fem
    root.addObject("RequiredPlugin", name="SofaBoundaryCondition")  # constraints
    root.addObject("RequiredPlugin", name="SofaEngine")  # Box Roi
    root.addObject("RequiredPlugin", name="SofaImplicitOdeSolver")  # implicit solver
    root.addObject("RequiredPlugin", name="SofaMiscForceField")  # meshmatrix
    root.addObject("RequiredPlugin", name="SofaGeneralEngine")  # TextureInterpolation
    root.addObject("RequiredPlugin", name="CImgPlugin")  # for loading a bmp image for texture
    root.addObject("RequiredPlugin", name="SofaBaseLinearSolver")
    root.addObject("RequiredPlugin", name="SofaGeneralVisual")
    root.addObject("RequiredPlugin", name="SofaTopologyMapping")
    root.addObject("RequiredPlugin", name="SofaGeneralTopology")
    root.addObject("RequiredPlugin", name="SofaGeneralLoader")

    ### these are just some things that stay still and move around
    # so you know the animation is actually happening
    root.gravity = [0, -1.0, 0]
    root.addObject("VisualStyle", displayFlags="showAll")
    root.addObject("MeshGmshLoader", name="meshLoaderCoarse", filename="mesh/liver.msh")
    root.addObject("MeshObjLoader", name="meshLoaderFine", filename="mesh/liver-smooth.obj")

    root.addObject("EulerImplicitSolver")
    root.addObject("CGLinearSolver", iterations="200", tolerance="1e-09", threshold="1e-09")

    liver = root.addChild("liver")

    liver.addObject("TetrahedronSetTopologyContainer", name="topo", src="@../meshLoaderCoarse")
    liver.addObject("TetrahedronSetGeometryAlgorithms", template="Vec3d", name="GeomAlgo")
    liver.addObject("MechanicalObject", template="Vec3d", name="MechanicalModel", showObject="1", showObjectScale="3")

    liver.addObject("TetrahedronFEMForceField", name="fem", youngModulus="1000", poissonRatio="0.4", method="large")

    liver.addObject("MeshMatrixMass", massDensity="1")
    liver.addObject("FixedConstraint", indices="2 3 50")
    visual = liver.addChild("visual")
    visual.addObject("MeshObjLoader", name="meshLoader_0", filename="mesh/liver-smooth.obj", handleSeams="1")
    visual.addObject("OglModel", name="VisualModel", src="@meshLoader_0", color="red")
    visual.addObject("BarycentricMapping", input="@..", output="@VisualModel", name="visual mapping")

    # place light and a camera
    root.addObject("LightManager")
    root.addObject("DirectionalLight", direction=[0, 1, 0])
    root.addObject("InteractiveCamera", name="camera", position=[0, 15, 0], lookAt=[0, 0, 0], distance=37, fieldOfView=45, zNear=0.63, zFar=55.69)


if __name__ == "__main__":

    root = Sofa.Core.Node("myroot")
    createScene(root)
    Sofa.Simulation.init(root)

    with_window = True
    if with_window:
        init_display(root)

    for i in range(300):
        Sofa.Simulation.animate(root, root.getDt())
        Sofa.Simulation.updateVisual(root)
        if with_window:
            simple_render(root)
        if i % 10 == 0:
            if with_window:
                pygame.display.quit()
            Sofa.Simulation.unload(root)
            createScene(root)
            Sofa.Simulation.init(root)
            if with_window:
                init_display(root)

The leak seems to be related to the OpenGL context, because for 31 init_display calls, I get and AddressSanitizer output of

Indirect leak of 59521240 byte(s) in 31 object(s) allocated from:
    #0 0x7f8a1c4cf808 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cc:144
    #1 0x7f8a0be337c3 in SDL_malloc_REAL /sdl_build/SDL2-2.0.16/src/stdlib/SDL_malloc.c:5388

that matches the lost amount of memory. And it also veeeery closely matches the amount of memory required to store 31 RGBA buffers of the set image size. :D

>>> (600*800*4*31)/59521240
0.9999791671006854

If no display is created, there is no leak.

@fredroy Do you maybe know what could be the issue here?

Cheers, Paul

ScheiklP avatar Oct 06 '22 18:10 ScheiklP

Hello, As you say, it seems there is a leak of a copy of the framebuffer or something like that. if you are using the built-in GUIs (qt, etc), I would have not been surprised (as the gui code is not really.... clean lets say) But here in your case, you invoke your own GUI (pygame). I would first try to try to locate where it could come from so I would just enable showVisualModel in the VisualStyle component. This will only call the draw() from Visual components (and thus limit the number of guys to investigate...)

One thing I see in your scene which could create/store framebuffers would be the light, which can comnpute depth buffers to compute (optionnally) shadows. You can try to just disable the LightManager/DirectionalLight to see if this is the culprit.

fredroy avatar Oct 07 '22 00:10 fredroy

Just one thing for your sceme : if you are using the latest version, you dont need anymore the CImgPlugin to load bmp, as it is now handled natively in the core by the STB utility header (Sofa.Helper) CImgPlugin is only useful to load TIFF files now.

fredroy avatar Oct 07 '22 00:10 fredroy

Hi @fredroy Thanks for the tip!

Sadly it even leaks, when there are absolutely no components but the root node.

import Sofa
from tqdm import tqdm
import Sofa.Core
import Sofa.Simulation
import Sofa.SofaGL
# import SofaRuntime
import os
import time
import numpy as np

sofa_directory = os.environ["SOFA_ROOT"]
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *

display_size = (600, 600)


def init_display(node):
    pygame.display.init()
    pygame.display.set_mode(display_size, pygame.DOUBLEBUF | pygame.OPENGL)

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glEnable(GL_LIGHTING)
    glEnable(GL_DEPTH_TEST)
    Sofa.SofaGL.glewInit()
    Sofa.Simulation.initVisual(node)
    Sofa.Simulation.initTextures(node)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45, (display_size[0] / display_size[1]), 0.1, 50.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()


def simple_render(rootNode):
    """
    Get the OpenGL Context to render an image (snapshot) of the simulation state
    """
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glEnable(GL_LIGHTING)
    glEnable(GL_DEPTH_TEST)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45, (display_size[0] / display_size[1]), 0.1, 50.0)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    # cameraMVM = rootNode.InteractiveCamera.getOpenGLModelViewMatrix()
    cameraMVM = np.identity(4)

    glMultMatrixd(cameraMVM)
    Sofa.SofaGL.draw(rootNode)

    pygame.display.get_surface().fill((0, 0, 0))
    pygame.display.flip()


def createScene(root):
    pass
    # root.addObject("InteractiveCamera", widthViewport=display_size[0], heightViewport=display_size[1])


if __name__ == "__main__":

    root = Sofa.Core.Node("myroot")
    createScene(root)
    Sofa.Simulation.init(root)

    with_window = True
    if with_window:
        init_display(root)

    for i in tqdm(range(300)):
        Sofa.Simulation.animate(root, root.getDt())
        Sofa.Simulation.updateVisual(root)
        if with_window:
            simple_render(root)
        if i % 10 == 0:
            if with_window:
                pygame.display.quit()
            Sofa.Simulation.unload(root)
            createScene(root)
            Sofa.Simulation.init(root)
            if with_window:
                init_display(root)
            else:
                time.sleep(0.001)

ScheiklP avatar Oct 07 '22 11:10 ScheiklP