Java2D: interaction between XWindows events and frame rate
Asked Answered
S

1

12

I'm experiencing an unexpected interaction between system events and the window refresh rate in simple Java2D applications on Linux/XWindows. It is best demonstrated with the small example below.

This program creates a small window in which a half-circle is displayed at different rotations. The graphics are updated at 60 frames per second in order to produce a flickering display. This is achieved through a BufferStrategy, namely by calling its show method.

However, I note that when I (a) move the mouse over the window so that the window receives mouse-over events or (b) hold down a key on the keyboard so that the window receives keyboard events, the flickering increases visibly.

As the rate at which BufferStrategy.show() is called is not affected by these events, as can be seen by the print-outs on the console (they should constantly be at around 60 fps). However, the faster flickering indicates that the rate at which the display is actually updated, does indeed change.

It looks to me that actual, i.e., visible 60 frames per second are not achieved unless mouse or keyboard events are generated.

public class Test {
    // pass the path to 'test.png' as command line parameter
    public static void main(String[] args) throws Exception {
        BufferedImage image = ImageIO.read(new File(args[0]));

        // create window
        JFrame frame = new JFrame();
        Canvas canvas = new Canvas();
        canvas.setPreferredSize(new Dimension(100, 100));
        frame.getContentPane().add(canvas);
        frame.pack();
        frame.setVisible(true);

        int fps = 0;
        long nsPerFrame = 1000000000 / 60; // 60 = target fps
        long showTime = System.nanoTime() + nsPerFrame;
        long printTime = System.currentTimeMillis() + 1000;
        for (int tick = 0; true; tick++) {
            BufferStrategy bs = canvas.getBufferStrategy();
            if (bs == null) {
                canvas.createBufferStrategy(2);
                continue;
            }

            // draw frame
            Graphics g = bs.getDrawGraphics();
            int framex = (tick % 4) * 64;
            g.drawImage(image, 18, 18, 82, 82, framex, 0, framex+64, 64, null);
            g.dispose();
            bs.show();

            // enforce frame rate
            long sleepTime = showTime - System.nanoTime();
            if (sleepTime > 0) {
                long sleepMillis = sleepTime / 1000000;
                int sleepNanos = (int) (sleepTime - (sleepMillis * 1000000));
                try {
                    Thread.sleep(sleepMillis, sleepNanos);
                } catch (InterruptedException ie) {
                    /* ignore */
                }
            }
            showTime += nsPerFrame;

            // print frame rate achieved
            fps++;
            if (System.currentTimeMillis() > printTime) {
                System.out.println("fps: " + fps);
                fps = 0;
                printTime += 1000;
            }
        }
    }
}

A sample image to use with this program (the path to which must be passed as a command line argument) is this:

enter image description here

So my (2-part) question is:

Why is this effect happening? How can I reach actual 60 fps?

(Bonus question for commenters: do you experience the same effect also under other operating systems?)

Santasantacruz answered 17/11, 2016 at 17:43 Comment(8)
I tried running your code several times in my machine which runs on windows 10. Sometimes I observed, that the flickering is slowed down. Did you ever face this issue? Did you try safeguarding your code against frame loss??Columbium
@sbaitmangalkar Thanks for trying the code out. Frame loss is a separate issue that I'm currently not looking at. But just to be clear: when you run the program, you do not see a speed-up of the animation as soon as you move the mouse cursor over the window?Santasantacruz
The buffers in buffer strategy are mostly VolatileImage which may lose the contents based on the OS factors like you said and thus resulting in the behaviour being mentioned. Hence by limiting your looping condition for lost buffers might solve your problem. Again, I'm not really sure if this is really the reason behind the code behaviour.Columbium
In my test runs, the contents were never lost, and still the described behavior was visible.Santasantacruz
Did you observe the fps value being changed while you did that?? For me, I could observe the fps value being changed between the range of 59 to 61 and the refreshing was as required.Columbium
The printed fps value stays roughly the same independent of the observed fps (compare to my original question). The difference I observe between with or without mouse motion is not subtle: it's a very visible increase.Santasantacruz
Let us continue this discussion in chat.Columbium
Works in OSX El Capitan. Probably because it's not susceptible to X11 command buffer batching.Amosamount
A
6

Q: Why is this effect happening?

Short answer: One must call canvas.getToolkit().sync() or Toolkit.getDefaultToolkit().sync() after BufferStrategy#show

Long answer: Without an explicit Toolkit#sync pixels do not make it to the screen until X11 command buffer is full or another event, such as mouse move, forces a flush.

Quote from http://www.java-gaming.org/index.php/topic,15000.

... even if we (Java2D) issue our rendering commands immediately the video driver may chose not to execute them right away. Classic example is X11 - try timing a loop of Graphics.fillRects - see how many you can issue in a couple of seconds. Without toolkit.sync() (which in case of X11 pipeline does XFlush()) after each call or at the end of the loop what you actually measure is how fast Java2D can call X11 lib's XFillRect method, which just batches up those calls until it's command buffer is full, only then they're sent to the X server for execution.

Q: How can I reach actual 60 fps?

A: Flush the command buffer and consider turning on OpenGL acceleration for Java2D with System.setProperty("sun.java2d.opengl", "true"); in code or -Dsun.java2d.opengl=true command line option.

See also:

Amosamount answered 24/11, 2016 at 20:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.