The main problem you have is that your timer loop is leaking time, causing it to sync poorly with the monitors vertical refresh.
The below code corrects this :-
import java.awt.Color;
import java.awt.DisplayMode;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;
public class CTest
implements Runnable {
static final int APP_WIDTH = 640;
static final int APP_HEIGHT = 480;
private GraphicsEnvironment mGraphicsEnv = null;
private GraphicsDevice mGraphicsDev = null;
private GraphicsConfiguration mGraphicsConf = null;
private BufferStrategy mBufferStrategy = null;
private JFrame mFrame = null;
private int targetFps;
// changed box width to 1, so tearing is much more obvious.
public static final int BOX_WIDTH = 1;
public static final int BOX_HEIGHT = 300;
private int mBoxX = 200;
private int mBoxY = 200;
private int mBoxDX = 1;
private int mBoxDY = 1;
CTest() {
mGraphicsEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
mGraphicsDev = mGraphicsEnv.getDefaultScreenDevice();
mGraphicsConf = mGraphicsDev.getDefaultConfiguration();
final int currentRefreshrate = mGraphicsDev.getDisplayMode().getRefreshRate();
// if the refresh rate is unknown, we hope 60 is ok.
// Alternatively, you could switch to fullscreen mode for a few second, and attempt to use the vsync lock of a
// page flipping BufferStrategy to determine the refreshrate.
System.out.println("Reported Refreshrate=" + currentRefreshrate);
targetFps = (currentRefreshrate==DisplayMode.REFRESH_RATE_UNKNOWN?60:currentRefreshrate);
mFrame = new JFrame(mGraphicsConf);
mFrame.setIgnoreRepaint(true);
mFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mFrame.setLocation(100, 100);
mFrame.setSize(APP_WIDTH, APP_HEIGHT);
mFrame.setFocusTraversalKeysEnabled(false);
mFrame.setResizable(false);
mFrame.setVisible(true);
mFrame.createBufferStrategy(2);
mBufferStrategy = mFrame.getBufferStrategy();
Thread thread = new Thread(this);
thread.start();
}
public void run() {
long startTime = System.nanoTime();
long frameCount = 0;
while(true) {
frameCount++;
// this isn't needed for timing
// I just use it to gauge how much drawing can be done in the frame
// without over-running the frameEndTime. (which would cause tearing)
final long frameStartTime = System.nanoTime();
// time at which this frame should end so it doesn't run over the sync-time
final long frameEndTime = startTime+(frameCount*1000000000)/targetFps;
mBoxX += mBoxDX;
mBoxY += mBoxDY;
// this was incorrect - it was '>', where-as it should be '>='
if(mBoxX >= APP_WIDTH - BOX_WIDTH)
mBoxDX = -1;
if(mBoxX < 0)
mBoxDX = 1;
// this was incorrect - it was '>', where-as it should be '>='
if(mBoxY >= APP_HEIGHT - BOX_HEIGHT)
mBoxDY = -1;
if(mBoxY < 0)
mBoxDY = 1;
Graphics2D graphics = (Graphics2D)mBufferStrategy.getDrawGraphics();
// Vary the amount of painting work.
// This makes the game loop more representative of how a real game would behave.
// and better demonstrates why the bufferStrategy.show() should be placed
// after the timing loop, not before it.
final long paintEndTime = frameStartTime + (long)((frameEndTime-frameStartTime)*0.8*Math.random());
while(System.nanoTime()<paintEndTime) {
graphics.clearRect(0, 0, APP_WIDTH, APP_HEIGHT);
// Toolkit sync() is needed to stop the Java2D pipeline buffering these commands
// (which can cause an uncontrollable oscillation in the number of draws.)
// Try taking this out, and run the app. a few times.
// You will see a very bizarre effect occurring!
Toolkit.getDefaultToolkit().sync();
}
graphics.setColor(Color.RED);
graphics.fillRect(mBoxX, mBoxY, BOX_WIDTH, BOX_HEIGHT);
graphics.dispose();
// mBufferStrategy.show(); // alternate (bad) place to have the show()
// timing loop - you do not want to do this when vsync is supported (in fullscreen exclusive mode)
// as it will interfere with the synchronisation performed by the vsync.
while(System.nanoTime()<frameEndTime) {
Thread.yield();
}
// do the show() after the timing loop,
// so the copy to the screen buffer occurs at approx. the same time each frame.
// if you did it before the timing loop, the time at which the copy occurs
// will vary according to how busy the game loop is each frame - this will cause excessive tearing.
// To see what I mean, try moving it above the timing loop
// The fluctuations will dramatically increase the amount of tearing.
mBufferStrategy.show();
}
}
public static void main(String[] args) {
new CTest();
}
}
It is worth noting that there are still problems with the above solution.
The most important of these is that there is no vsync in windowed mode. (fullscreen exclusive mode does not have this limitation)
The lack of vsync will manifest itself as periodic tearing of the rendered image. You should see this in the code i’ve posted, as a judder that gradually moves up the length of the red box/line.
Unfortunately in windowed mode there is no good solution to this without using native code to access the necessary vsync functionality.
You might think you’d be able to create a timer that was accurately synchronized with the monitor’s vertical refresh…
However, this is impossible because System.nanoTime() drifts over quite short periods of time(it’ll drift approx. 3/60th of a second every minute), and System.currentTimeMillis() is not accurate enough.
:edit:
Improved the comments in the above code, and added a chunk of code to vary the amount of drawing done each frame.
This makes this synthetic example more representative of a real game,
and better demonstrates why the bufferStrategy.show() should be after the timing loop, not before it.