001/*
002 * Copyright (C) Photon Vision.
003 *
004 * This program is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package org.photonvision.vision.pipe.impl;
019
020import static com.jogamp.opengl.GL.*;
021import static com.jogamp.opengl.GL2ES2.*;
022
023import com.jogamp.opengl.*;
024import com.jogamp.opengl.util.GLBuffers;
025import com.jogamp.opengl.util.texture.Texture;
026import com.jogamp.opengl.util.texture.TextureData;
027import java.nio.ByteBuffer;
028import java.nio.FloatBuffer;
029import java.nio.IntBuffer;
030import jogamp.opengl.GLOffscreenAutoDrawableImpl;
031import org.opencv.core.CvType;
032import org.opencv.core.Mat;
033import org.photonvision.common.logging.LogGroup;
034import org.photonvision.common.logging.Logger;
035import org.photonvision.vision.pipe.CVPipe;
036
037public class GPUAcceleratedHSVPipe extends CVPipe<Mat, Mat, HSVPipe.HSVParams> {
038    private static final String k_vertexShader =
039            String.join(
040                    "\n",
041                    "#version 100",
042                    "",
043                    "attribute vec4 position;",
044                    "",
045                    "void main() {",
046                    "  gl_Position = position;",
047                    "}");
048    private static final String k_fragmentShader =
049            String.join(
050                    "\n",
051                    "#version 100",
052                    "",
053                    "precision highp float;",
054                    "precision highp int;",
055                    "",
056                    "uniform vec3 lowerThresh;",
057                    "uniform vec3 upperThresh;",
058                    "uniform vec2 resolution;",
059                    "uniform sampler2D texture0;",
060                    "",
061                    "vec3 rgb2hsv(vec3 c) {",
062                    "  vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);",
063                    "  vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));",
064                    "  vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));",
065                    "",
066                    "  float d = q.x - min(q.w, q.y);",
067                    "  float e = 1.0e-10;",
068                    "  return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);",
069                    "}",
070                    "",
071                    "bool inRange(vec3 hsv) {",
072                    "  bvec3 botBool = greaterThanEqual(hsv, lowerThresh);",
073                    "  bvec3 topBool = lessThanEqual(hsv, upperThresh);",
074                    "  return all(botBool) && all(topBool);",
075                    "}",
076                    "",
077                    "void main() {",
078                    "  vec2 uv = gl_FragCoord.xy/resolution;",
079                    // Important! We do this .bgr swizzle because the image comes in as BGR, but we pretend
080                    // it's RGB for convenience+speed
081                    "  vec3 col = texture2D(texture0, uv).bgr;",
082                    // Only the first value in the vec4 gets used for GL_RED, and only the last value gets
083                    // used for GL_ALPHA
084                    "  gl_FragColor = inRange(rgb2hsv(col)) ? vec4(1.0, 0.0, 0.0, 1.0) : vec4(0.0, 0.0, 0.0, 0.0);",
085                    "}");
086    private static final int k_startingWidth = 1280, k_startingHeight = 720;
087    private static final float[] k_vertexPositions = {
088        // Set up a quad that covers the screen
089        -1f, +1f, +1f, +1f, -1f, -1f, +1f, -1f
090    };
091    private static final int k_positionVertexAttribute =
092            0; // ID for the vertex shader position variable
093
094    public enum PBOMode {
095        NONE,
096        SINGLE_BUFFERED,
097        DOUBLE_BUFFERED
098    }
099
100    private final IntBuffer vertexVBOIds = GLBuffers.newDirectIntBuffer(1),
101            unpackPBOIds = GLBuffers.newDirectIntBuffer(2),
102            packPBOIds = GLBuffers.newDirectIntBuffer(2);
103
104    private final GL2ES2 gl;
105    private final GLProfile profile;
106    private final int outputFormat;
107    private final PBOMode pboMode;
108    private final GLOffscreenAutoDrawableImpl.FBOImpl drawable;
109    private final Texture texture;
110    // The texture uniform holds the image that's being processed
111    // The resolution uniform holds the current image resolution
112    // The lower and upper uniforms hold the lower and upper HSV limits for thresholding
113    private final int textureUniformId, resolutionUniformId, lowerUniformId, upperUniformId;
114
115    private final Logger logger = new Logger(GPUAcceleratedHSVPipe.class, LogGroup.General);
116
117    private byte[] inputBytes, outputBytes;
118    private Mat outputMat = new Mat(k_startingHeight, k_startingWidth, CvType.CV_8UC1);
119    private int previousWidth = -1, previousHeight = -1;
120    private int unpackIndex = 0, unpackNextIndex = 0, packIndex = 0, packNextIndex = 0;
121
122    public GPUAcceleratedHSVPipe(PBOMode pboMode) {
123        this.pboMode = pboMode;
124
125        // Set up GL profile and ask for specific capabilities
126        profile = GLProfile.get(pboMode == PBOMode.NONE ? GLProfile.GLES2 : GLProfile.GL4ES3);
127        final var capabilities = new GLCapabilities(profile);
128        capabilities.setHardwareAccelerated(true);
129        capabilities.setFBO(true);
130        capabilities.setDoubleBuffered(false);
131        capabilities.setOnscreen(false);
132        capabilities.setRedBits(8);
133        capabilities.setBlueBits(0);
134        capabilities.setGreenBits(0);
135        capabilities.setAlphaBits(0);
136
137        // Set up the offscreen area we're going to draw to
138        final var factory = GLDrawableFactory.getFactory(profile);
139        drawable =
140                (GLOffscreenAutoDrawableImpl.FBOImpl)
141                        factory.createOffscreenAutoDrawable(
142                                factory.getDefaultDevice(),
143                                capabilities,
144                                new DefaultGLCapabilitiesChooser(),
145                                k_startingWidth,
146                                k_startingHeight);
147        drawable.display();
148        drawable.getContext().makeCurrent();
149
150        // Get an OpenGL context; OpenGL ES 2.0 and OpenGL 2.0 are compatible with all the coprocs we
151        // care about but not compatible with PBOs. Open GL ES 3.0 and OpenGL 4.0 are compatible with
152        // select coprocs *and* PBOs
153        gl = pboMode == PBOMode.NONE ? drawable.getGL().getGLES2() : drawable.getGL().getGL4ES3();
154        final int programId = gl.glCreateProgram();
155
156        if (pboMode == PBOMode.NONE && !gl.glGetString(GL_EXTENSIONS).contains("GL_EXT_texture_rg")) {
157            logger.warn(
158                    "OpenGL ES 2.0 implementation does not have the 'GL_EXT_texture_rg' extension, falling back to GL_ALPHA instead of GL_RED output format");
159            outputFormat = GL_ALPHA;
160        } else {
161            outputFormat = GL_RED;
162        }
163
164        // JOGL creates a framebuffer color attachment that has RGB set as the format, which is not
165        // appropriate for us because we want a single-channel format
166        // We make ourown FBO color attachment to remedy this
167        // Detach and destroy the FBO color attachment that JOGL made for us
168        drawable.getFBObject(GL_FRONT).detachColorbuffer(gl, 0, true);
169        // Equivalent to calling glBindFramebuffer
170        drawable.getFBObject(GL_FRONT).bind(gl);
171        if (true) { // For now renderbuffers are disabled
172            // Create a color attachment texture to hold our rendered output
173            var colorBufferIds = GLBuffers.newDirectIntBuffer(1);
174            gl.glGenTextures(1, colorBufferIds);
175            gl.glBindTexture(GL_TEXTURE_2D, colorBufferIds.get(0));
176            gl.glTexImage2D(
177                    GL_TEXTURE_2D,
178                    0,
179                    outputFormat == GL_RED ? GL_R8 : GL_ALPHA8,
180                    k_startingWidth,
181                    k_startingHeight,
182                    0,
183                    outputFormat,
184                    GL_UNSIGNED_BYTE,
185                    null);
186            gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
187            gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
188            // Attach the texture to the framebuffer
189            gl.glBindTexture(GL_TEXTURE_2D, 0);
190            gl.glFramebufferTexture2D(
191                    GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBufferIds.get(0), 0);
192            // Cleanup
193            gl.glBindTexture(GL_TEXTURE_2D, 0);
194        } else {
195            // Create a color attachment texture to hold our rendered output
196            var renderBufferIds = GLBuffers.newDirectIntBuffer(1);
197            gl.glGenRenderbuffers(1, renderBufferIds);
198            gl.glBindRenderbuffer(GL_RENDERBUFFER, renderBufferIds.get(0));
199            gl.glRenderbufferStorage(
200                    GL_RENDERBUFFER,
201                    outputFormat == GL_RED ? GL_R8 : GL_ALPHA8,
202                    k_startingWidth,
203                    k_startingHeight);
204            // Attach the texture to the framebuffer
205            gl.glFramebufferRenderbuffer(
206                    GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBufferIds.get(0));
207            // Cleanup
208            gl.glBindRenderbuffer(GL_RENDERBUFFER, 0);
209        }
210        drawable.getFBObject(GL_FRONT).unbind(gl);
211
212        // Check that the FBO is attached
213        int fboStatus = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER);
214        if (fboStatus == GL_FRAMEBUFFER_UNSUPPORTED) {
215            throw new RuntimeException(
216                    "GL implementation does not support rendering to internal format '"
217                            + String.format("0x%08X", outputFormat == GL_RED ? GL_R8 : GL_ALPHA8)
218                            + "' with type '"
219                            + String.format("0x%08X", GL_UNSIGNED_BYTE)
220                            + "'");
221        } else if (fboStatus != GL_FRAMEBUFFER_COMPLETE) {
222            throw new RuntimeException(
223                    "Framebuffer is not complete; framebuffer status is "
224                            + String.format("0x%08X", fboStatus));
225        }
226
227        logger.debug(
228                "Created an OpenGL context with renderer '"
229                        + gl.glGetString(GL_RENDERER)
230                        + "', version '"
231                        + gl.glGetString(GL.GL_VERSION)
232                        + "', and profile '"
233                        + profile
234                        + "'");
235
236        var fmt = GLBuffers.newDirectIntBuffer(1);
237        gl.glGetIntegerv(GLES3.GL_IMPLEMENTATION_COLOR_READ_FORMAT, fmt);
238        var type = GLBuffers.newDirectIntBuffer(1);
239        gl.glGetIntegerv(GLES3.GL_IMPLEMENTATION_COLOR_READ_TYPE, type);
240
241        // Tell OpenGL that the attribute in the vertex shader named position is bound to index 0 (the
242        // index for the generic position input)
243        gl.glBindAttribLocation(programId, 0, "position");
244
245        // Compile and set up our two shaders with our program
246        final int vertexId = createShader(gl, programId, k_vertexShader, GL_VERTEX_SHADER);
247        final int fragmentId = createShader(gl, programId, k_fragmentShader, GL_FRAGMENT_SHADER);
248
249        // Link our program together and check for errors
250        gl.glLinkProgram(programId);
251        IntBuffer status = GLBuffers.newDirectIntBuffer(1);
252        gl.glGetProgramiv(programId, GL_LINK_STATUS, status);
253        if (status.get(0) == GL_FALSE) {
254            IntBuffer infoLogLength = GLBuffers.newDirectIntBuffer(1);
255            gl.glGetProgramiv(programId, GL_INFO_LOG_LENGTH, infoLogLength);
256
257            ByteBuffer bufferInfoLog = GLBuffers.newDirectByteBuffer(infoLogLength.get(0));
258            gl.glGetProgramInfoLog(programId, infoLogLength.get(0), null, bufferInfoLog);
259            byte[] bytes = new byte[infoLogLength.get(0)];
260            bufferInfoLog.get(bytes);
261            String strInfoLog = new String(bytes);
262
263            throw new RuntimeException("Linker failure: " + strInfoLog);
264        }
265        gl.glValidateProgram(programId);
266
267        // Cleanup shaders that are now compiled in
268        gl.glDetachShader(programId, vertexId);
269        gl.glDetachShader(programId, fragmentId);
270        gl.glDeleteShader(vertexId);
271        gl.glDeleteShader(fragmentId);
272
273        // Tell OpenGL to use our program
274        gl.glUseProgram(programId);
275
276        // Set up our texture
277        texture = new Texture(GL_TEXTURE_2D);
278        texture.setTexParameteri(gl, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
279        texture.setTexParameteri(gl, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
280        texture.setTexParameteri(gl, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
281        texture.setTexParameteri(gl, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
282
283        // Set up a uniform holding our image as a texture
284        textureUniformId = gl.glGetUniformLocation(programId, "texture0");
285        gl.glUniform1i(textureUniformId, 0);
286
287        // Set up a uniform to hold image resolution
288        resolutionUniformId = gl.glGetUniformLocation(programId, "resolution");
289
290        // Set up uniforms for the HSV thresholds
291        lowerUniformId = gl.glGetUniformLocation(programId, "lowerThresh");
292        upperUniformId = gl.glGetUniformLocation(programId, "upperThresh");
293
294        // Set up a quad that covers the entire screen so that our fragment shader draws onto the entire
295        // screen
296        gl.glGenBuffers(1, vertexVBOIds);
297
298        FloatBuffer vertexBuffer = GLBuffers.newDirectFloatBuffer(k_vertexPositions);
299        gl.glBindBuffer(GL_ARRAY_BUFFER, vertexVBOIds.get(0));
300        gl.glBufferData(
301                GL_ARRAY_BUFFER,
302                (long) vertexBuffer.capacity() * Float.BYTES,
303                vertexBuffer,
304                GL_STATIC_DRAW);
305
306        // Set up pixel unpack buffer (a PBO to transfer image data to the GPU)
307        if (pboMode != PBOMode.NONE) {
308            gl.glGenBuffers(2, unpackPBOIds);
309
310            gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(0));
311            gl.glBufferData(
312                    GLES3.GL_PIXEL_UNPACK_BUFFER,
313                    k_startingHeight * k_startingWidth * 3,
314                    null,
315                    GLES3.GL_STREAM_DRAW);
316            if (pboMode == PBOMode.DOUBLE_BUFFERED) {
317                gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(1));
318                gl.glBufferData(
319                        GLES3.GL_PIXEL_UNPACK_BUFFER,
320                        k_startingHeight * k_startingWidth * 3,
321                        null,
322                        GLES3.GL_STREAM_DRAW);
323            }
324            gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, 0);
325        }
326
327        // Set up pixel pack buffer (a PBO to transfer the processed image back from the GPU)
328        if (pboMode != PBOMode.NONE) {
329            gl.glGenBuffers(2, packPBOIds);
330
331            gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(0));
332            gl.glBufferData(
333                    GLES3.GL_PIXEL_PACK_BUFFER,
334                    k_startingHeight * k_startingWidth,
335                    null,
336                    GLES3.GL_STREAM_READ);
337            if (pboMode == PBOMode.DOUBLE_BUFFERED) {
338                gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(1));
339                gl.glBufferData(
340                        GLES3.GL_PIXEL_PACK_BUFFER,
341                        k_startingHeight * k_startingWidth,
342                        null,
343                        GLES3.GL_STREAM_READ);
344            }
345            gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, 0);
346        }
347    }
348
349    private static int createShader(GL2ES2 gl, int programId, String glslCode, int shaderType) {
350        int shaderId = gl.glCreateShader(shaderType);
351        if (shaderId == 0) throw new RuntimeException("Shader ID is zero");
352
353        IntBuffer length = GLBuffers.newDirectIntBuffer(new int[] {glslCode.length()});
354        gl.glShaderSource(shaderId, 1, new String[] {glslCode}, length);
355        gl.glCompileShader(shaderId);
356
357        IntBuffer intBuffer = IntBuffer.allocate(1);
358        gl.glGetShaderiv(shaderId, GL_COMPILE_STATUS, intBuffer);
359
360        if (intBuffer.get(0) != 1) {
361            gl.glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, intBuffer);
362            int size = intBuffer.get(0);
363            if (size > 0) {
364                ByteBuffer byteBuffer = ByteBuffer.allocate(size);
365                gl.glGetShaderInfoLog(shaderId, size, intBuffer, byteBuffer);
366                System.err.println(new String(byteBuffer.array()));
367            }
368            throw new RuntimeException("Couldn't compile shader");
369        }
370
371        gl.glAttachShader(programId, shaderId);
372
373        return shaderId;
374    }
375
376    @Override
377    protected Mat process(Mat in) {
378        if (in.width() != previousWidth && in.height() != previousHeight) {
379            logger.debug("Resizing OpenGL viewport, byte buffers, and PBOs");
380
381            drawable.setSurfaceSize(in.width(), in.height());
382            gl.glViewport(0, 0, in.width(), in.height());
383
384            previousWidth = in.width();
385            previousHeight = in.height();
386
387            inputBytes = new byte[in.width() * in.height() * 3];
388
389            outputBytes = new byte[in.width() * in.height()];
390            outputMat = new Mat(k_startingHeight, k_startingWidth, CvType.CV_8UC1);
391
392            if (pboMode != PBOMode.NONE) {
393                gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(0));
394                gl.glBufferData(
395                        GLES3.GL_PIXEL_PACK_BUFFER,
396                        (long) in.width() * in.height(),
397                        null,
398                        GLES3.GL_STREAM_READ);
399
400                if (pboMode == PBOMode.DOUBLE_BUFFERED) {
401                    gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(1));
402                    gl.glBufferData(
403                            GLES3.GL_PIXEL_PACK_BUFFER,
404                            (long) in.width() * in.height(),
405                            null,
406                            GLES3.GL_STREAM_READ);
407                }
408            }
409        }
410
411        if (pboMode == PBOMode.DOUBLE_BUFFERED) {
412            unpackIndex = (unpackIndex + 1) % 2;
413            unpackNextIndex = (unpackIndex + 1) % 2;
414        }
415
416        // Reset the fullscreen quad
417        gl.glBindBuffer(GL_ARRAY_BUFFER, vertexVBOIds.get(0));
418        gl.glEnableVertexAttribArray(k_positionVertexAttribute);
419        gl.glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0);
420        gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
421
422        // Load and bind our image as a 2D texture
423        gl.glActiveTexture(GL_TEXTURE0);
424        texture.enable(gl);
425        texture.bind(gl);
426
427        // Load our image into the texture
428        in.get(0, 0, inputBytes);
429        if (pboMode == PBOMode.NONE) {
430            ByteBuffer buf = ByteBuffer.wrap(inputBytes);
431            // (We're actually taking in BGR even though this says RGB; it's much easier and faster to
432            // switch it around in the fragment shader)
433            texture.updateImage(
434                    gl,
435                    new TextureData(
436                            profile,
437                            GL_RGB8,
438                            in.width(),
439                            in.height(),
440                            0,
441                            GL_RGB,
442                            GL_UNSIGNED_BYTE,
443                            false,
444                            false,
445                            false,
446                            buf,
447                            null));
448        } else {
449            // Bind the PBO to the texture
450            gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(unpackIndex));
451
452            // Copy pixels from the PBO to the texture object
453            gl.glTexSubImage2D(
454                    GLES3.GL_TEXTURE_2D,
455                    0,
456                    0,
457                    0,
458                    in.width(),
459                    in.height(),
460                    GLES3.GL_RGB8,
461                    GLES3.GL_UNSIGNED_BYTE,
462                    0);
463
464            // Bind (potentially) another PBO to update the texture source
465            gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(unpackNextIndex));
466
467            // This call with a nullptr for the data arg tells OpenGL *not* to wait to be in sync with the
468            // GPU
469            // This causes the previous data in the PBO to be discarded
470            gl.glBufferData(
471                    GLES3.GL_PIXEL_UNPACK_BUFFER,
472                    (long) in.width() * in.height() * 3,
473                    null,
474                    GLES3.GL_STREAM_DRAW);
475
476            // Map the buffer of GPU memory into a place that's accessible by us
477            var buf =
478                    gl.glMapBufferRange(
479                            GLES3.GL_PIXEL_UNPACK_BUFFER,
480                            0,
481                            (long) in.width() * in.height() * 3,
482                            GLES3.GL_MAP_WRITE_BIT);
483            buf.put(inputBytes);
484
485            gl.glUnmapBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER);
486            gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, 0);
487        }
488
489        // Put values in a uniform holding the image resolution
490        gl.glUniform2f(resolutionUniformId, in.width(), in.height());
491
492        // Put values in threshold uniforms
493        var lowr = params.getHsvLower().val;
494        var upr = params.getHsvUpper().val;
495        gl.glUniform3f(lowerUniformId, (float) lowr[0], (float) lowr[1], (float) lowr[2]);
496        gl.glUniform3f(upperUniformId, (float) upr[0], (float) upr[1], (float) upr[2]);
497
498        // Draw the fullscreen quad
499        gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, k_vertexPositions.length);
500
501        // Cleanup
502        texture.disable(gl);
503        gl.glDisableVertexAttribArray(k_positionVertexAttribute);
504        gl.glUseProgram(0);
505
506        if (pboMode == PBOMode.NONE) {
507            return saveMatNoPBO(gl, in.width(), in.height());
508        } else {
509            return saveMatPBO((GLES3) gl, in.width(), in.height(), pboMode == PBOMode.DOUBLE_BUFFERED);
510        }
511    }
512
513    private Mat saveMatNoPBO(GL2ES2 gl, int width, int height) {
514        ByteBuffer buffer = GLBuffers.newDirectByteBuffer(width * height);
515        // We use GL_RED/GL_ALPHA to get things in a single-channel format
516        // Note that which pixel format you use is *very* important to performance
517        // E.g. GL_ALPHA is super slow in this case
518        gl.glReadPixels(0, 0, width, height, outputFormat, GL_UNSIGNED_BYTE, buffer);
519
520        return new Mat(height, width, CvType.CV_8UC1, buffer);
521    }
522
523    private Mat saveMatPBO(GLES3 gl, int width, int height, boolean doubleBuffered) {
524        if (doubleBuffered) {
525            packIndex = (packIndex + 1) % 2;
526            packNextIndex = (packIndex + 1) % 2;
527        }
528
529        // Set the target framebuffer attachment to read
530        gl.glReadBuffer(GLES3.GL_COLOR_ATTACHMENT0);
531
532        // Read pixels from the framebuffer to the PBO
533        gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(packIndex));
534        // We use GL_RED (which is always supported in GLES3) to get things in a single-channel format
535        // Note that which pixel format you use is *very* important to performance
536        // E.g. GL_ALPHA is super slow in this case
537        gl.glReadPixels(0, 0, width, height, GLES3.GL_RED, GLES3.GL_UNSIGNED_BYTE, 0);
538
539        // Map the PBO into the CPU's memory
540        gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(packNextIndex));
541        var buf =
542                gl.glMapBufferRange(
543                        GLES3.GL_PIXEL_PACK_BUFFER, 0, (long) width * height, GLES3.GL_MAP_READ_BIT);
544        buf.get(outputBytes);
545        outputMat.put(0, 0, outputBytes);
546        gl.glUnmapBuffer(GLES3.GL_PIXEL_PACK_BUFFER);
547        gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, 0);
548
549        return outputMat;
550    }
551}