Java: How to Do Double-Buffering in Swing

Java swing double buffering

Drawing is performed on whole frame area, not on gameField. Actually, the only cause of temporary JScrollPane appearing is that it calls paintImmediately somewhere in mouse-drag handler.

The first thing that comes to mind is to replace JPanel with Canvas, as I_Love_Thinking wrote, then get bufferStategy from that canvas instance and use it for rendering. But unfortunately it not works as expected. Lightweight JScrollPane can't properly drive heavyweight Canvas. Canvas refuses to draw content outside area (0, 0, viewport_width, viewport_height). This is known bug and it will not fixed. Mixing mixing heavyweight components with lightweight is bad idea.

Replacing JScrollPane with heavyweight ScrollPane seems working. Almost. There are noticeable rendering artifacts on scroll bars under some circumstances.

At this point, I've decided to stop beating the wall and give up. Scroll panes are the source of numerous problems and not worth to use for lazy scrolling. It is time to look on scrolling from another view. The Canvas is not game field itself, but window that we use to observe game world. This is well-known rendering strategy in gamedev. The size of canvas is exactly as observable area. When scrolling needed, we just render the world with some offset. The value for offset may be taken from some external scrollbar.

I suggest next changes:

  1. Throw away JScrollPane
  2. Add canvas directly to frame.
  3. Add single horizontal JScrollBar under canvas.
  4. Use value of scrollbar to shift rendering.

The example is based on your code. This implementation not respects resizing of frame. In real life some places need to be synchronized with size of viewport (that what scrollpane did before for us). Also, example violates Swing threading rules and reads value of scrollbar from rendering thread.

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Toolkit;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;

import javax.swing.JFrame;
import javax.swing.JScrollBar;

public class BufferStrategyDemo extends JFrame {
private BufferStrategy bufferStrategy;
private Canvas gameField;
private JScrollBar scroll;

public BufferStrategyDemo() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
getContentPane().setLayout(new BorderLayout());
getContentPane().setPreferredSize(new Dimension(800, 600));

gameField = new Canvas();
gameField.setIgnoreRepaint(true);
gameField.setPreferredSize(new Dimension(800, 580));
getContentPane().add(gameField, BorderLayout.CENTER);

scroll = new JScrollBar(JScrollBar.HORIZONTAL);
scroll.setPreferredSize(new Dimension(800, 20));
scroll.setMaximum(1400 - 800); // image width - viewport width
getContentPane().add(scroll, BorderLayout.SOUTH);

this.pack();

gameField.createBufferStrategy(2);
bufferStrategy = gameField.getBufferStrategy();

new Renderer().start();
}

private class Renderer extends Thread {
private BufferedImage imageOfGameField;

public Renderer() {
// NOTE: image size is fixed now, but better to bind image size to the size of viewport
imageOfGameField = new BufferedImage(1400, 580, BufferedImage.TYPE_INT_ARGB);
}

public void run() {
while (true) {
Graphics g = null;
try {
g = bufferStrategy.getDrawGraphics();
drawSprites(g);
} finally {
g.dispose();
}

bufferStrategy.show();
Toolkit.getDefaultToolkit().sync();

try {
Thread.sleep(1000 / 60);
} catch (InterruptedException ie) {
}
}
}

private void drawSprites(Graphics g) {
Graphics2D g2d = (Graphics2D) g;

Graphics g2d2 = imageOfGameField.createGraphics();
g2d2.setColor(Color.YELLOW); // clear background
g2d2.fillRect(0, 0, 1400, 580); // again, fixed width/height only for SSCCE
g2d2.setColor(Color.BLACK);

int shift = -scroll.getValue(); // here it is - get shift value

g2d2.fillRect(100 + shift, 100, 20, 20); // i am ugly black sprite
g2d2.fillRect(900 + shift, 100, 20, 20); // i am other black sprite
// located outside of default view

g2d.drawImage(imageOfGameField, 0, 0, null);

g2d2.dispose();
}
}

public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
BufferStrategyDemo mf = new BufferStrategyDemo();
mf.setVisible(true);
}
});
}
}

And another example, mostly based on code from Java Games: Active Rendering article. It properly synchronized with value of JScrollBar and size of Canvas. Also, it does painting directly to Graphics, obtained from BufferStrategy instance (instead of intermediate BuffereImage object). The result is something like this:

Active Rendering Demo

How do I double-buffer in Java Swing on a Retina display without losing the higher resolution?

It turns out I needed to change the way I animated the frame to account for the doubling of the scale.

First, I needed to detect the scale. I added this code, which requires Java 9 or greater to work correctly. (It compiles under java 8, but fails to execute correctly, always returning 1 for any screen.)

private static final int SCALE = calculateScaleForDefaultScreen();

private static int calculateScaleForDefaultScreen() {
// scale will be 2.0 for a Retina screen, and 1.0 for an older screen
double scale = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration()
.getDefaultTransform()
.getScaleX(); // Requires Java 9+ to work. Compiles under Java 8 but always returns 1.0.
//noinspection NumericCastThatLosesPrecision
return (int) Math.round(scale);
}

When I prepared my two off-screen graphics, I needed to do so at twice the scale:

Graphics2D graphics2D = (Graphics2D) priorScreen.getGraphics();
graphics2D.scale(SCALE, SCALE);
liveComponent.paint(graphics2D); // paint the current state of liveComponent into the image
graphics2D.dispose();

And…

Graphics2D graphics2D = (Graphics2D) upcomingScreen.getGraphics();
graphics2D.scale(SCALE, SCALE);
liveComponent.paint(graphics2D); // paint the upcoming state of liveComponent into the image
graphics2D.dispose();

Then, when I did my animation, I needed to include the SCALE in the drawing.

  if (swipeDirection == SwipeDirection.SWIPE_RIGHT) {
g.drawImage(uScreen, 0, 0, xLimit, height, 0, 0, xLimit*SCALE, height*SCALE, c);
g.drawImage(pScreen, xLimit, 0, width, height, xLimit*SCALE, 0, width*SCALE, height*SCALE, c);
} else {
g.drawImage(uScreen, xLimit, 0, width, height, xLimit*SCALE, 0, width*SCALE, height*SCALE, c);
g.drawImage(pScreen, 0, 0, xLimit, height, 0, 0, xLimit*SCALE, height*SCALE, c);
}

There are several other places where I multiplied widths and heights by 2. I changed those to SCALE as well.

I wish there were a more elegant solution, but this works.

Java Double Buffering

If you want more control over when the window is updated and to take advantage of hardware page flipping (if available), you can use the BufferStrategy class.

Your Draw method would then look something like this:

@Override
protected void Draw() {
BufferStrategy bs = getBufferStrategy();
Graphics g = bs.getDrawGraphics(); // acquire the graphics

// draw stuff here

bs.show(); // swap buffers
}

The downside is that this approach does not mix well with event-driven rendering. You generally have to choose one or the other. Also getBufferStrategy is implemented only in Canvas and Window making it incompatible with Swing components.

Tutorials can be found here, here and here.

How do I remove flickering with double buffering (java swing)

but I'm struggling to understand how to properly double buffer in order to remove flickering

I just want to make one thing very clear. When custom painting is done correctly, Swing components are automatically double buffered.

Calling super.paint in movePlayer is an inappropriate abuse of the painting system.

Don't use getGraphics, this is not how custom painting works. Also, don't dispose of a Graphics context you didn't create, this can actually prevent components which are schedule to painted after yours from been painted.

Swing uses a passive rendering algorithm, this means that it will schedule painting to occur when ever it decides it needs to be done. You don't have control over it, the best you can do is provide "hints" to the system that you would like a component or some part of the component repainted (ie repaint). The painting subsystem will then decide when and what should be painted. Your components will be requested to perform a paint operation via their paint methods.

When done this way, Swing will double buffer the paint operation, so all components been painted are buffered and when the paint pass is complete, the image updated to the screen

I would, highly recommend, reading through:

  • Performing Custom Painting
  • Painting in AWT and Swing

for more details about how the painting system works in Swing and how you are expected to work with it

As a side note, I would also avoid loading resources (or performing any time consuming tasks) in any paint functionality. Painting should run as fast as possible

Double Buffer a JFrame

Override the JPanel's paintComponent() Method and paint the content into a BufferedImage image first. Once done, copy the content of the BufferedImage into the graphics context you get from paintComponent().

protected void paintComponent(Graphics g) 
{
BufferedImage bufferedImage = new BufferedImage(500, 500, BufferedImage.TYPE_ARGB);
Graphics2D g2d = bufferedImage.createGraphics();
//paint using g2d ...

Graphics2D g2dComponent = (Graphics2D) g;
g2dComponent.drawImage(bufferedImage, null, 0, 0);
}


Related Topics



Leave a reply



Submit