简体   繁体   中英

Delayed Pixel Buffer Read from Multiple Framebuffers on GL_TEXTURE_2D_ARRAY

Using Android, OpenGL ES 3.0

Attempting to write multiple layers of a GL_TEXTURE_2D_ARRAY using framebuffer objects to create heightmaps in sequence, and then read from all of the layers of the GL_TEXTURE_2D_ARRAY in sequence in a later looped operation.

If I create a heightmap texture layer, and then immediately read from the texture, using a pixel buffer object, the read occurs successfully. IE, this pattern works.

for( int i = 0; i < initialQuads; i++ ){
    calcHeightmap( i );
    readHMTexture( i );
}

If I create all the heightmap texture layers, then read from the texture layers in a delayed loop, then the read fails (all reads return 0 values) and OpenGl rendering hangs. IE, this pattern does not work.

for( int i = 0; i < initialQuads; i++ ){
    calcHeightmap( i );
}
for( int i = 0; i < initialQuads; i++ ){
    readHMTexture( i );
}

I want to do this, because I'm hoping to add an intermediary step between the initial generation step and the read step that will use all the layers simultaneously and modify the texture layers, similar to this shader I wrote on Shadertoy https://www.shadertoy.com/view/mssXRB (Just way more complex)

for( int i = 0; i < initialQuads; i++ ){
    calcHeightmap( i );
}
for( int i = 0; i < numErosionIterations; i++ ){
    erodeHeightmap();
}
for( int i = 0; i < initialQuads; i++ ){
    readHMTexture( i );
}

The code to generate the heightmaps is as follows. It verifiably works and produces heightmaps such as can be seen here: https://i.imgur.com/wQw3ZRP.png

public void calcHeightmap( int hm ){
    GLES30.glUseProgram( shader_hm.getProgram() );
    GLES30.glViewport( 0, 0, texWH, texWH );

    // Get the previously defined framebuffer and attach our textures as the targets
    // Using multi-target rendering
    int[] buffers = new int[]{ GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_COLOR_ATTACHMENT1 };
    GLES30.glBindFramebuffer( GLES30.GL_FRAMEBUFFER, fbos[ hm ] );
    GLES30.glFramebufferTextureLayer( GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, hmTexHandles[0], 0, hm );
    GLES30.glFramebufferTextureLayer( GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT1, hmTexHandles[1], 0, hm );
    GLES30.glDrawBuffers( 2, IntBuffer.wrap( buffers ) );

    // Perform all the pre-render steps for the attributes and uniforms (enabling, assigning values, making active, binding)
    for( String attr : shader_hm.attributes.keySet() ){
        RefMethodwArgs preRenderStep = ( RefMethodwArgs ) shader_hm.attributes.get( attr )[ 2 ];
        if( preRenderStep != null ){
            preRenderStep.invoke( shader_hm_PreRenderArgs.get( attr ) );
        }
    }
    for( String uni : shader_hm.uniforms.keySet() ){
        RefMethodwArgs preRenderStep = ( RefMethodwArgs ) shader_hm.uniforms.get( uni )[ 2 ];
        if( preRenderStep != null ){
            preRenderStep.invoke( shader_hm_PreRenderArgs.get( uni ) );
        }
    }

    geom_hm.drawListBuffer.position( 0 );

    // Draw the square
    GLES30.glDrawElements( GLES30.GL_TRIANGLES, geom_hm.drawOrder.length, GLES30.GL_UNSIGNED_INT, geom_hm.drawListBuffer );

    // Perform the post-render steps (if they exist)
    for( String attr : shader_hm.attributes.keySet() ){
        RefMethodwArgs postRenderStep = ( RefMethodwArgs ) shader_hm.attributes.get( attr )[ 3 ];
        if( postRenderStep != null ){
            postRenderStep.invoke();
        }
    }
    for( String uni : shader_hm.uniforms.keySet() ){
        RefMethodwArgs postRenderStep = ( RefMethodwArgs ) shader_hm.uniforms.get( uni )[ 3 ];
        if( postRenderStep != null ){
            postRenderStep.invoke();
        }
    }

    // Make sure we're no longer using a program or framebuffer
    GLES30.glUseProgram( 0 );
    GLES30.glBindFramebuffer( GLES30.GL_FRAMEBUFFER, 0 );
    GLES30.glViewport( 0, 0, FutureWarRenderer.getRef().viewport_w, FutureWarRenderer.getRef().viewport_h );
}

The code to read the heightmaps is as follows. Also verifiably works (if done immediately after a draw to a framebuffer). Checked that values were actually read and properly being used in later operations.

public void readHMTexture( int hm ){
    GLES30.glViewport( 0, 0, texWH, texWH );

    // Get the previously defined framebuffer and attach our texture as the target
    GLES30.glBindFramebuffer( GLES30.GL_FRAMEBUFFER, fbos[ hm ] );
    GLES30.glFramebufferTextureLayer( GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, hmTexHandles[0], 0, hm );
    GLES30.glFramebufferTextureLayer( GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT1, hmTexHandles[1], 0, hm );

    GLES30.glBindBuffer( GLES30.GL_PIXEL_PACK_BUFFER, pbos[0] );
    GLES30.glBufferData( GLES30.GL_PIXEL_PACK_BUFFER, texWH * texWH * 1, null, GLES30.GL_DYNAMIC_READ );
    GLES30.glReadBuffer( GLES30.GL_COLOR_ATTACHMENT0 );

    GLES30.glReadPixels( 0, 0, texWH, texWH, GLES30.GL_RED, GLES30.GL_UNSIGNED_BYTE, 0 );

    ByteBuffer pixelBuffer0;
    pixelBuffer0 = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, texWH * texWH * 1, GLES30.GL_MAP_READ_BIT); //downdload from the GPU to CPU

    ByteBuffer pixelBuffer0_clone = ByteBuffer.allocate( pixelBuffer0.capacity() );
    pixelBuffer0.rewind();//copy from the beginning
    pixelBuffer0_clone.put( pixelBuffer0 );
    pixelBuffer0.rewind();
    texLayerByteBuffers0.add( pixelBuffer0_clone );

    GLES30.glBindBuffer( GLES30.GL_PIXEL_PACK_BUFFER, pbos[1] );
    GLES30.glBufferData( GLES30.GL_PIXEL_PACK_BUFFER, texWH * texWH * 1, null, GLES30.GL_DYNAMIC_READ );
    GLES30.glReadBuffer( GLES30.GL_COLOR_ATTACHMENT1 );
    GLES30.glReadPixels( 0, 0, texWH, texWH, GLES30.GL_RED, GLES30.GL_UNSIGNED_BYTE, 0 );

    ByteBuffer pixelBuffer1;
    pixelBuffer1 = (ByteBuffer) GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER, 0, texWH * texWH * 1, GLES30.GL_MAP_READ_BIT); //downdload from the GPU to CPU
    ByteBuffer pixelBuffer1_clone = ByteBuffer.allocate( pixelBuffer1.capacity() );
    pixelBuffer1.rewind();//copy from the beginning
    pixelBuffer1_clone.put( pixelBuffer1 );
    pixelBuffer1.rewind();
    texLayerByteBuffers1.add( pixelBuffer1_clone );

    GLES30.glUnmapBuffer( GLES30.GL_PIXEL_PACK_BUFFER );
    GLES30.glBindBuffer( GLES30.GL_PIXEL_PACK_BUFFER, 0 );
    GLES30.glBindFramebuffer( GLES30.GL_FRAMEBUFFER, 0 );
    GLES30.glViewport( 0, 0, FutureWarRenderer.getRef().viewport_w, FutureWarRenderer.getRef().viewport_h );
}

Any suggestions or thoughts on why a delayed read of the layers fails, but an immediate read succeeds? Or a way to restructure the read portion so that it will work in a bulk delayed read?

Edit Based on rokuz suggestion below, took out PBO, and replaced with glReadPixels directly to ByteBuffer. Now has almost weirder behavior. If I run using the initial method (always works), then run using the alternative I'm trying to switch to, it will run a single time, yet then never run again successfully unless I switch back to the original method.

Edit 2 Based on above behavior, checked whether the program was actually exiting properly, and appears that the second method hangs the phone even when it successfully renders the first time. Per image at: https://i.imgur.com/kKCpZ8J.png process does not actually exit, and then hangs phone if I attempt to close it.

Edit 3 New wrinkle. Based on @solidpixel advice below, attempted to use glFenceSync. Then realized glClientWaitSync(fence) is going to stall the CPU, and it ends up with no gain for using a PBO. Then thought, maybe be clever, and use glGetSynciv( long sync, int pname, int bufSize, int[] length, int lengthOffset, int[] values, int valuesOffset ).

However, glGetSynciv acts rather strange. Initially, I set up the read using:

GLES30.glBindBuffer( GLES30.GL_PIXEL_PACK_BUFFER, pbos[ pboIndex ] );
GLES30.glBufferData( GLES30.GL_PIXEL_PACK_BUFFER, texWH * texWH * 1, null, GLES30.GL_DYNAMIC_READ );
GLES30.glReadBuffer( GLES30.GL_COLOR_ATTACHMENT0 );

// Start the first pixel buffer read
GLES30.glReadPixels( 0, 0, texWH, texWH, GLES30.GL_RED, GLES30.GL_UNSIGNED_BYTE, 0 );
hmReadFences[pboIndex] = GLES30.glFenceSync( GLES30.GL_SYNC_GPU_COMMANDS_COMPLETE, 0 );

// Set up an update to wait for the read
RefMethodwArgs finishHmRead0 = new RefMethodwArgs( this, "finishHMRead", new Object[]{ pboIndex } );
GameState.getRef().addUpdate( finishHmRead0 );

GLES30.glBindBuffer( GLES30.GL_PIXEL_PACK_BUFFER, pbos[ pboIndex+1 ] );
GLES30.glBufferData( GLES30.GL_PIXEL_PACK_BUFFER, texWH * texWH * 1, null, GLES30.GL_DYNAMIC_READ );
GLES30.glReadBuffer( GLES30.GL_COLOR_ATTACHMENT1 );

// Start the second pixel buffer read
GLES30.glReadPixels( 0, 0, texWH, texWH, GLES30.GL_RED, GLES30.GL_UNSIGNED_BYTE, 0 );
hmReadFences[pboIndex+1] = GLES30.glFenceSync( GLES30.GL_SYNC_GPU_COMMANDS_COMPLETE, 0 );

// Set up an update to wait for the read
RefMethodwArgs finishHmRead1 = new RefMethodwArgs( this, "finishHMRead", new Object[]{ pboIndex+1 } );
GameState.getRef().addUpdate( finishHmRead1 );

// Check that the fences got created and appear to have the correct status.
int[] length0 = new int[1];  // Shows length 1 for all cases
int[] status0 = new int[1];  // Shows 37144 (GLES30.GL_UNSIGNALED)
GLES30.glGetSynciv( hmReadFences[ pboIndex ], GLES30.GL_SYNC_STATUS, 1, length0, 0, status0, 0 );
int signalStatus    = status0[0];
int[] length1 = new int[1];  // Shows length 1 for all cases
int[] status1 = new int[1];  // Shows 37144 (GLES30.GL_UNSIGNALED)
GLES30.glGetSynciv( hmReadFences[ pboIndex ], GLES30.GL_SYNC_STATUS, 1, length1, 0, status1, 0 );
signalStatus    = status1[0];

Seems ok. Both fences show a status of 37144 (GLES30.GL_UNSIGNALED) and appear to exist. RefMethodwArgs finishHmRead is a call to an asynchronous update function that will check whether the fences have completed, runs in a Runnable gameUpdateTask = new Runnable(), and appears to call correctly to the reference method specified. "finishHMRead" shown below.

public void finishHMRead( int pboIndex ){
    int[] length = new int[1];
    int[] status = new int[1];
    GLES30.glGetSynciv( hmReadFences[ pboIndex ], GLES30.GL_SYNC_STATUS, 1, length, 0, status, 0 );
    int length       = length[0]   // Now shows 0 on every fence?
    int signalStatus = status[0];  // Now just returns 0 on every fence?
    int glSignaled   = GLES30.GL_SIGNALED;
    if( signalStatus == glSignaled ){
        // Do all the buffer data copying now that glReadPixels is done
    } else {
        // Resubmit the check for completion
        RefMethodwArgs finishHmRead = new RefMethodwArgs( this, "finishHMRead", new Object[]{ pboIndex } );
        FutureWarGameState.getRef().addUpdate( finishHmRead );
    }
}

However, when finishHMRead( int pboIndex ) calls. Suddenly, the fences return nothing from glGetSynciv. The hmReadFences[ pboIndex ] still exist, the call just returns all 0's.

Edit 4 Alright, finally got this to work. Per @solidpixel suggestion below, had to change delayed method calls checking on the status of the pixel read to an update loop that only executes in the render thread. Add checks for pixel read completion with something like. UpdateList.getRef().addRenderUpdate( finishHmRead );

Then in the renderer call an update to check on the status of the pixel read with:

public void renderUpdate(){
    long timeStart = System.currentTimeMillis();
    long timeElapsed = 0;
    int renderUpdateListLen = renderUpdateList.size();
    if( renderUpdateListLen > 0 ) {
        while( renderUpdateListLen > 0 ) {
            RefMethodwArgs curMethod = renderUpdateList.get( 0 );
            if( timeElapsed < longestRenderUpdateTime ){
                // If we still have time available, run the update
                if( curMethod != null ){
                    curMethod.invoke();
                }
            } else {
                // If we're out of time this cycle add the rest of the updates for the next cycle
                renderUpdateListNextCycle.add( curMethod );
            }
            renderUpdateList.remove( 0 );
            renderUpdateListLen = renderUpdateList.size();
            timeElapsed = System.currentTimeMillis() - timeStart;
        }
    }

    // Push all the remaining updates to the next cycle
    if( renderUpdateListNextCycle.size() > 0 ){
        renderUpdateList.addAll( renderUpdateListNextCycle );
        renderUpdateListNextCycle.clear();
    }
}

Changed above (called from main render thread) "finishHMRead" to:

public void finishHMRead( int pboIndex ){
    int[] length = new int[1];
    int[] status = new int[1];
    GLES30.glGetSynciv( hmReadFences[ pboIndex ], GLES30.GL_SYNC_STATUS, 1, length, 0, status, 0 );
    int signalStatus = status[0];
    int glSignaled   = GLES30.GL_SIGNALED;
    if( signalStatus == glSignaled ){
        // Ready a temporary ByteBuffer for mapping (we'll unmap the pixel buffer and lose this) and a permanent ByteBuffer
        ByteBuffer pixelBuffer;
        texLayerByteBuffers[ pboIndex ] = ByteBuffer.allocate( texWH * texWH );

        // map data to a bytebuffer
        GLES30.glBindBuffer( GLES30.GL_PIXEL_PACK_BUFFER, pbos[ pboIndex ] );
        pixelBuffer = ( ByteBuffer ) GLES30.glMapBufferRange( GLES30.GL_PIXEL_PACK_BUFFER, 0, texWH * texWH * 1, GLES30.GL_MAP_READ_BIT );
        
        // Copy to the long term ByteBuffer
        pixelBuffer.rewind(); //copy from the beginning
        texLayerByteBuffers[ pboIndex ].put( pixelBuffer );
        
        // Unmap and unbind the currently bound pixel buffer
        GLES30.glUnmapBuffer( GLES30.GL_PIXEL_PACK_BUFFER );
        GLES30.glBindBuffer( GLES30.GL_PIXEL_PACK_BUFFER, 0 );
        Log.i( "myTag", "Finished copy for pbo data for " + pboIndex + " at: " + (System.currentTimeMillis() - initSphereStart) );
        acknowledgeHMReadComplete();
    } else {
        // If it wasn't done, resubmit for another check in the next render update cycle
        RefMethodwArgs finishHmRead = new RefMethodwArgs( this, "finishHMRead", new Object[]{ pboIndex } );
        UpdateList.getRef().addRenderUpdate( finishHmRead );
    }
}

Then I do stuff after all the pixel buffers have successfully returned, and finally start rendering the completed object (and doing the erosion steps that I mentioned initially once per frame. Results in quick startup and erosion images such as: https://i.imgur.com/0ju49lk.png

Using glReadPixels() to populate a pixel buffer is an asynchronous operation. You need to wait for it to complete before you read the results on the CPU.

After the glReadPixels() insert a fence:

fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)

... and then wait on it:

glClientWaitSync(fence)

I think you also need to insert glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT) before mapping the buffer to ensure visibility.

Note that glReadPixels() is slow , so ideally you'd wait for the fence in a second thread rather than stalling the application main thread.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM