Отложенное чтение пиксельного буфера из нескольких кадровых буферов на GL_TEXTURE_2D_ARRAY

Использование Android, OpenGL ES 3.0

Попытка записи нескольких слоев GL_TEXTURE_2D_ARRAY с использованием объектов кадрового буфера для последовательного создания карт высот, а затем последовательное чтение из всех слоев GL_TEXTURE_2D_ARRAY в более поздней зацикленной операции.

Если я создаю слой текстуры карты высот, а затем сразу же читаю из текстуры, используя объект пиксельного буфера, чтение происходит успешно. IE, этот шаблон работает.

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

Если я создам все слои текстуры карты высот, а затем прочитаю из слоев текстуры в цикле с задержкой, то чтение завершится ошибкой (все операции чтения возвращают 0 значений) и рендеринг OpenGl зависнет. IE, этот шаблон не работает.

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

Я хочу сделать это, потому что я надеюсь добавить промежуточный шаг между начальным этапом генерации и этапом чтения, который будет использовать все слои одновременно и изменять слои текстур, подобно этому шейдеру, который я написал на Shadertoy https: //www.shadertoy.com/view/mssXRB (просто сложнее)

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 );
}

Код для создания карт высот выглядит следующим образом. Он достоверно работает и создает карты высот, такие как здесь: 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 );
}

Код для чтения карт высот выглядит следующим образом. Также проверяемо работает (если это делается сразу после отрисовки в фреймбуфер). Проверено, что значения действительно считывались и правильно использовались в последующих операциях.

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 );
}

Любые предложения или мысли о том, почему отложенное чтение слоев не удается, а немедленное чтение выполняется успешно? Или способ реструктурировать часть чтения, чтобы она работала при массовом отложенном чтении?

Изменить. Основываясь на приведенном ниже предложении rokuz, удалил PBO и заменил glReadPixels непосредственно на ByteBuffer. Теперь имеет почти более странное поведение. Если я запускаю исходный метод (всегда работает), а затем запускаю альтернативный вариант, на который я пытаюсь переключиться, он будет запускаться один раз, но затем никогда не будет успешно запускаться снова, если я не переключусь обратно на исходный метод.

Редактировать 2. На основании приведенного выше поведения проверено, действительно ли программа правильно завершает работу, и кажется, что второй метод зависает на телефоне, даже если он успешно отображается в первый раз. На изображении: https://i.imgur.com/kKCpZ8J.png процесс на самом деле не завершается, а затем зависает телефон, если я пытаюсь его закрыть.

Редактировать 3 Новая морщина. Основываясь на приведенном ниже совете @solidpixel, попытался использовать glFenceSync. Затем понял, что glClientWaitSync(fence) будет останавливать ЦП, и в итоге он не получит выгоды от использования PBO. Потом подумал, а может поумничать и использовать glGetSynciv( long sync, int pname, int bufSize, int[] length, int lengthOffset, int[] values, int valuesOffset ).

Однако glGetSynciv ведет себя довольно странно. Первоначально я настроил чтение, используя:

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];

Кажется, все в порядке. Оба забора имеют статус 37144 (GLES30.GL_UNSIGNALED) и, похоже, существуют. RefMethodwArgs finishHmRead — это вызов асинхронной функции обновления, которая проверяет, завершены ли заборы, запускается в Runnable gameUpdateTask = new Runnable() и, по-видимому, правильно вызывает указанный ссылочный метод. "finishHMRead", показанный ниже.

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 );
    }
}

Однако при вызове finishHMRead(int pboIndex). Внезапно заборы ничего не возвращают из glGetSynciv. hmReadFences[ pboIndex ] все еще существует, вызов просто возвращает все 0.

Редактировать 4 Хорошо, наконец-то заработало. В соответствии с предложением @solidpixel, приведенным ниже, пришлось изменить отложенные вызовы методов, проверяющие статус считанного пикселя в цикле обновления, который выполняется только в потоке рендеринга. Добавьте проверки завершения чтения пикселей с помощью чего-то вроде. UpdateList.getRef().addRenderUpdate(finishHmRead);

Затем в средстве визуализации вызовите обновление, чтобы проверить статус считанного пикселя с помощью:

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();
    }
}

Изменено выше (вызывается из основного потока рендеринга) "finishHMRead" на:

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 );
    }
}

Затем я делаю что-то после того, как все пиксельные буферы успешно возвращены, и, наконец, начинаю рендеринг завершенного объекта (и выполняю шаги эрозии, о которых я упоминал изначально, один раз за кадр. Результатом является быстрый запуск и эрозия изображений, таких как: https:// i.imgur.com/0ju49lk.png

Вы пытались использовать glReadPixels без PBO? Просто для проверки, сохраняется ли проблема в этом случае.

rokuz 05.12.2022 12:15

Спасибо за быстрое предложение @rokuz. Полное избавление от пиксельных буферов действительно решает проблему. Однако теперь он отмечает, что «критическая блокировка JNI удерживалась в течение 5,752 с» прямо перед первым вызовом glReadPixels. Итак, "решает" вопрос, просто вводит массовый стойл. Также видно при профилировании. «Завершенный расчет карты высот на: 2473. Критическая блокировка JNI удерживалась в течение 5,752 с. Завершено glReadPixels 0 (readHM) на итере 0 на: 9379». По крайней мере, дает представление о том, что может быть не так.

G. Putnam 05.12.2022 18:19

Сотрите предыдущий результат. Теперь я еще больше запутался. Запустился один раз успешно, затем каждый раз после этого он возвращался к тому же поведению. Переходит к пустому белому экрану, продолжает предоставлять информацию, например, о попытке рендеринга, но затем зависает эмулятор и даже не позволяет закрыть задачу. (с PBO или без, два разных эмулируемых телефона)

G. Putnam 05.12.2022 20:15

Что ж, это хороший знак, что glReadPixels как-то работает, мы можем локализовать проблему в PBO. что касается остановки, согласно вашему коду raw glReadPixels не должен сильно отличаться от PBO+glMapBufferRange. В этом случае приложение должно ждать glMapBufferRange, пока не будут выполнены команды рендеринга. Вы можете проверить этот блог для подробного объяснения - roxlu.com/2014/048/… Кроме того, во втором случае вы часто переназначаете фреймбуферы, это также дорого.

rokuz 05.12.2022 22:13
1
4
74
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Использование glReadPixels() для заполнения пиксельного буфера является асинхронной операцией. Вам нужно дождаться его завершения, прежде чем читать результаты на процессоре.

После glReadPixels() вставьте забор:

fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)

... и затем подождите:

glClientWaitSync(fence)

Я думаю, вам также нужно вставить glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT) перед отображением буфера, чтобы обеспечить видимость.

Обратите внимание, что glReadPixels() работает медленно, поэтому в идеале вы должны дождаться ограничения во втором потоке, а не останавливать основной поток приложения.

Спасибо за совет. Я попробую рекомендацию и дам вам знать, если это сработает. Не знал, что такое метод забора/барьера и как его искать. После публикации этого я узнал, насколько openGL является асинхронным, чего я действительно не понимал. Подразумевалось, что если я вызову glDraw[...], его нужно будет завершить, прежде чем он позволит мне снова вызвать с теми же ресурсами/целью рендеринга, но, по-видимому, я могу вызвать 1000 кадров в цикле, и он просто отправляет и потом зависает вся система.

G. Putnam 08.12.2022 22:15

Хорошо, изучил glFenceSync, и я не думаю, что glClientWaitSync(fence) будет работать. Он намеренно останавливает ЦП до тех пор, пока не будут выполнены все команды графического процессора. Тем не менее, я нашел glGetSynciv, который возвращает статус glFenceSync, и кажется, что он отлично сработает для вашего последнего предложения «подождите забора во втором потоке». Однако. реализовано, и кажется, что заборы исчезают, согласно описанию в Редактировании 3 выше. Мысли?

G. Putnam 09.12.2022 01:35

Имеет ли второй поток связанный EGLContext в той же общей группе, что и основной поток?

solidpixel 09.12.2022 08:36

Подозреваю, что нет. Глядя на Grafika, «Показать + захватить камеру» из другого комментария, который, по-видимому, использует GLSurfaceView с общим EGLContext.

G. Putnam 09.12.2022 21:40

В приведенном выше редактировании это работало с предложением об отсутствии EGLContext во втором потоке. Посмотрел на совместное использование контекстов, и это выглядело очень запутанным, поэтому просто переместил проверки в ограниченное по времени обновление в потоке рендеринга. Примите приведенный выше ответ, поскольку он был полезен и дал подсказки.

G. Putnam 09.12.2022 23:18

Другие вопросы по теме