Weird issue occuring when rendering fonts with stbtt_truetype

Started by Dubstepzedd, April 27, 2022, 21:40:15

Previous topic - Next topic

Dubstepzedd

Hi!

I experience some weird behaviour when rendering a BakedFont object in OpenGL.

I have written some code that uses stbtt_truetype's BakedFont in order to render a String with the specified font.
However, for days I have been trying to solve a very weird issue when it comes to the ByteBuffers.

The code below works fine. It does not crash or do anything else crazy.
However, if I add
stbtt_FreeBitMap(ttf)
after the
stbtt_BakeFontBitmap(...)
call - it crashes.
The same thing occurs if I free the bitmap object after the
glTexImage2D(...)]
call.
Not only this, but it also crashes if I remove the ByteBuffer ttf as a parameter into the Font object.
Basically, it seems like the ttf object has be kept alive in order for it to NOT crash.

Just to be clear, it does not crash in the creating process - but rather during the rendering process.
I can't see any visible flaws in my code (but there must be... because it crashes), and I have difficulties understanding the crash report from Java.
QuoteEXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffe7c96829a, pid=9772, tid=17808
This tells me that I access memory that is NULL (I guess?). This makes be believe that the program tries to access the data after it has been removed.
Here is a pastebin link to the full crash report:
https://pastebin.pl/view/aa818779

The creating process:

/** Creates a Font object with a fixed size from a path to a .ttf file.
     * @param path - the path to the .ttf file.
     * @param fontSize - the size in pixels
     * @return the generated Font object that is needed upon rendering.
     */
    @SuppressWarnings("unused")
    public Font loadFont(final String path, final int fontSize) {

        //Convert .ttf file to data we can deal with, that being a ByteBuffer.
        ByteBuffer ttf = Resources.loadFileAsByteBuffer(path);
        STBTTFontinfo info = STBTTFontinfo.create();

        if(ttf == null || !STBTruetype.stbtt_InitFont(info,ttf)) {
            throw new IllegalStateException("Failed to initialize font information. Check the log for exact reason.");
        }

        try (MemoryStack stack = stackPush()) {
            IntBuffer pAscent  = stack.mallocInt(1);
            IntBuffer pDescent = stack.mallocInt(1);
            IntBuffer pLineGap = stack.mallocInt(1);

            stbtt_GetFontVMetrics(info, pAscent, pDescent, pLineGap);

        }

        //Capacity is the ASCII max
        STBTTBakedChar.Buffer charData = STBTTBakedChar.malloc(215);

        int bitmapWidth  = fontSize * 16;
        int bitmapHeight = fontSize * 16;

        ByteBuffer bitmap = BufferUtils.createByteBuffer(bitmapWidth * bitmapHeight);

        stbtt_BakeFontBitmap(ttf, fontSize, bitmap, bitmapWidth, bitmapHeight, 32, charData);
        int texId = glGenTextures();
        glBindTexture(GL_TEXTURE_2D, texId);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R,GL_ONE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G,GL_ONE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B,GL_ONE);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA8, bitmapWidth, bitmapHeight, 0, GL_ALPHA, GL_UNSIGNED_BYTE, bitmap);

        glBindTexture(GL_TEXTURE_2D, 0);

        Texture2D texture2D = new Texture2D(texId, bitmapWidth, bitmapHeight);

        return new Font(fontSize,ttf,info,charData,texture2D);
    }


This returns a Font object. This contains the information that we want to keep until later. Such as the STBTTFontInfo.

The rendering process
/** Draws a string with a specified font,color and transform to the screen.
     *
     * @param font - the Font object. This contains necessary information regarding the font that is used during rendering.
     * @param text - the string that is to be rendered.
     * @param color - the RGBA color.
     * @param transform - the Matrix4f transform.
     */
    public void drawText(final Font font, final String text, final Vector4f color,
                         final Matrix4f transform) {

        FloatBuffer x = BufferUtils.createFloatBuffer(1),y = BufferUtils.createFloatBuffer(1);
        STBTTAlignedQuad q = STBTTAlignedQuad.create();
        for(int i = 0; i < text.length(); i++) {
            stbtt_GetBakedQuad(font.getCharData(), font.getBitmapWidth(), font.getBitMapHeight(), text.charAt(i) - 32,
                    x, y, q, false);

            Vertex[] vertices = new Vertex[]{
                    new Vertex(new Vector2f( q.x0(), q.y0()), color, transform, new Vector2f(q.s0(), q.t0()),
                            font.getTexture2d().getRenderSlot()),

                    new Vertex(new Vector2f(q.x1(), q.y0()), color, transform, new Vector2f(q.s1(), q.t0()),
                            font.getTexture2d().getRenderSlot()),

                    new Vertex(new Vector2f(q.x1(), q.y1()), color, transform, new Vector2f(q.s1(), q.t1()),
                            font.getTexture2d().getRenderSlot()),

                    new Vertex(new Vector2f(q.x0(), q.y1()), color, transform, new Vector2f(q.s0(), q.t1()),
                            font.getTexture2d().getRenderSlot())
            };

            DefaultModel model = new DefaultModel(vertices, quadIndices);

            batchRenderer.addToDrawCall(model, font.getTexture2d(), defaultShader);
        }


This renders a String with the created font. A Vertex object simply contains the following data: (Vector2f pos, Vector4f color, Matrix4f transform, Vector2f textureCoords).
These vertices are then put into a Model object which is then inserted into a batch rendering system. All the batches are drawn at the end of each render call.

Does anyone know why it crashes? If so, how can I modify my code to make it stop? I tried using getCodePointBitmap(...) but it crashes as well.
If you need additional information, feel free to ask!

Thanks in advance :)



spasi

Hey Dubstepzedd,

Your analysis is correct, the ttf buffer must be kept alive for as long as you're rendering. stb_truetype does not copy the buffer internally and calls like stbtt_GetBakedQuad access its data directly.

Dubstepzedd

Quote from: spasi on May 05, 2022, 08:29:55
Hey Dubstepzedd,

Your analysis is correct, the ttf buffer must be kept alive for as long as you're rendering. stb_truetype does not copy the buffer internally and calls like stbtt_GetBakedQuad access its data directly.


Thanks for your reply Spasi!

So there is nothing wrong with my code? I thought you were supposed to free some buffers - but maybe that's just in C/C++?

spasi

I can't see any obvious problem in the code above. I'm assuming that Resources.loadFileAsByteBuffer returns a ByteBuffer that has been allocated with ByteBuffer.allocateDirect, which returns a GC-managed buffer. You just have to make sure that there's a strong reference to that buffer while rendering, otherwise the GC will eventually free it (at an unspecified, usually surprising, point in time).

Dubstepzedd

Quote from: spasi on May 05, 2022, 09:34:53
I can't see any obvious problem in the code above. I'm assuming that Resources.loadFileAsByteBuffer returns a ByteBuffer that has been allocated with ByteBuffer.allocateDirect, which returns a GC-managed buffer. You just have to make sure that there's a strong reference to that buffer while rendering, otherwise the GC will eventually free it (at an unspecified, usually surprising, point in time).

Exactly, it converts it into a ByteBuffer. I will post the code for it in case you want to take a look. I have yet to experience such runtime errors. :)

/** Loads a file in the resource folder as an InputStream.
     *
     * @param file - the name or path of the file that is to be loaded.
     * @return an InputStream containing the loaded data.
     */
    public static InputStream loadFileAsInputStream(String file) {
        return ClassLoader.getSystemResourceAsStream(file);
    }

    /** Loads a file in the resource folder as a ByteBuffer object
     *
     * @param file - the name or path of the file that is to be loaded.
     * @return a ByteBuffer object containing the loaded data. Returns null if the file does not exist.
     */
    public static ByteBuffer loadFileAsByteBuffer(final String file) {
        InputStream s = loadFileAsInputStream(file);

        if(s == null) {
            LOGGER.error("Given file or path does not exist in the resources folder. File: " + file);
            return null;
        }

        try {
            byte[] data = s.readAllBytes();
            //LWJGL only accepts DIRECT NIO Buffers, so we must fulfill their requirements.
            ByteBuffer buffer = ByteBuffer.allocateDirect(data.length);
            buffer.put(data);

            buffer.flip();

            return buffer;

        }
        catch (IOException e) {
            e.printStackTrace();
            LOGGER.error("Failed to read contents of file: " + file);
            return null;
        }

    }


When am I supposed to free the ttf buffer? When exiting the application?

Dubstepzedd

Quote from: spasi on May 05, 2022, 09:34:53
I can't see any obvious problem in the code above. I'm assuming that Resources.loadFileAsByteBuffer returns a ByteBuffer that has been allocated with ByteBuffer.allocateDirect, which returns a GC-managed buffer. You just have to make sure that there's a strong reference to that buffer while rendering, otherwise the GC will eventually free it (at an unspecified, usually surprising, point in time).

Is ttf the only buffer that I can't free? I just tried to free the temporary bitmap and it crashed due to an EXCEPTION_ACCESS_VIOLATION. Does glTexImage2D(...) free it? Because I can't access it after
that method call.

Thanks, Liam

spasi

No, glTexImage2D does not free anything. Buffers created by ByteBuffer.allocateDirect are freed automatically by the GC, when the object (that holds the offheap pointer) is no longer accessible. Freeing such buffers explicitly is not possible, because it would cause a double-free (and a crash). I recommend reading https://blog.lwjgl.org/memory-management-in-lwjgl-3/.

Dubstepzedd

Quote from: spasi on May 05, 2022, 16:12:43
No, glTexImage2D does not free anything. Buffers created by ByteBuffer.allocateDirect are freed automatically by the GC, when the object (that holds the offheap pointer) is no longer accessible. Freeing such buffers explicitly is not possible, because it would cause a double-free (and a crash). I recommend reading https://blog.lwjgl.org/memory-management-in-lwjgl-3/.

Well, That explains the crashes...

What if you use ByteBuffer.createByteBuffer(...)? Is that also directly freed by the garbage collector? I read in the article that you sent that allocateDirect(...) is "horrible". So I guess I have to change that accordingly.

Dubstepzedd

Quote from: spasi on May 05, 2022, 16:12:43
No, glTexImage2D does not free anything. Buffers created by ByteBuffer.allocateDirect are freed automatically by the GC, when the object (that holds the offheap pointer) is no longer accessible. Freeing such buffers explicitly is not possible, because it would cause a double-free (and a crash). I recommend reading https://blog.lwjgl.org/memory-management-in-lwjgl-3/.

Thanks for linking the article. I replaced the ByteBuffer.allocateDirect(...) method from the old LWJGL with the class that was recommended in the article: org.lwjgl.system.libc.Stdlib.

EDIT:
This fixed the stbtt_freeBitmap issue regarding the temporary bitmap ByteBuffer. It can now be freed without crashing.

Thanks for the help Spasi!