奇怪的BufferStrategy问题 - 游戏只在英特尔GPU上运行得很快

26

我遇到了一个非常奇怪的问题,我试图寻找答案已经好几天了。我的游戏刚刚添加了一个新的粒子系统,但运行起来太慢了,导致无法玩耍。不幸的是,BufferedImage转换非常缓慢。爆炸效果由约200个从.png文件加载的白色精灵组成,这些精灵被随机旋转、缩放和着色,并以随机速度移动。

我尝试通过三重/双重缓冲来提高性能,并遇到了一些问题。

我的第一次尝试是使用绘制游戏的JPanel。我在JFrame的类(Main)中设置了缓冲区,然后在Game(扩展JPanel)类中进行绘画,但是没有使用Graphics g = bufferstrategy.getDrawGraphics();。然后,在绘画方法的末尾,如果缓冲区没有丢失,我就显示它。缓冲区总是“丢失”的,因为我没有用它的Graphics对象进行绘画。但是!游戏运行得飞快!实际上没有使用缓冲区!但是,为什么呢?

这个尝试最终没有出现图形错误,并且大大提高了性能-但只有在nVidia/AMD显卡上。英特尔GPU无法处理这个问题,屏幕会闪白色。

因此,我最终正确地设置和使用了BufferStrategy。现在Game类扩展Canvas而不是JPanel,因为从JFrame获取Graphics并在JPanel上绘制会导致偏移,因为它在标题栏下面绘制。仍然很快,固定60FPS。

现在,当我在JFrame(Main类)中创建BufferStrategy时,根本没有图片显示。我通过在Game类(Canvas)中设置BufferStrategy来纠正了这一点。现在图片是正确的,但游戏本身像蜗牛一样缓慢。一个爆炸会使FPS降至约10帧,但只有在nVidia / AMD上。具有讽刺意味的是,即使是旧的英特尔GPU也能以60FPS运行,我设法在5-6年前的集成英特尔GPU上实现了10000个粒子在运动中以60FPS的速度运行。我的显卡在发生爆炸时负载达到100%。

以下是我的主要代码(整个代码不清楚且很长):

public class Game extends Canvas {
 -snip-
 public void tick() {
    BufferStrategy bf = getBufferStrategy();
    Graphics g = null;
    try {
        g = bf.getDrawGraphics();
        paint(g);
    } finally {
        g.dispose();
    }
    if (!bf.contentsLost()) {
        bf.show();
    } else {
        System.err.println("Buffer lost!");
    }
    Toolkit.getDefaultToolkit().sync();
 }
 public void setBuffers() {
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice gs = ge.getDefaultScreenDevice();
    GraphicsConfiguration gc = gs.getDefaultConfiguration();

    if (gc.getBufferCapabilities().isMultiBufferAvailable()) {
        createBufferStrategy(3);
        System.out.println("Triple buffering active");
    } else {
        createBufferStrategy(2);
        System.err.println("Triple buffering not supported by the GPU");
        System.out.println("Double buffering active");
    }
    System.out.println("FullScreen required: " + getBufferStrategy().getCapabilities().isFullScreenRequired());
    System.out.println("Page flipping: " + getBufferStrategy().getCapabilities().isPageFlipping());
 }
 public void paint(Graphics g) {
    super.paint(g);
    //set up RenderingHints, draw stuff
 }
 -snip snip-
}
当然,在游戏启动时我会立刻调用 setBuffers()。
希望你能帮帮我,这个问题非常重要,因为在我看来使用 VolatileImage 不会提高性能,因为图像操作需要使用 BufferedImage。我想我可能错过了一些微不足道的东西,或者做错了什么。
这是我的电脑配置,只是为了显示这不是一个硬件问题: Intel Core i7-3770k @ 4.3GHz, nVidia GTX 460, 12 GB内存
"快速"计算机: Intel Core 2 Duo @ 2.7 GHz, 集成的英特尔图形, 2 GB内存
谢谢你的帮助和时间! :)
编辑 VolatileImage 有用吗? 如果我没记错的话,必须使用 BufferedImage 进行图像操作,但绘制它们会很慢。

我尝试过的电脑有:带有内置英特尔GPU的Core 2 Duo(JRE 6),带有HD4000集成GPU的i3-3225(JDK 7),带有HD2500的赛扬笔记本电脑(JRE 7)。在我的电脑上,我在NetBeans(JDK)和使用标准JRE 7中得到了相同的结果。我设法稍微减少了延迟,但仍然很卡顿。请注意,这个游戏还没有完成,要“基准测试”,只需在单人游戏中射击任何移动的东西,使用导弹可以获得额外的延迟。阅读readme.txt以获取控件说明:) FPS计数器不准确。这里是链接:link - Simon Tamás
我上传了一个异常的pastebin。由于缺少符号,它可能并不是非常有用且可能存在错误,但似乎可以作为开始查找的依据。 - doug65536
@DannyVarod 这看起来很合理,很可能就是问题所在。我会处理这种内存管理的问题,但我以前从未做过这样的事情。提前感谢您 :) - Simon Tamás
每秒35 x 10^6个故障是可怕的。程序的工作集大于进程可用的物理内存。当出现延迟时,您需要分析内存使用情况。这可能还与GPU内存映射有关。只是猜测,但也许nV正在使用内存管理硬件来控制CPU/GPU传输,并且这显示为高换页率。这与@SimonTamás所说的一致。 - Gene
@DannyVarod,你能给我更多关于如何设置Java使用CPU+GDRAM进行渲染的帮助吗?sun.java2d.ddforcevram没有用:( - Simon Tamás
显示剩余15条评论
3个回答

2
以下是需要翻译的内容:

这里有几个需要检查的问题:


如果不知道 setBuffers 函数的控制台 / 错误输出,很难确定问题所在。Intel 是否使用 createBufferStrategy(2); 而 NV 使用 createBufferStrategy(3); ? 如果是这样的话,那就可能导致它使用额外的内存。


你尝试过 java2d System.properties 吗?http://docs.oracle.com/javase/1.5.0/docs/guide/2d/flags.html, 特别是用于调试的 trace 属性。

仅适用于 Windows。

System.setProperty("sun.java2d.transaccel", "True");
System.setProperty("sun.java2d.d3d", "True");
System.setProperty("sun.java2d.ddforcevram", "True");

所有平台

System.setProperty("sun.java2d.opengl", "True");

用于调试

System.setProperty("sun.java2d.trace", "timestamp,log,count");
//// -Dsun.java2d.trace=[log[,timestamp]],[count],[out:<filename>],[help],[verbose]

Toolkit.getDefaultToolkit().sync();并不能强制监视器VSync,这是由BufferCapabilities(在setBuffers函数中)完成的。

    BufferStrategy bf = getBufferStrategy();
    if (bf != null) {
        BufferCapabilities caps = bf.getCapabilities();
        try {
            Class ebcClass = Class.forName(
                "sun.java2d.pipe.hw.ExtendedBufferCapabilities");
            Class vstClass = Class.forName(
                "sun.java2d.pipe.hw.ExtendedBufferCapabilities$VSyncType");

            Constructor ebcConstructor = ebcClass.getConstructor(
                new Class[] { BufferCapabilities.class, vstClass });
            Object vSyncType = vstClass.getField("VSYNC_ON").get(null);

            BufferCapabilities newCaps = (BufferCapabilities)ebcConstructor.newInstance(
                new Object[] { caps, vSyncType });

            createBufferStrategy(2, newCaps);

            // TODO: if success, setCanChangeRefreshRate(false) and setRefreshRate(60). 
            // Possibly override refreshRateSync()?
        }
        catch (Throwable t) {
            // Ignore
            t.printStackTrace();
        }
    }

编辑 这段代码来自(http://pulpcore.googlecode.com/hg-history/3c4001969922b93048e0a166395115df059a2059/src/pulpcore/platform/applet/BufferStrategySurface.java)

此外,你应该在Canvas构造函数运行并添加到JFrame之后调用setBuffers。


我“相对确定”你不需要调用super.paint(g); 我不知道它是否导致了你的特定问题,但你应该从paint函数中删除这行代码。


尽管你没有展示代码,使用200个随机移动的精灵的爆炸机制可能是错误的主要原因。找出你想要在屏幕上同时拥有的最大爆炸数量,并在手头生成N * 200个精灵,将它们放入ArrayList(N)中,然后在你的代码中循环引用它们。ExplosionClass = explosionList.get(currentExplosionIndex % explosionList.size());


谢谢,但不幸的是系统属性似乎对此无济于事。Vsync并不是真正的问题,我认为这可能是@DannyVarod期望的错误。 - Simon Tamás
生成粒子非常快,但移动它们却不是那么快。我尝试使用一个低粒子计数的发射器。 - Simon Tamás
@Simon Tamás - 我本以为至少java2d.trace系统属性可以让您返回更具体的错误消息。 - Louis Ricci
当没有粒子活动时,调用D3DTextureToSurfaceBlit;当有粒子时,调用D3DSwtoSurfaceBlit。请注意,只有粒子是BufferedImages,其他所有内容都是简单的Image。 - Simon Tamás
此外,强制使用OpenGL会导致游戏冻结,黑屏,没有任何痕迹 :( - Simon Tamás

1
检查图片与缓存的大小比较,并尝试诊断缓存未命中的数量。阅读数据导向设计,通常是其中的一部分。
另一种选择是改变爆炸的方式,不要加载200个精灵并将它们散开(您是重复使用它们还是每次进行爆炸时都加载它们?)。制作动画爆炸需要更少的功率,虽然可能不太壮观,但您需要进行一些动画处理。
Java也不是制作游戏的最优语言。您正在高级别地工作,JVM会稍微减慢速度。我的朋友在制作小游戏时遇到了类似的问题,很难制作一个没有大量延迟的游戏。

我只加载一次精灵图像,在粒子“诞生”时进行调整大小和旋转,然后持续重新着色。 - Simon Tamás

0

当您将东西发送到GPU时,GPU会从GDRAM中读取信息。如果通过调用getDrawGraphics将数据读入内存,则内存可能会从图形卡返回到RAM中进行读取。CPU只能访问RAM(DRAM),GPU只能访问GDRAM。然而,对于集成显卡来说情况有所不同,因为它们没有自己的RAM,而是使用常规RAM的一部分。

为了使您的代码运行快速,请确定您拥有哪种硬件,然后相应地调用适当的方法(在更改代码之前或之后的方法)。


问题是,我没有调用任何其他方法,是Java在做一些不同的事情。不幸的是,基于硬件更改代码不是一个选项,因为开发会更加困难,因为一个是JPanel,一个是Canvas,而且从一个复制代码到另一个需要耗费时间。 - Simon Tamás
两个选项都调用一个实用类,该类执行不依赖于调用者的工作,或者使用接口将两个调用者通过适配器提供抽象,以便实用程序可以通过相同的接口访问它们。 - Danny Varod

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