使用动画计时器绘制图像时,ScalaFX画布性能较差

时间:2018-10-11 13:23:55

标签: scala javafx benchmarking scalafx

我计划通过在画布上使用ScalaFX来制作节奏游戏, 当我尝试运行代码时,我发现它消耗了大量的GPU,有时帧速率下降了30 fps,甚至我只在画布上绘制了一张图像,而没有绘制任何动画笔记,舞者,过程规等。

enter image description here

canvas

下面是我的代码

import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.canvas.{Canvas, GraphicsContext}
import scalafx.scene.image.Image
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green

object MainApp extends JFXApp{
  var MainScene: Scene = new Scene {
    fill = Green
  }
  var MainStage: JFXApp.PrimaryStage = new JFXApp.PrimaryStage {
    scene = MainScene
    height = 720
    width = 1280
  }

  var gameCanvas:Canvas = new Canvas(){
    layoutY=0
    layoutX=0
    height=720
    width=1280
  }
  var gameImage:Image = new Image("notebar.png")

  var gc:GraphicsContext = gameCanvas.graphicsContext2D
  MainScene.root = new Pane(){
    children=List(gameCanvas)
  }

  var a:Long = 0
  val animateTimer = AnimationTimer(t => {
    val nt:Long = t/1000000
    val frameRate:Long = 1000/ (if((nt-a)==0) 1 else nt-a)

    //check the frame rate 
    println(frameRate)
    a = nt
    gc.clearRect(0,0,1280,720)
    gc.drawImage(gameImage,0,0,951,160)

  })

  animateTimer.start()
}

如何提高性能,或者有什么更好的方法可以在不使用画布的情况下做同样的事情?

1 个答案:

答案 0 :(得分:3)

有一些因素可能会降低帧速率:

  • 您正在每帧将帧频输出到控制台。这是一个非常慢的操作,并且可以预期会降低帧速率。 (这可能是影响最大的性能。)
  • 您正在计算帧渲染期间的帧速率。从哲学上讲,这是海森堡不确定性原理的一个很好的例子,因为通过测量帧速率,您会干扰它并放慢它的速度;;-)
  • 每次要重新绘制图像时,您都将清除整个画布,而不是仅保留其中的那一部分。 (最初证明这不是我的代码版本中的重要因素,但是当我禁用 JavaFX 速度限制时(请参见下面的更新),事实证明有很大的不同。)

关于帧速率,在下面的版本中,我记录第一帧的时间(以纳秒为单位),并计算绘制的帧数。当应用程序退出时,它将报告平均帧速率。这是一种更简单的计算,不会过多地影响动画处理程序内部的操作,并且是总体性能的良好衡量标准。 (由于垃圾回收,其他进程,JIT编译改进等原因,每个帧的时间会有很大差异。我们将通过查看平均速率来跳过所有这些内容。)

我还更改了代码以仅清除图像所占据的区域。

我还简化了您的代码,使其在使用 ScalaFX 时更加常规(例如,也使用主类的stage成员)因为更多地使用类型推断):

import scalafx.animation.AnimationTimer
import scalafx.application.JFXApp
import scalafx.scene.Scene
import scalafx.scene.canvas.Canvas
import scalafx.scene.image.Image
import scalafx.scene.layout.Pane
import scalafx.scene.paint.Color.Green

object MainApp
extends JFXApp {

  // Nanoseconds per second.
  val NanoPerSec = 1.0e9

  // Height & width of canvas. Change in a single place.
  val canvasHeight = 720
  val canvasWidth = 1280

  // Fixed canvas size.
  val gameCanvas = new Canvas(canvasWidth, canvasHeight)

  // The image.
  val gameImage = new Image("notebar.png")
  val gc = gameCanvas.graphicsContext2D

  stage = new JFXApp.PrimaryStage {
    height = canvasHeight
    width = canvasWidth
    scene = new Scene {
      fill = Green
      root = new Pane {
        children=List(gameCanvas)
      }
    }
  }

  // Class representing an initial frame time, last frame time and number of frames
  // drawn. The first frame is not counted.
  //
  // (Ideally, this would be declared in its own source file. I'm putting it here for
  // convenience.)
  final case class FrameRate(initTime: Long, lastTime: Long = 0L, frames: Long = 0L) {
    // Convert to time in seconds
    def totalTime: Double = if(frames == 0L) 1.0 else (lastTime - initTime) / NanoPerSec

    def mean: Double = frames / totalTime
    def update(time: Long): FrameRate = copy(lastTime = time, frames = frames + 1)
  }

  // Current frame rate.
  private var frames: Option[FrameRate] = None

  val animateTimer = AnimationTimer {t =>

    // Update frame rate.
    frames = Some(frames.fold(FrameRate(t))(_.update(t)))

    // Send information to console. Comment out to determine impact on frame rate.
    //println(s"Frame rate: ${frames.fold("Undefined")(_.mean.toString)}")

    // Clear region of canvas.
    //
    // First clears entire canvas, second only image. Comment one out.
    //gc.clearRect(0, 0, canvasWidth, canvasHeight)
    gc.clearRect(0, 0, gameImage.width.value, gameImage.height.value)

    // Redraw the image. This version doesn't need to know the size of the image.
    gc.drawImage(gameImage, 0, 0)
  }

  animateTimer.start()

  // When the application terminates, output the mean frame rate.
  override def stopApp(): Unit = {
    println(s"Mean frame rate: ${frames.fold("Undefined")(_.mean.toString)}")
  }
}

(顺便说一句:尽可能避免在 Scala 中使用var语句。使用 JavaFX /时不可避免地会出现共享的可变状态 ScalaFX ,但是Property提供了更好的机制来处理它,请养成使用val元素声明的习惯,除非它们确实确实需要{ {1}}。如果确实需要使用var,则应该几乎总是将它们声明为var,以防止不受控制的外部访问和修改。)

Java 程序进行基准测试是一种艺术形式,但显然,每个版本的运行时间越长,平均帧速率就越好。在我的机器上(带有我自己的图像),在运行该应用程序5分钟后,我获得了以下结果,这是不科学的:

  • 清除整个画布并写入控制台:39.69 fps
  • 清除整个画布,控制台无输出:59.85 fps
  • 仅清除图像,无输出到控制台:59.86 fps

仅清除图像而不是清除整个画布似乎效果不大,这让我有些惊讶。但是,输出到控制台会对帧速率产生巨大影响。

除了使用画布之外,另一种可能性是简单地将图像放置在场景组内,然后通过更改其坐标来移动它。下面的代码(使用属性间接移动图像)如下:

private

在运行5分钟后,这为我产生了59.86 fps的平均帧频,几乎与使用画布相同。

在此示例中,动作有点生涩,这很可能是由垃圾收集周期引起的。也许尝试尝试使用不同的 GC

顺便说一句,我在此版本中移动图像以强制发生某些事情。如果属性没有更改,那么我怀疑该图像将不会在该帧中更新。确实,如果我每次都将属性设置为相同的值,则帧速率将变为:62.05 fps。

使用画布意味着您必须确定要绘制的内容以及如何重新绘制它。但是使用 JavaFX 场景图(如上一个示例所示)意味着 JavaFX 负责弄清楚是否甚至需要重绘框架。在这种特殊情况下,它并没有太大的区别,但是如果连续帧之间的内容差异很小,则可能会加快处理速度。要记住的事情。

够快吗?顺便说一句,在此特定示例中,与内容相关的开销很大。如果在动画中添加其他元素对帧速率几乎没有影响,我一点也不惊讶。最好尝试一下看看。交给你...

(有关动画的另一种可能性,请参阅 ScalaFX 源随附的 ColorfulCircles 演示。)

更新:我在评论中提到了这一点,但可能值得在主要答案中强调一下: JavaFX 的默认速度限制为60 fps,这也会影响上述基准测试,并且也解释了为什么不能更好地利用CPU和GPU。

要使您的应用程序以尽可能高的帧速率运行(如果您希望最大程度地增加笔记本电脑的电池电量或提高整体应用程序性能,可能不是一个好主意),请在运行应用程序时启用以下属性:

import scalafx.animation.AnimationTimer import scalafx.application.JFXApp import scalafx.beans.property.DoubleProperty import scalafx.scene.{Group, Scene} import scalafx.scene.image.ImageView import scalafx.scene.layout.Pane import scalafx.scene.paint.Color.Green import scalafx.scene.shape.Rectangle object MainApp extends JFXApp { // Height & width of app. Change in a single place. val canvasHeight = 720 val canvasWidth = 1280 // Nanoseconds per second. val NanoPerSec = 1.0e9 // Center of the circle about which the image will move. val cX = 200.0 val cY = 200.0 // Radius about which we'll move the image. val radius = 100.0 // Properties for positioning the image (might be initial jump). val imX = DoubleProperty(cX + radius) val imY = DoubleProperty(cY) // Image view. It's co-ordinates are bound to the above properties. As the properties // change, so does the image's position. val imageView = new ImageView("notebar.png") { x <== imX // Bind to property y <== imY // Bind to property } stage = new JFXApp.PrimaryStage { height = canvasHeight width = canvasWidth scene = new Scene {thisScene => // thisScene is a self reference fill = Green root = new Group { children=Seq( new Rectangle { // Background width <== thisScene.width // Bind to scene/stage width height <== thisScene.height // Bind to scene/stage height fill = Green }, imageView ) } } } // Class representing an initial frame time, last frame time and number of frames // drawn. The first frame is not counted. // // (Ideally, this would be declared in its own source file. I'm putting it here for // convenience.) final case class FrameRate(initTime: Long, lastTime: Long = 0L, frames: Long = 0L) { // Convert to time in seconds def totalTime: Double = if(frames == 0L) 1.0 else (lastTime - initTime) / NanoPerSec def mean: Double = frames / totalTime def update(time: Long) = copy(lastTime = time, frames = frames + 1) } // Current frame rate. var frames: Option[FrameRate] = None val animateTimer = AnimationTimer {t => // Update frame rate. frames = Some(frames.fold(FrameRate(t))(_.update(t))) // Change the position of the image. We'll make the image move around a circle // clockwise, doing 1 revolution every 10 seconds. The center of the circle will be // (cX, cY). The angle is therefore the modulus of the time in seconds divided by 10 // as a proportion of 2 pi radians. val angle = (frames.get.totalTime % 10.0) * 2.0 * Math.PI / 10.0 // Update X and Y co-ordinates related to the center and angle. imX.value = cX + radius * Math.cos(angle) imY.value = cY + radius * Math.sin(angle) } animateTimer.start() // When the application terminates, output the mean frame rate. override def stopApp(): Unit = { println(s"Mean frame rate: ${frames.fold("Undefined")(_.mean.toString)}") } }

请注意,此属性未记录且不受支持,这意味着它可能在 JavaFX 的未来版本中消失。

我使用该属性集重新运行了基准测试,并观察到以下结果:

使用画布:

  • 清除整个画布并写入控制台:64.72 fps
  • 清除整个画布,无输出到控制台:144.74 fps
  • 仅清除图像,无输出到控制台:159.48 fps

动画场景图:

  • 控制台无输出:217.68 fps

这些结果大大改变了我的原始结论:

  • 使用场景图渲染图像(甚至对其进行动画处理)比在画布上绘制图像时获得的最佳结果要有效得多(就帧速率而言,提高了36%)。鉴于场景图已经过优化以提高性能,因此这并不意外。
  • 如果使用画布,则与清除整个画布相比,仅清除图像占据的区域的帧速率大约好10%(对于本示例)。

有关-Djavafx.animation.fullspeed=true属性的更多详细信息,请参见this answer