Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fixing Severe Flickering in Window with Multiple Viewports

Summary

I have a fairly sized system where a JFrame with a GLCanvas are being used to render scenes (using OpenGL). The drawing surface of the canvas can be divided into several (e.g. 4) viewports. Scene objects are rendered at different times, and multiple invocations of canvas.display() are necessary before the contents of the entire scene have been processed.

I've set canvas.setAutoSwapBufferMode(false); and manually invoke canvas.swapBuffers();, as per the documentation. I do that after the contents of every viewport have been rendered, so that the back buffer is swapped once per frame as opposed to once per viewport, which is what JOGL does automatically by default after every display(GLAutoDrawable) pass. (Note that the issue does not go away by simply keeping the default behavior, but there's still a need to do this manually.)

The problem I'm running into is that I see a severe flickering effect in some OS/GPU setups. (Please see screen-shots below for examples.) I'm able to test my code in the following setups:

  • Kubuntu 17.04 + NVIDIA GTX-960M (main dev system)
  • Windows 10 + NVIDIA GTX-960M
  • Kubuntu 17.04 + NVIDIA GTX-770
  • Windows 7 + NVIDIA GTX-770M

Out of those setups, only my main development system appears correct, but in the other setups severe flickering and/or other artifacts are observed in different ways.

It looks like a back-buffer management issue, but it's not clear to me what the cause is, and I've observed no OpenGL error codes.

I wrote a MCVE (code below) using the approach in the larger system to reproduce the issue separately. Note that I use glViewport and glScissor in order to limit the viewport area to be updated during a specific display(GLAutoDrawable) invocation. (Yes, GL_SCISSOR_TEST is enabled.)

My questions are:

  1. Does buffer management in my MCVE look correct to you?
  2. What is the proper way to render to multiple viewports in the same window?
  3. Does anything else stand out as a potential cause of the problem?

Thanks in advance.


I've gone over several other questions, but their probems are different (e.g. overlapping viewports, but I'm not trying to overlap them). In addition, they're not using JOGL and its infrastructure, but mine is.

I've also seen older questions pointing to out-dated NeHe tutorials (e.g. this one) but even trying out what the article proposes (i.e. clear color buffer once before rendering starts and then only the depth buffer before rendering each viewport) does not work as intended and introduces other issues outside the scope of this post.


MCVE Code Sample

This code sample requires JOGL and OpenGL 4.0 at a minimum. Feel free to copy/paste and run it locally.

import java.awt.*;
import java.awt.event.*;
import java.nio.*;
import java.util.*;
import java.util.Timer;
import java.util.concurrent.atomic.*;

import javax.swing.*;

import com.jogamp.opengl.*;
import com.jogamp.opengl.awt.*;
import com.jogamp.opengl.util.glsl.*;

public class ManualViewportBufferClearingTest implements GLEventListener, KeyListener {

    private Timer                renderLoopTimer  = new Timer();
    private JFrame               frame            = new JFrame(ManualViewportBufferClearingTest.class.getName());
    private GLCanvas             canvas;

    private ShaderProgram        shaderProgram;

    private int[]                vaos             = new int[1];
    private int[]                vbos             = new int[2];

    private Viewport[]           viewports;
    private Viewport             activeViewport;

    /**
     * Avoid performing display logic (e.g. automatically on initialization) unless
     * the client has explicitly requested it.
     *
     * This is set/unset in the AWT Event Thread, but checked in the GLEventListener
     * Thread.
     */
    private AtomicBoolean        displayRequested = new AtomicBoolean(false);

    // @formatter:off
    private static final float[] vertexPositions    = new float[] {
         .25f,  .25f, 0f, 1f,
        -.25f, -.25f, 0f, 1f,
         .25f, -.25f, 0f, 1f
    };
    private static final float[] vertexColors       = new float[] {
        1f, 1f, 1f, 1f,
        1f, 1f, 1f, 1f,
        1f, 1f, 1f, 1f
    };
    // @formatter:on

    private FloatBuffer          vertices         = FloatBuffer.wrap(vertexPositions);
    private FloatBuffer          offsets          = FloatBuffer.wrap(new float[] { 0, 0, 0, 0 });
    private FloatBuffer          colors           = FloatBuffer.wrap(vertexColors);

    public ManualViewportBufferClearingTest() {
        final GLCapabilities caps = new GLCapabilities(GLProfile.get(GLProfile.GL4));
        caps.setBackgroundOpaque(true);
        caps.setDoubleBuffered(true);
        caps.setRedBits(8);
        caps.setGreenBits(8);
        caps.setBlueBits(8);
        caps.setAlphaBits(8);

        canvas = new GLCanvas(caps);
        canvas.addGLEventListener(this);
        canvas.addKeyListener(this);
        canvas.setAutoSwapBufferMode(false); // <<--- IMPORTANT!! See Manual Swapping Later.

        final int pixelWidth = 1024;
        final int pixelHeight = 768;

        frame.setSize(pixelWidth, pixelHeight);
        frame.setLocationRelativeTo(null);
        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(canvas, BorderLayout.CENTER);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        resetViewports(pixelWidth, pixelHeight);

        frame.setVisible(true);
    }

    @Override
    public void init(GLAutoDrawable glad) {
        GL4 gl = (GL4) glad.getGL();

        gl.glEnable(GL4.GL_DEPTH_TEST);
        gl.glEnable(GL4.GL_SCISSOR_TEST);

        gl.glGenVertexArrays(vaos.length, vaos, 0);
        gl.glBindVertexArray(vaos[0]);

        setupBuffers(gl);
        buildProgram(gl);

        shaderProgram.useProgram(gl, true);

        renderLoopTimer.scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                renderToViewports();
            }
        }, 0, 16); // draw every 16ms, for 60 FPS
    }

    @Override
    public void display(GLAutoDrawable glad) {
        if (!displayRequested.get())
            return;

        // apply a simple animation
        final double value = System.currentTimeMillis() / 503.0;
        offsets.put(0, (float) (Math.sin(value) * 0.5));
        offsets.put(1, (float) (Math.cos(value) * 0.6));

        GL4 gl = (GL4) glad.getGL();

        gl.glViewport(activeViewport.x, activeViewport.y, activeViewport.width, activeViewport.height);
        gl.glScissor(activeViewport.x, activeViewport.y, activeViewport.width, activeViewport.height);

        gl.glClearBufferfv(GL4.GL_COLOR, 0, activeViewport.colorBuffer);
        gl.glClearBufferfv(GL4.GL_DEPTH, 0, activeViewport.depthBuffer);

        gl.glVertexAttrib4fv(/* layout (location = */1, offsets);
        gl.glDrawArrays(GL4.GL_TRIANGLES, 0, 3);
    }

    @Override
    public void dispose(GLAutoDrawable glad) {
        GL4 gl = (GL4) glad.getGL();
        shaderProgram.destroy(gl);
        gl.glDeleteVertexArrays(vaos.length, vaos, 0);
        gl.glDeleteBuffers(vbos.length, vbos, 0);
    }

    @Override
    public void reshape(GLAutoDrawable glad, int x, int y, int width, int height) {
        GL4 gl = glad.getGL().getGL4();

        resetViewports(width, height);

        Viewport vp = viewports[0];
        gl.glViewport(vp.x, vp.y, vp.width, vp.height);
        gl.glScissor(vp.x, vp.y, vp.width, vp.height);
    }

    @Override
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_ESCAPE:
                cleanup();
                frame.dispose();
                System.exit(0);
                break;
        }
    }

    private void setupBuffers(GL4 gl) {
        gl.glGenBuffers(vbos.length, vbos, 0);

        gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, vbos[0]);
        gl.glBufferData(GL4.GL_ARRAY_BUFFER, vertices.capacity() * Float.BYTES, vertices, GL4.GL_STATIC_DRAW);
        gl.glVertexAttribPointer(/* layout (location = */ 0, /* vec4 = */ 4 /* floats */, GL4.GL_FLOAT, false, 0, 0);
        gl.glEnableVertexAttribArray(0);

        gl.glBindBuffer(GL4.GL_ARRAY_BUFFER, vbos[1]);
        gl.glBufferData(GL4.GL_ARRAY_BUFFER, colors.capacity() * Float.BYTES, colors, GL4.GL_STATIC_DRAW);
        gl.glVertexAttribPointer(/* layout (location = */ 2, /* vec4 = */ 4 /* floats */, GL4.GL_FLOAT, false, 0, 0);
        gl.glEnableVertexAttribArray(2);
    }

    private void resetViewports(int width, int height) {
        final int halfW = width / 2;
        final int halfH = height / 2;

        // @formatter:off
        viewports = new Viewport[] {
            new Viewport(0    , 0    , halfW, halfH, Color.BLUE),   // bot left
            new Viewport(halfW, 0    , halfW, halfH, Color.GRAY),   // bot right
            new Viewport(0    , halfH, halfW, halfH, Color.RED),    // top left
            new Viewport(halfW, halfH, halfW, halfH, Color.GREEN)   // top right
        };
        // @formatter:on
    }

    private void renderToViewports() {
        for (int i = 0; i < viewports.length; ++i) {
            activeViewport = viewports[i];

            displayRequested.set(true);
            canvas.display();
            displayRequested.set(false);
        }
        canvas.swapBuffers(); // <<--- MANUAL SWAP REQUIRED; See canvas.setAutoSwapBufferMode(false)!!
    }

    private void cleanup() {
        renderLoopTimer.cancel();
        canvas.disposeGLEventListener(this, true);

        vertices.clear();
        offsets.clear();
        colors.clear();

        viewports = null;
        activeViewport = null;
        vertices = null;
        offsets = null;
        colors = null;
    }

    private static String getVertexSource() {
        // @formatter:off
        return
            "#version 400 core                                      \n"
            + "                                                     \n"
            + "layout (location = 0) in vec4 vertex_position;       \n"
            + "layout (location = 1) in vec4 vertex_offset;         \n"
            + "layout (location = 2) in vec4 vertex_color;          \n"
            + "                                                     \n"
            + "out vertex_t {                                       \n"
            + "    vec4 color;                                      \n"
            + "} vs;                                                \n"
            + "                                                     \n"
            + "void main() {                                        \n"
            + "    vs.color      = vertex_color;                    \n"
            + "    gl_Position   = vertex_position + vertex_offset; \n"
            + "}                                                    \n";
        // @formatter:on
    }

    private static String getFragmentSource() {
        // @formatter:off
        return
            "#version 400 core                                      \n"
            + "                                                     \n"
            + "in vertex_t {                                        \n"
            + "    vec4 color;                                      \n"
            + "} fs;                                                \n"
            + "                                                     \n"
            + "out vec4 fragment;                                   \n"
            + "                                                     \n"
            + "void main() {                                        \n"
            + "    fragment = fs.color;                             \n"
            + "}                                                    \n";
        // @formatter:on
    }

    private void buildProgram(GL4 gl) {
        shaderProgram = new ShaderProgram();

        ShaderCode vs = createShader(gl, GL4.GL_VERTEX_SHADER, getVertexSource());
        ShaderCode fs = createShader(gl, GL4.GL_FRAGMENT_SHADER, getFragmentSource());

        shaderProgram.init(gl);
        shaderProgram.add(vs);
        shaderProgram.add(fs);

        shaderProgram.link(gl, System.err);
        if (!shaderProgram.validateProgram(gl, System.err))
            throw new RuntimeException("Program failed to link");

        vs.destroy(gl);
        fs.destroy(gl);
    }

    private ShaderCode createShader(GL4 gl, int shaderType, String source) {
        String[][] sources = new String[1][1];
        sources[0] = new String[] { source };

        ShaderCode shader = new ShaderCode(shaderType, sources.length, sources);

        if (!shader.compile(gl, System.err))
            throw new RuntimeException("Shader compilation failed\n" + source);

        return shader;
    }

    @Override
    public void keyReleased(KeyEvent e) {}

    @Override
    public void keyTyped(KeyEvent e) {}

    public static void main(String[] args) {
        new ManualViewportBufferClearingTest();
    }

    /**
     * Utility class for a window viewport.
     */
    private class Viewport {

        public int         x, y;
        public int         width, height;

        public FloatBuffer colorBuffer;
        public FloatBuffer depthBuffer;

        public Viewport(int x, int y, int width, int height, Color color, float depth) {
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
            this.depthBuffer = FloatBuffer.wrap(new float[] { depth });

            float[] components = color.getColorComponents(null);
            colorBuffer = FloatBuffer.wrap(new float[] { components[0], components[1], components[2], 0 });
        }

        public Viewport(int x, int y, int width, int height, Color color) {
            this(x, y, width, height, color, 1f);
        }

    }

}

Screenshots

Kubuntu 17.04 + GTX-960M (reference)

Kubuntu 17.04 + GTX-960M (correct)

Windows 10 + GTX-960M (not correct)

These were taken during the same run a few seconds apart after resizing the window. The issue looks different in Windows 7, but these should be enough to show the problem.

Windows 10 + GTX-960M (1)

Windows 10 + GTX-960M (2)

like image 268
code_dredd Avatar asked Nov 07 '22 17:11

code_dredd


1 Answers

Your problem is right here: You're swapping the buffers after rendering each single viewport.

private void renderToViewports() {
    for (int i = 0; i < viewports.length; ++i) {
        activeViewport = viewports[i];

        displayRequested.set(true);
        canvas.display();
        displayRequested.set(false);

        canvas.swapBuffers();
    }
}

Of course it flickers that way.

Change it to this and you should be good:

 private void renderToViewports() {
     for (int i = 0; i < viewports.length; ++i) {
        /* ... */

        // <<<< remove buffer swap here and... 
     }

     // >>>>> move it here!

     canvas.swapBuffers();
 }
like image 64
datenwolf Avatar answered Nov 15 '22 11:11

datenwolf