LWJGL GL30.glGenVertexArrays() returns duplicate ids

Hi,

[caution: german author]

a short description of what im aiming for with my program:
I want to create a 3D Game Engine.
At the moment i only use LWJGL.

The structure of the world is pretty simple:
I load my terrain (only surface, no caves or sth alike) out of heightmaps and colour it by using blendmaps.
The objects (static objects like trees or moving entities like deers, persons,or in the beginning the standard monkey face [-> Blender]) consist out of their 3D model and a texture, which shall colour the displayed model in the end.

For ensuring a constant frame rate in the foreground i tried/am trying to do the loading and unloading of textures and meshes in the background in seperate threads.
The BackgroundTextureLoader and the BackgroundMeshLoader both got lists in which the main program fills the relative texture and mesh paths which shall be thrown out of the memory or loaded into it.

If or if not this method is good practice or totaly nuts is not the main discussion in this thread, but i wd be very happy about some comments that can help me to improve the program, too.

I use VAOs to store terrain data and object data on the graphic card.
When the program starts i first load in the terrains (13*13 square, with the player position in the central terrain).
So the glGen method returns id values from 1 to 169, just like expected.

But when i load in the objects (currently only one dummy object created with blender) the method returns ids starting with 1 counting upwards.

When i now render the objects using the vao ids a part of a terrain is shown.
Of course, because i load in the terrains first, telling lwjgl that i wont change the values during the process.

Why does the vao ids start with 1 again even if i use the same gl method?
The max number of stored elements on the graphic card is far more than 169 elements.

Maybe important to say, that the terrains are loaded directly, so not filled into a load list, but directly with the loadToVao-method (s.b.).
The objects are afterwards filled into those lists and then bgtl.process() and bgml.process() are called.
Here the code parts i think are most important for the problem:
Game Constructor - the class whose instance holds all game related objects


public Game(BackgroundTextureLoader bgtl, BackgroundMeshLoader bgml) {
		this.bgtl = bgtl;
		this.bgml = bgml;
		this.bgtrl = new BackgroundTerrainLoader(this.bgtl,this.bgml);
		this.bgem = new BackgroundEntityManager(this.bgtl,this.bgml);
		this.p = new Player(/*TODO MODEL*/null,new Vector3f(0.0f,0.0f,0.0f),0.0f,0.0f,0.0f,1.0f);
		this.c = new Camera(this.p);
		this.bgtrl.initLoadTerrains(this.p, this);     //<------------load Terrains
		this.bgem.start();                                //<--start BackgroundEntityManager and initially load objects
		this.createProjectionMatrix();
		this.tshad = new TerrainShader();
		this.tren = new TerrainRenderer(tshad,projectionMatrix,this.bgtl);
		this.eshad = new EntityShader();
		this.eren = new EntityRenderer(eshad,projectionMatrix,this.bgtl,this.bgml);
	}

BackgroundMeshLoader - the class that loads meshes to the graphic card memory


package loader;

//imports

public class BackgroundMeshLoader extends Loader {
    private static List<String> toLoad;
    private static List<String> toUnload;
    private static Map<String, Integer[]> map;
    //<rel. paths, {vaoID, indices count}>
    private static Map<Integer, Integer[]> vaoTOvboIDs;
   
    private boolean appeared = false;
   //Test
   
//flags
    private boolean go;
    private boolean ready;
    private boolean interrupt;
    private boolean running;
   
    private SharedDrawable sd;
   
    public BackgroundMeshLoader(SharedDrawable sd) {
        this.go = false;
        this.ready = true;
        this.interrupt = false;
        this.running = false;
       
        this.sd = sd;
       
        toLoad = new ArrayList<String>();
        toUnload = new ArrayList<String>();
        map = new HashMap<String, Integer[]>();
        vaoTOvboIDs = new HashMap<Integer, Integer[]>();
    }
   
//fill path into load list
    public void load(String path) {
        if (!toLoad.contains(path)) {
            toLoad.add(path);
        }
    }
   
//fill path into unload list
    public void unload(String path) {
        if (!toLoad.contains(path) && !toUnload.contains(path) && map.containsKey(path)) {
            toUnload.add(path);
        }
    }
   
//clear vao in the memory
    public void unload(int vaoID) {
        if (vaoTOvboIDs.containsKey(vaoID)) {
            GL30.glDeleteVertexArrays(vaoID);
            for (Integer i : vaoTOvboIDs.get(vaoID)) {
                GL15.glDeleteBuffers(i);
            }
            vaoTOvboIDs.remove(vaoID);
        }
    }
   
    public void process() {
        if (toLoad.size() > 0 | toUnload.size() > 0) {
            ready = false;
            this.go = true;
        }
    }
   
    public boolean isReady() {
        return this.ready;
    }
   
//Main-runnable method
    public void start() {          
        new Thread(new Runnable() {
            public void run() {
                System.err.println(" - BACKGROUNDMESHLOADER started - ");
                init();

                System.err.println(" - BACKGROUNDMESHLOADER running - ");
                running = true;
               
                while( !interrupt ) {      
                    go = false;
                   
                    while( !go ) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException ie) {
                            ie.printStackTrace();
                        }
                        if (interrupt) {
                            break;
                        }
                    }
                   
                    for (String str : toUnload) {
                        System.out.println("unload "+ str);
                        int i = (int)map.get(str)[0];
                        GL30.glDeleteVertexArrays(i);
                       
                        Integer[] vboIds = vaoTOvboIDs.get(i);
                        for (Integer integer : vboIds) {
                            GL15.glDeleteBuffers(integer);
                        }
                        map.remove(str);
                    }
                   
                    for (String s : toLoad) {
                        System.out.println("load "+s);
                        try {      
                           
                            if (map.containsKey(s)) {
                                throw new IllegalArgumentException("Mesh already loaded");
                            }
                           
                            //modelData -> VAO,Size
                            int[] modelData = loadOBJModel(s);  
                            System.err.println("Der zugehörige VAO Index ist: "+modelData[0]);
                            map.put(s, new Integer[]{modelData[0], modelData[1]});
                           
                        } catch (Exception io) {
                            io.printStackTrace();
                        }
                    }                  
                   
                    clearLists();
                    ready = true;
                }
               
                running = false;
               
                System.err.println(" - BACKGROUNDMESHLOADER finished - ");
            }
           
            private void init() {
               
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
               
                try {
                    sd.makeCurrent();
                } catch (LWJGLException e) {
                    e.printStackTrace();
                }
               
            }
           
        }).start();      
    }
   
//load data directly to a vao
    public int loadToVAO(float[] positions, float[] textureCoords, float[] normals, int[] indices) {
        int vaoID = createVAO();
       
        if (vaoID == 1 && !appeared) {
            appeared = true;
        } else if  (vaoID == 1 && appeared){
            //throw new IllegalArgumentException("He really creates two ids with index 1");
        }
       
        Integer vbos[] = new Integer[4];
        vbos[0] = (bindIndicesBuffer(indices));
        vbos[1] = (storeDataInAttributeList(0,3,positions));
        vbos[2] = (storeDataInAttributeList(1,2,textureCoords));
        vbos[3] = (storeDataInAttributeList(2,3,normals));
        unbindVAO();
        vaoTOvboIDs.put(vaoID,vbos);
        return vaoID;
    }

//Load a model from an .obj-file
    public int[] loadOBJModel(String fileName) {      
        FileReader fr = null;
        //Pathing:
        //models
        //-buildings
        //-objects
        //-scene
        //-test
        try {
            fr = new FileReader(new File("res/models/"+fileName+".obj"));          
        } catch (FileNotFoundException e) {
            System.err.println("Couldnt find file: res/models/"+fileName+".obj");
            e.printStackTrace();
        }
       
        BufferedReader br = new BufferedReader(fr);
        String line;
        List<Vector3f> vertices = new ArrayList<Vector3f>();
        List<Vector2f> textures = new ArrayList<Vector2f>();
        List<Vector3f> normals = new ArrayList<Vector3f>();
        List<Integer> indices = new ArrayList<Integer>();
       
        float[] verticesArray = null;
        float[] normalsArray = null;
        float[] textureArray = null;
        int[] indicesArray = null;
       
        try {          
            while(true) {
                line = br.readLine();
                String[] currentLine = line.split(" ");
               
                if (line.startsWith("v ")) {
                    Vector3f vertex = new Vector3f(Float.parseFloat(currentLine[1]),Float.parseFloat(currentLine[2]),Float.parseFloat(currentLine[3]));
                    vertices.add(vertex);
                } else if (line.startsWith("vt ")) {
                    Vector2f texturecoord = new Vector2f(Float.parseFloat(currentLine[1]),Float.parseFloat(currentLine[2]));
                    textures.add(texturecoord);
                } else if (line.startsWith("vn ")) {
                    Vector3f normal = new Vector3f(Float.parseFloat(currentLine[1]),Float.parseFloat(currentLine[2]),Float.parseFloat(currentLine[3]));
                    normals.add(normal);
                } else if (line.startsWith("f ")) {
                    textureArray = new float[vertices.size() * 2];
                    normalsArray = new float[vertices.size() * 3];
                    break;
                }
            }
               
            while(line != null) {
                if (!line.startsWith("f ")) {
                        line = br.readLine();
                        continue;
                }
                String[] currentLine = line.split(" ");
                String[] vertex1 = currentLine[1].split("/");
                String[] vertex2 = currentLine[2].split("/");
                String[] vertex3 = currentLine[3].split("/");
               
                processFVertex(vertex1,indices,textures,normals,textureArray,normalsArray);
                processFVertex(vertex2,indices,textures,normals,textureArray,normalsArray);
                processFVertex(vertex3,indices,textures,normals,textureArray,normalsArray);
                line = br.readLine();
            }
            br.close();
           
        } catch(Exception e) {
            e.printStackTrace();
        }
       
        verticesArray = new float[vertices.size() * 3];
        indicesArray = new int[indices.size()];
       
        int vertexPointer = 0;
        for (Vector3f vec : vertices) {
            verticesArray[vertexPointer++] = vec.x;
            verticesArray[vertexPointer++] = vec.y;
            verticesArray[vertexPointer++] = vec.z;
        }
       
        for (int i = 0 ;i < indices.size();i++) {
            indicesArray[i] = indices.get(i);
        }

        return new int[] {loadToVAO(verticesArray, textureArray, normalsArray, indicesArray),indices.size()};
    }
   
//process the data lists
    private void processFVertex(String[] vertexData, List<Integer> indices, List<Vector2f> textures, List<Vector3f> normals, float[] textureArray, float[] normalsArray) {
        int currentVertexPointer = Integer.parseInt(vertexData[0]) - 1;
        indices.add(currentVertexPointer);
        Vector2f currentTex = textures.get(Integer.parseInt(vertexData[1]) - 1);
        textureArray[currentVertexPointer * 2] = currentTex.x;
        textureArray[currentVertexPointer * 2 + 1] = 1 - currentTex.y;
        Vector3f currentNorm = normals.get(Integer.parseInt(vertexData[2]) - 1);
        normalsArray[currentVertexPointer * 3] = currentNorm.x;
        normalsArray[currentVertexPointer * 3 + 1] = currentNorm.y;
        normalsArray[currentVertexPointer * 3 + 2] = currentNorm.z;
     }
   
    public Map<String, Integer[]> getMeshIDMap() {
        return map;
    }
   
//interrupt the thread
    public void stop() {
        interrupt = true;
        System.out.println("bgml interrupted");
    }
   
    public boolean isRunning() {
        return this.running;
    }
   
//clear the toLoad and the toUnload list
    public void clearLists() {
        toLoad.clear();
        toUnload.clear();
    }
   
//clear all lists
    public void clear() {
        //clears VAOs and VBOs
        Set<Integer> l = vaoTOvboIDs.keySet();
        for (Integer i  : l) {
            GL30.glDeleteVertexArrays(i);
        }
       
        Collection<Integer[]> vboIds = vaoTOvboIDs.values();
        for (Integer[] i : vboIds) {
            for (Integer integer : i) {
                GL15.glDeleteBuffers(integer);
            }
        }
       
        vaoTOvboIDs.clear();
        toLoad.clear();
        toUnload.clear();
        map.clear();
    }
}

Loader - class extended in all Loaders


package loader;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;

public class Loader {
   
    protected static int createVAO() {
        //creates a new VAO
        int vaoID = GL30.glGenVertexArrays();
        GL30.glBindVertexArray(vaoID);      
        return vaoID;
    }
   
    protected static void unbindVAO() {
        GL30.glBindVertexArray(0);
    }
   
    protected static int storeDataInAttributeList(int attributeNumber, int coordinateSize, float[] data) {
        int vboID = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboID);
        FloatBuffer buffer = storeDataInFloatBuffer(data);
        //-> static draw : gl now knows, we dont want to edit the stored data any more
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
        GL20.glVertexAttribPointer(attributeNumber, coordinateSize, GL11.GL_FLOAT, false, 0,0);
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
        return vboID;
    }
   
    protected static int bindIndicesBuffer(int[] indices) {
        int vboID = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER,  vboID);
        IntBuffer buffer = storeDataInIntBuffer(indices);
        GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, buffer, GL15.GL_STATIC_DRAW);
        return vboID;
    }
   
    private static IntBuffer storeDataInIntBuffer (int[] data) {
        IntBuffer buffer = BufferUtils.createIntBuffer(data.length);
        buffer.put(data);
        buffer.flip();
       
        return buffer;
    }
   
    private static FloatBuffer storeDataInFloatBuffer(float[] data) {
        FloatBuffer buffer = BufferUtils.createFloatBuffer(data.length);
        buffer.put(data);
        buffer.flip();
       
        return buffer;
    }
}

BackgroundTerrainLoader - method used to load terrains into the graphic card memory


public void load(int x, int z) {  
        if (isCurrentlyLoaded(x,z)) {
            System.out.println("Tell me a lie");
            return;
        }
        File f = new File("res/textures/terrains/"+x+"_"+z+"_h.png");
        if (!f.exists()) {
            System.err.println(f.getAbsolutePath());
            System.err.println(x+ " | "+z+" nicht gefunden [FILE]");
            throw new IllegalArgumentException("!!!");
        }
        BufferedImage image = null;
        try {
            image = ImageIO.read(f);
        } catch (IOException io) {
            io.printStackTrace();
        }
       
        //Lade die Hilfsdateien für die Normalen an den Kanten
        if (wantToKnow) System.out.println("Für "+x+"|"+z+" geladen");
        f = new File("res/textures/terrains/"+(x-1)+"_"+z+"_h.png");
        BufferedImage hxmimage = null;
        if (f.exists()) {
            try {
                hxmimage = ImageIO.read(f);
            } catch (IOException io) {
                io.printStackTrace();
            }
        }
        f = new File("res/textures/terrains/"+(x+1)+"_"+z+"_h.png");
        BufferedImage hxpimage = null;
        if (f.exists()) {
            try {
                hxpimage = ImageIO.read(f);
            } catch (IOException io) {
                io.printStackTrace();
            }
        }
        f = new File("res/textures/terrains/"+(x)+"_"+(z-1)+"_h.png");
        BufferedImage hzmimage = null;
        if (f.exists()) {
            try {
                hzmimage = ImageIO.read(f);
            } catch (IOException io) {
                io.printStackTrace();
            }
        }
        f = new File("res/textures/terrains/"+(x)+"_"+(z+1)+"_h.png");
        BufferedImage hzpimage = null;
        if (f.exists()) {
            try {
                hzpimage = ImageIO.read(f);
            } catch (IOException io) {
                io.printStackTrace();
            }
        }
       
        int VERTEX_COUNT = image.getHeight();
        //image.getHeight() is usually 256
        int count = VERTEX_COUNT * VERTEX_COUNT;
        float[] vertices = new float[count * 3];
        float[][] heights = new float[VERTEX_COUNT][VERTEX_COUNT];
        float[] normals = new float[count * 3];
        float[] textureCoords = new float[count * 2];
        int[] indices = new int[6 * (VERTEX_COUNT - 1) * (VERTEX_COUNT - 1)];
        int vertexPointer = 0;
        for (int i = 0;i < VERTEX_COUNT;i++) {
            for (int j = 0;j < VERTEX_COUNT;j++) {
                vertices[vertexPointer * 3] = (float)j / ((float) VERTEX_COUNT - 1) * Terrain.SIZE;
                float height = getHeight(j,i,image);
                vertices[vertexPointer * 3 + 1] = height;
                heights[j][i] = height;
                vertices[vertexPointer * 3 + 2] = (float)i / ((float) VERTEX_COUNT - 1) * Terrain.SIZE;
                Vector3f normal = calculateNormal(j,i,image,hxmimage,hxpimage,hzmimage,hzpimage);
                normals[vertexPointer*3] = normal.x;
                normals[vertexPointer*3 + 1] = normal.y;
                normals[vertexPointer*3 + 2] = normal.z;
                textureCoords[vertexPointer * 2] = (float)j / ((float)VERTEX_COUNT - 1);
                textureCoords[vertexPointer * 2 + 1] = (float)i / ((float)VERTEX_COUNT - 1);
                vertexPointer++;
            }          
        }
       
        vertexPointer = 0;
        for (int gz = 0; gz < VERTEX_COUNT - 1; gz++) {
            for (int gx = 0; gx < VERTEX_COUNT - 1;gx++) {
                int topLeft = (gz * VERTEX_COUNT + gx);
                int topRight = topLeft + 1;
                int bottomLeft = (gz + 1) * VERTEX_COUNT + gx;
                int bottomRight = bottomLeft + 1;
                indices[vertexPointer++] = topLeft;
                indices[vertexPointer++] = bottomLeft;
                indices[vertexPointer++] = topRight;
                indices[vertexPointer++] = topRight;
                indices[vertexPointer++] = bottomLeft;
                indices[vertexPointer++] = bottomRight;
            }          
        }
               
        TerrainTexturePack ttp = new TerrainTexturePack(null,null,null,null,null);
        this.bgtl.load(new Texture("terrains/"+x+"_"+z+"_b",(byte) 0));
        ttp.setBlendMapPath("terrains/"+x+"_"+z+"_b");
       
        File file = new File("res/game/terrains/"+x+"_"+z+"_t.txt");
        char[] content = new char[(int)file.length()];
        try {
            FileReader fr = new FileReader(file);
            fr.read(content);
            fr.close();
        } catch (IOException io) {
            io.printStackTrace();
        }
        String str = new String(content);
       
        String[] strings = str.split(":");
       
        if (!strings[0].equals("null")) {
            ttp.setRTexturePath(strings[0]);
            this.bgtl.load(new Texture(strings[0],(byte) 0));
        } else {
            ttp.setRTexturePath("NULL");
        }
        if (!strings[1].equals("null")) {
            ttp.setGTexturePath(strings[1]);
            this.bgtl.load(new Texture(strings[1],(byte) 0));
        } else {
            ttp.setGTexturePath("NULL");
        }
        if (!strings[2].equals("null")) {
            ttp.setBTexturePath(strings[2]);
            this.bgtl.load(new Texture(strings[2],(byte) 0));
        } else {
            ttp.setBTexturePath("NULL");
        }
        if (!strings[3].equals("null")) {
            ttp.setBackgroundTexturePath(strings[3]);
            this.bgtl.load(new Texture(strings[3],(byte) 0));
        }
       
        int vaoID = this.bgml.loadToVAO(vertices,textureCoords,normals,indices);
        System.err.println("Terrain VAO ID: "+vaoID);
        this.allTerrains.add(new Terrain(x,z,ttp,vaoID,heights));

It is a really long post and i wd have loved to not type this all in, but the LWJGL descriptions on this topic are very poorly.

If you can link me a good description on how the LWJGL methods work, i would be very happy, too.
Im happy about each answer i get, even if its just about how i can get my program into a better structure.

Greetings,

eMmiE

You are for sure somewhere calling glDeleteVertexArrays() in between requesting the first set of ids and requesting the second set of ids. At least the Nvidia implementation reuses previously allocated and deleted ids again, and does not go on sequentially.
My suggestion: Install a breakpoint on all calls to glDeleteVertexArrays() and see when it hits.

As for the LWJGL documentation: It is complete. It just is the OpenGL documentation. Look at:

VAO’s aren’t shared, even with shared contexts. The same ID can refer to different VAOs in different contexts.

Thank you for the quick answers :slight_smile:

glDeleteVertexArray() can only be called in the Runnable-Loop of BackgroundMeshLoader if there are mesh-paths in the toUnload-list.
This is not the case when the program is initially loading terrains and standard objects.
(Ive tested that to make sure)

So the second reply might be the answer.

But how can I assure that the vaoIDs are either in one order or that i can access the right object in the right vao space (if there are 2 seperated)

Greetings,

eMmiE

I think i dont understand the glGenVertexArrays(IntBuffer) Method exactly.
Does the IntBuffer only hold the data of one object or can it even hold data from multiple objects, so that i can initially load all objects and after gathering the data save those to the graphic card via VAOs?

Or is there another way to achieve something alike?

Greetings,

eMmiE

glGenVertexArrays(IntBuffer) fills the given buffer with new VAO IDs. If your buffer can fit 5 values, glGenVertexArrays(IntBuffer) will create 5 VAOs for you.

The point of this is to improve performance. If you’re creating thousands of VAOs you can avoid an expensive JNI call and involving the driver for each and every one of those VAOs, but in reality there is a VERY minor difference in performance. Since it increases the complexity of your code a lot, it is much easier to use the simplified [icode]int glGenVertexArrays()[/icode] method that returns a single new VAO as an int directly.

But if i would use the glGenVertexArray(IntBuffer) i would get a list of IDs that perfectly fit together.
So i wd create the IDs and then load the data of my objects, terrains and models, into those vaos.

But i dont see the problem right now.
What am i doing wrong with my current implementation?
What do i have to change?

Greetings,

eMmiE

Did you read my first post? You are using a shared OpenGL context (SharedDrawable) to load things on a background thread. Then you use things in the main thread later. The problem is that not everything is shared between contexts. The rule of thumb is that things that contain data (textures, buffers, samplers, renderbuffers, queries) are shared, but things that contain references to other objects (framebuffer objects, VAOs, a couple of others) are NOT shared. This means that although the data objects you’ve loaded in the background are shared (VBOs, textures) are visible to the main context, the VAOs and FBOs you use to actually read and write to them are NOT visible.

In other words:

  • If you create a buffer on the background thread with ID 1, that exact same buffer can be used on the main thread as well.
  • If you create a VAO on the background thread with ID 1, it is NOT shared with the main thread. If the main thread then creates its own VAOs, it is free to return ID 1 “again”, because ID 1 refers to two different VAOs depending on the OpenGL context.

The solution is to load your buffers and textures on the background thread, create VAOs and FBOs on the main thread afterwards.

Thanks,

i was somehow confused, but i now know what to try.

Greetings,

eMmiE

Just to close this thread:

I am now loading the vbos inside the Mesh Loader and TerrainLoader first, then put the models into vaos inside the main loop.

That works.

Thanks for your help

not sure if you got there already but … most of the glGen[…] methods do not really create objects, rather “reserve” names/ids. they become “real” when bound for the first time. so, either bind/unbind them directly after the gen command - or use a opengl 4.5 equivalent method which does not follow this rule.

yet, this is related to what agent said.