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:
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:
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.
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);
}
}
}
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.
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With