Java2D:XWindows事件和帧速率之间的交互

12

我在Linux/XWindows上简单的Java2D应用中经历了系统事件和窗口刷新率之间的意外交互。以下小例子最能说明这一点。

该程序创建一个小窗口,在其中以不同的旋转显示半圆形。图形每秒更新60帧,以产生闪烁的显示效果。这是通过BufferStrategy实现的,即通过调用其show方法来实现的。

然而,我注意到当我(a)将鼠标移动到窗口上,使窗口接收到鼠标事件,或者(b)按住键盘上的某个键,以便窗口接收到键盘事件时,闪烁明显增加。

由于BufferStrategy.show()被调用的频率不受这些事件的影响,可以从控制台上的输出看出(它们应该始终保持在大约60 fps)。然而,更快的闪烁表明,实际上更新显示的速度确实发生了变化。

在我看来,只有在生成鼠标或键盘事件时,才能实现实际的,即可见的每秒60帧。

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;
            }
        }
    }
}

一个可以与该程序一起使用的示例图像(必须将其路径作为命令行参数传递)如下所示:

enter image description here

所以我有一个(由两部分组成的)问题:

为什么会出现这种效果?我如何实现 实际 的 60 fps?

(攻略者的额外问题:您是否在其他操作系统下也遇到了同样的问题?)


我在我的运行Windows 10的计算机上尝试多次运行您的代码。有时我观察到闪烁减慢了。您是否遇到过这个问题?您是否尝试过保护您的代码以防止帧丢失? - Shyam Baitmangalkar
@sbaitmangalkar 感谢您尝试代码。帧丢失是我目前没有考虑的另一个问题。但是,只是为了明确:当您运行程序时,您并没有看到动画加速,只要您将鼠标光标移动到窗口上方? - Thomas
1
缓冲策略中的缓冲区大多是VolatileImage,根据您所说的操作系统因素可能会丢失其内容,从而导致出现所述行为。因此,通过限制丢失缓冲区的循环条件可能会解决您的问题。再次强调,我不确定这是否真正是代码行为背后的原因。 - Shyam Baitmangalkar
你有注意到在你这样做的时候fps值是否发生了变化吗?对我来说,我可以观察到fps值在59到61之间变化,刷新也是按要求进行的。 - Shyam Baitmangalkar
1
可以在OSX El Capitan上运行。可能是因为它不容易受到X11命令缓冲区批处理的影响。 - anttix
显示剩余3条评论
1个回答

6

问题: 为什么会出现这种情况?

简短回答: 在 BufferStrategy#show 后必须调用 canvas.getToolkit().sync()Toolkit.getDefaultToolkit().sync()

详细回答: 没有显式的 Toolkit#sync,像素直到X11命令缓冲区满或其他事件(例如鼠标移动)强制刷新时才会显示在屏幕上。

引用自http://www.java-gaming.org/index.php/topic,15000

... 即使我们(Java2D)立即发出渲染命令,视频驱动程序也可能选择不立即执行这些命令。经典示例就是X11-尝试计算Graphics.fillRects循环的时间-看一下你能在几秒钟内发行多少个。没有 toolkit.sync()(在 X11 管道的情况下执行 XFlush())对于每个调用或在循环结束时,你实际测量的是 Java2D 可以多快地调用 X11 lib 的 XFillRect 方法,该方法只是将这些调用批处理,直到其命令缓冲区已满,然后它们才会被发送到 X 服务器执行。

问题: 如何实现实际的60 fps?

答案: 刷新命令缓冲区,并考虑在代码中使用 System.setProperty("sun.java2d.opengl", "true")-Dsun.java2d.opengl=true 命令行选项开启Java2D的OpenGL加速。

也可参见:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接