HumbleUI / Skija

Java bindings for Skia
Apache License 2.0
498 stars 34 forks source link

Memory Leak: Runtime Effect + Runtime generated Images #62

Closed AquilaAbriel closed 10 months ago

AquilaAbriel commented 10 months ago

I'm currently trying to implement a runtime effect which uses runtime generated images as shader inputs. I tried to replicate the RuntimeEffectScene example: https://github.com/HumbleUI/Skija/blob/master/examples/scenes/src/RuntimeEffectScene.java

But instead of a static texture image from a file, I wanted to use runtime created images drawn to offscreen surfaces. My implementation "works" (The output looks like what I expected), but it also introduced a memory leak issue. I tried to track down the leaking object, but so far with no avail. Maybe my approach is simply wrong... If somebody could help me figure this out, that would be awesome.

Some minimalist code to showcase my current implementation:

public class Main
{
    public static void main(String [] args) throws Exception
    {
        GLFWErrorCallback.createPrint(System.out).set();
        if (!GLFW.glfwInit())
        {
            throw new IllegalStateException("Error: Unable to initialize GLFW");
        }
        loop();
    }

    private static void loop()
    {
        //Create window (Setup GLFW and SKIJA)
        GameWindow window = new GameWindow();

        //Create main offscreen target
        OffscreenTarget mainTarget = new OffscreenTarget(window.getContext(), 640, 480, 0);

        //Create debug effect
        RuntimeEffect effect = RuntimeEffect.makeForShader(
                "uniform shader in_texture0;\n" +
                "uniform shader in_mask0;\n" +
                "\n" +
                "float4 main(float2 p)\n" +
                "{\n" +
                "    float4 texel = in_texture0.eval(p);\n" +
                "    float4 texelMask = in_mask0.eval(p);\n" +
                "    float4 result = float4(texel.r * texelMask.r, texel.g * texelMask.r, texel.b * texelMask.r, texel.a * texelMask.r);\n" +
                "\treturn result;\n" +
                "}"
        );

        while (window.isAlive())
        {
            GLFW.glfwMakeContextCurrent(window.getWindowHandle());
            GLFW.glfwPollEvents();

            //Create debug offscreen targets
            OffscreenTarget debugTarget1 = new OffscreenTarget(window.getContext(), 400, 400, 0);
            OffscreenTarget debugTarget2 = new OffscreenTarget(window.getContext(), 400, 400, 0);

            //Draw gradient to debug target 1
            debugTarget1.clear(Color.makeARGB(0, 0, 0, 0));
            Shader gradientShader = Shader.makeLinearGradient(new Point(0, 0), new Point(400, 400), new int[] { 0xFF247ba0, 0xFFf3ffbd });
            Paint pt1 = new Paint();
            pt1.setShader(gradientShader);
            debugTarget1.getSurface().getCanvas().drawRect(new Rect(0, 0, 400, 400), pt1);
            gradientShader.close();

            //Draw black and white mask to debug target 2
            debugTarget2.clear(Color.makeARGB(255, 0, 0, 0));
            Paint pt2 = new Paint();
            pt2.setAntiAlias(true);
            pt2.setColor(Color.makeARGB(255, 255, 255, 255));
            debugTarget2.getSurface().getCanvas().drawOval(new Rect(0, 0, 400, 400), pt2);

            //Create runtime shader from debug offscreen target 1 + 2 images
            Shader[] debugShaderChildShaders = new Shader[2];
            Image debugImage1 = debugTarget1.getSurface().makeImageSnapshot();
            debugShaderChildShaders[0] = debugImage1.makeShader();
            Image debugImage2 = debugTarget2.getSurface().makeImageSnapshot();
            debugShaderChildShaders[1] = debugImage2.makeShader();

            //Create runtime shader of debug effect
            Shader runtimeShader = effect.makeShader(null, debugShaderChildShaders, null);

            //Clear main offscreen target
            mainTarget.clear(Color.makeARGB(255, 32, 32, 32));

            //Draw runtime debug shader to main target
            mainTarget.getSurface().getCanvas().save();
            Paint pt3 = new Paint();
            pt3.setShader(runtimeShader);
            mainTarget.getSurface().getCanvas().translate(120, 40);
            mainTarget.getSurface().getCanvas().drawRect(new Rect(0, 0, 400, 400), pt3);
            mainTarget.getSurface().getCanvas().restore();

            //Try to close all runtime objects -->
            runtimeShader.close();
            debugShaderChildShaders[0].close();
            debugShaderChildShaders[1].close();
            debugImage1.close();
            debugImage2.close();
            debugTarget1.dispose();
            debugTarget2.dispose();
            //...Still loosing about 200mb of ram + vram per second

            //Draw main offscreen target to window
            window.draw(mainTarget);

            window.getContext().flush();
            GLFW.glfwSwapBuffers(window.getWindowHandle());
        }

        effect.close();
        mainTarget.dispose();
        window.closeWindow();
    }
}

The OffscreenTarget.dispose() method just calls Surface.close() on the encapsulated Skija Surface.

dzaima commented 10 months ago

Could try also adding pt3.close(); and/or System.gc(); to the loop to debug further.

AquilaAbriel commented 10 months ago

Oh my god... that was quick! And it fixed the memory leak ^^ All I had to do was to close the Paint object. You are just awesome, thank you very much.