[Tips] Huge texture uploading (memory tricks)

Started by AeroShark333, April 03, 2020, 08:52:58 PM

Previous topic - Next topic

AeroShark333

Hello,

I have been digging around a little for the memory issues I have concerning texture uploading since I do use pretty huge textures.
It seems that jPCT-AE requires twice the size of a texture (in peak memory usage) in JVM RAM: the original pixeldata of the texture + the buffer used for uploading.
But I believe that on modern day devices you can actually use tricks to consume near nothing in the JVM RAM when loading such huge textures.
I believe jPCT-AE doesn't support these features out of the box because it's been directly ported from the PC version and to provide backwards compatibility with the very old devices.

Here's the class I use to upload textures now (feel free to use it or make your own modifications to it):
package com.aeroshark333.artofearthify.utils;

import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLUtils;

import com.threed.jpct.RGBColor;
import com.threed.jpct.Texture;

// Created by Abiram/AeroShark333
public class CustomTexture extends Texture {
    final Bitmap bmp;

    public CustomTexture(Bitmap image) {
        super(2, 2, RGBColor.BLACK);
        this.bmp = image;
    }

    public int loadTexture(final boolean forceNearest, final boolean keepInMemory){
        final int[] textureHandle = new int[1];

        GLES20.glGenTextures(1, textureHandle, 0);

        if (textureHandle[0] != 0)
        {
            // Bind to the texture in OpenGL
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0]);

            // Set filtering
            if (!forceNearest) {
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
            }else {
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
                GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
            }

            // Load the bitmap into the bound texture.
            GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bmp, 0);

            // Recycle the bitmap, since its data has been loaded into OpenGL.
            if (!keepInMemory) {
                bmp.recycle();
            }
        }

        if (textureHandle[0] == 0)
        {
            throw new RuntimeException("Error loading texture.");
        }

        this.setExternalId(textureHandle[0],GLES20.GL_TEXTURE_2D);
        return textureHandle[0];
    }
}


One could use the CustomTexture constructor from any Thread but the CustomTexture#loadTexture(boolean,boolean) method needs to be called from the GLThread.
This implementation is kinda very simple and it doesn't cover all of jPCT-AE's texture features (and it probably isn't really (easily) possible to have texture manipulation/effects like jPCT-AE supports them out of the box). However, the uploading process seems much faster using this method too. Also, this could potentially solve memory issues if you have them.

Now, I did say you could use near nothing in JVM RAM, so what about the Bitmap that is passed to this CustomTexture?
Well, as since Android 8.0, you can load Bitmap's immediately into the hardware-level (probably GPU..?), meaning that the Bitmap is not stored in the JVM RAM.
That is if you use Bitmap.Config.HARDWARE when you decode your image file (from resources, online of file): https://developer.android.com/reference/android/graphics/Bitmap.Config#HARDWARE.
Unfortunately, jPCT-AE does not seem to support Bitmap's that have been created using this Config, because of the immutability and inaccessibility of the Bitmap pixels.
However, but the CustomTexture class would work with Bitmap's that are using this Config.

I hope this could be useful for someone someday.
Feel free to give your thoughts and ideas :)

Cheers,
Abiram

EgonOlsen

Thanks for the contribution. setExternalId() was actually meant to be used in such edge cases. You are right about the memory consumption. It's almost doubled for a normal configuration. You can work against this by using either the Virtualizer (http://www.jpct.net/jpct-ae/doc/com/threed/jpct/Virtualizer.html) or by setting keepPixelData on a Texture to false. Both methods will still peak out at double the memory usage at reload time.

There are several reasons for keeping the pixels. One is, that the upload has to happen again on a context change. Context changes happen far less now than on older Android versions but they still might. In case they do, you have to make sure to upload your texture again yourself or you'll get garbage (or all white). Another reason is that I need the pixel data to calculate the mipmaps myself for some devices. On some older ones, the mipmap calculation is simply broken and doesn't work at all. And the last one is, that the texture isn't aware of the GL rendering context or thread. It's a simple container and it can be created in any thread you like. So you somehow have to buffer the data until the upload is feasible.

EgonOlsen


AeroShark333

Quote from: EgonOlsen on April 07, 2020, 10:20:38 AM
BTW: Do you mind if I add this to the wiki?
I don't mind, it was meant to be written to help out other people for their future projects anyway :)